← back to Model Arena
Agent-driven pivot: DW Color Story Studio (frontend-developer, plan+execute, 72KB self-contained luxury tool) — stripped Google Fonts for CSP-safety, registered as featured arcade game
7527c24c8aeb323543c1060d40df7c4e3dd2c0fa · 2026-07-25 08:50:18 -0700 · Steve Abrams
Files touched
M data/arcade-games.jsonM idea-run/run-next.shA public/games/dw-color-story-studio.html
Diff
commit 7527c24c8aeb323543c1060d40df7c4e3dd2c0fa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jul 25 08:50:18 2026 -0700
Agent-driven pivot: DW Color Story Studio (frontend-developer, plan+execute, 72KB self-contained luxury tool) — stripped Google Fonts for CSP-safety, registered as featured arcade game
---
data/arcade-games.json | 22 +-
idea-run/run-next.sh | 2 +-
public/games/dw-color-story-studio.html | 2434 +++++++++++++++++++++++++++++++
3 files changed, 2454 insertions(+), 4 deletions(-)
diff --git a/data/arcade-games.json b/data/arcade-games.json
index 7198b05..9d32459 100644
--- a/data/arcade-games.json
+++ b/data/arcade-games.json
@@ -1,6 +1,22 @@
{
"featured": [
- { "slug": "tetris", "title": "Tetris", "credit": "claude-code", "note": "Arena champion — All-Models Tetris" },
- { "slug": "star-clock", "title": "Constellation Clock", "credit": "claude-code", "note": "Arena champion — Free-Roster Constellation Clock" }
+ {
+ "slug": "tetris",
+ "title": "Tetris",
+ "credit": "claude-code",
+ "note": "Arena champion \u2014 All-Models Tetris"
+ },
+ {
+ "slug": "star-clock",
+ "title": "Constellation Clock",
+ "credit": "claude-code",
+ "note": "Arena champion \u2014 Free-Roster Constellation Clock"
+ },
+ {
+ "slug": "dw-color-story-studio",
+ "title": "DW Color Story Studio",
+ "credit": "frontend-developer",
+ "note": "Agent-built flagship \u2014 seasonal wallcovering color-story tool for the DW social team"
+ }
]
-}
+}
\ No newline at end of file
diff --git a/idea-run/run-next.sh b/idea-run/run-next.sh
index 8db3e7e..d28adcc 100755
--- a/idea-run/run-next.sh
+++ b/idea-run/run-next.sh
@@ -21,7 +21,7 @@ PROMPT="$PROMPT DW LUXURY UI REQUIREMENTS (mandatory): (a) a fixed top bar with
python3 - "$TITLE" "$PROMPT" > /tmp/dwaa-payload.json <<'PY'
import json,sys
json.dump({"title":sys.argv[1],"prompt":sys.argv[2],
- "models":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","hf-qwen-coder-32b","claude-code"],"designTools":True},
+ "models":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","hf-qwen-coder-32b","claude-code","kimi","gpt","grok"],"designTools":True},
sys.stdout)
PY
diff --git a/public/games/dw-color-story-studio.html b/public/games/dw-color-story-studio.html
new file mode 100644
index 0000000..133a5bd
--- /dev/null
+++ b/public/games/dw-color-story-studio.html
@@ -0,0 +1,2434 @@
+<!--
+================================================================================
+DESIGNER WALLCOVERINGS — SEASONAL COLOR STORY STUDIO
+Plan Document (v1.0 — Flagship Build)
+================================================================================
+
+CONCEPT
+-------
+A single-page brand tool for the DW social media team to generate, preview,
+and export "Seasonal Color Stories" — curated 5-swatch wallcovering palettes
+tied to a chosen season. Each story translates into a complete Instagram
+carousel (1080x1080 cover + 3 color slides + CTA) with ready-to-copy caption
+and hashtag blocks, plus an exportable palette summary card.
+
+The aesthetic is uncompromisingly luxury: Cormorant Garamond wordmark, warm
+near-black background (#0d0b09), brass accent (#c9a961), hairline strokes,
+generous whitespace, and rich CSS/SVG textures that convey tactile material.
+
+TARGET USER
+-----------
+DW social manager / content coordinator. They need:
+ 1. Choose a season in one click → instant curated palette appears
+ 2. Preview exactly how each Instagram slide looks before posting
+ 3. Copy caption + hashtags without leaving the tool
+ 4. Export/share a palette summary card (print-ready PNG-style card via CSS)
+
+UX FLOW
+-------
+ [1] LANDING: Full-width hero with wordmark + season selector (4 seasonal tabs)
+ → Default = current / near-current season (Autumn for this build)
+ [2] COLOR STORY REVEAL: 5 swatches animate in (staggered 80ms), each showing:
+ • CSS/SVG texture swatch (distinct motif per color)
+ • Designer color name (e.g. "Tarnished Brass", "Smoked Sage")
+ • One-line styling note in italic serif
+ • Hex chip
+ [3] CAROUSEL PREVIEW: Below swatches, an 1080x1080 Instagram preview panel
+ • Cover slide + 3 color story slides + CTA slide = 5 slides
+ • Left/right arrow nav + dot indicators
+ • Keyboard ← → navigation
+ • Touch/swipe support
+ [4] CAPTION COPY: Accordion below carousel with the full Instagram caption
+ text and hashtag block, with one-click copy-to-clipboard
+ (in-page toast notification — no alert())
+ [5] PALETTE CARD: "Export Palette Card" button triggers an overlay/modal
+ showing a beautifully typeset palette summary card, designed for
+ screenshot/print, with all 5 swatches + brand lockup
+
+VISUAL SYSTEM
+-------------
+Palette tokens:
+ --bg: #0d0b09 (warm near-black — primary surface)
+ --bg-raised: #131008 (slightly lifted panels)
+ --ink: #1c1a17 (darkest text, on light surfaces)
+ --ink-inv: #e8e3da (primary text on dark surfaces)
+ --ink-dim: #9a9184 (secondary/muted text)
+ --accent: #c9a961 (DW brass — CTAs, active states, swatch borders)
+ --accent-dim:#7a6538 (brass hover darken)
+ --line: rgba(201,169,97,0.18) (hairline borders)
+ --shadow: 0 4px 32px rgba(0,0,0,0.55)
+ --serif: 'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif
+ --sans: 'Optima','Gill Sans','Trebuchet MS',system-ui,sans-serif
+ --ease: cubic-bezier(0.25, 0.46, 0.45, 0.94)
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1)
+
+Typography scale:
+ wordmark: 44px, letter-spacing .18em, serif, uppercase
+ h2 section: 28px serif
+ color name: 22px serif italic
+ label: 11px sans uppercase tracked .14em
+ body copy: 16px sans, lh 1.7
+
+Spacing scale (4-base): 4 8 12 16 24 32 48 64 80 120px
+
+Motion:
+ swatch reveal: opacity 0→1, translateY 20px→0, staggered 80ms
+ carousel slide: translateX, 360ms --ease
+ toast: translateY -20px→0, opacity, 2.8s total
+ overlay: opacity + scale(0.96)→scale(1), 280ms
+
+SEASONAL COLOR STORIES (content model)
+---------------------------------------
+Each season = { name, tagline, palette[5], caption, hashtags }
+
+Each swatch = {
+ name: string, // designer color name
+ hex: string, // actual CSS color
+ texture: 'grasscloth' | 'damask' | 'linen' | 'silk' | 'paperweave' | 'raffia',
+ note: string // one-line styling note
+}
+
+AUTUMN palette (default):
+ 1. Burnished Terracotta #b5614a damask Anchor a feature wall in a study
+ 2. Smoked Sage #6e7d5e linen Balance warmth with botanical calm
+ 3. Tarnished Brass #a87f3c silk-stripe Layer with aged leather and walnut
+ 4. Oatmeal Grasscloth #c9b89a grasscloth Ground the palette in natural texture
+ 5. Ink Charcoal #2e2a25 raffia-weave A deeply moody base for art-wall drama
+
+WINTER palette:
+ 1. Alabaster #e8e0d2 damask Float porcelain objets against serene quiet
+ 2. Ink Navy #1a2235 silk-stripe A commanding depth for a library or salon
+ 3. Celadon Mist #afc5be grasscloth Ethereal, spa-like, and endlessly calming
+ 4. Champagne #d4b87a linen Warm up cool marble with a gilded haze
+ 5. Carbon Matte #2c2c2e raffia-weave Understated power — a backdrop for metallics
+
+SPRING palette:
+ 1. Blush Oyster #e8d5c8 linen The softest possible opening note
+ 2. Garden Sage #8aa87d grasscloth Fresh, grounded, botanically charged
+ 3. Pale Aubergine #8c7a8e damask A whisper of opulence in a dressing room
+ 4. Greige Flannel #b0a696 raffia-weave The unsung hero of every layered scheme
+ 5. Leaf Vein Green #4a5e42 silk-stripe Close the palette with forest authority
+
+SUMMER palette:
+ 1. Raw Linen #d6c9af linen Effortless coastal ease — walls that breathe
+ 2. Sea Glass #7fbdae grasscloth Pulls in ocean light without pastiche
+ 3. Driftwood #9c8870 raffia-weave Warm natural grain for relaxed entertaining
+ 4. Antique White #ede8df damask An airy foil that lets other pieces sing
+ 5. Deep Indigo #2d3a5a silk-stripe A night-sky accent of true sophistication
+
+CAROUSEL SLIDES
+---------------
+Slide 0 (Cover): Season name large, tagline, brand wordmark, year
+Slide 1: Colors 1 + 2 side-by-side, names + notes
+Slide 2: Colors 3 + 4 side-by-side, names + notes
+Slide 3: Color 5 full-bleed feature, name + styling note
+Slide 4 (CTA): "Explore the full collection" + handle @designerwallcoverings
+
+CAPTION TEMPLATE
+----------------
+[Season] is the story we've been composing all year. Five wallcovering hues —
+each hand-selected for texture, depth, and the way they hold light — now live
+on our site. Which speaks to your current project?
+
+→ Link in bio to explore the full [Season] Collection.
+
+[hashtag block]
+
+================================================================================
+-->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Seasonal Color Story Studio — Designer Wallcoverings</title>
+<!-- No external network dependencies: all imagery/textures are CSS/inline SVG -->
+<!-- Cormorant Garamond loads from Google Fonts when online (non-blocking); degrades beautifully to Palatino/Georgia offline -->
+<style>
+/* ============================================================
+ DESIGN TOKENS
+ ============================================================ */
+:root {
+ --bg: #0d0b09;
+ --bg-raised: #131008;
+ --bg-panel: #1a1710;
+ --bg-light: #f4f1ea;
+ --ink: #1c1a17;
+ --ink-inv: #e8e3da;
+ --ink-dim: #9a9184;
+ --ink-faint: #4a4540;
+ --accent: #c9a961;
+ --accent-dim: #7a6538;
+ --accent-glow: rgba(201,169,97,0.12);
+ --line: rgba(201,169,97,0.18);
+ --line-strong: rgba(201,169,97,0.35);
+ --shadow: 0 4px 32px rgba(0,0,0,0.55);
+ --shadow-lg: 0 16px 64px rgba(0,0,0,0.7);
+ --serif: 'Cormorant Garamond','Playfair Display','EB Garamond',Georgia,serif;
+ --sans: 'Optima','Gill Sans MT','Trebuchet MS',system-ui,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: 2px;
+ --radius-lg: 6px;
+}
+
+/* ============================================================
+ HERO BAND
+ ============================================================ */
+.hero-band {
+ position: relative;
+ padding: 80px 48px 0;
+ overflow: hidden;
+}
+
+.hero-band::before {
+ /* Subtle radial glow from accent */
+ content: '';
+ position: absolute;
+ top: -40px;
+ right: -80px;
+ width: 500px;
+ height: 500px;
+ background: radial-gradient(circle, rgba(201,169,97,0.06) 0%, transparent 70%);
+ pointer-events: none;
+}
+
+.hero-inner {
+ max-width: 680px;
+ position: relative;
+ z-index: 1;
+}
+
+.hero-eyebrow {
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .24em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 20px;
+}
+
+.hero-headline {
+ font-family: var(--serif);
+ font-size: clamp(44px, 7vw, 80px);
+ font-weight: 300;
+ letter-spacing: .02em;
+ color: var(--ink-inv);
+ line-height: 1.0;
+ margin-bottom: 28px;
+}
+
+.hero-body {
+ font-family: var(--sans);
+ font-size: 15px;
+ color: var(--ink-dim);
+ line-height: 1.75;
+ max-width: 520px;
+ margin-bottom: 48px;
+}
+
+.hero-divider {
+ width: 48px;
+ height: 1px;
+ background: linear-gradient(90deg, var(--accent) 0%, transparent 100%);
+ margin-bottom: 48px;
+}
+
+.hero-accent-bar {
+ position: relative;
+ margin: 0 -48px;
+ height: 4px;
+ margin-bottom: 0;
+}
+
+.hero-color-stripe {
+ display: flex;
+ height: 4px;
+ width: 100%;
+ overflow: hidden;
+}
+
+.hero-stripe-seg {
+ flex: 1;
+ transition: background 600ms var(--ease);
+}
+
+@media (max-width: 768px) {
+ .hero-band { padding: 48px 24px 0; }
+ .hero-band::before { display: none; }
+ .hero-accent-bar { margin: 0 -24px; }
+}
+
+/* ============================================================
+ RESET + BASE
+ ============================================================ */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+html {
+ scroll-behavior: smooth;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ background: var(--bg);
+ color: var(--ink-inv);
+ font-family: var(--sans);
+ font-size: 16px;
+ line-height: 1.7;
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+
+/* ============================================================
+ GRAIN OVERLAY (luxury texture on body)
+ ============================================================ */
+body::before {
+ content: '';
+ position: fixed;
+ inset: 0;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
+ opacity: 0.4;
+ pointer-events: none;
+ z-index: 0;
+}
+
+/* ============================================================
+ TYPOGRAPHY UTILITIES
+ ============================================================ */
+.serif { font-family: var(--serif); }
+.sans { font-family: var(--sans); }
+.tracked { letter-spacing: .14em; }
+.caps { text-transform: uppercase; }
+.dim { color: var(--ink-dim); }
+.accent-c { color: var(--accent); }
+
+/* ============================================================
+ HEADER / WORDMARK
+ ============================================================ */
+header {
+ position: relative;
+ z-index: 10;
+ padding: 32px 48px 24px;
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 16px;
+ background: var(--bg);
+}
+
+.wordmark {
+ font-family: var(--serif);
+ font-size: clamp(28px, 3.5vw, 44px);
+ font-weight: 300;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ color: var(--ink-inv);
+ line-height: 1;
+ user-select: none;
+}
+
+.wordmark span.accent-rule {
+ display: block;
+ width: 100%;
+ height: 1px;
+ background: linear-gradient(90deg, var(--accent) 0%, transparent 100%);
+ margin-top: 8px;
+ opacity: 0.6;
+}
+
+.header-tag {
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ color: var(--accent);
+ align-self: flex-end;
+ padding-bottom: 2px;
+}
+
+/* ============================================================
+ MAIN LAYOUT
+ ============================================================ */
+main {
+ position: relative;
+ z-index: 1;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 48px 120px;
+}
+
+@media (max-width: 768px) {
+ header { padding: 24px 24px 20px; }
+ main { padding: 0 24px 80px; }
+}
+
+/* ============================================================
+ SECTION HEADERS
+ ============================================================ */
+.section-label {
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .22em;
+ text-transform: uppercase;
+ color: var(--accent);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+.section-label::after {
+ content: '';
+ flex: 1;
+ height: 1px;
+ background: var(--line);
+}
+
+.section-title {
+ font-family: var(--serif);
+ font-size: clamp(24px, 3vw, 36px);
+ font-weight: 300;
+ color: var(--ink-inv);
+ margin-bottom: 8px;
+ line-height: 1.2;
+}
+
+.section-sub {
+ font-size: 14px;
+ color: var(--ink-dim);
+ margin-bottom: 48px;
+ max-width: 480px;
+}
+
+/* ============================================================
+ SEASON SELECTOR
+ ============================================================ */
+.season-section {
+ padding-top: 64px;
+}
+
+.season-tabs {
+ display: flex;
+ gap: 0;
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ overflow: hidden;
+ width: fit-content;
+ margin-bottom: 64px;
+}
+
+.season-tab {
+ background: transparent;
+ border: none;
+ border-right: 1px solid var(--line);
+ padding: 14px 32px;
+ font-family: var(--sans);
+ font-size: 11px;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ color: var(--ink-dim);
+ cursor: pointer;
+ transition: all 240ms var(--ease);
+ position: relative;
+ overflow: hidden;
+}
+.season-tab:last-child { border-right: none; }
+
+.season-tab::after {
+ content: '';
+ position: absolute;
+ bottom: 0; left: 0; right: 0;
+ height: 2px;
+ background: var(--accent);
+ transform: scaleX(0);
+ transition: transform 240ms var(--ease);
+}
+
+.season-tab:hover { color: var(--ink-inv); background: var(--accent-glow); }
+.season-tab.active {
+ color: var(--accent);
+ background: var(--accent-glow);
+}
+.season-tab.active::after { transform: scaleX(1); }
+
+/* ============================================================
+ COLOR STORY — PALETTE DISPLAY
+ ============================================================ */
+.palette-section { margin-bottom: 80px; }
+
+.palette-header {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 24px;
+ margin-bottom: 40px;
+}
+
+.season-tagline {
+ font-family: var(--serif);
+ font-size: clamp(16px, 2vw, 20px);
+ font-style: italic;
+ color: var(--ink-dim);
+ max-width: 420px;
+ line-height: 1.5;
+}
+
+.swatches-grid {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 2px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+}
+
+@media (max-width: 900px) {
+ .swatches-grid { grid-template-columns: repeat(3, 1fr); }
+}
+@media (max-width: 600px) {
+ .swatches-grid { grid-template-columns: 1fr 1fr; }
+}
+
+.swatch-card {
+ position: relative;
+ cursor: pointer;
+ transition: transform 320ms var(--ease);
+ opacity: 0;
+ transform: translateY(20px);
+}
+.swatch-card.revealed {
+ opacity: 1;
+ transform: translateY(0);
+ transition: opacity 400ms var(--ease), transform 400ms var(--ease);
+}
+.swatch-card:hover { z-index: 2; transform: scale(1.02); }
+.swatch-card.active-swatch { z-index: 3; transform: scale(1.03); }
+.swatch-card.active-swatch .swatch-face::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border: 2px solid var(--accent);
+ pointer-events: none;
+}
+
+.swatch-face {
+ width: 100%;
+ aspect-ratio: 1 / 1.4;
+ position: relative;
+ overflow: hidden;
+}
+
+.swatch-info {
+ background: var(--bg-raised);
+ padding: 16px;
+ border-top: 1px solid var(--line);
+}
+
+.swatch-name {
+ font-family: var(--serif);
+ font-size: 17px;
+ font-style: italic;
+ color: var(--ink-inv);
+ display: block;
+ margin-bottom: 4px;
+ line-height: 1.2;
+}
+
+.swatch-note {
+ font-family: var(--sans);
+ font-size: 11px;
+ color: var(--ink-dim);
+ line-height: 1.5;
+ margin-bottom: 10px;
+}
+
+.swatch-hex {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .1em;
+ text-transform: uppercase;
+ color: var(--ink-faint);
+}
+.swatch-hex-chip {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ border: 1px solid rgba(255,255,255,0.1);
+}
+
+/* ============================================================
+ TEXTURE RENDERERS (CSS/SVG — no external images)
+ ============================================================ */
+
+/* Grasscloth — horizontal woven lines with slight variation */
+.texture-grasscloth {
+ background-color: var(--swatch-color);
+ background-image:
+ repeating-linear-gradient(
+ 0deg,
+ transparent,
+ transparent 3px,
+ rgba(0,0,0,0.07) 3px,
+ rgba(0,0,0,0.07) 4px
+ ),
+ repeating-linear-gradient(
+ 90deg,
+ transparent,
+ transparent 7px,
+ rgba(255,255,255,0.05) 7px,
+ rgba(255,255,255,0.05) 8px
+ );
+}
+
+/* Damask — SVG medallion repeated */
+.texture-damask {
+ background-color: var(--swatch-color);
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='80'%3E%3Cg fill='none' stroke='rgba(0,0,0,0.1)' stroke-width='0.8'%3E%3Cellipse cx='30' cy='40' rx='10' ry='16'/%3E%3Cellipse cx='30' cy='40' rx='16' ry='10'/%3E%3Ccircle cx='30' cy='40' r='5'/%3E%3Cpath d='M30 24 L30 20 M30 56 L30 60 M14 40 L10 40 M46 40 L50 40'/%3E%3Cpath d='M18 28 L14 24 M42 52 L46 56 M42 28 L46 24 M18 52 L14 56'/%3E%3C/g%3E%3C/svg%3E");
+ background-size: 60px 80px;
+}
+
+/* Linen — fine diagonal weave */
+.texture-linen {
+ background-color: var(--swatch-color);
+ background-image:
+ repeating-linear-gradient(
+ 45deg,
+ rgba(0,0,0,0.05) 0px,
+ rgba(0,0,0,0.05) 1px,
+ transparent 1px,
+ transparent 5px
+ ),
+ repeating-linear-gradient(
+ -45deg,
+ rgba(0,0,0,0.04) 0px,
+ rgba(0,0,0,0.04) 1px,
+ transparent 1px,
+ transparent 5px
+ );
+}
+
+/* Silk stripe — refined vertical satin bands */
+.texture-silk {
+ background-color: var(--swatch-color);
+ background-image:
+ repeating-linear-gradient(
+ 90deg,
+ rgba(255,255,255,0.12) 0px,
+ rgba(255,255,255,0.12) 1px,
+ transparent 1px,
+ transparent 8px,
+ rgba(255,255,255,0.06) 8px,
+ rgba(255,255,255,0.06) 9px,
+ transparent 9px,
+ transparent 16px
+ );
+}
+
+/* Paperweave — tight horizontal + vertical grid (washi-like) */
+.texture-paperweave {
+ background-color: var(--swatch-color);
+ background-image:
+ repeating-linear-gradient(
+ 0deg,
+ rgba(0,0,0,0.06) 0px,
+ rgba(0,0,0,0.06) 1px,
+ transparent 1px,
+ transparent 6px
+ ),
+ repeating-linear-gradient(
+ 90deg,
+ rgba(0,0,0,0.06) 0px,
+ rgba(0,0,0,0.06) 1px,
+ transparent 1px,
+ transparent 6px
+ );
+}
+
+/* Raffia weave — chunky diagonal basket pattern */
+.texture-raffia {
+ background-color: var(--swatch-color);
+ background-image:
+ repeating-linear-gradient(
+ 45deg,
+ rgba(255,255,255,0.06) 0px,
+ rgba(255,255,255,0.06) 4px,
+ transparent 4px,
+ transparent 10px
+ ),
+ repeating-linear-gradient(
+ -45deg,
+ rgba(0,0,0,0.08) 0px,
+ rgba(0,0,0,0.08) 4px,
+ transparent 4px,
+ transparent 10px
+ );
+}
+
+/* ============================================================
+ INSTAGRAM CAROUSEL PREVIEW
+ ============================================================ */
+.carousel-section {
+ margin-bottom: 80px;
+}
+
+.carousel-outer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+}
+
+/* Phone frame mock */
+.phone-frame {
+ width: 100%;
+ max-width: 420px;
+ background: #1a1a1a;
+ border-radius: 36px;
+ padding: 12px;
+ box-shadow: var(--shadow-lg), 0 0 0 1px rgba(255,255,255,0.05);
+ position: relative;
+}
+
+.phone-notch {
+ width: 100px;
+ height: 22px;
+ background: #1a1a1a;
+ border-radius: 0 0 14px 14px;
+ margin: 0 auto 8px;
+ position: relative;
+ z-index: 2;
+}
+
+.phone-screen {
+ border-radius: 26px;
+ overflow: hidden;
+ background: #000;
+}
+
+/* IG chrome top bar */
+.ig-chrome-top {
+ background: #fff;
+ padding: 10px 14px 6px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ border-bottom: 1px solid #e0e0e0;
+}
+.ig-avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--accent) 0%, #8b4a2a 100%);
+ flex-shrink: 0;
+}
+.ig-handle {
+ font-family: -apple-system, sans-serif;
+ font-size: 12px;
+ font-weight: 600;
+ color: #1a1a1a;
+ flex: 1;
+}
+.ig-more {
+ color: #1a1a1a;
+ font-size: 16px;
+ line-height: 1;
+}
+
+/* Carousel viewport */
+.carousel-viewport {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ overflow: hidden;
+}
+
+.carousel-track {
+ display: flex;
+ height: 100%;
+ transition: transform 360ms var(--ease);
+ will-change: transform;
+}
+
+.carousel-slide {
+ flex: 0 0 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+/* Slide: Cover */
+.slide-cover {
+ /* background set by JS */
+ padding: 32px;
+}
+
+.slide-cover-inner {
+ text-align: center;
+ position: relative;
+ z-index: 2;
+}
+
+.slide-cover .cover-year {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .28em;
+ text-transform: uppercase;
+ color: rgba(255,255,255,0.5);
+ margin-bottom: 20px;
+}
+
+.slide-cover .cover-season {
+ font-family: var(--serif);
+ font-size: clamp(36px, 10vw, 52px);
+ font-weight: 300;
+ letter-spacing: .08em;
+ text-transform: uppercase;
+ color: #fff;
+ line-height: 1;
+ margin-bottom: 8px;
+}
+
+.slide-cover .cover-sub {
+ font-family: var(--serif);
+ font-size: clamp(14px, 3vw, 18px);
+ font-style: italic;
+ color: rgba(255,255,255,0.75);
+ margin-bottom: 28px;
+ line-height: 1.4;
+}
+
+.slide-cover .cover-rule {
+ width: 40px;
+ height: 1px;
+ background: var(--accent);
+ margin: 0 auto 20px;
+}
+
+.slide-cover .cover-brand {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .22em;
+ text-transform: uppercase;
+ color: var(--accent);
+}
+
+/* Cover texture overlay */
+.slide-cover .cover-texture {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+}
+
+/* Slide: 2-color panel */
+.slide-duo {
+ padding: 0;
+ flex-direction: row;
+ align-items: stretch;
+}
+
+.slide-duo-half {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ padding: 20px 16px;
+ position: relative;
+ overflow: hidden;
+}
+
+.slide-duo-half .duo-color-name {
+ font-family: var(--serif);
+ font-size: clamp(14px, 3.5vw, 18px);
+ font-style: italic;
+ font-weight: 400;
+ color: #fff;
+ text-shadow: 0 1px 8px rgba(0,0,0,0.5);
+ margin-bottom: 4px;
+ position: relative;
+ z-index: 2;
+}
+
+.slide-duo-half .duo-color-note {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .1em;
+ text-transform: uppercase;
+ color: rgba(255,255,255,0.65);
+ position: relative;
+ z-index: 2;
+}
+
+.slide-duo-half .duo-divider {
+ position: absolute;
+ top: 0; right: 0;
+ width: 1px;
+ height: 100%;
+ background: rgba(255,255,255,0.15);
+}
+
+/* Slide: Feature (full bleed single color) */
+.slide-feature {
+ padding: 32px;
+ align-items: flex-start;
+ justify-content: flex-end;
+}
+
+.slide-feature .feature-content {
+ position: relative;
+ z-index: 2;
+}
+
+.slide-feature .feature-num {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .22em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 8px;
+}
+
+.slide-feature .feature-name {
+ font-family: var(--serif);
+ font-size: clamp(24px, 6vw, 36px);
+ font-style: italic;
+ color: #fff;
+ text-shadow: 0 2px 16px rgba(0,0,0,0.4);
+ line-height: 1.1;
+ margin-bottom: 8px;
+}
+
+.slide-feature .feature-rule {
+ width: 32px;
+ height: 1px;
+ background: var(--accent);
+ margin-bottom: 10px;
+}
+
+.slide-feature .feature-note {
+ font-family: var(--sans);
+ font-size: 11px;
+ color: rgba(255,255,255,0.75);
+ max-width: 220px;
+ line-height: 1.5;
+}
+
+/* Slide: CTA */
+.slide-cta {
+ background: var(--bg) !important;
+ padding: 32px;
+ text-align: center;
+}
+
+.slide-cta .cta-rule-top {
+ width: 1px;
+ height: 40px;
+ background: var(--line-strong);
+ margin: 0 auto 24px;
+}
+
+.slide-cta .cta-eyebrow {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .22em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 16px;
+}
+
+.slide-cta .cta-headline {
+ font-family: var(--serif);
+ font-size: clamp(22px, 5vw, 30px);
+ font-weight: 300;
+ color: var(--ink-inv);
+ line-height: 1.25;
+ margin-bottom: 20px;
+}
+
+.slide-cta .cta-handle {
+ font-family: var(--sans);
+ font-size: 12px;
+ letter-spacing: .08em;
+ color: var(--accent);
+ margin-bottom: 24px;
+}
+
+.slide-cta .cta-pill {
+ display: inline-block;
+ border: 1px solid var(--line-strong);
+ padding: 10px 24px;
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .2em;
+ text-transform: uppercase;
+ color: var(--ink-inv);
+ border-radius: var(--radius);
+}
+
+/* IG chrome bottom bar */
+.ig-chrome-bottom {
+ background: #fff;
+ padding: 8px 14px 10px;
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+.ig-icon {
+ width: 22px;
+ height: 22px;
+ color: #1a1a1a;
+}
+.ig-icon svg { width: 100%; height: 100%; }
+.ig-save-icon { margin-left: auto; }
+
+/* Carousel dots */
+.carousel-dots {
+ display: flex;
+ gap: 5px;
+ justify-content: center;
+}
+.c-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--ink-faint);
+ transition: all 240ms var(--ease);
+ cursor: pointer;
+}
+.c-dot.active {
+ background: var(--accent);
+ width: 16px;
+ border-radius: 3px;
+}
+
+/* Carousel nav arrows */
+.carousel-nav {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+}
+.carousel-arrow {
+ background: transparent;
+ border: 1px solid var(--line);
+ color: var(--ink-dim);
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ transition: all 240ms var(--ease);
+ font-family: var(--sans);
+}
+.carousel-arrow:hover:not(:disabled) {
+ border-color: var(--accent);
+ color: var(--accent);
+ background: var(--accent-glow);
+}
+.carousel-arrow:disabled {
+ opacity: 0.25;
+ cursor: not-allowed;
+}
+
+.slide-counter {
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .12em;
+ color: var(--ink-dim);
+ text-align: center;
+ margin-top: 4px;
+}
+
+/* ============================================================
+ CAPTION SECTION
+ ============================================================ */
+.caption-section {
+ margin-bottom: 80px;
+}
+
+.caption-box {
+ border: 1px solid var(--line);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+}
+
+.caption-header {
+ padding: 20px 24px;
+ background: var(--bg-raised);
+ border-bottom: 1px solid var(--line);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.caption-title {
+ font-family: var(--sans);
+ font-size: 11px;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ color: var(--ink-dim);
+}
+
+.copy-btn {
+ background: transparent;
+ border: 1px solid var(--line-strong);
+ color: var(--accent);
+ padding: 8px 20px;
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .16em;
+ text-transform: uppercase;
+ cursor: pointer;
+ border-radius: var(--radius);
+ transition: all 240ms var(--ease);
+}
+.copy-btn:hover {
+ background: var(--accent-glow);
+ border-color: var(--accent);
+}
+.copy-btn.copied {
+ color: #6bbd6b;
+ border-color: #6bbd6b;
+}
+
+.caption-body {
+ padding: 28px 28px 20px;
+ background: var(--bg-panel);
+}
+
+.caption-text {
+ font-family: var(--serif);
+ font-size: 17px;
+ font-weight: 300;
+ color: var(--ink-inv);
+ line-height: 1.8;
+ white-space: pre-wrap;
+ margin-bottom: 20px;
+}
+
+.hashtag-block {
+ font-family: var(--sans);
+ font-size: 12px;
+ color: var(--accent);
+ line-height: 2;
+ opacity: 0.8;
+}
+
+/* ============================================================
+ EXPORT / PALETTE CARD SECTION
+ ============================================================ */
+.export-section {
+ margin-bottom: 80px;
+}
+
+.export-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ background: var(--accent-glow);
+ border: 1px solid var(--line-strong);
+ color: var(--accent);
+ padding: 16px 36px;
+ font-family: var(--sans);
+ font-size: 11px;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ cursor: pointer;
+ border-radius: var(--radius);
+ transition: all 280ms var(--ease);
+}
+.export-btn:hover {
+ background: rgba(201,169,97,0.2);
+ border-color: var(--accent);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 20px rgba(201,169,97,0.1);
+}
+.export-btn svg { width: 16px; height: 16px; }
+
+/* ============================================================
+ PALETTE CARD MODAL
+ ============================================================ */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.85);
+ backdrop-filter: blur(4px);
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 280ms var(--ease);
+}
+.modal-overlay.open {
+ opacity: 1;
+ pointer-events: all;
+}
+
+.modal-inner {
+ transform: scale(0.95);
+ transition: transform 280ms var(--ease-spring);
+}
+.modal-overlay.open .modal-inner {
+ transform: scale(1);
+}
+
+/* The exportable palette card itself */
+.palette-card {
+ background: var(--bg-light);
+ width: min(680px, 100%);
+ padding: 0;
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+ border-radius: 2px;
+ position: relative;
+}
+
+.palette-card-header {
+ background: var(--bg);
+ padding: 36px 40px 28px;
+ border-bottom: 1px solid rgba(201,169,97,0.2);
+}
+
+.palette-card-brand {
+ font-family: var(--serif);
+ font-size: 26px;
+ font-weight: 300;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ color: var(--ink-inv);
+ line-height: 1;
+ margin-bottom: 6px;
+}
+
+.palette-card-rule {
+ width: 40px;
+ height: 1px;
+ background: var(--accent);
+ margin-bottom: 10px;
+}
+
+.palette-card-season-name {
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .22em;
+ text-transform: uppercase;
+ color: var(--accent);
+}
+
+/* Horizontal swatch row on card */
+.palette-card-swatches {
+ display: flex;
+ height: 200px;
+}
+
+.palette-card-swatch {
+ flex: 1;
+ position: relative;
+ overflow: hidden;
+}
+
+.palette-card-swatch-label {
+ position: absolute;
+ bottom: 0; left: 0; right: 0;
+ background: linear-gradient(transparent, rgba(0,0,0,0.6));
+ padding: 20px 10px 10px;
+ text-align: center;
+}
+
+.palette-card-swatch-name {
+ font-family: var(--serif);
+ font-size: 11px;
+ font-style: italic;
+ color: #fff;
+ display: block;
+ line-height: 1.2;
+}
+
+/* Color name strip below swatches */
+.palette-card-names {
+ background: var(--bg-light);
+ display: flex;
+ border-top: 1px solid rgba(0,0,0,0.08);
+}
+
+.palette-card-name-item {
+ flex: 1;
+ padding: 14px 10px;
+ text-align: center;
+ border-right: 1px solid rgba(0,0,0,0.06);
+}
+.palette-card-name-item:last-child { border-right: none; }
+
+.palette-card-hex {
+ display: block;
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .08em;
+ color: #6a6560;
+ margin-bottom: 3px;
+}
+.palette-card-cn {
+ display: block;
+ font-family: var(--serif);
+ font-size: 11px;
+ font-style: italic;
+ color: var(--ink);
+}
+
+/* Notes section on card */
+.palette-card-notes {
+ background: var(--bg-light);
+ padding: 24px 40px 20px;
+ border-top: 1px solid rgba(0,0,0,0.06);
+}
+
+.palette-card-notes-title {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .2em;
+ text-transform: uppercase;
+ color: #888;
+ margin-bottom: 12px;
+}
+
+.palette-card-note-row {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ margin-bottom: 6px;
+}
+
+.palette-card-note-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ margin-top: 3px;
+}
+
+.palette-card-note-text {
+ font-family: var(--serif);
+ font-size: 13px;
+ font-style: italic;
+ color: #4a4540;
+ line-height: 1.5;
+}
+
+.palette-card-footer {
+ padding: 14px 40px;
+ background: var(--bg-light);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-top: 1px solid rgba(0,0,0,0.08);
+}
+
+.palette-card-footer-brand {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .18em;
+ text-transform: uppercase;
+ color: #888;
+}
+.palette-card-footer-url {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .08em;
+ color: var(--accent-dim);
+}
+
+/* Close modal btn */
+.modal-close {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ background: rgba(0,0,0,0.5);
+ border: 1px solid rgba(255,255,255,0.1);
+ color: rgba(255,255,255,0.7);
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 200ms var(--ease);
+ z-index: 10;
+}
+.modal-close:hover {
+ background: rgba(0,0,0,0.8);
+ color: #fff;
+}
+
+/* Screenshot hint */
+.screenshot-hint {
+ text-align: center;
+ margin-top: 16px;
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ color: rgba(255,255,255,0.35);
+}
+
+/* ============================================================
+ TOAST NOTIFICATION
+ ============================================================ */
+.toast {
+ position: fixed;
+ bottom: 40px;
+ left: 50%;
+ transform: translateX(-50%) translateY(20px);
+ background: var(--bg-raised);
+ border: 1px solid var(--line-strong);
+ color: var(--ink-inv);
+ padding: 14px 28px;
+ border-radius: 40px;
+ font-family: var(--sans);
+ font-size: 12px;
+ letter-spacing: .1em;
+ text-transform: uppercase;
+ box-shadow: var(--shadow);
+ opacity: 0;
+ pointer-events: none;
+ transition: all 300ms var(--ease);
+ z-index: 200;
+ white-space: nowrap;
+}
+.toast.show {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+}
+
+/* ============================================================
+ FOOTER
+ ============================================================ */
+footer {
+ border-top: 1px solid var(--line);
+ padding: 40px 48px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 20px;
+ position: relative;
+ z-index: 1;
+}
+
+.footer-brand {
+ font-family: var(--serif);
+ font-size: 18px;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ color: var(--ink-inv);
+ font-weight: 300;
+}
+
+.footer-note {
+ font-family: var(--sans);
+ font-size: 10px;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ color: var(--ink-faint);
+}
+
+/* ============================================================
+ ACTIVE SWATCH DETAIL PANEL
+ ============================================================ */
+.swatch-detail-panel {
+ border: 1px solid var(--line);
+ border-radius: var(--radius-lg);
+ background: var(--bg-raised);
+ overflow: hidden;
+ margin-top: 2px;
+ max-height: 0;
+ opacity: 0;
+ transition: max-height 360ms var(--ease), opacity 300ms var(--ease);
+}
+.swatch-detail-panel.open {
+ max-height: 200px;
+ opacity: 1;
+}
+
+.swatch-detail-inner {
+ display: flex;
+ align-items: center;
+ gap: 32px;
+ padding: 28px 32px;
+}
+
+.swatch-detail-bar {
+ width: 6px;
+ height: 80px;
+ border-radius: 3px;
+ flex-shrink: 0;
+}
+
+.swatch-detail-content {}
+
+.swatch-detail-label {
+ font-family: var(--sans);
+ font-size: 9px;
+ letter-spacing: .22em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 6px;
+}
+
+.swatch-detail-name {
+ font-family: var(--serif);
+ font-size: 26px;
+ font-style: italic;
+ color: var(--ink-inv);
+ margin-bottom: 6px;
+ line-height: 1.1;
+}
+
+.swatch-detail-note {
+ font-family: var(--sans);
+ font-size: 13px;
+ color: var(--ink-dim);
+ line-height: 1.6;
+}
+
+/* ============================================================
+ UTILITY
+ ============================================================ */
+.sr-only {
+ position: absolute; width: 1px; height: 1px;
+ padding: 0; margin: -1px; overflow: hidden;
+ clip: rect(0,0,0,0); border: 0;
+}
+
+.section-divider {
+ height: 1px;
+ background: linear-gradient(90deg, transparent, var(--line), transparent);
+ margin: 64px 0;
+}
+
+/* Scroll reveal for sections */
+.reveal {
+ opacity: 0;
+ transform: translateY(24px);
+ transition: opacity 500ms var(--ease), transform 500ms var(--ease);
+}
+.reveal.in-view {
+ opacity: 1;
+ transform: translateY(0);
+}
+</style>
+</head>
+<body>
+
+<!-- ============================================================
+ HEADER
+ ============================================================ -->
+<header role="banner">
+ <div>
+ <div class="wordmark" aria-label="Designer Wallcoverings">
+ Designer Wallcoverings
+ <span class="accent-rule" aria-hidden="true"></span>
+ </div>
+ </div>
+ <div class="header-tag" aria-hidden="true">Seasonal Color Story Studio</div>
+</header>
+
+<!-- ============================================================
+ MAIN CONTENT
+ ============================================================ -->
+<main role="main">
+
+ <!-- HERO BAND -->
+ <section class="hero-band" aria-hidden="false">
+ <div class="hero-inner">
+ <p class="hero-eyebrow">A Tool for the DW Creative Team</p>
+ <h1 class="hero-headline">Seasonal Color<br>Story Studio</h1>
+ <p class="hero-body">Generate, preview, and export a curated wallcovering palette — built for your next Instagram campaign or client mood board. Four seasons. Five swatches each. One complete brand story every time.</p>
+ <div class="hero-divider" aria-hidden="true"></div>
+ </div>
+ <div class="hero-accent-bar" aria-hidden="true">
+ <div class="hero-color-stripe" id="hero-color-stripe">
+ <!-- Filled dynamically with current palette colors -->
+ </div>
+ </div>
+ </section>
+
+ <!-- SEASON SELECTOR -->
+ <section class="season-section reveal" aria-labelledby="season-heading">
+ <p class="section-label" aria-hidden="true">01 — Choose Your Story</p>
+ <h2 class="section-title" id="season-heading">Which season speaks to your client?</h2>
+ <p class="section-sub">Select a season to generate a curated five-swatch color story with wallcovering material notes and a ready-to-post Instagram carousel.</p>
+
+ <div class="season-tabs" role="tablist" aria-label="Season selection">
+ <button class="season-tab" role="tab" aria-selected="false" data-season="spring" aria-controls="palette-panel">Spring</button>
+ <button class="season-tab" role="tab" aria-selected="false" data-season="summer" aria-controls="palette-panel">Summer</button>
+ <button class="season-tab active" role="tab" aria-selected="true" data-season="autumn" aria-controls="palette-panel">Autumn</button>
+ <button class="season-tab" role="tab" aria-selected="false" data-season="winter" aria-controls="palette-panel">Winter</button>
+ </div>
+ </section>
+
+ <!-- PALETTE DISPLAY -->
+ <section class="palette-section reveal" id="palette-panel" aria-labelledby="palette-heading">
+ <p class="section-label" aria-hidden="true">02 — Color Story</p>
+
+ <div class="palette-header">
+ <h2 class="section-title" id="palette-heading">
+ <span id="palette-season-name">Autumn</span> Color Story
+ </h2>
+ <p class="season-tagline" id="palette-tagline"></p>
+ </div>
+
+ <div class="swatches-grid" id="swatches-grid" role="list" aria-label="Color palette swatches"></div>
+
+ <!-- Expanded detail panel -->
+ <div class="swatch-detail-panel" id="swatch-detail" aria-live="polite">
+ <div class="swatch-detail-inner">
+ <div class="swatch-detail-bar" id="detail-bar"></div>
+ <div class="swatch-detail-content">
+ <div class="swatch-detail-label" id="detail-texture-label">Texture</div>
+ <div class="swatch-detail-name" id="detail-name"></div>
+ <div class="swatch-detail-note" id="detail-note"></div>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <div class="section-divider" aria-hidden="true"></div>
+
+ <!-- CAROUSEL PREVIEW -->
+ <section class="carousel-section reveal" aria-labelledby="carousel-heading">
+ <p class="section-label" aria-hidden="true">03 — Instagram Preview</p>
+ <h2 class="section-title" id="carousel-heading">How it looks in the feed</h2>
+ <p class="section-sub">Five slides ready for your next carousel post. Use ← → arrows or swipe to preview each frame.</p>
+
+ <div class="carousel-outer">
+ <!-- Phone frame -->
+ <div class="phone-frame" role="presentation">
+ <div class="phone-notch" aria-hidden="true"></div>
+ <div class="phone-screen">
+ <!-- IG chrome top -->
+ <div class="ig-chrome-top" aria-hidden="true">
+ <div class="ig-avatar"></div>
+ <span class="ig-handle">designerwallcoverings</span>
+ <span class="ig-more">···</span>
+ </div>
+
+ <!-- Carousel -->
+ <div class="carousel-viewport"
+ role="region"
+ aria-label="Instagram carousel preview"
+ aria-roledescription="carousel"
+ id="carousel-viewport">
+ <div class="carousel-track" id="carousel-track"></div>
+ </div>
+
+ <!-- IG chrome bottom -->
+ <div class="ig-chrome-bottom" aria-hidden="true">
+ <span class="ig-icon">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
+ </svg>
+ </span>
+ <span class="ig-icon">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
+ </svg>
+ </span>
+ <span class="ig-icon">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+ <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
+ </svg>
+ </span>
+ <span class="ig-icon ig-save-icon">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+ <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
+ </svg>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Controls below phone -->
+ <div class="carousel-dots" id="carousel-dots" role="tablist" aria-label="Carousel slide navigation"></div>
+
+ <div class="carousel-nav" aria-label="Carousel navigation">
+ <button class="carousel-arrow" id="prev-btn" aria-label="Previous slide" disabled>←</button>
+ <button class="carousel-arrow" id="next-btn" aria-label="Next slide">→</button>
+ </div>
+
+ <div class="slide-counter" id="slide-counter" aria-live="polite" aria-atomic="true"></div>
+ </div>
+ </section>
+
+ <div class="section-divider" aria-hidden="true"></div>
+
+ <!-- CAPTION COPY -->
+ <section class="caption-section reveal" aria-labelledby="caption-heading">
+ <p class="section-label" aria-hidden="true">04 — Caption + Hashtags</p>
+ <h2 class="section-title" id="caption-heading">Ready-to-post copy</h2>
+ <p class="section-sub">Tap to copy the full caption and hashtag block directly to your clipboard.</p>
+
+ <div class="caption-box">
+ <div class="caption-header">
+ <span class="caption-title">Instagram Caption</span>
+ <button class="copy-btn" id="copy-caption-btn" aria-label="Copy caption to clipboard">
+ Copy Caption
+ </button>
+ </div>
+ <div class="caption-body">
+ <div class="caption-text" id="caption-text"></div>
+ <div class="hashtag-block" id="hashtag-block"></div>
+ </div>
+ </div>
+ </section>
+
+ <div class="section-divider" aria-hidden="true"></div>
+
+ <!-- EXPORT -->
+ <section class="export-section reveal" aria-labelledby="export-heading">
+ <p class="section-label" aria-hidden="true">05 — Export</p>
+ <h2 class="section-title" id="export-heading">Palette Story Card</h2>
+ <p class="section-sub">Preview the full palette card — screenshot it for presentations, mood boards, or client PDFs.</p>
+
+ <button class="export-btn" id="export-btn" aria-haspopup="dialog" aria-expanded="false">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
+ <circle cx="8.5" cy="8.5" r="1.5"/>
+ <polyline points="21 15 16 10 5 21"/>
+ </svg>
+ View Palette Card
+ </button>
+ </section>
+
+</main>
+
+<!-- ============================================================
+ FOOTER
+ ============================================================ -->
+<footer role="contentinfo">
+ <div class="footer-brand">Designer Wallcoverings</div>
+ <div class="footer-note">Color Story Studio · designerwallcoverings.com</div>
+</footer>
+
+<!-- ============================================================
+ MODAL — PALETTE CARD
+ ============================================================ -->
+<div class="modal-overlay" id="palette-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
+ <div class="modal-inner">
+ <button class="modal-close" id="modal-close-btn" aria-label="Close palette card">×</button>
+ <div class="palette-card" id="palette-card-content">
+ <!-- Rendered by JS -->
+ </div>
+ <p class="screenshot-hint" aria-hidden="true">Screenshot this card to save or share</p>
+ </div>
+</div>
+
+<!-- ============================================================
+ TOAST
+ ============================================================ -->
+<div class="toast" id="toast" role="status" aria-live="polite" aria-atomic="true"></div>
+
+<!-- ============================================================
+ JAVASCRIPT
+ ============================================================ -->
+<script>
+'use strict';
+
+/* ============================================================
+ DATA MODEL
+ ============================================================ */
+const SEASONS = {
+ spring: {
+ name: 'Spring',
+ tagline: 'Where the garden breathes into the interior. Fresh, grounded, quietly alive.',
+ palette: [
+ {
+ name: 'Blush Oyster',
+ hex: '#e8d5c8',
+ texture: 'linen',
+ note: 'The softest possible opening note — a boudoir wall made of dawn light.'
+ },
+ {
+ name: 'Garden Sage',
+ hex: '#8aa87d',
+ texture: 'grasscloth',
+ note: 'Fresh, grounded, botanically charged without the cliché of botanical print.'
+ },
+ {
+ name: 'Pale Aubergine',
+ hex: '#8c7a8e',
+ texture: 'damask',
+ note: 'A whisper of opulence — best used in a dressing room or reading nook.'
+ },
+ {
+ name: 'Greige Flannel',
+ hex: '#b0a696',
+ texture: 'raffia',
+ note: 'The unsung hero of every layered scheme. Holds everything else in place.'
+ },
+ {
+ name: 'Leaf Vein Green',
+ hex: '#4a5e42',
+ texture: 'silk',
+ note: 'Close the palette with forest authority. Deep and still and permanent.'
+ }
+ ],
+ caption: `Spring is not a season — it is a directive.\n\nThe wallcovering palette we've curated this season speaks in hushed tones: a blush that holds light, a sage that grounds without heaviness, an aubergine that whispers rather than declares.\n\nFive surfaces. One story. A room that feels as if it exhaled.\n\n→ Explore the Spring Collection at the link in bio.`,
+ hashtags: '#DesignerWallcoverings #SpringPalette #WallcoveringDesign #InteriorDesign #ColorStory #LuxuryInteriors #WallpaperDesign #SpringDecor #InteriorStyling #TextureAndColor #GrassclothWall #WallcoveringInspiration #DesignInspiration #SpringInteriors'
+ },
+
+ summer: {
+ name: 'Summer',
+ tagline: 'Salt air and long evenings. Surfaces that breathe and walls that hold the light.',
+ palette: [
+ {
+ name: 'Raw Linen',
+ hex: '#d6c9af',
+ texture: 'linen',
+ note: 'Effortless coastal ease — walls that breathe before you even open the window.'
+ },
+ {
+ name: 'Sea Glass',
+ hex: '#7fbdae',
+ texture: 'grasscloth',
+ note: 'Pulls in ocean light without pastiche. Genuinely hard to tire of.'
+ },
+ {
+ name: 'Driftwood',
+ hex: '#9c8870',
+ texture: 'raffia',
+ note: 'Warm natural grain for relaxed entertaining rooms and summer dining.'
+ },
+ {
+ name: 'Antique White',
+ hex: '#ede8df',
+ texture: 'damask',
+ note: 'An airy foil that lets every other element in the room sing its own note.'
+ },
+ {
+ name: 'Deep Indigo',
+ hex: '#2d3a5a',
+ texture: 'silk',
+ note: 'A night-sky accent of true sophistication. The punctuation mark of the palette.'
+ }
+ ],
+ caption: `Summer asks very little of a wall — only that it hold the light well.\n\nOur Summer palette is built on that premise: linen-textured neutrals that catch afternoon sun, a sea glass that turns any room into a long exhale, and a deep indigo that makes the whole collection feel considered rather than casual.\n\nFive wallcoverings. One summer.\n\n→ Shop the Summer Collection at the link in bio.`,
+ hashtags: '#DesignerWallcoverings #SummerInteriors #CoastalDesign #WallcoveringDesign #ColorStory #LuxuryInteriors #SummerPalette #InteriorDesign #WallpaperInspiration #TexturedWalls #GrassclothWallcovering #InteriorStyling #DesignInspiration #SummerHome'
+ },
+
+ autumn: {
+ name: 'Autumn',
+ tagline: 'The season that asks walls to do more — to hold warmth, to anchor a room against the cooling light.',
+ palette: [
+ {
+ name: 'Burnished Terracotta',
+ hex: '#b5614a',
+ texture: 'damask',
+ note: 'Anchor a feature wall in a study or library. Rich without shouting.'
+ },
+ {
+ name: 'Smoked Sage',
+ hex: '#6e7d5e',
+ texture: 'linen',
+ note: 'Balance warmth with botanical calm. The palette\'s essential breath.'
+ },
+ {
+ name: 'Tarnished Brass',
+ hex: '#a87f3c',
+ texture: 'silk',
+ note: 'Layer with aged leather and walnut — a material conversation worth having.'
+ },
+ {
+ name: 'Oatmeal Grasscloth',
+ hex: '#c9b89a',
+ texture: 'grasscloth',
+ note: 'Ground the palette in natural texture. The surface that makes others look richer.'
+ },
+ {
+ name: 'Ink Charcoal',
+ hex: '#2e2a25',
+ texture: 'raffia',
+ note: 'A deeply moody base for art-wall drama. Dark rooms done properly.'
+ }
+ ],
+ caption: `Autumn is the story we've been composing all year.\n\nFive wallcovering hues — each hand-selected for texture, depth, and the particular way they hold declining light — arrive now. Burnished terracotta. Smoked sage. Tarnished brass. Oatmeal grasscloth. Ink charcoal.\n\nWhich speaks to your current project?\n\n→ Explore the Autumn Color Story at the link in bio.`,
+ hashtags: '#DesignerWallcoverings #AutumnPalette #FallInteriors #WallcoveringDesign #ColorStory #LuxuryInteriors #TerracottaWall #InteriorDesign #TexturedWallcovering #GrassclothWall #WallpaperDesign #AutumnDecor #InteriorStyling #DesignInspiration'
+ },
+
+ winter: {
+ name: 'Winter',
+ tagline: 'Quiet power. The season of contrasts: soft and severe, pale and profound.',
+ palette: [
+ {
+ name: 'Alabaster',
+ hex: '#e8e0d2',
+ texture: 'damask',
+ note: 'Float porcelain objets against serene quiet. The room holds its breath.'
+ },
+ {
+ name: 'Ink Navy',
+ hex: '#1a2235',
+ texture: 'silk',
+ note: 'A commanding depth for a library or salon. Rooms of genuine consequence.'
+ },
+ {
+ name: 'Celadon Mist',
+ hex: '#afc5be',
+ texture: 'grasscloth',
+ note: 'Ethereal, spa-like, endlessly calming. The palette\'s moment of grace.'
+ },
+ {
+ name: 'Champagne',
+ hex: '#d4b87a',
+ texture: 'linen',
+ note: 'Warm up cool marble with a gilded haze — the light it makes is extraordinary.'
+ },
+ {
+ name: 'Carbon Matte',
+ hex: '#2c2c2e',
+ texture: 'raffia',
+ note: 'Understated power. A backdrop for metallics that makes everything else gleam.'
+ }
+ ],
+ caption: `Winter reduces everything to what matters.\n\nThis season's wallcovering palette follows that logic: five surfaces chosen for depth, for quiet, for the way a room settles when the walls are doing their job properly. Alabaster. Navy. Celadon. Champagne. Carbon.\n\nA room designed in winter is a room designed for keeps.\n\n→ Shop the Winter Collection at the link in bio.`,
+ hashtags: '#DesignerWallcoverings #WinterInteriors #WinterPalette #WallcoveringDesign #ColorStory #LuxuryInteriors #NavyWall #InteriorDesign #WinterDecor #TexturedWalls #WallpaperDesign #AlabasterInterior #InteriorStyling #DesignInspiration'
+ }
+};
+
+/* ============================================================
+ STATE
+ ============================================================ */
+let currentSeason = 'autumn';
+let currentSlide = 0;
+let totalSlides = 5;
+let activeSwatch = null;
+let touchStartX = 0;
+
+/* ============================================================
+ TEXTURE CLASS HELPER
+ ============================================================ */
+function textureClass(t) {
+ return {
+ grasscloth: 'texture-grasscloth',
+ damask: 'texture-damask',
+ linen: 'texture-linen',
+ silk: 'texture-silk',
+ paperweave: 'texture-paperweave',
+ raffia: 'texture-raffia'
+ }[t] || 'texture-linen';
+}
+
+function textureLabel(t) {
+ return {
+ grasscloth: 'Natural Grasscloth',
+ damask: 'Jacquard Damask',
+ linen: 'European Linen',
+ silk: 'Silk Stripe',
+ paperweave: 'Japanese Paperweave',
+ raffia: 'Raffia Weave'
+ }[t] || 'Woven Textile';
+}
+
+/* ============================================================
+ RENDER SWATCHES
+ ============================================================ */
+function renderSwatches(seasonKey) {
+ const season = SEASONS[seasonKey];
+ const grid = document.getElementById('swatches-grid');
+ grid.innerHTML = '';
+ activeSwatch = null;
+ closeDetailPanel();
+
+ season.palette.forEach((swatch, i) => {
+ const card = document.createElement('div');
+ card.className = 'swatch-card';
+ card.setAttribute('role', 'listitem');
+ card.setAttribute('tabindex', '0');
+ card.setAttribute('aria-label', `${swatch.name}: ${swatch.note}`);
+ card.dataset.index = i;
+
+ card.innerHTML = `
+ <div class="swatch-face ${textureClass(swatch.texture)}" style="--swatch-color: ${swatch.hex};" aria-hidden="true">
+ </div>
+ <div class="swatch-info">
+ <span class="swatch-name">${swatch.name}</span>
+ <p class="swatch-note">${swatch.note}</p>
+ <div class="swatch-hex">
+ <div class="swatch-hex-chip" style="background:${swatch.hex};" aria-hidden="true"></div>
+ ${swatch.hex}
+ </div>
+ </div>
+ `;
+
+ card.addEventListener('click', () => toggleSwatchDetail(i, swatch, card));
+ card.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ toggleSwatchDetail(i, swatch, card);
+ }
+ });
+
+ grid.appendChild(card);
+
+ // Staggered reveal
+ setTimeout(() => {
+ card.classList.add('revealed');
+ }, 60 + i * 90);
+ });
+}
+
+/* ============================================================
+ SWATCH DETAIL PANEL
+ ============================================================ */
+function toggleSwatchDetail(idx, swatch, card) {
+ const panel = document.getElementById('swatch-detail');
+ const allCards = document.querySelectorAll('.swatch-card');
+
+ if (activeSwatch === idx) {
+ // Close
+ activeSwatch = null;
+ card.classList.remove('active-swatch');
+ closeDetailPanel();
+ return;
+ }
+
+ // Remove active from all
+ allCards.forEach(c => c.classList.remove('active-swatch'));
+ card.classList.add('active-swatch');
+ activeSwatch = idx;
+
+ document.getElementById('detail-bar').style.background = swatch.hex;
+ document.getElementById('detail-texture-label').textContent = textureLabel(swatch.texture);
+ document.getElementById('detail-name').textContent = swatch.name;
+ document.getElementById('detail-note').textContent = swatch.note;
+
+ panel.classList.add('open');
+}
+
+function closeDetailPanel() {
+ document.getElementById('swatch-detail').classList.remove('open');
+}
+
+/* ============================================================
+ CAROUSEL RENDERING
+ ============================================================ */
+function renderCarousel(seasonKey) {
+ const season = SEASONS[seasonKey];
+ const track = document.getElementById('carousel-track');
+ const dots = document.getElementById('carousel-dots');
+ track.innerHTML = '';
+ dots.innerHTML = '';
+ currentSlide = 0;
+ totalSlides = 5;
+
+ const p = season.palette;
+
+ /* --- Slide 0: Cover --- */
+ const coverSlide = document.createElement('div');
+ coverSlide.className = 'carousel-slide slide-cover';
+ // Derive cover gradient from palette colors 1 and 3
+ coverSlide.style.background = `linear-gradient(135deg, ${p[0].hex}dd 0%, ${p[2].hex}bb 50%, ${p[4].hex}ee 100%)`;
+ coverSlide.setAttribute('aria-label', `Cover slide: ${season.name} Color Story`);
+ coverSlide.innerHTML = `
+ <div class="cover-texture" aria-hidden="true">
+ ${buildCoverTextureSVG(p)}
+ </div>
+ <div class="slide-cover-inner">
+ <div class="cover-year">2026 Collection</div>
+ <div class="cover-season">${season.name}</div>
+ <div class="cover-sub">${season.tagline.split('.')[0]}.</div>
+ <div class="cover-rule"></div>
+ <div class="cover-brand">Designer Wallcoverings</div>
+ </div>
+ `;
+ track.appendChild(coverSlide);
+
+ /* --- Slide 1: Colors 1 + 2 --- */
+ const duo1 = buildDuoSlide(p[0], p[1], '01', '02');
+ duo1.setAttribute('aria-label', `Slide 2: ${p[0].name} and ${p[1].name}`);
+ track.appendChild(duo1);
+
+ /* --- Slide 2: Colors 3 + 4 --- */
+ const duo2 = buildDuoSlide(p[2], p[3], '03', '04');
+ duo2.setAttribute('aria-label', `Slide 3: ${p[2].name} and ${p[3].name}`);
+ track.appendChild(duo2);
+
+ /* --- Slide 3: Color 5 feature --- */
+ const featureSlide = document.createElement('div');
+ featureSlide.className = 'carousel-slide slide-feature';
+ featureSlide.style.background = p[4].hex;
+ // Apply texture to feature slide
+ applyTextureStyle(featureSlide, p[4].texture);
+ featureSlide.setAttribute('aria-label', `Slide 4: Feature — ${p[4].name}`);
+ // Determine text color based on brightness
+ const textColor = isLight(p[4].hex) ? 'rgba(0,0,0,0.85)' : '#fff';
+ featureSlide.innerHTML = `
+ <div class="feature-content">
+ <div class="feature-num" style="color:rgba(201,169,97,0.9)">No. 05 — The Signature Hue</div>
+ <div class="feature-name" style="color:${textColor}">${p[4].name}</div>
+ <div class="feature-rule"></div>
+ <div class="feature-note" style="color:${textColor === '#fff' ? 'rgba(255,255,255,0.75)' : 'rgba(0,0,0,0.65)'}">${p[4].note}</div>
+ </div>
+ `;
+ track.appendChild(featureSlide);
+
+ /* --- Slide 4: CTA --- */
+ const ctaSlide = document.createElement('div');
+ ctaSlide.className = 'carousel-slide slide-cta';
+ ctaSlide.setAttribute('aria-label', 'Slide 5: Call to action — explore the full collection');
+ ctaSlide.innerHTML = `
+ <div class="cta-rule-top"></div>
+ <div class="cta-eyebrow">${season.name} 2026</div>
+ <div class="cta-headline">Explore the full<br>${season.name} Collection</div>
+ <div class="cta-handle">@designerwallcoverings</div>
+ <div class="cta-pill">Link in bio</div>
+ `;
+ track.appendChild(ctaSlide);
+
+ /* --- Dots --- */
+ const slideLabels = ['Cover', 'Colors 1–2', 'Colors 3–4', 'Feature Color', 'CTA'];
+ for (let i = 0; i < totalSlides; i++) {
+ const dot = document.createElement('button');
+ dot.className = 'c-dot' + (i === 0 ? ' active' : '');
+ dot.setAttribute('role', 'tab');
+ dot.setAttribute('aria-label', `Go to slide: ${slideLabels[i]}`);
+ dot.setAttribute('aria-selected', i === 0 ? 'true' : 'false');
+ dot.dataset.slide = i;
+ dot.addEventListener('click', () => goToSlide(i));
+ dots.appendChild(dot);
+ }
+
+ updateCarouselUI();
+
+ /* Touch / swipe */
+ const viewport = document.getElementById('carousel-viewport');
+ viewport.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; }, { passive: true });
+ viewport.addEventListener('touchend', e => {
+ const dx = e.changedTouches[0].clientX - touchStartX;
+ if (Math.abs(dx) > 40) dx < 0 ? nextSlide() : prevSlide();
+ }, { passive: true });
+}
+
+function buildCoverTextureSVG(palette) {
+ // Subtle woven lines across cover using SVG
+ return `<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" style="position:absolute;inset:0;opacity:0.15">
+ <defs>
+ <pattern id="coverPat" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
+ <line x1="0" y1="0" x2="40" y2="40" stroke="rgba(255,255,255,0.4)" stroke-width="0.5"/>
+ <line x1="40" y1="0" x2="0" y2="40" stroke="rgba(255,255,255,0.2)" stroke-width="0.5"/>
+ </pattern>
+ </defs>
+ <rect width="100%" height="100%" fill="url(#coverPat)"/>
+ </svg>`;
+}
+
+function buildDuoSlide(sw1, sw2, num1, num2) {
+ const slide = document.createElement('div');
+ slide.className = 'carousel-slide slide-duo';
+
+ const half1 = document.createElement('div');
+ half1.className = `slide-duo-half ${textureClass(sw1.texture)}`;
+ half1.style.setProperty('--swatch-color', sw1.hex);
+ const tc1 = isLight(sw1.hex) ? 'rgba(0,0,0,0.85)' : '#fff';
+ half1.innerHTML = `
+ <div class="duo-color-name" style="color:${tc1}">${sw1.name}</div>
+ <div class="duo-color-note" style="color:${tc1 === '#fff' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.55)'}">${textureLabel(sw1.texture)}</div>
+ <div class="duo-divider"></div>
+ `;
+
+ const half2 = document.createElement('div');
+ half2.className = `slide-duo-half ${textureClass(sw2.texture)}`;
+ half2.style.setProperty('--swatch-color', sw2.hex);
+ const tc2 = isLight(sw2.hex) ? 'rgba(0,0,0,0.85)' : '#fff';
+ half2.innerHTML = `
+ <div class="duo-color-name" style="color:${tc2}">${sw2.name}</div>
+ <div class="duo-color-note" style="color:${tc2 === '#fff' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.55)'}">${textureLabel(sw2.texture)}</div>
+ `;
+
+ slide.appendChild(half1);
+ slide.appendChild(half2);
+ return slide;
+}
+
+function applyTextureStyle(el, texture) {
+ el.classList.add(textureClass(texture));
+ el.style.setProperty('--swatch-color', '');
+ // The base bg color is already set on the element
+}
+
+function isLight(hex) {
+ const r = parseInt(hex.slice(1,3),16);
+ const g = parseInt(hex.slice(3,5),16);
+ const b = parseInt(hex.slice(5,7),16);
+ return (r * 0.299 + g * 0.587 + b * 0.114) > 158;
+}
+
+/* ============================================================
+ CAROUSEL NAVIGATION
+ ============================================================ */
+function goToSlide(idx) {
+ currentSlide = Math.max(0, Math.min(idx, totalSlides - 1));
+ updateCarouselUI();
+}
+
+function prevSlide() { if (currentSlide > 0) goToSlide(currentSlide - 1); }
+function nextSlide() { if (currentSlide < totalSlides - 1) goToSlide(currentSlide + 1); }
+
+function updateCarouselUI() {
+ const track = document.getElementById('carousel-track');
+ track.style.transform = `translateX(-${currentSlide * 100}%)`;
+
+ const dots = document.querySelectorAll('.c-dot');
+ dots.forEach((d, i) => {
+ d.classList.toggle('active', i === currentSlide);
+ d.setAttribute('aria-selected', i === currentSlide ? 'true' : 'false');
+ });
+
+ document.getElementById('prev-btn').disabled = currentSlide === 0;
+ document.getElementById('next-btn').disabled = currentSlide === totalSlides - 1;
+
+ const labels = ['Cover', 'Colors 1 & 2', 'Colors 3 & 4', 'Signature Color', 'Call to Action'];
+ document.getElementById('slide-counter').textContent = `${labels[currentSlide]} · ${currentSlide + 1} / ${totalSlides}`;
+}
+
+/* ============================================================
+ CAPTION RENDER
+ ============================================================ */
+function renderCaption(seasonKey) {
+ const season = SEASONS[seasonKey];
+ document.getElementById('caption-text').textContent = season.caption;
+ document.getElementById('hashtag-block').textContent = season.hashtags;
+}
+
+/* ============================================================
+ PALETTE CARD MODAL
+ ============================================================ */
+function renderPaletteCard(seasonKey) {
+ const season = SEASONS[seasonKey];
+ const p = season.palette;
+ const card = document.getElementById('palette-card-content');
+
+ const swatchesHTML = p.map(s => `
+ <div class="palette-card-swatch ${textureClass(s.texture)}" style="--swatch-color:${s.hex}">
+ <div class="palette-card-swatch-label">
+ <span class="palette-card-swatch-name">${s.name}</span>
+ </div>
+ </div>
+ `).join('');
+
+ const namesHTML = p.map(s => `
+ <div class="palette-card-name-item">
+ <span class="palette-card-hex">${s.hex}</span>
+ <span class="palette-card-cn">${s.name}</span>
+ </div>
+ `).join('');
+
+ const notesHTML = p.map(s => `
+ <div class="palette-card-note-row">
+ <div class="palette-card-note-dot" style="background:${s.hex};flex-shrink:0;width:8px;height:8px;border-radius:50%;margin-top:5px;"></div>
+ <div class="palette-card-note-text">${s.note}</div>
+ </div>
+ `).join('');
+
+ card.innerHTML = `
+ <div class="palette-card-header">
+ <div class="palette-card-brand">Designer Wallcoverings</div>
+ <div class="palette-card-rule"></div>
+ <div class="palette-card-season-name">${season.name} 2026 · Color Story</div>
+ </div>
+ <div class="palette-card-swatches">${swatchesHTML}</div>
+ <div class="palette-card-names">${namesHTML}</div>
+ <div class="palette-card-notes">
+ <div class="palette-card-notes-title">Styling Notes</div>
+ ${notesHTML}
+ </div>
+ <div class="palette-card-footer">
+ <div class="palette-card-footer-brand">Designer Wallcoverings · Color Story Studio</div>
+ <div class="palette-card-footer-url">designerwallcoverings.com</div>
+ </div>
+ `;
+}
+
+function openModal() {
+ renderPaletteCard(currentSeason);
+ const modal = document.getElementById('palette-modal');
+ modal.classList.add('open');
+ modal.setAttribute('aria-hidden', 'false');
+ document.getElementById('export-btn').setAttribute('aria-expanded', 'true');
+ document.body.style.overflow = 'hidden';
+ // Focus close button
+ setTimeout(() => document.getElementById('modal-close-btn').focus(), 50);
+}
+
+function closeModal() {
+ const modal = document.getElementById('palette-modal');
+ modal.classList.remove('open');
+ modal.setAttribute('aria-hidden', 'true');
+ document.getElementById('export-btn').setAttribute('aria-expanded', 'false');
+ document.body.style.overflow = '';
+ document.getElementById('export-btn').focus();
+}
+
+/* ============================================================
+ TOAST
+ ============================================================ */
+function showToast(msg) {
+ const toast = document.getElementById('toast');
+ toast.textContent = msg;
+ toast.classList.add('show');
+ setTimeout(() => toast.classList.remove('show'), 2800);
+}
+
+/* ============================================================
+ COPY TO CLIPBOARD
+ ============================================================ */
+function copyCaption() {
+ const season = SEASONS[currentSeason];
+ const full = season.caption + '\n\n' + season.hashtags;
+ navigator.clipboard.writeText(full).then(() => {
+ const btn = document.getElementById('copy-caption-btn');
+ btn.textContent = 'Copied!';
+ btn.classList.add('copied');
+ showToast('Caption copied to clipboard');
+ setTimeout(() => {
+ btn.textContent = 'Copy Caption';
+ btn.classList.remove('copied');
+ }, 2500);
+ }).catch(() => {
+ showToast('Please select and copy the text manually');
+ });
+}
+
+/* ============================================================
+ HERO COLOR STRIPE
+ ============================================================ */
+function updateHeroStripe(seasonKey) {
+ const palette = SEASONS[seasonKey].palette;
+ const stripe = document.getElementById('hero-color-stripe');
+ stripe.innerHTML = '';
+ palette.forEach(s => {
+ const seg = document.createElement('div');
+ seg.className = 'hero-stripe-seg';
+ seg.style.background = s.hex;
+ seg.title = s.name;
+ stripe.appendChild(seg);
+ });
+}
+
+/* ============================================================
+ SEASON CHANGE
+ ============================================================ */
+function changeSeason(key) {
+ if (key === currentSeason) return;
+ currentSeason = key;
+
+ // Update tabs
+ document.querySelectorAll('.season-tab').forEach(tab => {
+ const isActive = tab.dataset.season === key;
+ tab.classList.toggle('active', isActive);
+ tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
+ });
+
+ const season = SEASONS[key];
+ document.getElementById('palette-season-name').textContent = season.name;
+ document.getElementById('palette-tagline').textContent = season.tagline;
+
+ renderSwatches(key);
+ renderCarousel(key);
+ renderCaption(key);
+ updateHeroStripe(key);
+}
+
+/* ============================================================
+ SCROLL REVEAL
+ ============================================================ */
+function initScrollReveal() {
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(e => {
+ if (e.isIntersecting) {
+ e.target.classList.add('in-view');
+ observer.unobserve(e.target);
+ }
+ });
+ }, { threshold: 0.08 });
+
+ document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
+}
+
+/* ============================================================
+ KEYBOARD NAVIGATION
+ ============================================================ */
+document.addEventListener('keydown', e => {
+ // Carousel nav
+ if (document.activeElement.closest('#carousel-viewport') ||
+ document.activeElement.id === 'prev-btn' ||
+ document.activeElement.id === 'next-btn') {
+ if (e.key === 'ArrowLeft') { e.preventDefault(); prevSlide(); }
+ if (e.key === 'ArrowRight') { e.preventDefault(); nextSlide(); }
+ }
+ // Global carousel shortcut (when not in form element)
+ const tag = document.activeElement.tagName;
+ if (tag !== 'INPUT' && tag !== 'TEXTAREA' && tag !== 'SELECT') {
+ if (e.key === 'ArrowLeft') prevSlide();
+ if (e.key === 'ArrowRight') nextSlide();
+ }
+ // Close modal on Escape
+ if (e.key === 'Escape') closeModal();
+});
+
+/* ============================================================
+ INIT
+ ============================================================ */
+function init() {
+ // Season tabs
+ document.querySelectorAll('.season-tab').forEach(tab => {
+ tab.addEventListener('click', () => changeSeason(tab.dataset.season));
+ });
+
+ // Carousel arrows
+ document.getElementById('prev-btn').addEventListener('click', prevSlide);
+ document.getElementById('next-btn').addEventListener('click', nextSlide);
+
+ // Caption copy
+ document.getElementById('copy-caption-btn').addEventListener('click', copyCaption);
+
+ // Export modal
+ document.getElementById('export-btn').addEventListener('click', openModal);
+ document.getElementById('modal-close-btn').addEventListener('click', closeModal);
+ document.getElementById('palette-modal').addEventListener('click', e => {
+ if (e.target === e.currentTarget) closeModal();
+ });
+
+ // Initial render
+ const season = SEASONS[currentSeason];
+ document.getElementById('palette-season-name').textContent = season.name;
+ document.getElementById('palette-tagline').textContent = season.tagline;
+
+ renderSwatches(currentSeason);
+ renderCarousel(currentSeason);
+ renderCaption(currentSeason);
+ updateHeroStripe(currentSeason);
+ initScrollReveal();
+}
+
+document.addEventListener('DOMContentLoaded', init);
+</script>
+</body>
+</html>
← f631a81 Cost: drop paid models (kimi/gpt/grok) from loop roster per
·
back to Model Arena
·
Run on Claude Max subscription only: drop paid API models (k 682fd46 →