← back to Wallco Ai
feat(pdp): V4-V10 theme variants — full 10-variant Theme submenu
34281f4f0fea6a49a4231528e34a24385a5b1e8d · 2026-05-28 18:05:16 -0700 · Steve Abrams
Adds 7 standalone PDP layouts at /design/:id/{hero,vertical,drawer,editorial,
spec,config,stack}, each fetches /api/design/:id. Hamburger Theme submenu now
lists all 10 + Classic. Shared themeVariantSend() helper. No template-literal
surgery; all variants live in public/theme-variants/v{N}-*.html.
V4 hero — full-bleed top hero + narrow 640px buy column + 11 accordions
V5 vertical — 720px stack + 14 accordions + fixed floating buy bar
V6 drawer — minimal page + 12 chips that open right-side dark drawers
V7 editorial — magazine layout: 70vh hero w/ pull-quote, 2-col body + marginalia
V8 spec — wine-list 20-row spec table right, image left
V9 config — calculator UP TOP (4-step) + big-CTA, hero shrunk to 1/3
V10 stack — single 1080px column of standalone section cards
Files touched
A public/theme-variants/v10-stack.htmlA public/theme-variants/v4-hero.htmlA public/theme-variants/v5-vertical.htmlA public/theme-variants/v6-drawer.htmlA public/theme-variants/v7-editorial.htmlA public/theme-variants/v8-spec.htmlA public/theme-variants/v9-config.htmlM server.js
Diff
commit 34281f4f0fea6a49a4231528e34a24385a5b1e8d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 18:05:16 2026 -0700
feat(pdp): V4-V10 theme variants — full 10-variant Theme submenu
Adds 7 standalone PDP layouts at /design/:id/{hero,vertical,drawer,editorial,
spec,config,stack}, each fetches /api/design/:id. Hamburger Theme submenu now
lists all 10 + Classic. Shared themeVariantSend() helper. No template-literal
surgery; all variants live in public/theme-variants/v{N}-*.html.
V4 hero — full-bleed top hero + narrow 640px buy column + 11 accordions
V5 vertical — 720px stack + 14 accordions + fixed floating buy bar
V6 drawer — minimal page + 12 chips that open right-side dark drawers
V7 editorial — magazine layout: 70vh hero w/ pull-quote, 2-col body + marginalia
V8 spec — wine-list 20-row spec table right, image left
V9 config — calculator UP TOP (4-step) + big-CTA, hero shrunk to 1/3
V10 stack — single 1080px column of standalone section cards
---
public/theme-variants/v10-stack.html | 353 +++++++++++++++++++
public/theme-variants/v4-hero.html | 602 ++++++++++++++++++++++++++++++++
public/theme-variants/v5-vertical.html | 420 ++++++++++++++++++++++
public/theme-variants/v6-drawer.html | 402 +++++++++++++++++++++
public/theme-variants/v7-editorial.html | 353 +++++++++++++++++++
public/theme-variants/v8-spec.html | 342 ++++++++++++++++++
public/theme-variants/v9-config.html | 376 ++++++++++++++++++++
server.js | 43 +++
8 files changed, 2891 insertions(+)
diff --git a/public/theme-variants/v10-stack.html b/public/theme-variants/v10-stack.html
new file mode 100644
index 0000000..8a7f508
--- /dev/null
+++ b/public/theme-variants/v10-stack.html
@@ -0,0 +1,353 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Card-Stack PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3; --ink: #1a1611; --muted: #8a8276; --line: #e2dccd;
+ --panel: #f3efe5; --panel-2: #ebe5d6; --accent: #c9b07a; --accent-dk: #1a1611;
+ --hero-bg: #e6dfd2; --calc: #ddd4ec; --coord: #d3e3e5;
+ --share: #f3dcce; --room: #cfdac4; --rate: #f0e7c3;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+ --serif: "Cormorant Garamond", Georgia, serif;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; }
+ body { background: var(--bg); color: var(--ink); font: 14.5px/1.55 var(--sans); -webkit-font-smoothing: antialiased; }
+ a { color: inherit; } button { font: inherit; cursor: pointer; }
+
+ header.topbar {
+ position: sticky; top: 0; z-index: 30;
+ background: rgba(250,248,243,.92); backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--line); padding: 10px 28px;
+ display: flex; align-items: center; gap: 24px;
+ }
+ header .brand { font: 600 18px/1 var(--serif); letter-spacing: .05em; }
+ header nav { font-size: 12.5px; color: var(--muted); display: flex; gap: 18px; flex-wrap: wrap; }
+ header nav a { text-decoration: none; }
+ header .grow { flex: 1; }
+ header .switcher { font-size: 11px; color: var(--muted); }
+ header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
+
+ .crumb { max-width: 1080px; margin: 0 auto; padding: 18px 28px 0; display: flex; gap: 16px; font-size: 12px; color: var(--muted); }
+ .crumb .grow { flex: 1; }
+ .crumb a { color: var(--ink); text-decoration: none; }
+
+ main.stack {
+ max-width: 1080px; margin: 0 auto; padding: 20px 28px 60px;
+ display: flex; flex-direction: column; gap: 22px;
+ }
+
+ /* card primitive */
+ .card {
+ background: var(--bg); border: 1px solid var(--line);
+ border-radius: 14px; padding: 22px;
+ box-shadow: 0 1px 0 var(--line);
+ }
+ .card h3 { margin: 0 0 14px; font: 600 11px/1.2 var(--sans); text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
+ .card.tinted-cream { background: var(--panel); }
+ .card.tinted-room { background: var(--room); }
+ .card.tinted-coord { background: var(--coord); }
+ .card.tinted-calc { background: var(--calc); }
+ .card.tinted-share { background: var(--share); }
+ .card.dark { background: var(--accent-dk); color: #f0eada; border-color: transparent; }
+ .card.dark h3 { color: var(--accent); }
+
+ /* hero card */
+ .card.hero { padding: 0; overflow: hidden; aspect-ratio: 4/3; position: relative; background: var(--hero-bg); border: 1px solid var(--line); }
+ .card.hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .card.hero .pill { position: absolute; top: 16px; right: 16px; background: rgba(250,248,243,.94); color: var(--ink); font: 600 10.5px/1 var(--sans); letter-spacing: .06em; padding: 7px 12px; border-radius: 999px; text-transform: uppercase; }
+ .card.hero .info { position: absolute; left: 16px; bottom: 16px; background: rgba(26,22,17,.78); color: #f0eada; font: 600 11.5px/1 var(--sans); letter-spacing: .06em; padding: 9px 14px; border-radius: 6px; text-transform: uppercase; }
+
+ /* title card */
+ .card.title { padding: 22px 26px; }
+ .card.title h1 { font: 500 34px/1.1 var(--serif); margin: 0 0 8px; }
+ .card.title .meta { color: var(--muted); font-size: 13.5px; }
+ .card.title .meta .sep { padding: 0 6px; opacity: .55; }
+ .card.title .chips { margin-top: 16px; display: flex; flex-wrap: wrap; gap: 6px; }
+ .chip { display: inline-flex; align-items: center; gap: 6px; background: var(--panel-2); color: var(--ink); border-radius: 999px; padding: 5px 11px; font: 500 11.5px/1 var(--sans); letter-spacing: .02em; border: 1px solid var(--line); }
+ .chip .dot { width: 10px; height: 10px; border-radius: 999px; }
+
+ /* buy card */
+ .card.buy { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
+ .cta { display: flex; align-items: center; justify-content: center; border-radius: 10px; padding: 0 24px; font: 700 13px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase; height: 64px; border: 1px solid transparent; cursor: pointer; }
+ .cta.sample { background: var(--accent); color: var(--accent-dk); }
+ .cta.buy { background: var(--accent-dk); color: #f0eada; }
+
+ /* calc */
+ .calc-card .row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
+ .calc-card label { font-size: 11.5px; color: var(--muted); display: block; margin-bottom: 3px; }
+ .calc-card input, .calc-card select { width: 100%; padding: 8px 10px; border: 1px solid var(--line); border-radius: 6px; font: inherit; background: #fff; }
+ .calc-card .total { padding: 12px 14px; background: rgba(255,255,255,.6); border-radius: 8px; font-size: 14px; }
+ .calc-card .total b { font: 500 18px var(--serif); }
+
+ .palette-card .swatches { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
+ .sw { width: 44px; height: 44px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); cursor: pointer; position: relative; }
+ .sw:hover .lbl { display: block; }
+ .sw .lbl { display: none; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); margin-top: 6px; white-space: nowrap; font: 500 10px/1 var(--sans); background: var(--ink); color: var(--bg); padding: 4px 6px; border-radius: 4px; z-index: 5; }
+ .palette-card .dom { font-size: 12px; color: var(--muted); margin-top: 12px; text-align: center; }
+
+ .adjust-card .sl { display: grid; grid-template-columns: 90px 1fr 36px; align-items: center; gap: 10px; font-size: 12px; padding: 4px 0; }
+ .adjust-card .sl input[type=range] { accent-color: var(--accent); width: 100%; }
+ .adjust-card .sl .v { font-variant-numeric: tabular-nums; color: var(--muted); text-align: right; }
+
+ /* 2-col card row */
+ .row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 22px; }
+
+ .room-card { height: 280px; background-size: cover; background-position: center; padding: 0; position: relative; overflow: hidden; }
+ .room-card .badge { position: absolute; top: 16px; left: 16px; background: rgba(26,22,17,.78); color: #f0eada; font: 600 10.5px/1 var(--sans); letter-spacing: .06em; padding: 7px 12px; border-radius: 999px; text-transform: uppercase; }
+ .room-card .placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--muted); font-size: 12px; font-style: italic; }
+
+ .pair-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
+ .pair-grid .pair { aspect-ratio: 1/1; background: rgba(255,255,255,.55); border-radius: 6px; background-size: cover; background-position: center; }
+
+ /* accordions card */
+ .acc-card { padding: 14px; }
+ details.acc { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 12px 16px; font-size: 13px; margin-bottom: 8px; }
+ details.acc:last-child { margin-bottom: 0; }
+ details.acc[open] { padding-bottom: 14px; }
+ details.acc summary { cursor: pointer; font-weight: 600; list-style: none; display: flex; justify-content: space-between; align-items: center; }
+ details.acc summary::after { content: '▾'; color: var(--muted); transition: transform .2s; font-size: 11px; }
+ details.acc[open] summary::after { transform: rotate(180deg); }
+ details.acc summary .hint { font-weight: 400; font-size: 11.5px; color: var(--muted); margin-left: auto; margin-right: 10px; }
+ details.acc .acc-body { margin-top: 12px; font-size: 12.5px; line-height: 1.55; }
+
+ .specs-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .specs-row:last-child { border-bottom: 0; }
+ .specs-row .k { color: var(--muted); }
+ .specs-row .v { font-weight: 600; text-align: right; }
+ .pill-list { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 9px; border-radius: 999px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); padding: 10px 8px; border-radius: 8px; font: 600 11.5px/1.2 var(--sans); color: var(--ink); text-align: center; text-decoration: none; }
+ .tile-btn:hover { background: #fff; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 28px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); margin-top: 30px; }
+ footer.site .h { font: 500 14px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading-shell { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+
+ dialog.modal { border: 0; border-radius: 14px; padding: 0; max-width: 540px; width: 92vw; box-shadow: 0 24px 80px rgba(0,0,0,.25); background: var(--bg); }
+ dialog.modal::backdrop { background: rgba(20,17,14,.55); backdrop-filter: blur(4px); }
+ dialog.modal header { display: flex; justify-content: space-between; align-items: center; padding: 18px 22px; border-bottom: 1px solid var(--line); }
+ dialog.modal header h3 { margin: 0; font: 500 18px/1 var(--serif); }
+ dialog.modal .close { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ dialog.modal .body { padding: 18px 22px 24px; }
+
+ @media (max-width: 720px) {
+ header.topbar { flex-wrap: wrap; padding: 10px 14px; }
+ main.stack { padding: 14px; gap: 16px; }
+ .card { padding: 18px; }
+ .card.title h1 { font-size: 26px; }
+ .card.buy { grid-template-columns: 1fr; }
+ .row-2 { grid-template-columns: 1fr; }
+ .pair-grid { grid-template-columns: repeat(3, 1fr); }
+ }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <a href="/" class="brand">wallco.ai</a>
+ <nav><a href="/designs?sort=newest">Designs</a><a href="/designs?cat=monterey-mural">Murals</a><a href="/samples">Samples</a><a href="/basket">Basket</a></nav>
+ <span class="grow"></span>
+ <span class="switcher">Theme: Card-Stack (V10) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<div id="root" class="loading-shell">Loading design…</div>
+
+<dialog class="modal" id="m-share"><header><h3>Share</h3><button class="close" onclick="document.getElementById('m-share').close()">×</button></header><div class="body" id="m-share-body"></div></dialog>
+<dialog class="modal" id="m-furnish"><header><h3>Furnish</h3><button class="close" onclick="document.getElementById('m-furnish').close()">×</button></header><div class="body" id="m-furnish-body"></div></dialog>
+<dialog class="modal" id="m-trade"><header><h3>Trade tools</h3><button class="close" onclick="document.getElementById('m-trade').close()">×</button></header><div class="body" id="m-trade-body"></div></dialog>
+
+<script>
+(async function () {
+ const pathMatch = location.pathname.match(/\/design\/(\d+)/);
+ const idFromQuery = new URLSearchParams(location.search).get('id');
+ const id = pathMatch ? parseInt(pathMatch[1], 10) : (idFromQuery ? parseInt(idFromQuery, 10) : null);
+ if (!Number.isFinite(id) || id < 1) { document.getElementById('root').textContent = 'No id'; return; }
+ document.getElementById('back-to-classic').href = '/design/' + id;
+
+ let design;
+ try {
+ const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const j = await r.json();
+ design = j.design;
+ } catch (e) { document.getElementById('root').textContent = 'Failed: ' + e.message; return; }
+
+ const title = design.title || ('Design No.' + id);
+ const cat = design.category || 'wallpaper';
+ const catDisplay = (cat || '').replace(/^botanical-wonders · /, 'Botanical Wonders — ').replace(/^designer-zoo-calm/, 'Designer Zoo Calm').replace(/^cactus-pine-scenic/, 'Cactus Pine Scenic').replace(/^monterey-mural/, 'Coastal Scenic').replace(/^tree-mural/, 'Full-Room Scenic').replace(/^cactus-11ft-mural/, 'Single-Subject Mural').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : design.kind === 'mural_panel' ? 'Mural Panel' : design.kind === 'mural' ? 'Single-Subject Mural' : (design.kind || 'pattern');
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#888';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const w = Number(design.width_in) || 24, h = Number(design.height_in) || 24;
+ const repeatLabel = w + '"' + (w !== h ? ' × ' + h + '"' : '');
+ const rollOrPanel = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const roomMockup = (design.room_mockups && (typeof design.room_mockups === 'object' && !Array.isArray(design.room_mockups) ? Object.keys(design.room_mockups)[0] : design.room_mockups[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<div class="crumb">
+ <a href="/designs">Designs</a><span>›</span><a href="/designs?cat=${encodeURIComponent(cat)}">${escapeHtml(catDisplay)}</a>
+ <span class="grow"></span>
+ <span><a href="/design/${id - 2}?theme=stack">← prev</a> · <a href="/design/${id + 2}?theme=stack">next →</a></span>
+</div>
+
+<main class="stack">
+
+ <!-- HERO CARD -->
+ <div class="card hero">
+ <img loading="eager" src="${img}" alt="${escapeAttr(title)}">
+ <span class="pill">${design.is_published ? 'Live' : 'Curating'}</span>
+ <span class="info">${escapeHtml(title)}</span>
+ </div>
+
+ <!-- TITLE CARD -->
+ <div class="card title">
+ <h1>${escapeHtml(title)}</h1>
+ <div class="meta">${escapeHtml(catDisplay)}<span class="sep">·</span>${escapeHtml(kindLabel)}<span class="sep">·</span>${rollOrPanel}</div>
+ <div class="chips">
+ <span class="chip"><span class="dot" style="background:${escapeAttr(dominant)}"></span>${escapeHtml(dominant)}</span>
+ <span class="chip">No.${id}</span>
+ <span class="chip">${escapeHtml(handle)}</span>
+ <span class="chip">${escapeHtml(generator)}</span>
+ </div>
+ </div>
+
+ <!-- BUY CARD -->
+ <div class="card buy">
+ <button class="cta sample" onclick="addSample()">Add sample · $4.25</button>
+ <button class="cta buy" onclick="document.getElementById('stkw').focus()">Buy by the roll →</button>
+ </div>
+
+ <!-- CALCULATOR CARD -->
+ <div class="card tinted-calc calc-card">
+ <h3 style="color:#5a4f73">Calculator</h3>
+ <div class="row">
+ <div><label>Wall W (ft)</label><input id="stkw" type="number" min="1" max="80" step="0.5" value="10"></div>
+ <div><label>Wall H (ft)</label><input id="stkh" type="number" min="1" max="20" step="0.5" value="9"></div>
+ </div>
+ <div class="row">
+ <div><label>Roll width</label><select id="stkrw"><option value="24">24" residential</option><option value="36">36" wide-roll</option><option value="52">52" commercial</option></select></div>
+ <div><label>Repeat</label><input value='${repeatLabel}' readonly></div>
+ </div>
+ <div class="total" id="stktotal">— rolls · $—</div>
+ </div>
+
+ <!-- 2-col: PALETTE + ADJUST -->
+ <div class="row-2">
+ <div class="card tinted-cream palette-card">
+ <h3>Palette · drag to recolor</h3>
+ <div class="swatches">
+ ${palette.length ? palette.slice(0, 5).map(p => `<div class="sw" style="background:${escapeAttr(p.hex)}"><span class="lbl">${escapeHtml(p.hex)} · ${(p.pct || 0).toFixed(1)}%</span></div>`).join('') : `<div class="sw" style="background:${escapeAttr(dominant)}"><span class="lbl">${escapeHtml(dominant)}</span></div>`}
+ </div>
+ <div class="dom">Dominant ${escapeHtml(dominant)}</div>
+ </div>
+ <div class="card tinted-cream adjust-card">
+ <h3>Image adjustments</h3>
+ <div class="sl"><label>Hue</label><input type="range" min="-180" max="180" value="0"><span class="v">0</span></div>
+ <div class="sl"><label>Saturation</label><input type="range" min="0" max="200" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Brightness</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Contrast</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ </div>
+ </div>
+
+ <!-- ROOM CARD -->
+ <div class="card tinted-room room-card" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : ''}>
+ <span class="badge">See in a room</span>
+ ${roomImgUrl ? '' : '<div class="placeholder">Room mockup not yet generated</div>'}
+ </div>
+
+ <!-- PAIRS WELL CARD -->
+ <div class="card tinted-coord">
+ <h3>Pairs well with — 6 designer wallpapers</h3>
+ <div class="pair-grid">
+ <div class="pair"></div><div class="pair"></div><div class="pair"></div>
+ <div class="pair"></div><div class="pair"></div><div class="pair"></div>
+ </div>
+ </div>
+
+ <!-- ACCORDIONS CARD (rest of the long-tail content) -->
+ <div class="card acc-card">
+ <h3 style="padding:0 8px 4px">More about this design</h3>
+ <details class="acc"><summary>Specs <span class="hint">12 fields</span></summary><div class="acc-body">
+ <div class="specs-row"><span class="k">Category</span><span class="v">${escapeHtml(catDisplay)}</span></div>
+ <div class="specs-row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div>
+ <div class="specs-row"><span class="k">Width</span><span class="v">${w}"</span></div>
+ <div class="specs-row"><span class="k">Repeat</span><span class="v">${repeatLabel}</span></div>
+ <div class="specs-row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div>
+ <div class="specs-row"><span class="k">${isMural ? 'Panel size' : 'Roll length'}</span><span class="v">${isMural ? (w + '" × 11 ft') : '27 ft'}</span></div>
+ <div class="specs-row"><span class="k">Material</span><span class="v">Type II vinyl</span></div>
+ <div class="specs-row"><span class="k">Designed by</span><span class="v">${escapeHtml(generator)}</span></div>
+ <div class="specs-row"><span class="k">Pattern No.</span><span class="v">${id}</span></div>
+ <div class="specs-row"><span class="k">Handle</span><span class="v">${escapeHtml(handle)}</span></div>
+ <div class="specs-row"><span class="k">Dominant hex</span><span class="v">${escapeHtml(dominant)}</span></div>
+ </div></details>
+ <details class="acc"><summary>Coordinate paints <span class="hint">SW · BM · F&B</span></summary><div class="acc-body"><div class="pill-list">
+ <a class="pill" href="#">Sherwin-Williams</a><a class="pill" href="#">Dunn-Edwards</a><a class="pill" href="#">Benjamin Moore</a><a class="pill" href="#">Farrow & Ball</a><a class="pill" href="#">Behr</a><a class="pill" href="#">PPG</a><a class="pill" href="#">RAL</a>
+ </div></div></details>
+ <details class="acc"><summary>Coordinate stripes & plaids <span class="hint">24 generated</span></summary><div class="acc-body"><div class="pair-grid">${Array.from({length: 12}).map(() => '<div class="pair"></div>').join('')}</div></div></details>
+ <details class="acc"><summary>Furnish this design <span class="hint">Res · Com · Hosp · Health</span></summary><div class="acc-body"><div class="tile-btns">
+ <a class="tile-btn" href="#" onclick="openFurnish();return false">Residential</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Commercial</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Hospitality</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Healthcare</a>
+ </div></div></details>
+ <details class="acc"><summary>Share this design <span class="hint">Pin · IG · Copy</span></summary><div class="acc-body"><div class="tile-btns">
+ <a class="tile-btn" href="#" onclick="openShare();return false">Pinterest</a><a class="tile-btn" href="#" onclick="openShare();return false">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a>
+ </div></div></details>
+ <details class="acc"><summary>Trade tools <span class="hint">Tear · Email · Compare · Save</span></summary><div class="acc-body"><div class="tile-btns">
+ <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#" onclick="openTrade();return false">Email</a><a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a><a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
+ </div></div></details>
+ <details class="acc"><summary>Ratings & reviews <span class="hint">★★★★☆ · ${escapeHtml(design.user_vote_count || 0)} votes</span></summary><div class="acc-body"><div style="font-size:22px; letter-spacing:4px; color:#b89a4a">★★★★☆</div><div style="font-size:11.5px; color:var(--muted); margin-top:4px">Curated · 4.2</div></div></details>
+ <details class="acc"><summary>Generator + prompt <span class="hint">wallco.ai · seed ${escapeHtml(design.seed || '—')}</span></summary><div class="acc-body"><pre style="font:11px/1.5 ui-monospace,Menlo,monospace; background:var(--panel); padding:10px; border-radius:6px; max-height:200px; overflow:auto; margin:0; white-space:pre-wrap; word-break:break-word">${escapeHtml((design.prompt || '').slice(0, 1200))}${(design.prompt && design.prompt.length > 1200) ? '\n…[truncated]' : ''}</pre></div></details>
+ </div>
+
+</main>
+
+<footer class="site">
+ <div class="h">wallco.ai — Curated-original wallpaper & murals</div>
+ <div>A Designer Wallcoverings company · Sherman Oaks, CA</div>
+</footer>
+`;
+
+ const stkw = document.getElementById('stkw'), stkh = document.getElementById('stkh'), stkrw = document.getElementById('stkrw');
+ const stktotal = document.getElementById('stktotal');
+ function recalc() {
+ const ww = parseFloat(stkw.value) || 0, wh = parseFloat(stkh.value) || 0;
+ const rwi = parseFloat(stkrw.value) || 24;
+ if (ww < 1 || wh < 1) { stktotal.textContent = '— rolls · $—'; return; }
+ if (isMural) { const p = Math.ceil((ww * 12) / rwi); stktotal.innerHTML = '<b>' + p + '</b> panels · <b>$' + (p * 245).toLocaleString() + '</b>'; }
+ else { const wp = Math.ceil((ww * 12) / rwi); const r = wp * Math.ceil(wh / 27); stktotal.innerHTML = '<b>' + r + '</b> rolls · <b>$' + (r * 165).toLocaleString() + '</b> · ' + wp + ' panels wide'; }
+ }
+ [stkw, stkh, stkrw].forEach(el => el.addEventListener('input', recalc));
+ recalc();
+
+ document.querySelectorAll('.adjust-card .sl').forEach(row => {
+ const r = row.querySelector('input[type=range]'); const v = row.querySelector('.v');
+ r.addEventListener('input', () => { v.textContent = r.value; });
+ });
+
+ document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
+ document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
+ document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
+})();
+
+function openShare() { document.getElementById('m-share').showModal(); }
+function openFurnish() { document.getElementById('m-furnish').showModal(); }
+function openTrade() { document.getElementById('m-trade').showModal(); }
+async function copyLink() { try { await navigator.clipboard.writeText(location.href.replace(/\/stack$/, '')); alert('Link copied'); } catch { alert('Copy failed'); } }
+function addSample() { alert('Sample added (stub)'); }
+function saveDesign(id) { alert('Saved #' + id + ' (stub)'); }
+function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
+function escapeAttr(v) { return escapeHtml(v); }
+</script>
+</body>
+</html>
diff --git a/public/theme-variants/v4-hero.html b/public/theme-variants/v4-hero.html
new file mode 100644
index 0000000..845bfd2
--- /dev/null
+++ b/public/theme-variants/v4-hero.html
@@ -0,0 +1,602 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Hero-First Storefront PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3;
+ --ink: #1a1611;
+ --muted: #8a8276;
+ --line: #e2dccd;
+ --panel: #f3efe5;
+ --panel-2: #ebe5d6;
+ --accent: #c9b07a;
+ --accent-dk: #1a1611;
+ --hero-bg: #e6dfd2;
+ --calc: #ddd4ec;
+ --coord: #d3e3e5;
+ --share: #f3dcce;
+ --room: #cfdac4;
+ --rate: #f0e7c3;
+ --trade: #e6ddd2;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ --serif: "Cormorant Garamond", "Times New Roman", Georgia, serif;
+ --r: 10px;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; }
+ body {
+ background: var(--bg);
+ color: var(--ink);
+ font: 14px/1.5 var(--sans);
+ -webkit-font-smoothing: antialiased;
+ }
+ a { color: inherit; }
+ button { font: inherit; cursor: pointer; }
+
+ /* ── topbar ─────────────────────────────────────────────────────────── */
+ header.topbar {
+ position: sticky; top: 0; z-index: 30;
+ background: rgba(250,248,243,.92);
+ backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--line);
+ padding: 10px 28px;
+ display: flex; align-items: center; gap: 24px;
+ }
+ header .brand { font: 600 18px/1 var(--serif); letter-spacing: .05em; }
+ header nav { font-size: 12.5px; color: var(--muted); display: flex; gap: 18px; flex-wrap: wrap; }
+ header nav a { text-decoration: none; }
+ header nav a:hover { color: var(--ink); }
+ header .grow { flex: 1; }
+ header .switcher { font-size: 11px; color: var(--muted); }
+ header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
+
+ /* ── full-bleed hero ────────────────────────────────────────────────── */
+ .hero-wrap {
+ position: relative;
+ width: 100%;
+ height: min(78vh, 920px);
+ background: var(--hero-bg);
+ overflow: hidden;
+ }
+ .hero-wrap img {
+ width: 100%; height: 100%; object-fit: cover; display: block;
+ }
+ .hero-wrap .pill {
+ position: absolute; top: 18px; right: 24px;
+ background: rgba(250,248,243,.94); color: var(--ink);
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em;
+ padding: 7px 12px; border-radius: 999px; text-transform: uppercase;
+ }
+ .hero-wrap .overlay {
+ position: absolute; left: 0; right: 0; bottom: 0;
+ padding: 40px 28px 28px;
+ background: linear-gradient(to top, rgba(7,6,5,.85) 0%, rgba(7,6,5,.55) 60%, rgba(7,6,5,0) 100%);
+ color: #f0eada;
+ }
+ .hero-wrap .overlay .name {
+ font: 400 44px/1.05 var(--serif);
+ margin: 0 0 8px;
+ max-width: 900px;
+ }
+ .hero-wrap .overlay .meta {
+ font-size: 13px; letter-spacing: .04em;
+ color: #d8cea8;
+ max-width: 900px;
+ }
+ .hero-wrap .overlay .meta .sep { padding: 0 8px; opacity: .55; }
+ .hero-wrap .overlay .chips { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 6px; }
+ .hero-wrap .chip {
+ display: inline-flex; align-items: center; gap: 6px;
+ background: rgba(250,248,243,.16); color: #f0eada;
+ border-radius: 999px; padding: 5px 11px;
+ font: 500 11px/1 var(--sans); letter-spacing: .02em;
+ border: 1px solid rgba(250,248,243,.18);
+ backdrop-filter: blur(4px);
+ }
+ .hero-wrap .chip .dot { width: 10px; height: 10px; border-radius: 999px; }
+
+ /* ── breadcrumb under hero ─────────────────────────────────────────── */
+ .crumb {
+ max-width: 1180px; margin: 0 auto; padding: 18px 28px 0;
+ display: flex; align-items: center; gap: 16px; font-size: 12px; color: var(--muted);
+ }
+ .crumb .grow { flex: 1; }
+ .crumb a { color: var(--ink); text-decoration: none; }
+ .crumb .nav a { color: var(--muted); }
+
+ /* ── narrow centered buy column ─────────────────────────────────────── */
+ main.narrow {
+ max-width: 640px; margin: 0 auto; padding: 28px 24px 80px;
+ display: flex; flex-direction: column; gap: 14px;
+ }
+
+ .cta {
+ display: flex; align-items: center; justify-content: center;
+ border-radius: var(--r); padding: 0 18px;
+ font: 700 13px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase;
+ height: 64px;
+ border: 1px solid transparent;
+ }
+ .cta.sample { background: var(--accent); color: var(--accent-dk); }
+ .cta.buy { background: var(--accent-dk); color: #f0eada; }
+ .cta:hover { filter: brightness(1.04); }
+ .cta:active { transform: translateY(1px); }
+
+ .card {
+ background: var(--panel);
+ border: 1px solid var(--line);
+ border-radius: var(--r);
+ padding: 16px;
+ }
+ .card h4 {
+ margin: 0 0 10px;
+ font: 600 11px/1.2 var(--sans);
+ text-transform: uppercase; letter-spacing: .08em;
+ color: var(--muted);
+ }
+
+ .calc { background: var(--calc); }
+ .calc .row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 8px 9px; border: 1px solid var(--line); border-radius: 6px; font: inherit; background: #fff; }
+ .calc .total { margin-top: 6px; padding: 10px; background: rgba(255,255,255,.6); border-radius: 8px; font-size: 13px; }
+ .calc .total b { font-size: 17px; font-family: var(--serif); font-weight: 500; }
+
+ .palette { background: var(--panel); }
+ .palette .swatches { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 4px; justify-content: center; }
+ .palette .sw { width: 40px; height: 40px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); position: relative; cursor: pointer; }
+ .palette .sw:hover .lbl { display: block; }
+ .palette .sw .lbl { display: none; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); margin-top: 6px; white-space: nowrap; font: 500 10px/1 var(--sans); background: var(--ink); color: var(--bg); padding: 4px 6px; border-radius: 4px; z-index: 5; }
+ .palette .dom { font-size: 11px; color: var(--muted); margin-top: 10px; text-align: center; }
+
+ /* ── accordions ────────────────────────────────────────────────────── */
+ details.acc {
+ background: #fff; border: 1px solid var(--line); border-radius: 8px;
+ padding: 12px 16px; font-size: 13px;
+ }
+ details.acc[open] { padding-bottom: 16px; }
+ details.acc summary {
+ cursor: pointer; font-weight: 600; list-style: none;
+ display: flex; justify-content: space-between; align-items: center;
+ color: var(--ink); gap: 8px;
+ }
+ details.acc summary::after { content: '▾'; color: var(--muted); transition: transform .2s; font-size: 11px; flex-shrink: 0; }
+ details.acc[open] summary::after { transform: rotate(180deg); }
+ details.acc summary .hint { font-weight: 400; font-size: 11.5px; color: var(--muted); margin-left: auto; }
+ details.acc .acc-body { margin-top: 14px; color: var(--ink); line-height: 1.6; font-size: 12.5px; }
+
+ .specs-row { display: flex; justify-content: space-between; padding: 7px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .specs-row:last-child { border-bottom: 0; }
+ .specs-row .k { color: var(--muted); }
+ .specs-row .v { font-weight: 600; text-align: right; }
+
+ .pill-list { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 9px; border-radius: 999px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); }
+
+ .pair-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
+ .pair-grid .pair {
+ aspect-ratio: 1/1;
+ background: rgba(0,0,0,.06);
+ border-radius: 6px;
+ background-size: cover; background-position: center;
+ }
+
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn {
+ flex: 1; min-width: 90px;
+ background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06);
+ padding: 10px 8px; border-radius: 8px;
+ font: 600 11.5px/1.2 var(--sans); color: var(--ink); text-align: center;
+ text-decoration: none;
+ }
+ .tile-btn:hover { background: #fff; }
+
+ .room-mini {
+ height: 200px; border-radius: 8px;
+ background: var(--room); background-size: cover; background-position: center;
+ position: relative;
+ }
+ .room-mini .placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--muted); font-size: 12px; font-style: italic; }
+
+ .atscale-mini {
+ height: 180px; border-radius: 8px;
+ background: var(--hero-bg);
+ background-repeat: repeat;
+ background-size: 160px 160px;
+ margin-top: 10px;
+ }
+
+ .adjust .sl { display: grid; grid-template-columns: 84px 1fr 36px; align-items: center; gap: 8px; font-size: 11.5px; padding: 3px 0; }
+ .adjust .sl input[type=range] { accent-color: var(--accent); width: 100%; }
+ .adjust .sl .v { font-variant-numeric: tabular-nums; color: var(--muted); text-align: right; }
+
+ .prevnext-band {
+ max-width: 640px; margin: 12px auto 0;
+ display: flex; padding: 0 24px; font-size: 12.5px;
+ }
+ .prevnext-band a { color: var(--muted); text-decoration: none; }
+ .prevnext-band a:hover { color: var(--ink); }
+ .prevnext-band .grow { flex: 1; }
+
+ /* ── footer ────────────────────────────────────────────────────────── */
+ footer.site {
+ border-top: 1px solid var(--line);
+ padding: 28px; text-align: center; color: var(--muted); font-size: 12px;
+ background: var(--panel); margin-top: 40px;
+ }
+ footer.site .h { font: 500 14px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+
+ .loading-shell { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; font-size: 14px; }
+
+ /* ── modals ─────────────────────────────────────────────────────────── */
+ dialog.modal {
+ border: 0; border-radius: 14px; padding: 0;
+ max-width: 540px; width: 92vw;
+ box-shadow: 0 24px 80px rgba(0,0,0,.25);
+ background: var(--bg);
+ }
+ dialog.modal::backdrop { background: rgba(20,17,14,.55); backdrop-filter: blur(4px); }
+ dialog.modal header { display: flex; justify-content: space-between; align-items: center; padding: 18px 22px; border-bottom: 1px solid var(--line); }
+ dialog.modal header h3 { margin: 0; font: 500 18px/1 var(--serif); }
+ dialog.modal .close { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ dialog.modal .body { padding: 18px 22px 24px; }
+
+ /* ── responsive ────────────────────────────────────────────────────── */
+ @media (max-width: 640px) {
+ header.topbar { flex-wrap: wrap; padding: 10px 14px; }
+ .hero-wrap { height: min(70vh, 560px); }
+ .hero-wrap .overlay { padding: 30px 16px 18px; }
+ .hero-wrap .overlay .name { font-size: 32px; }
+ .crumb { padding: 14px 14px 0; }
+ main.narrow { padding: 22px 14px 50px; }
+ }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <a href="/" class="brand">wallco.ai</a>
+ <nav>
+ <a href="/designs?sort=newest">Designs</a>
+ <a href="/designs?cat=monterey-mural">Murals</a>
+ <a href="/samples">Samples</a>
+ <a href="/basket">Basket</a>
+ <a href="/designer-studio">Designer Studio</a>
+ </nav>
+ <span class="grow"></span>
+ <span class="switcher">Theme: Hero-First Storefront (V4) <a id="back-to-classic" href="#">← Classic view</a></span>
+</header>
+
+<div id="root" class="loading-shell">Loading design…</div>
+
+<dialog class="modal" id="m-share"><header><h3>Share this design</h3><button class="close" onclick="document.getElementById('m-share').close()">×</button></header><div class="body" id="m-share-body"></div></dialog>
+<dialog class="modal" id="m-furnish"><header><h3>Furnish this design</h3><button class="close" onclick="document.getElementById('m-furnish').close()">×</button></header><div class="body" id="m-furnish-body"></div></dialog>
+<dialog class="modal" id="m-trade"><header><h3>Trade tools</h3><button class="close" onclick="document.getElementById('m-trade').close()">×</button></header><div class="body" id="m-trade-body"></div></dialog>
+
+<script>
+(async function () {
+ const pathMatch = location.pathname.match(/\/design\/(\d+)/);
+ const idFromQuery = new URLSearchParams(location.search).get('id');
+ const id = pathMatch ? parseInt(pathMatch[1], 10) : (idFromQuery ? parseInt(idFromQuery, 10) : null);
+ if (!Number.isFinite(id) || id < 1) {
+ document.getElementById('root').textContent = 'No design id in URL.';
+ return;
+ }
+ document.getElementById('back-to-classic').href = '/design/' + id;
+
+ let design;
+ try {
+ const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const j = await r.json();
+ design = j.design;
+ } catch (e) {
+ document.getElementById('root').textContent = 'Failed to load design #' + id + ' — ' + e.message;
+ return;
+ }
+
+ const title = design.title || ('Design No.' + id);
+ const cat = design.category || 'wallpaper';
+ const catDisplay = (cat || '')
+ .replace(/^botanical-wonders · /, 'Botanical Wonders — ')
+ .replace(/^designer-zoo-calm/, 'Designer Zoo Calm')
+ .replace(/^cactus-pine-scenic/, 'Cactus Pine Scenic')
+ .replace(/^monterey-mural/, 'Coastal Scenic')
+ .replace(/^tree-mural/, 'Full-Room Scenic')
+ .replace(/^cactus-11ft-mural/, 'Single-Subject Mural')
+ .replace(/-/g, ' ')
+ .replace(/\b\w/g, c => c.toUpperCase());
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")'
+ : design.kind === 'mural_panel' ? 'Mural Panel'
+ : design.kind === 'mural' ? 'Single-Subject Mural' : (design.kind || 'pattern');
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#888';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const w = Number(design.width_in) || 24;
+ const h = Number(design.height_in) || 24;
+ const repeatLabel = w + '"' + (w !== h ? ' × ' + h + '"' : '');
+ const rollOrPanel = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const roomMockup = (design.room_mockups && (
+ typeof design.room_mockups === 'object' && !Array.isArray(design.room_mockups)
+ ? Object.keys(design.room_mockups)[0]
+ : design.room_mockups[0]
+ )) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+
+<!-- FULL-BLEED HERO ────────────────────────────────────────────────── -->
+<div class="hero-wrap">
+ <img loading="eager" src="${img}" alt="${escapeAttr(title)}">
+ <span class="pill">${design.is_published ? 'Live' : 'Curating'}</span>
+ <div class="overlay">
+ <h1 class="name">${escapeHtml(title)}</h1>
+ <div class="meta">
+ ${escapeHtml(catDisplay)}<span class="sep">·</span>${escapeHtml(kindLabel)}<span class="sep">·</span>${rollOrPanel}
+ </div>
+ <div class="chips">
+ <span class="chip"><span class="dot" style="background:${escapeAttr(dominant)}"></span>${escapeHtml(dominant)}</span>
+ <span class="chip">No.${id}</span>
+ <span class="chip">${escapeHtml(handle)}</span>
+ </div>
+ </div>
+</div>
+
+<div class="crumb">
+ <a href="/designs">Designs</a>
+ <span>›</span>
+ <a href="/designs?cat=${encodeURIComponent(cat)}">${escapeHtml(catDisplay)}</a>
+ <span class="grow"></span>
+ <span class="nav">
+ <a href="/design/${id - 2}?theme=hero">← prev</a>
+ <a href="/design/${id + 2}?theme=hero">next →</a>
+ </span>
+</div>
+
+<!-- NARROW CENTERED BUY COLUMN ─────────────────────────────────────── -->
+<main class="narrow">
+
+ <button class="cta sample" onclick="addSample()">Add sample · $4.25</button>
+ <button class="cta buy" onclick="document.getElementById('calc-input').focus()">Buy by the roll →</button>
+
+ <div class="card calc">
+ <h4 style="color:#5a4f73">Calculator</h4>
+ <div class="row">
+ <div><label>Wall W (ft)</label><input id="calc-input" type="number" min="1" max="80" step="0.5" value="10"></div>
+ <div><label>Wall H (ft)</label><input id="calc-h" type="number" min="1" max="20" step="0.5" value="9"></div>
+ </div>
+ <div class="row">
+ <div><label>Roll width</label>
+ <select id="calc-rw">
+ <option value="24">24" residential</option>
+ <option value="36">36" wide-roll</option>
+ <option value="52">52" commercial</option>
+ </select>
+ </div>
+ <div><label>Repeat</label><input value='${repeatLabel}' readonly></div>
+ </div>
+ <div class="total" id="calc-total">— rolls · $—</div>
+ </div>
+
+ <div class="card palette">
+ <h4>Palette · drag to recolor</h4>
+ <div class="swatches">
+ ${palette.length ? palette.slice(0, 5).map(p =>
+ `<div class="sw" style="background:${escapeAttr(p.hex)}"><span class="lbl">${escapeHtml(p.hex)} · ${(p.pct || 0).toFixed(1)}%</span></div>`
+ ).join('') : `<div class="sw" style="background:${escapeAttr(dominant)}"><span class="lbl">${escapeHtml(dominant)}</span></div>`}
+ </div>
+ <div class="dom">Dominant ${escapeHtml(dominant)}</div>
+ </div>
+
+ <details class="acc">
+ <summary>Specs <span class="hint">Kind · W · Repeat · Roll · Material</span></summary>
+ <div class="acc-body">
+ <div class="specs-row"><span class="k">Category</span><span class="v">${escapeHtml(catDisplay)}</span></div>
+ <div class="specs-row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div>
+ <div class="specs-row"><span class="k">Width</span><span class="v">${w}"</span></div>
+ <div class="specs-row"><span class="k">Repeat</span><span class="v">${repeatLabel}</span></div>
+ <div class="specs-row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div>
+ <div class="specs-row"><span class="k">${isMural ? 'Panel size' : 'Roll length'}</span><span class="v">${isMural ? (w + '" × 11 ft') : '27 ft'}</span></div>
+ <div class="specs-row"><span class="k">Material</span><span class="v">Type II vinyl</span></div>
+ <div class="specs-row"><span class="k">Designed by</span><span class="v">${escapeHtml(generator)}</span></div>
+ <div class="specs-row"><span class="k">Pattern No.</span><span class="v">${id}</span></div>
+ <div class="specs-row"><span class="k">Handle</span><span class="v">${escapeHtml(handle)}</span></div>
+ <div class="specs-row"><span class="k">Dominant hex</span><span class="v">${escapeHtml(dominant)}</span></div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Coordinate paints <span class="hint">SW · BM · F&B · Behr · PPG</span></summary>
+ <div class="acc-body">
+ <div class="pill-list">
+ <a class="pill" href="#">Sherwin-Williams</a>
+ <a class="pill" href="#">Dunn-Edwards</a>
+ <a class="pill" href="#">Benjamin Moore</a>
+ <a class="pill" href="#">Farrow & Ball</a>
+ <a class="pill" href="#">Behr</a>
+ <a class="pill" href="#">PPG</a>
+ <a class="pill" href="#">RAL</a>
+ </div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Coordinate stripes & plaids <span class="hint">24 generated</span></summary>
+ <div class="acc-body">
+ <div class="pair-grid">
+ ${Array.from({length: 12}).map(() => '<div class="pair"></div>').join('')}
+ </div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>See in a room <span class="hint">residential preview</span></summary>
+ <div class="acc-body">
+ <div class="room-mini" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : ''}>
+ ${roomImgUrl ? '' : '<div class="placeholder">Room mockup not yet generated</div>'}
+ </div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>At-scale preview <span class="hint">10 × 9 ft wall</span></summary>
+ <div class="acc-body">
+ <div class="atscale-mini" style="background-image:url('${img}')"></div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Pairs well with <span class="hint">6 designs</span></summary>
+ <div class="acc-body">
+ <div class="pair-grid">
+ <div class="pair"></div><div class="pair"></div><div class="pair"></div>
+ <div class="pair"></div><div class="pair"></div><div class="pair"></div>
+ </div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Furnish this design <span class="hint">Res · Com · Hosp · Health</span></summary>
+ <div class="acc-body">
+ <div class="tile-btns">
+ <a class="tile-btn" href="#" onclick="openFurnish();return false">Residential</a>
+ <a class="tile-btn" href="#" onclick="openFurnish();return false">Commercial</a>
+ <a class="tile-btn" href="#" onclick="openFurnish();return false">Hospitality</a>
+ <a class="tile-btn" href="#" onclick="openFurnish();return false">Healthcare</a>
+ </div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Share this design <span class="hint">Pin · IG · Copy</span></summary>
+ <div class="acc-body">
+ <div class="tile-btns">
+ <a class="tile-btn" href="#" onclick="openShare();return false">Pinterest</a>
+ <a class="tile-btn" href="#" onclick="openShare();return false">Instagram</a>
+ <a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a>
+ </div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Trade tools <span class="hint">Tear · Email · Compare · Save</span></summary>
+ <div class="acc-body">
+ <div class="tile-btns">
+ <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a>
+ <a class="tile-btn" href="#" onclick="openTrade();return false">Email client</a>
+ <a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a>
+ <a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
+ </div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Image adjustments <span class="hint">hue/sat/bri/contrast</span></summary>
+ <div class="acc-body adjust">
+ <div class="sl"><label>Hue</label><input type="range" min="-180" max="180" value="0"><span class="v">0</span></div>
+ <div class="sl"><label>Saturation</label><input type="range" min="0" max="200" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Brightness</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Contrast</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Ratings & reviews <span class="hint">★★★★☆ · ${escapeHtml(design.user_vote_count || 0)}</span></summary>
+ <div class="acc-body">
+ <div style="font-size:24px; letter-spacing:4px; color:#b89a4a">★★★★☆</div>
+ <div style="font-size:11.5px; color:var(--muted); margin-top:4px">Curated · 4.2 · ${escapeHtml(design.user_vote_count || 0)} votes</div>
+ </div>
+ </details>
+
+ <details class="acc">
+ <summary>Generator + prompt <span class="hint">wallco.ai · seed ${escapeHtml(design.seed || '—')}</span></summary>
+ <div class="acc-body">
+ <pre style="font:11px/1.5 ui-monospace,Menlo,monospace; background:var(--panel); padding:10px; border-radius:6px; max-height:200px; overflow:auto; margin:0; white-space:pre-wrap; word-break:break-word">${escapeHtml((design.prompt || '').slice(0, 1200))}${(design.prompt && design.prompt.length > 1200) ? '\n…[truncated]' : ''}</pre>
+ </div>
+ </details>
+
+</main>
+
+<div class="prevnext-band">
+ <a href="/design/${id - 2}?theme=hero">← previous pattern</a>
+ <span class="grow"></span>
+ <a href="/design/${id + 2}?theme=hero">next pattern →</a>
+</div>
+
+<footer class="site">
+ <div class="h">wallco.ai — Curated-original wallpaper & murals</div>
+ <div>A Designer Wallcoverings company · Sherman Oaks, CA · steve@designerwallcoverings.com</div>
+</footer>
+`;
+
+ // ── calculator ──
+ const calcW = root.querySelector('#calc-input');
+ const calcH = root.querySelector('#calc-h');
+ const calcRW = root.querySelector('#calc-rw');
+ const total = root.querySelector('#calc-total');
+ function recalc() {
+ const ww = parseFloat(calcW.value) || 0;
+ const wh = parseFloat(calcH.value) || 0;
+ const rwInches = parseFloat(calcRW.value) || 24;
+ if (ww < 1 || wh < 1) { total.textContent = '— rolls · $—'; return; }
+ if (isMural) {
+ const panels = Math.ceil((ww * 12) / rwInches);
+ const cost = panels * 245;
+ total.innerHTML = '<b>' + panels + '</b> panels · <b>$' + cost.toLocaleString() + '</b>';
+ } else {
+ const widthPanels = Math.ceil((ww * 12) / rwInches);
+ const rollsPerPanel = Math.ceil(wh / 27);
+ const rolls = widthPanels * rollsPerPanel;
+ const cost = rolls * 165;
+ total.innerHTML = '<b>' + rolls + '</b> rolls · <b>$' + cost.toLocaleString() + '</b> · ' + widthPanels + ' panels wide';
+ }
+ }
+ [calcW, calcH, calcRW].forEach(el => el.addEventListener('input', recalc));
+ recalc();
+
+ // ── adjust sliders ──
+ root.querySelectorAll('.adjust .sl').forEach(row => {
+ const r = row.querySelector('input[type=range]');
+ const v = row.querySelector('.v');
+ r.addEventListener('input', () => { v.textContent = r.value; });
+ });
+
+ // ── modal bodies ──
+ document.getElementById('m-share-body').innerHTML =
+ '<p>Share <b>' + escapeHtml(title) + '</b> on:</p>' +
+ '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
+ document.getElementById('m-furnish-body').innerHTML =
+ '<p>Generate AI furnishings curated to this pattern\'s palette.</p>' +
+ '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
+ document.getElementById('m-trade-body').innerHTML =
+ '<p>Trade-only tools (interior designers + specifiers).</p>' +
+ '<div class="tile-btns" style="margin-top:8px"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email to client</a><a class="tile-btn" href="#">Compare 2 patterns</a><a class="tile-btn" href="#">Add to saved</a></div>';
+})();
+
+function openShare() { document.getElementById('m-share').showModal(); }
+function openFurnish() { document.getElementById('m-furnish').showModal(); }
+function openTrade() { document.getElementById('m-trade').showModal(); }
+async function copyLink() {
+ try { await navigator.clipboard.writeText(location.href.replace(/\/hero$/, '')); alert('Link copied'); }
+ catch { alert('Copy failed — ' + location.href); }
+}
+function addSample() { alert('Sample added (stub — wire to /api/cart/sample/' + (new URL(location).pathname.match(/\d+/) || [0])[0] + ')'); }
+function saveDesign(id) { alert('Saved design #' + id + ' (stub — wire to /api/saved)'); }
+
+function escapeHtml(v) {
+ if (v == null) return '';
+ return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
+}
+function escapeAttr(v) { return escapeHtml(v); }
+</script>
+</body>
+</html>
diff --git a/public/theme-variants/v5-vertical.html b/public/theme-variants/v5-vertical.html
new file mode 100644
index 0000000..cc044bf
--- /dev/null
+++ b/public/theme-variants/v5-vertical.html
@@ -0,0 +1,420 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Mobile-Up Vertical PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3; --ink: #1a1611; --muted: #8a8276; --line: #e2dccd;
+ --panel: #f3efe5; --panel-2: #ebe5d6; --accent: #c9b07a; --accent-dk: #1a1611;
+ --hero-bg: #e6dfd2; --calc: #ddd4ec; --coord: #d3e3e5;
+ --share: #f3dcce; --room: #cfdac4; --rate: #f0e7c3;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+ --serif: "Cormorant Garamond", Georgia, serif;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; }
+ body { background: var(--bg); color: var(--ink); font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; padding-bottom: 96px; }
+ a { color: inherit; } button { font: inherit; cursor: pointer; }
+
+ header.topbar {
+ position: sticky; top: 0; z-index: 30;
+ background: rgba(250,248,243,.92); backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--line); padding: 10px 28px;
+ display: flex; align-items: center; gap: 24px;
+ }
+ header .brand { font: 600 18px/1 var(--serif); letter-spacing: .05em; }
+ header nav { font-size: 12.5px; color: var(--muted); display: flex; gap: 18px; flex-wrap: wrap; }
+ header nav a { text-decoration: none; }
+ header nav a:hover { color: var(--ink); }
+ header .grow { flex: 1; }
+ header .switcher { font-size: 11px; color: var(--muted); }
+ header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
+
+ main.vertical {
+ max-width: 720px; margin: 0 auto; padding: 18px 16px 30px;
+ display: flex; flex-direction: column; gap: 10px;
+ }
+
+ .hero {
+ aspect-ratio: 1/1; background: var(--hero-bg);
+ border-radius: 12px; overflow: hidden; position: relative;
+ }
+ .hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .hero .pill {
+ position: absolute; top: 12px; right: 12px;
+ background: rgba(250,248,243,.94); color: var(--ink);
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em;
+ padding: 6px 10px; border-radius: 999px; text-transform: uppercase;
+ }
+
+ .title-card {
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 10px; padding: 14px 16px;
+ }
+ .title-card .name { font: 500 22px/1.15 var(--serif); margin: 2px 0 4px; }
+ .title-card .meta { color: var(--muted); font-size: 12px; }
+ .title-card .meta .sep { padding: 0 5px; opacity: .55; }
+
+ .chip-strip {
+ display: flex; flex-wrap: wrap; gap: 6px;
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 10px; padding: 10px 12px;
+ font: 500 11px/1 var(--sans); align-items: center;
+ }
+ .chip {
+ display: inline-flex; align-items: center; gap: 6px;
+ background: #fff; color: var(--ink);
+ border-radius: 999px; padding: 4px 9px;
+ border: 1px solid var(--line);
+ }
+ .chip .dot { width: 9px; height: 9px; border-radius: 999px; }
+
+ details.acc {
+ background: #fff; border: 1px solid var(--line); border-radius: 10px;
+ padding: 12px 14px; font-size: 13px;
+ }
+ details.acc[open] { padding-bottom: 14px; }
+ details.acc summary {
+ cursor: pointer; font-weight: 600; list-style: none;
+ display: flex; justify-content: space-between; align-items: center;
+ color: var(--ink); gap: 8px;
+ }
+ details.acc summary::after { content: '▾'; color: var(--muted); transition: transform .2s; font-size: 11px; flex-shrink: 0; }
+ details.acc[open] summary::after { transform: rotate(180deg); }
+ details.acc summary .hint { font-weight: 400; font-size: 11.5px; color: var(--muted); margin-left: auto; }
+ details.acc .acc-body { margin-top: 12px; line-height: 1.6; font-size: 12.5px; }
+
+ .specs-row { display: flex; justify-content: space-between; padding: 7px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .specs-row:last-child { border-bottom: 0; }
+ .specs-row .k { color: var(--muted); }
+ .specs-row .v { font-weight: 600; text-align: right; }
+
+ .pill-list { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 9px; border-radius: 999px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); }
+
+ .pair-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
+ .pair-grid .pair { aspect-ratio: 1/1; background: rgba(0,0,0,.06); border-radius: 6px; background-size: cover; background-position: center; }
+
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn {
+ flex: 1; min-width: 100px;
+ background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06);
+ padding: 10px 8px; border-radius: 8px;
+ font: 600 11.5px/1.2 var(--sans); color: var(--ink); text-align: center; text-decoration: none;
+ }
+ .tile-btn:hover { background: #fff; }
+
+ .calc-mini .row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
+ .calc-mini label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc-mini input, .calc-mini select { width: 100%; padding: 7px 8px; border: 1px solid var(--line); border-radius: 6px; font: inherit; background: #fff; }
+ .calc-mini .total { padding: 8px 10px; background: var(--calc); border-radius: 6px; font-size: 13px; margin-top: 4px; }
+ .calc-mini .total b { font-size: 16px; font-family: var(--serif); font-weight: 500; }
+
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+
+ .adjust .sl { display: grid; grid-template-columns: 80px 1fr 32px; align-items: center; gap: 8px; font-size: 11.5px; padding: 3px 0; }
+ .adjust .sl input[type=range] { accent-color: var(--accent); width: 100%; }
+ .adjust .sl .v { font-variant-numeric: tabular-nums; color: var(--muted); text-align: right; }
+
+ .room-mini { height: 200px; border-radius: 8px; background: var(--room); background-size: cover; background-position: center; }
+ .atscale-mini { height: 200px; border-radius: 8px; background: var(--hero-bg); background-repeat: repeat; background-size: 160px 160px; }
+
+ /* floating buy bar */
+ .buy-bar {
+ position: fixed; bottom: 0; left: 0; right: 0; z-index: 40;
+ background: var(--accent-dk); color: #f0eada;
+ padding: 12px 16px;
+ display: flex; gap: 10px; align-items: center;
+ box-shadow: 0 -8px 24px rgba(0,0,0,.18);
+ }
+ .buy-bar .sample {
+ background: var(--accent); color: var(--accent-dk);
+ padding: 12px 18px; border-radius: 8px; border: 0;
+ font: 700 12px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase;
+ cursor: pointer;
+ }
+ .buy-bar .buy {
+ flex: 1; background: rgba(255,255,255,.08); color: #f0eada;
+ padding: 12px 18px; border-radius: 8px; border: 1px solid rgba(255,255,255,.12);
+ font: 700 12px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase;
+ cursor: pointer; text-align: center;
+ }
+ .buy-bar .est { font-size: 12px; color: #c2b88a; white-space: nowrap; }
+
+ footer.site {
+ border-top: 1px solid var(--line);
+ padding: 22px 16px; text-align: center; color: var(--muted); font-size: 12px;
+ background: var(--panel);
+ }
+ footer.site .h { font: 500 14px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading-shell { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; font-size: 14px; }
+
+ dialog.modal { border: 0; border-radius: 14px; padding: 0; max-width: 540px; width: 92vw; box-shadow: 0 24px 80px rgba(0,0,0,.25); background: var(--bg); }
+ dialog.modal::backdrop { background: rgba(20,17,14,.55); backdrop-filter: blur(4px); }
+ dialog.modal header { display: flex; justify-content: space-between; align-items: center; padding: 18px 22px; border-bottom: 1px solid var(--line); }
+ dialog.modal header h3 { margin: 0; font: 500 18px/1 var(--serif); }
+ dialog.modal .close { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ dialog.modal .body { padding: 18px 22px 24px; }
+
+ @media (max-width: 480px) {
+ header.topbar { flex-wrap: wrap; padding: 10px 14px; }
+ .hero { aspect-ratio: 4/5; }
+ }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <a href="/" class="brand">wallco.ai</a>
+ <nav>
+ <a href="/designs?sort=newest">Designs</a>
+ <a href="/designs?cat=monterey-mural">Murals</a>
+ <a href="/samples">Samples</a>
+ <a href="/basket">Basket</a>
+ <a href="/designer-studio">Designer Studio</a>
+ </nav>
+ <span class="grow"></span>
+ <span class="switcher">Theme: Mobile-Up Vertical (V5) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<div id="root" class="loading-shell">Loading design…</div>
+
+<dialog class="modal" id="m-share"><header><h3>Share</h3><button class="close" onclick="document.getElementById('m-share').close()">×</button></header><div class="body" id="m-share-body"></div></dialog>
+<dialog class="modal" id="m-furnish"><header><h3>Furnish</h3><button class="close" onclick="document.getElementById('m-furnish').close()">×</button></header><div class="body" id="m-furnish-body"></div></dialog>
+<dialog class="modal" id="m-trade"><header><h3>Trade tools</h3><button class="close" onclick="document.getElementById('m-trade').close()">×</button></header><div class="body" id="m-trade-body"></div></dialog>
+
+<script>
+(async function () {
+ const pathMatch = location.pathname.match(/\/design\/(\d+)/);
+ const idFromQuery = new URLSearchParams(location.search).get('id');
+ const id = pathMatch ? parseInt(pathMatch[1], 10) : (idFromQuery ? parseInt(idFromQuery, 10) : null);
+ if (!Number.isFinite(id) || id < 1) { document.getElementById('root').textContent = 'No design id'; return; }
+ document.getElementById('back-to-classic').href = '/design/' + id;
+
+ let design;
+ try {
+ const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const j = await r.json();
+ design = j.design;
+ } catch (e) { document.getElementById('root').textContent = 'Failed: ' + e.message; return; }
+
+ const title = design.title || ('Design No.' + id);
+ const cat = design.category || 'wallpaper';
+ const catDisplay = (cat || '')
+ .replace(/^botanical-wonders · /, 'Botanical Wonders — ')
+ .replace(/^designer-zoo-calm/, 'Designer Zoo Calm')
+ .replace(/^cactus-pine-scenic/, 'Cactus Pine Scenic')
+ .replace(/^monterey-mural/, 'Coastal Scenic')
+ .replace(/^tree-mural/, 'Full-Room Scenic')
+ .replace(/^cactus-11ft-mural/, 'Single-Subject Mural')
+ .replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")'
+ : design.kind === 'mural_panel' ? 'Mural Panel'
+ : design.kind === 'mural' ? 'Single-Subject Mural' : (design.kind || 'pattern');
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#888';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const w = Number(design.width_in) || 24, h = Number(design.height_in) || 24;
+ const repeatLabel = w + '"' + (w !== h ? ' × ' + h + '"' : '');
+ const rollOrPanel = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const roomMockup = (design.room_mockups && (
+ typeof design.room_mockups === 'object' && !Array.isArray(design.room_mockups)
+ ? Object.keys(design.room_mockups)[0] : design.room_mockups[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="vertical">
+ <div class="hero">
+ <img loading="eager" src="${img}" alt="${escapeAttr(title)}">
+ <span class="pill">${design.is_published ? 'Live' : 'Curating'}</span>
+ </div>
+
+ <div class="title-card">
+ <div class="name">${escapeHtml(title)}</div>
+ <div class="meta">${escapeHtml(catDisplay)}<span class="sep">·</span>${escapeHtml(kindLabel)}<span class="sep">·</span>${rollOrPanel}</div>
+ </div>
+
+ <div class="chip-strip">
+ <span class="chip"><span class="dot" style="background:${escapeAttr(dominant)}"></span>${escapeHtml(dominant)}</span>
+ <span class="chip">No.${id}</span>
+ <span class="chip">${escapeHtml(handle)}</span>
+ <span class="chip" style="background:${design.is_published ? '#d6eedb' : '#f1e7c5'}; color:${design.is_published ? '#1a5a30' : '#7a5520'}">${design.is_published ? 'Live' : 'Curating'}</span>
+ </div>
+
+ <details class="acc" open><summary>Buy (sample / roll) <span class="hint">$4.25 sample · per-roll</span></summary>
+ <div class="acc-body" style="display:flex; gap:8px;">
+ <button class="tile-btn" style="background:var(--accent); color:var(--accent-dk); flex:1" onclick="addSample()">Add sample $4.25</button>
+ <button class="tile-btn" style="background:var(--accent-dk); color:#f0eada; flex:1" onclick="document.getElementById('calc-input').focus()">Buy by roll</button>
+ </div>
+ </details>
+
+ <details class="acc" open><summary>Calculator <span class="hint">wall × panels × total</span></summary>
+ <div class="acc-body calc-mini">
+ <div class="row">
+ <div><label>Wall W (ft)</label><input id="calc-input" type="number" min="1" max="80" step="0.5" value="10"></div>
+ <div><label>Wall H (ft)</label><input id="calc-h" type="number" min="1" max="20" step="0.5" value="9"></div>
+ </div>
+ <div class="row">
+ <div><label>Roll width</label><select id="calc-rw"><option value="24">24" residential</option><option value="36">36" wide-roll</option><option value="52">52" commercial</option></select></div>
+ <div><label>Repeat</label><input value='${repeatLabel}' readonly></div>
+ </div>
+ <div class="total" id="calc-total">— rolls · $—</div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>Palette + recolor <span class="hint">${palette.length || 1} colors</span></summary>
+ <div class="acc-body">
+ <div class="swatches">
+ ${palette.length ? palette.slice(0, 5).map(p => `<div class="sw" title="${escapeAttr(p.hex)} · ${(p.pct || 0).toFixed(1)}%" style="background:${escapeAttr(p.hex)}"></div>`).join('') : `<div class="sw" style="background:${escapeAttr(dominant)}"></div>`}
+ </div>
+ <div style="font-size:11px; color:var(--muted); margin-top:8px">Dominant ${escapeHtml(dominant)}</div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>Image adjustments <span class="hint">hue/sat/bri/blur</span></summary>
+ <div class="acc-body adjust">
+ <div class="sl"><label>Hue</label><input type="range" min="-180" max="180" value="0"><span class="v">0</span></div>
+ <div class="sl"><label>Saturation</label><input type="range" min="0" max="200" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Brightness</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Contrast</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>Specs <span class="hint">Kind · W · Repeat · Roll</span></summary>
+ <div class="acc-body">
+ <div class="specs-row"><span class="k">Category</span><span class="v">${escapeHtml(catDisplay)}</span></div>
+ <div class="specs-row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div>
+ <div class="specs-row"><span class="k">Width</span><span class="v">${w}"</span></div>
+ <div class="specs-row"><span class="k">Repeat</span><span class="v">${repeatLabel}</span></div>
+ <div class="specs-row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div>
+ <div class="specs-row"><span class="k">${isMural ? 'Panel size' : 'Roll length'}</span><span class="v">${isMural ? (w + '" × 11 ft') : '27 ft'}</span></div>
+ <div class="specs-row"><span class="k">Material</span><span class="v">Type II vinyl</span></div>
+ <div class="specs-row"><span class="k">Designed by</span><span class="v">${escapeHtml(generator)}</span></div>
+ <div class="specs-row"><span class="k">Pattern No.</span><span class="v">${id}</span></div>
+ <div class="specs-row"><span class="k">Handle</span><span class="v">${escapeHtml(handle)}</span></div>
+ <div class="specs-row"><span class="k">Dominant hex</span><span class="v">${escapeHtml(dominant)}</span></div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>See in a room <span class="hint">residential preview</span></summary>
+ <div class="acc-body">
+ <div class="room-mini" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : ''}></div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>At-scale preview <span class="hint">10 × 9 ft wall</span></summary>
+ <div class="acc-body">
+ <div class="atscale-mini" style="background-image:url('${img}')"></div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>Pairs well with <span class="hint">6 designs</span></summary>
+ <div class="acc-body"><div class="pair-grid"><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div></div></div>
+ </details>
+
+ <details class="acc"><summary>Coordinate paints <span class="hint">SW · BM · F&B</span></summary>
+ <div class="acc-body"><div class="pill-list">
+ <a class="pill" href="#">Sherwin-Williams</a><a class="pill" href="#">Dunn-Edwards</a><a class="pill" href="#">Benjamin Moore</a><a class="pill" href="#">Farrow & Ball</a><a class="pill" href="#">Behr</a><a class="pill" href="#">PPG</a><a class="pill" href="#">RAL</a>
+ </div></div>
+ </details>
+
+ <details class="acc"><summary>Coordinate stripes & plaids <span class="hint">24 generated</span></summary>
+ <div class="acc-body"><div class="pair-grid">${Array.from({length: 12}).map(() => '<div class="pair"></div>').join('')}</div></div>
+ </details>
+
+ <details class="acc"><summary>Furnish this design <span class="hint">Res · Com · Hosp</span></summary>
+ <div class="acc-body"><div class="tile-btns">
+ <a class="tile-btn" href="#" onclick="openFurnish();return false">Residential</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Commercial</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Hospitality</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Healthcare</a>
+ </div></div>
+ </details>
+
+ <details class="acc"><summary>Share this design <span class="hint">Pin · IG · Copy</span></summary>
+ <div class="acc-body"><div class="tile-btns">
+ <a class="tile-btn" href="#" onclick="openShare();return false">Pinterest</a><a class="tile-btn" href="#" onclick="openShare();return false">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a>
+ </div></div>
+ </details>
+
+ <details class="acc"><summary>Trade tools <span class="hint">Tear · Email · Compare</span></summary>
+ <div class="acc-body"><div class="tile-btns">
+ <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#" onclick="openTrade();return false">Email</a><a class="tile-btn" href="#" onclick="openTrade();return false">Compare</a><a class="tile-btn" href="#" onclick="saveDesign(${id});return false">Save</a>
+ </div></div>
+ </details>
+
+ <details class="acc"><summary>Ratings & reviews <span class="hint">★★★★☆ · ${escapeHtml(design.user_vote_count || 0)}</span></summary>
+ <div class="acc-body"><div style="font-size:24px; letter-spacing:4px; color:#b89a4a">★★★★☆</div><div style="font-size:11.5px; color:var(--muted); margin-top:4px">Curated · 4.2</div></div>
+ </details>
+
+ <details class="acc"><summary>Generator + prompt <span class="hint">wallco.ai</span></summary>
+ <div class="acc-body"><pre style="font:11px/1.5 ui-monospace,Menlo,monospace; background:var(--panel); padding:10px; border-radius:6px; max-height:200px; overflow:auto; margin:0; white-space:pre-wrap; word-break:break-word">${escapeHtml((design.prompt || '').slice(0, 1000))}${(design.prompt && design.prompt.length > 1000) ? '\n…[truncated]' : ''}</pre></div>
+ </details>
+
+</main>
+
+<footer class="site">
+ <div class="h">wallco.ai</div>
+ <div>A Designer Wallcoverings company · Sherman Oaks, CA</div>
+</footer>
+
+<div class="buy-bar">
+ <button class="sample" onclick="addSample()">Sample $4.25</button>
+ <button class="buy" onclick="document.getElementById('calc-input').scrollIntoView({behavior:'smooth',block:'center'})">Buy by the roll →</button>
+ <span class="est" id="bar-est">calc above</span>
+</div>
+`;
+
+ const calcW = root.querySelector('#calc-input');
+ const calcH = root.querySelector('#calc-h');
+ const calcRW = root.querySelector('#calc-rw');
+ const total = root.querySelector('#calc-total');
+ const barEst = root.querySelector('#bar-est') || document.getElementById('bar-est');
+ function recalc() {
+ const ww = parseFloat(calcW.value) || 0, wh = parseFloat(calcH.value) || 0;
+ const rwInches = parseFloat(calcRW.value) || 24;
+ if (ww < 1 || wh < 1) { total.textContent = '— rolls · $—'; if(barEst)barEst.textContent='—'; return; }
+ if (isMural) {
+ const panels = Math.ceil((ww * 12) / rwInches); const cost = panels * 245;
+ total.innerHTML = '<b>' + panels + '</b> panels · <b>$' + cost.toLocaleString() + '</b>';
+ if(barEst)barEst.textContent = panels + ' panels · $' + cost.toLocaleString();
+ } else {
+ const widthPanels = Math.ceil((ww * 12) / rwInches);
+ const rollsPerPanel = Math.ceil(wh / 27);
+ const rolls = widthPanels * rollsPerPanel; const cost = rolls * 165;
+ total.innerHTML = '<b>' + rolls + '</b> rolls · <b>$' + cost.toLocaleString() + '</b> · ' + widthPanels + ' panels wide';
+ if(barEst)barEst.textContent = rolls + ' rolls · $' + cost.toLocaleString();
+ }
+ }
+ [calcW, calcH, calcRW].forEach(el => el.addEventListener('input', recalc));
+ recalc();
+
+ root.querySelectorAll('.adjust .sl').forEach(row => {
+ const r = row.querySelector('input[type=range]');
+ const v = row.querySelector('.v');
+ r.addEventListener('input', () => { v.textContent = r.value; });
+ });
+
+ document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
+ document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
+ document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
+})();
+
+function openShare() { document.getElementById('m-share').showModal(); }
+function openFurnish() { document.getElementById('m-furnish').showModal(); }
+function openTrade() { document.getElementById('m-trade').showModal(); }
+async function copyLink() { try { await navigator.clipboard.writeText(location.href.replace(/\/vertical$/, '')); alert('Link copied'); } catch { alert('Copy failed'); } }
+function addSample() { alert('Sample added (stub)'); }
+function saveDesign(id) { alert('Saved #' + id + ' (stub)'); }
+function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
+function escapeAttr(v) { return escapeHtml(v); }
+</script>
+</body>
+</html>
diff --git a/public/theme-variants/v6-drawer.html b/public/theme-variants/v6-drawer.html
new file mode 100644
index 0000000..c1ead2c
--- /dev/null
+++ b/public/theme-variants/v6-drawer.html
@@ -0,0 +1,402 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Drawer Heavy PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3; --ink: #1a1611; --muted: #8a8276; --line: #e2dccd;
+ --panel: #f3efe5; --panel-2: #ebe5d6; --accent: #c9b07a; --accent-dk: #1a1611;
+ --hero-bg: #e6dfd2; --calc: #ddd4ec; --coord: #d3e3e5;
+ --share: #f3dcce; --room: #cfdac4; --rate: #f0e7c3;
+ --drawer-bg: #1d1814; --drawer-ink: #f0eada; --drawer-mute: #9a8e72;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+ --serif: "Cormorant Garamond", Georgia, serif;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; }
+ body { background: var(--bg); color: var(--ink); font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; }
+ a { color: inherit; } button { font: inherit; cursor: pointer; }
+
+ header.topbar {
+ position: sticky; top: 0; z-index: 30;
+ background: rgba(250,248,243,.92); backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--line); padding: 10px 28px;
+ display: flex; align-items: center; gap: 24px;
+ }
+ header .brand { font: 600 18px/1 var(--serif); letter-spacing: .05em; }
+ header nav { font-size: 12.5px; color: var(--muted); display: flex; gap: 18px; flex-wrap: wrap; }
+ header nav a { text-decoration: none; }
+ header .grow { flex: 1; }
+ header .switcher { font-size: 11px; color: var(--muted); }
+ header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
+
+ .crumb { max-width: 1100px; margin: 0 auto; padding: 16px 28px 0; display: flex; gap: 16px; font-size: 12px; color: var(--muted); }
+ .crumb .grow { flex: 1; }
+ .crumb a { color: var(--ink); text-decoration: none; }
+
+ main.minimal {
+ max-width: 1100px; margin: 0 auto; padding: 20px 28px 60px;
+ display: flex; flex-direction: column; gap: 18px;
+ }
+
+ .hero {
+ aspect-ratio: 1/1; background: var(--hero-bg);
+ border-radius: 12px; overflow: hidden; position: relative;
+ max-height: 920px;
+ }
+ .hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .hero .pill {
+ position: absolute; top: 14px; right: 14px;
+ background: rgba(250,248,243,.94); color: var(--ink);
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em;
+ padding: 7px 11px; border-radius: 999px; text-transform: uppercase;
+ }
+ .hero .info {
+ position: absolute; left: 14px; bottom: 14px;
+ background: rgba(26,22,17,.78); color: #f0eada;
+ font: 600 11px/1 var(--sans); letter-spacing: .06em;
+ padding: 8px 12px; border-radius: 6px; text-transform: uppercase;
+ }
+
+ .title-card {
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 12px; padding: 16px 20px;
+ display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
+ }
+ .title-card .name { font: 500 26px/1.15 var(--serif); margin: 0; }
+ .title-card .meta { color: var(--muted); font-size: 13px; }
+ .title-card .meta .sep { padding: 0 6px; opacity: .55; }
+
+ .chip-rail {
+ display: flex; flex-wrap: wrap; gap: 8px;
+ padding: 6px 0;
+ }
+ .drawer-chip {
+ background: #fff; border: 1px solid var(--line);
+ border-radius: 8px; padding: 10px 14px;
+ font: 600 12px/1 var(--sans); color: var(--ink);
+ cursor: pointer; display: inline-flex; align-items: center; gap: 6px;
+ transition: all .12s;
+ }
+ .drawer-chip:hover { background: var(--accent); color: var(--accent-dk); border-color: var(--accent); }
+ .drawer-chip .arrow { color: var(--muted); font-size: 10px; }
+ .drawer-chip:hover .arrow { color: var(--accent-dk); }
+
+ .buy-box {
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 12px; padding: 18px;
+ display: grid; grid-template-columns: 1fr 1fr 1.2fr; gap: 12px;
+ align-items: stretch;
+ }
+ .buy-box .cta {
+ display: flex; align-items: center; justify-content: center;
+ border-radius: 8px; padding: 18px;
+ font: 700 12.5px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase;
+ border: 1px solid transparent;
+ }
+ .buy-box .cta.sample { background: var(--accent); color: var(--accent-dk); }
+ .buy-box .cta.buy { background: var(--accent-dk); color: #f0eada; }
+ .buy-box .summary {
+ background: var(--calc); border-radius: 8px; padding: 12px 14px;
+ font-size: 12px; color: var(--ink);
+ }
+ .buy-box .summary b { font: 500 16px var(--serif); display: block; }
+
+ /* ── drawers (dialog as right-side sheet) ──────────────────────────── */
+ dialog.drawer {
+ border: 0; padding: 0;
+ background: var(--drawer-bg); color: var(--drawer-ink);
+ width: 480px; max-width: 92vw;
+ height: 100vh; max-height: 100vh;
+ margin: 0 0 0 auto;
+ box-shadow: -16px 0 40px rgba(0,0,0,.32);
+ overflow-y: auto;
+ }
+ dialog.drawer::backdrop { background: rgba(20,17,14,.55); backdrop-filter: blur(4px); }
+ dialog.drawer .drawer-head {
+ position: sticky; top: 0; background: var(--drawer-bg);
+ padding: 18px 22px; border-bottom: 1px solid rgba(255,255,255,.08);
+ display: flex; align-items: center; gap: 12px; z-index: 5;
+ }
+ dialog.drawer .drawer-head h3 { margin: 0; font: 500 18px/1 var(--serif); color: var(--drawer-ink); flex: 1; }
+ dialog.drawer .drawer-close { background: transparent; border: 0; font-size: 22px; color: var(--drawer-mute); cursor: pointer; }
+ dialog.drawer .drawer-body { padding: 18px 22px 32px; }
+ dialog.drawer .drawer-row {
+ display: flex; justify-content: space-between; padding: 10px 0;
+ border-bottom: 1px solid rgba(255,255,255,.06); font-size: 13px;
+ }
+ dialog.drawer .drawer-row:last-child { border-bottom: 0; }
+ dialog.drawer .drawer-row .k { color: var(--drawer-mute); }
+ dialog.drawer .drawer-row .v { font-weight: 600; color: var(--drawer-ink); text-align: right; }
+ dialog.drawer .pill-list { display: flex; flex-wrap: wrap; gap: 6px; }
+ dialog.drawer .pill { font-size: 12px; padding: 6px 10px; border-radius: 999px; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12); color: var(--drawer-ink); }
+ dialog.drawer .pair-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
+ dialog.drawer .pair-grid .pair { aspect-ratio: 1/1; background: rgba(255,255,255,.08); border-radius: 6px; background-size: cover; background-position: center; }
+ dialog.drawer .tile-btns { display: flex; flex-wrap: wrap; gap: 8px; }
+ dialog.drawer .tile-btn { flex: 1; min-width: 110px; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12); padding: 12px 8px; border-radius: 8px; font: 600 12px/1.2 var(--sans); color: var(--drawer-ink); text-align: center; text-decoration: none; cursor: pointer; }
+ dialog.drawer .tile-btn:hover { background: rgba(255,255,255,.14); }
+ dialog.drawer .calc-mini { background: rgba(255,255,255,.05); padding: 14px; border-radius: 8px; }
+ dialog.drawer .calc-mini .row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
+ dialog.drawer .calc-mini label { font-size: 11px; color: var(--drawer-mute); display: block; margin-bottom: 2px; }
+ dialog.drawer .calc-mini input, dialog.drawer .calc-mini select { width: 100%; padding: 8px; border: 1px solid rgba(255,255,255,.12); border-radius: 6px; font: inherit; background: rgba(0,0,0,.25); color: var(--drawer-ink); }
+ dialog.drawer .calc-mini .total { padding: 10px; background: var(--accent); color: var(--accent-dk); border-radius: 6px; font-size: 13px; margin-top: 6px; }
+ dialog.drawer .calc-mini .total b { font: 500 17px var(--serif); }
+ dialog.drawer .swatches { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
+ dialog.drawer .sw { width: 38px; height: 38px; border-radius: 50%; border: 2px solid #2a241d; box-shadow: 0 0 0 1px var(--drawer-mute); }
+ dialog.drawer pre { white-space: pre-wrap; word-break: break-word; font: 11px/1.5 ui-monospace, Menlo, monospace; background: rgba(0,0,0,.32); color: var(--drawer-ink); padding: 12px; border-radius: 6px; max-height: 240px; overflow: auto; }
+ dialog.drawer .adjust .sl { display: grid; grid-template-columns: 80px 1fr 32px; align-items: center; gap: 8px; font-size: 12px; padding: 4px 0; }
+ dialog.drawer .adjust .sl input[type=range] { accent-color: var(--accent); width: 100%; }
+ dialog.drawer .adjust .sl .v { font-variant-numeric: tabular-nums; color: var(--drawer-mute); text-align: right; }
+ dialog.drawer .room-mini { height: 220px; border-radius: 8px; background: var(--room); background-size: cover; background-position: center; }
+ dialog.drawer .atscale-mini { height: 180px; border-radius: 8px; background: var(--hero-bg); background-repeat: repeat; background-size: 160px 160px; margin-top: 10px; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 28px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); margin-top: 30px; }
+ footer.site .h { font: 500 14px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading-shell { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; font-size: 14px; }
+
+ @media (max-width: 640px) {
+ header.topbar { flex-wrap: wrap; padding: 10px 14px; }
+ main.minimal { padding: 16px; }
+ .buy-box { grid-template-columns: 1fr; }
+ .title-card .name { font-size: 22px; }
+ }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <a href="/" class="brand">wallco.ai</a>
+ <nav>
+ <a href="/designs?sort=newest">Designs</a>
+ <a href="/designs?cat=monterey-mural">Murals</a>
+ <a href="/samples">Samples</a>
+ <a href="/basket">Basket</a>
+ <a href="/designer-studio">Designer Studio</a>
+ </nav>
+ <span class="grow"></span>
+ <span class="switcher">Theme: Drawer Heavy (V6) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<div id="root" class="loading-shell">Loading design…</div>
+
+<!-- 12 drawers — each chip opens one -->
+<dialog class="drawer" id="d-specs"><div class="drawer-head"><h3>Specs</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-specs-body"></div></dialog>
+<dialog class="drawer" id="d-calc"><div class="drawer-head"><h3>Calculator</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-calc-body"></div></dialog>
+<dialog class="drawer" id="d-recolor"><div class="drawer-head"><h3>Palette & recolor</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-recolor-body"></div></dialog>
+<dialog class="drawer" id="d-adjust"><div class="drawer-head"><h3>Image adjustments</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-adjust-body"></div></dialog>
+<dialog class="drawer" id="d-room"><div class="drawer-head"><h3>See in a room</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-room-body"></div></dialog>
+<dialog class="drawer" id="d-pairs"><div class="drawer-head"><h3>Pairs well with</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-pairs-body"></div></dialog>
+<dialog class="drawer" id="d-paints"><div class="drawer-head"><h3>Coordinate paints</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-paints-body"></div></dialog>
+<dialog class="drawer" id="d-stripes"><div class="drawer-head"><h3>Stripes & plaids</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-stripes-body"></div></dialog>
+<dialog class="drawer" id="d-share"><div class="drawer-head"><h3>Share this design</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-share-body"></div></dialog>
+<dialog class="drawer" id="d-furnish"><div class="drawer-head"><h3>Furnish this design</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-furnish-body"></div></dialog>
+<dialog class="drawer" id="d-trade"><div class="drawer-head"><h3>Trade tools</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-trade-body"></div></dialog>
+<dialog class="drawer" id="d-ratings"><div class="drawer-head"><h3>Ratings & reviews</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-ratings-body"></div></dialog>
+<dialog class="drawer" id="d-generator"><div class="drawer-head"><h3>Generator + prompt</h3><button class="drawer-close" onclick="this.closest('dialog').close()">×</button></div><div class="drawer-body" id="d-generator-body"></div></dialog>
+
+<script>
+(async function () {
+ const pathMatch = location.pathname.match(/\/design\/(\d+)/);
+ const idFromQuery = new URLSearchParams(location.search).get('id');
+ const id = pathMatch ? parseInt(pathMatch[1], 10) : (idFromQuery ? parseInt(idFromQuery, 10) : null);
+ if (!Number.isFinite(id) || id < 1) { document.getElementById('root').textContent = 'No id'; return; }
+ document.getElementById('back-to-classic').href = '/design/' + id;
+
+ let design;
+ try {
+ const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const j = await r.json();
+ design = j.design;
+ } catch (e) { document.getElementById('root').textContent = 'Failed: ' + e.message; return; }
+
+ const title = design.title || ('Design No.' + id);
+ const cat = design.category || 'wallpaper';
+ const catDisplay = (cat || '').replace(/^botanical-wonders · /, 'Botanical Wonders — ').replace(/^designer-zoo-calm/, 'Designer Zoo Calm').replace(/^cactus-pine-scenic/, 'Cactus Pine Scenic').replace(/^monterey-mural/, 'Coastal Scenic').replace(/^tree-mural/, 'Full-Room Scenic').replace(/^cactus-11ft-mural/, 'Single-Subject Mural').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : design.kind === 'mural_panel' ? 'Mural Panel' : design.kind === 'mural' ? 'Single-Subject Mural' : (design.kind || 'pattern');
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#888';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const w = Number(design.width_in) || 24, h = Number(design.height_in) || 24;
+ const repeatLabel = w + '"' + (w !== h ? ' × ' + h + '"' : '');
+ const rollOrPanel = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const roomMockup = (design.room_mockups && (typeof design.room_mockups === 'object' && !Array.isArray(design.room_mockups) ? Object.keys(design.room_mockups)[0] : design.room_mockups[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<div class="crumb">
+ <a href="/designs">Designs</a><span>›</span><a href="/designs?cat=${encodeURIComponent(cat)}">${escapeHtml(catDisplay)}</a>
+ <span class="grow"></span>
+ <span><a href="/design/${id - 2}?theme=drawer">← prev</a> · <a href="/design/${id + 2}?theme=drawer">next →</a></span>
+</div>
+
+<main class="minimal">
+ <div class="hero">
+ <img loading="eager" src="${img}" alt="${escapeAttr(title)}">
+ <span class="pill">${design.is_published ? 'Live' : 'Curating'}</span>
+ <span class="info">${escapeHtml(title)}</span>
+ </div>
+
+ <div class="title-card">
+ <h1 class="name">${escapeHtml(title)}</h1>
+ <div class="meta">${escapeHtml(catDisplay)}<span class="sep">·</span>${escapeHtml(kindLabel)}<span class="sep">·</span>${rollOrPanel}<span class="sep">·</span>${escapeHtml(dominant)}<span class="sep">·</span>${escapeHtml(handle)}</div>
+ </div>
+
+ <div class="chip-rail">
+ <button class="drawer-chip" data-drawer="specs">Specs <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="calc">Calc <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="recolor">Recolor <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="adjust">Adjust <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="room">Room <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="pairs">Pairs <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="paints">Paints <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="stripes">Stripes <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="share">Share <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="furnish">Furnish <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="trade">Trade <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="ratings">Ratings <span class="arrow">↗</span></button>
+ <button class="drawer-chip" data-drawer="generator">Generator <span class="arrow">↗</span></button>
+ </div>
+
+ <div class="buy-box">
+ <button class="cta sample" onclick="addSample()">Add sample · $4.25</button>
+ <button class="cta buy" onclick="document.getElementById('d-calc').showModal()">Buy by the roll →</button>
+ <div class="summary">
+ <b id="buy-summary">— rolls · $—</b>
+ <span>tap <b>Calc</b> to configure</span>
+ </div>
+ </div>
+</main>
+
+<footer class="site">
+ <div class="h">wallco.ai</div>
+ <div>A Designer Wallcoverings company · Sherman Oaks, CA</div>
+</footer>
+`;
+
+ // Populate drawer bodies
+ document.getElementById('d-specs-body').innerHTML = `
+ <div class="drawer-row"><span class="k">Category</span><span class="v">${escapeHtml(catDisplay)}</span></div>
+ <div class="drawer-row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div>
+ <div class="drawer-row"><span class="k">Width</span><span class="v">${w}"</span></div>
+ <div class="drawer-row"><span class="k">Repeat</span><span class="v">${repeatLabel}</span></div>
+ <div class="drawer-row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div>
+ <div class="drawer-row"><span class="k">${isMural ? 'Panel size' : 'Roll length'}</span><span class="v">${isMural ? (w + '" × 11 ft') : '27 ft'}</span></div>
+ <div class="drawer-row"><span class="k">Material</span><span class="v">Type II vinyl, water-resist</span></div>
+ <div class="drawer-row"><span class="k">Designed by</span><span class="v">${escapeHtml(generator)}</span></div>
+ <div class="drawer-row"><span class="k">Pattern No.</span><span class="v">${id}</span></div>
+ <div class="drawer-row"><span class="k">Handle</span><span class="v">${escapeHtml(handle)}</span></div>
+ <div class="drawer-row"><span class="k">Dominant hex</span><span class="v">${escapeHtml(dominant)}</span></div>
+ <div class="drawer-row"><span class="k">Status</span><span class="v">${design.is_published ? 'Live' : 'Curating'}</span></div>
+ `;
+
+ document.getElementById('d-calc-body').innerHTML = `
+ <div class="calc-mini">
+ <div class="row">
+ <div><label>Wall W (ft)</label><input id="dcw" type="number" min="1" max="80" step="0.5" value="10"></div>
+ <div><label>Wall H (ft)</label><input id="dch" type="number" min="1" max="20" step="0.5" value="9"></div>
+ </div>
+ <div class="row">
+ <div><label>Roll width</label><select id="dcrw"><option value="24">24" residential</option><option value="36">36" wide-roll</option><option value="52">52" commercial</option></select></div>
+ <div><label>Repeat</label><input value='${repeatLabel}' readonly></div>
+ </div>
+ <div class="total" id="dctotal">— rolls · $—</div>
+ </div>
+ `;
+ const dcw = document.getElementById('dcw'), dch = document.getElementById('dch'), dcrw = document.getElementById('dcrw');
+ const dctotal = document.getElementById('dctotal');
+ const buySummary = document.getElementById('buy-summary');
+ function recalc() {
+ const ww = parseFloat(dcw.value) || 0, wh = parseFloat(dch.value) || 0;
+ const rwInches = parseFloat(dcrw.value) || 24;
+ if (ww < 1 || wh < 1) { dctotal.textContent = '— rolls · $—'; buySummary.textContent = '— rolls · $—'; return; }
+ let txt;
+ if (isMural) {
+ const panels = Math.ceil((ww * 12) / rwInches); const cost = panels * 245;
+ txt = '<b>' + panels + '</b> panels · <b>$' + cost.toLocaleString() + '</b>';
+ buySummary.innerHTML = panels + ' panels · $' + cost.toLocaleString();
+ } else {
+ const widthPanels = Math.ceil((ww * 12) / rwInches);
+ const rollsPerPanel = Math.ceil(wh / 27);
+ const rolls = widthPanels * rollsPerPanel; const cost = rolls * 165;
+ txt = '<b>' + rolls + '</b> rolls · <b>$' + cost.toLocaleString() + '</b>';
+ buySummary.innerHTML = rolls + ' rolls · $' + cost.toLocaleString();
+ }
+ dctotal.innerHTML = txt;
+ }
+ [dcw, dch, dcrw].forEach(el => el.addEventListener('input', recalc));
+ recalc();
+
+ document.getElementById('d-recolor-body').innerHTML = `
+ <p style="font-size:13px; color:var(--drawer-mute); margin:0 0 14px">Drag a swatch onto the live preview to recolor a layer.</p>
+ <div class="swatches">
+ ${palette.length ? palette.slice(0, 5).map(p => `<div class="sw" title="${escapeAttr(p.hex)} · ${(p.pct || 0).toFixed(1)}%" style="background:${escapeAttr(p.hex)}"></div>`).join('') : `<div class="sw" style="background:${escapeAttr(dominant)}"></div>`}
+ </div>
+ <div style="font-size:11px; color:var(--drawer-mute); margin-top:10px">Dominant ${escapeHtml(dominant)}</div>
+ `;
+
+ document.getElementById('d-adjust-body').innerHTML = `
+ <div class="adjust">
+ <div class="sl"><label>Hue</label><input type="range" min="-180" max="180" value="0"><span class="v">0</span></div>
+ <div class="sl"><label>Saturation</label><input type="range" min="0" max="200" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Brightness</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Contrast</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Sepia</label><input type="range" min="0" max="100" value="0"><span class="v">0</span></div>
+ <div class="sl"><label>Blur</label><input type="range" min="0" max="20" value="0"><span class="v">0</span></div>
+ </div>
+ `;
+ document.querySelectorAll('#d-adjust-body .sl').forEach(row => {
+ const r = row.querySelector('input[type=range]'); const v = row.querySelector('.v');
+ r.addEventListener('input', () => { v.textContent = r.value; });
+ });
+
+ document.getElementById('d-room-body').innerHTML = `
+ <div class="room-mini" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : ''}></div>
+ <div class="atscale-mini" style="background-image:url('${img}')"></div>
+ <p style="font-size:12px; color:var(--drawer-mute); margin-top:12px">Curated rendering · swap room template above</p>
+ `;
+
+ document.getElementById('d-pairs-body').innerHTML = `<div class="pair-grid"><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div></div><p style="font-size:12px; color:var(--drawer-mute); margin-top:12px">6 designer wallpapers that pair with ${escapeHtml(title)}</p>`;
+
+ document.getElementById('d-paints-body').innerHTML = `<div class="pill-list">
+ <a class="pill" href="#">Sherwin-Williams</a><a class="pill" href="#">Dunn-Edwards</a><a class="pill" href="#">Benjamin Moore</a><a class="pill" href="#">Farrow & Ball</a><a class="pill" href="#">Behr</a><a class="pill" href="#">PPG</a><a class="pill" href="#">RAL</a>
+ </div>`;
+
+ document.getElementById('d-stripes-body').innerHTML = `<div class="pair-grid" style="grid-template-columns:repeat(4,1fr)">${Array.from({length: 12}).map(() => '<div class="pair"></div>').join('')}</div><p style="font-size:12px; color:var(--drawer-mute); margin-top:12px">24 stripes & plaids generated for this colorway</p>`;
+
+ document.getElementById('d-share-body').innerHTML = `<div class="tile-btns"><a class="tile-btn" href="#" onclick="event.preventDefault()">Pinterest</a><a class="tile-btn" href="#" onclick="event.preventDefault()">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();event.preventDefault()">Copy link</a></div>`;
+
+ document.getElementById('d-furnish-body').innerHTML = `<p style="font-size:13px; color:var(--drawer-mute); margin:0 0 14px">Generate AI furnishings curated to this palette.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>`;
+
+ document.getElementById('d-trade-body').innerHTML = `<div class="tile-btns"><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet PDF</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Compare 2</a><a class="tile-btn" href="#" onclick="saveDesign(${id});event.preventDefault()">Save</a></div>`;
+
+ document.getElementById('d-ratings-body').innerHTML = `<div style="font-size:30px; letter-spacing:6px; color:#e8c66e">★★★★☆</div><div style="font-size:12px; color:var(--drawer-mute); margin-top:6px">Curated · 4.2 · ${escapeHtml(design.user_vote_count || 0)} votes</div>`;
+
+ document.getElementById('d-generator-body').innerHTML = `<div style="font-size:12px; color:var(--drawer-mute); margin-bottom:8px">Generator: <b style="color:var(--drawer-ink)">${escapeHtml(generator)}</b> · seed <code style="color:var(--drawer-ink)">${escapeHtml(design.seed || '—')}</code></div><pre>${escapeHtml((design.prompt || '').slice(0, 1500))}${(design.prompt && design.prompt.length > 1500) ? '\n…[truncated]' : ''}</pre>`;
+
+ // Wire chip click → open drawer
+ document.querySelectorAll('[data-drawer]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const name = btn.getAttribute('data-drawer');
+ const d = document.getElementById('d-' + name);
+ if (d && typeof d.showModal === 'function') d.showModal();
+ });
+ });
+})();
+
+async function copyLink() { try { await navigator.clipboard.writeText(location.href.replace(/\/drawer$/, '')); alert('Link copied'); } catch { alert('Copy failed'); } }
+function addSample() { alert('Sample added (stub)'); }
+function saveDesign(id) { alert('Saved #' + id + ' (stub)'); }
+function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
+function escapeAttr(v) { return escapeHtml(v); }
+</script>
+</body>
+</html>
diff --git a/public/theme-variants/v7-editorial.html b/public/theme-variants/v7-editorial.html
new file mode 100644
index 0000000..2d0ab49
--- /dev/null
+++ b/public/theme-variants/v7-editorial.html
@@ -0,0 +1,353 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Editorial Magazine PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3; --ink: #1a1611; --muted: #8a8276; --line: #e2dccd;
+ --panel: #f3efe5; --panel-2: #ebe5d6; --accent: #c9b07a; --accent-dk: #1a1611;
+ --hero-bg: #e6dfd2; --calc: #ddd4ec; --coord: #d3e3e5;
+ --share: #f3dcce; --room: #cfdac4; --rate: #f0e7c3;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+ --serif: "Cormorant Garamond", Georgia, serif;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; }
+ body { background: var(--bg); color: var(--ink); font: 14.5px/1.65 var(--sans); -webkit-font-smoothing: antialiased; }
+ a { color: inherit; } button { font: inherit; cursor: pointer; }
+
+ header.topbar {
+ position: sticky; top: 0; z-index: 30;
+ background: rgba(250,248,243,.92); backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--line); padding: 10px 28px;
+ display: flex; align-items: center; gap: 24px;
+ }
+ header .brand { font: 600 18px/1 var(--serif); letter-spacing: .05em; }
+ header nav { font-size: 12.5px; color: var(--muted); display: flex; gap: 18px; flex-wrap: wrap; }
+ header nav a { text-decoration: none; }
+ header .grow { flex: 1; }
+ header .switcher { font-size: 11px; color: var(--muted); }
+ header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
+
+ /* hero — 70vh editorial */
+ .hero-wrap { position: relative; width: 100%; height: 78vh; max-height: 980px; min-height: 600px; background: var(--hero-bg); overflow: hidden; }
+ .hero-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .hero-wrap .overlay {
+ position: absolute; left: 0; right: 0; bottom: 0;
+ padding: 60px 40px 40px;
+ background: linear-gradient(to top, rgba(7,6,5,.88) 0%, rgba(7,6,5,.45) 60%, rgba(7,6,5,0) 100%);
+ color: #f0eada;
+ }
+ .hero-wrap .overlay .kicker { font: 600 11px/1 var(--sans); letter-spacing: .14em; color: var(--accent); text-transform: uppercase; margin-bottom: 12px; }
+ .hero-wrap .overlay h1 { font: 400 56px/1.05 var(--serif); margin: 0 0 12px; max-width: 1100px; }
+ .hero-wrap .overlay blockquote { font: 400 19px/1.5 var(--serif); font-style: italic; color: #d8cea8; margin: 0; max-width: 720px; padding-left: 18px; border-left: 2px solid var(--accent); }
+ .hero-wrap .overlay .meta { margin-top: 18px; font-size: 12.5px; color: #c2b88a; letter-spacing: .04em; }
+ .hero-wrap .pill { position: absolute; top: 18px; right: 24px; background: rgba(250,248,243,.94); color: var(--ink); font: 600 10.5px/1 var(--sans); letter-spacing: .06em; padding: 7px 12px; border-radius: 999px; text-transform: uppercase; }
+
+ /* breadcrumb */
+ .crumb { max-width: 1320px; margin: 0 auto; padding: 18px 32px 0; display: flex; gap: 16px; font-size: 12px; color: var(--muted); }
+ .crumb .grow { flex: 1; }
+ .crumb a { color: var(--ink); text-decoration: none; }
+
+ /* magazine body — 2 cols + marginalia */
+ main.editorial { max-width: 1320px; margin: 0 auto; padding: 28px 32px 0; }
+ .grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 280px;
+ gap: 32px;
+ align-items: start;
+ }
+
+ .col1 .lead { font: 400 18px/1.65 var(--serif); color: var(--ink); }
+ .col1 .lead .drop { font: 500 56px/.85 var(--serif); float: left; margin: 6px 8px 0 0; color: var(--accent); }
+ .col1 .lead p { margin: 0 0 14px; }
+
+ .col2 h3 { font: 500 16px/1.2 var(--serif); margin: 0 0 12px; color: var(--ink); }
+ .pair-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
+ .pair-grid .pair { aspect-ratio: 1/1; background: var(--panel); border-radius: 6px; background-size: cover; background-position: center; border: 1px solid var(--line); }
+ .col2 .designer-note { margin-top: 14px; font: 400 13px/1.55 var(--serif); color: var(--muted); font-style: italic; }
+
+ /* marginalia (specs + palette) */
+ .margin {
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 10px; padding: 16px;
+ font-size: 12.5px;
+ position: sticky; top: 70px;
+ }
+ .margin h4 { margin: 0 0 10px; font: 600 10.5px/1.2 var(--sans); text-transform: uppercase; letter-spacing: .12em; color: var(--muted); }
+ .specs-row { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid var(--line); font-size: 12px; }
+ .specs-row:last-child { border-bottom: 0; }
+ .specs-row .k { color: var(--muted); }
+ .specs-row .v { font-weight: 600; text-align: right; }
+ .margin .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0 4px; }
+ .margin .sw { width: 28px; height: 28px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .margin .pull { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--line); }
+ .margin .pull a { display: block; padding: 6px 0; font-size: 12px; color: var(--ink); text-decoration: none; }
+ .margin .pull a:hover { color: var(--accent); }
+ .margin .pull a:after { content: ' ↗'; color: var(--muted); }
+
+ /* at-scale interrupt */
+ .interrupt {
+ width: 100%;
+ height: 460px;
+ margin: 48px 0 0;
+ background: var(--hero-bg);
+ background-repeat: repeat;
+ background-size: 220px 220px;
+ position: relative;
+ }
+ .interrupt .label {
+ position: absolute; left: 32px; bottom: 28px;
+ background: rgba(26,22,17,.85); color: #f0eada;
+ font: 600 11.5px/1 var(--sans); letter-spacing: .06em;
+ padding: 10px 16px; border-radius: 6px; text-transform: uppercase;
+ }
+ .interrupt .slider {
+ position: absolute; right: 32px; bottom: 28px;
+ background: rgba(250,248,243,.96); border-radius: 999px; padding: 8px 16px;
+ font: 500 11px/1 var(--sans); display: flex; align-items: center; gap: 10px;
+ }
+ .interrupt .slider input[type=range] { width: 160px; accent-color: var(--accent); }
+
+ /* buy ribbon below interrupt */
+ .buy-ribbon {
+ background: var(--accent-dk); color: #f0eada;
+ padding: 26px 32px;
+ display: flex; gap: 18px; align-items: center; flex-wrap: wrap;
+ }
+ .buy-ribbon .cta {
+ display: inline-flex; align-items: center; justify-content: center;
+ padding: 16px 24px; border-radius: 8px; border: 0;
+ font: 700 12.5px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase;
+ }
+ .buy-ribbon .cta.sample { background: var(--accent); color: var(--accent-dk); }
+ .buy-ribbon .cta.buy { background: var(--bg); color: var(--ink); }
+ .buy-ribbon .calc { background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.14); padding: 14px 20px; border-radius: 8px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; flex: 1; min-width: 280px; }
+ .buy-ribbon .calc label { font-size: 11px; color: #c2b88a; }
+ .buy-ribbon .calc input { width: 64px; padding: 6px 8px; border-radius: 4px; border: 1px solid rgba(255,255,255,.18); background: rgba(0,0,0,.25); color: #f0eada; font: inherit; }
+ .buy-ribbon .calc .summary { font: 500 14px var(--serif); margin-left: auto; }
+
+ /* collapsed footer rows */
+ .footer-acc { max-width: 1320px; margin: 30px auto 0; padding: 0 32px 40px; display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
+ details.acc { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px 16px; font-size: 13px; }
+ details.acc[open] { padding-bottom: 14px; }
+ details.acc summary { cursor: pointer; font-weight: 600; list-style: none; display: flex; justify-content: space-between; align-items: center; }
+ details.acc summary::after { content: '▾'; color: var(--muted); transition: transform .2s; font-size: 11px; }
+ details.acc[open] summary::after { transform: rotate(180deg); }
+ details.acc summary .hint { font-weight: 400; font-size: 11.5px; color: var(--muted); margin-left: auto; margin-right: 10px; }
+ details.acc .acc-body { margin-top: 12px; line-height: 1.6; font-size: 12.5px; }
+ .pill-list { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 9px; border-radius: 999px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); padding: 10px 8px; border-radius: 8px; font: 600 11.5px/1.2 var(--sans); color: var(--ink); text-align: center; text-decoration: none; }
+ .tile-btn:hover { background: #fff; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 28px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 14px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading-shell { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+
+ dialog.modal { border: 0; border-radius: 14px; padding: 0; max-width: 540px; width: 92vw; box-shadow: 0 24px 80px rgba(0,0,0,.25); background: var(--bg); }
+ dialog.modal::backdrop { background: rgba(20,17,14,.55); backdrop-filter: blur(4px); }
+ dialog.modal header { display: flex; justify-content: space-between; align-items: center; padding: 18px 22px; border-bottom: 1px solid var(--line); }
+ dialog.modal header h3 { margin: 0; font: 500 18px/1 var(--serif); }
+ dialog.modal .close { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ dialog.modal .body { padding: 18px 22px 24px; }
+
+ @media (max-width: 1024px) {
+ .grid { grid-template-columns: 1fr; }
+ .margin { position: static; }
+ .hero-wrap .overlay h1 { font-size: 38px; }
+ .interrupt { height: 320px; }
+ }
+ @media (max-width: 640px) {
+ header.topbar { flex-wrap: wrap; padding: 10px 14px; }
+ .hero-wrap .overlay { padding: 30px 16px 18px; }
+ .hero-wrap .overlay h1 { font-size: 30px; }
+ main.editorial { padding: 18px 16px 0; }
+ .footer-acc { grid-template-columns: 1fr; padding: 0 16px 24px; }
+ }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <a href="/" class="brand">wallco.ai</a>
+ <nav>
+ <a href="/designs?sort=newest">Designs</a><a href="/designs?cat=monterey-mural">Murals</a><a href="/samples">Samples</a><a href="/basket">Basket</a><a href="/designer-studio">Designer Studio</a>
+ </nav>
+ <span class="grow"></span>
+ <span class="switcher">Theme: Editorial Magazine (V7) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<div id="root" class="loading-shell">Loading design…</div>
+
+<dialog class="modal" id="m-share"><header><h3>Share</h3><button class="close" onclick="document.getElementById('m-share').close()">×</button></header><div class="body" id="m-share-body"></div></dialog>
+<dialog class="modal" id="m-furnish"><header><h3>Furnish</h3><button class="close" onclick="document.getElementById('m-furnish').close()">×</button></header><div class="body" id="m-furnish-body"></div></dialog>
+<dialog class="modal" id="m-trade"><header><h3>Trade tools</h3><button class="close" onclick="document.getElementById('m-trade').close()">×</button></header><div class="body" id="m-trade-body"></div></dialog>
+
+<script>
+(async function () {
+ const pathMatch = location.pathname.match(/\/design\/(\d+)/);
+ const idFromQuery = new URLSearchParams(location.search).get('id');
+ const id = pathMatch ? parseInt(pathMatch[1], 10) : (idFromQuery ? parseInt(idFromQuery, 10) : null);
+ if (!Number.isFinite(id) || id < 1) { document.getElementById('root').textContent = 'No id'; return; }
+ document.getElementById('back-to-classic').href = '/design/' + id;
+
+ let design;
+ try {
+ const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const j = await r.json();
+ design = j.design;
+ } catch (e) { document.getElementById('root').textContent = 'Failed: ' + e.message; return; }
+
+ const title = design.title || ('Design No.' + id);
+ const cat = design.category || 'wallpaper';
+ const catDisplay = (cat || '').replace(/^botanical-wonders · /, 'Botanical Wonders — ').replace(/^designer-zoo-calm/, 'Designer Zoo Calm').replace(/^cactus-pine-scenic/, 'Cactus Pine Scenic').replace(/^monterey-mural/, 'Coastal Scenic').replace(/^tree-mural/, 'Full-Room Scenic').replace(/^cactus-11ft-mural/, 'Single-Subject Mural').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : design.kind === 'mural_panel' ? 'Mural Panel' : design.kind === 'mural' ? 'Single-Subject Mural' : (design.kind || 'pattern');
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#888';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const w = Number(design.width_in) || 24, h = Number(design.height_in) || 24;
+ const repeatLabel = w + '"' + (w !== h ? ' × ' + h + '"' : '');
+ const rollOrPanel = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const roomMockup = (design.room_mockups && (typeof design.room_mockups === 'object' && !Array.isArray(design.room_mockups) ? Object.keys(design.room_mockups)[0] : design.room_mockups[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+
+ // ── editorial copy (templated from design data) ──
+ const pullQuote = isMural
+ ? '“A scenic wallcovering meant to be lived inside, not merely seen.”'
+ : '“Tone-on-tone restraint — a pattern that listens before it speaks.”';
+ const leadCopy = `<p><span class="drop">${escapeHtml(title.charAt(0))}</span>${escapeHtml(title.slice(1))} arrives from the <em>${escapeHtml(catDisplay)}</em> series — a quiet study in ${escapeHtml(palette.length > 2 ? 'layered neutrals' : 'two-color restraint')} anchored by a ${escapeHtml(dominant)} ground. ${escapeHtml(kindLabel)}, ${rollOrPanel.toLowerCase()}.</p><p>The composition reads as a single hand-blocked screen rather than a digital pattern. Motifs are placed with the discipline of a herbarium plate: each instance the same scale, the same orientation, the same ink — repetition as architecture rather than decoration.</p><p>Specified by interior designers for ${escapeHtml(palette.length > 3 ? 'salon walls and powder rooms' : 'libraries and reading corners')}, it sits well alongside hand-loomed neutrals and lacquered millwork.</p>`;
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+
+<div class="hero-wrap">
+ <img loading="eager" src="${img}" alt="${escapeAttr(title)}">
+ <span class="pill">${design.is_published ? 'Live' : 'Curating'}</span>
+ <div class="overlay">
+ <div class="kicker">${escapeHtml(catDisplay)} · No.${id}</div>
+ <h1>${escapeHtml(title)}</h1>
+ <blockquote>${escapeHtml(pullQuote)}</blockquote>
+ <div class="meta">${escapeHtml(kindLabel)} · ${rollOrPanel} · ${escapeHtml(handle)}</div>
+ </div>
+</div>
+
+<div class="crumb">
+ <a href="/designs">Designs</a><span>›</span><a href="/designs?cat=${encodeURIComponent(cat)}">${escapeHtml(catDisplay)}</a>
+ <span class="grow"></span>
+ <span><a href="/design/${id - 2}?theme=editorial">← prev</a> · <a href="/design/${id + 2}?theme=editorial">next →</a></span>
+</div>
+
+<main class="editorial">
+ <div class="grid">
+ <div class="col1">
+ <div class="lead">${leadCopy}</div>
+ </div>
+ <div class="col2">
+ <h3>Pairs well with</h3>
+ <div class="pair-grid">
+ <div class="pair"></div><div class="pair"></div><div class="pair"></div>
+ <div class="pair"></div><div class="pair"></div><div class="pair"></div>
+ </div>
+ <p class="designer-note">Six designer wallpapers selected from the wallco.ai catalog whose palette and scale complement ${escapeHtml(title)}.</p>
+ </div>
+ <aside class="margin">
+ <h4>Specifications</h4>
+ <div class="specs-row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div>
+ <div class="specs-row"><span class="k">Width</span><span class="v">${w}"</span></div>
+ <div class="specs-row"><span class="k">Repeat</span><span class="v">${repeatLabel}</span></div>
+ <div class="specs-row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div>
+ <div class="specs-row"><span class="k">${isMural ? 'Panel' : 'Roll length'}</span><span class="v">${isMural ? (w + '" × 11 ft') : '27 ft'}</span></div>
+ <div class="specs-row"><span class="k">Material</span><span class="v">Type II vinyl</span></div>
+ <div class="specs-row"><span class="k">Pattern No.</span><span class="v">${id}</span></div>
+ <h4 style="margin-top:18px">Palette</h4>
+ <div class="swatches">
+ ${palette.length ? palette.slice(0, 5).map(p => `<div class="sw" title="${escapeAttr(p.hex)} · ${(p.pct || 0).toFixed(1)}%" style="background:${escapeAttr(p.hex)}"></div>`).join('') : `<div class="sw" style="background:${escapeAttr(dominant)}"></div>`}
+ </div>
+ <div style="font-size:11px; color:var(--muted); margin-top:6px">Dominant ${escapeHtml(dominant)}</div>
+ <div class="pull">
+ <a href="#interrupt">See at scale ↓</a>
+ <a href="#" onclick="openShare();return false">Share this design</a>
+ <a href="#" onclick="openFurnish();return false">Furnish with this</a>
+ <a href="/design/${id}/tearsheet" target="_blank">Tear sheet PDF</a>
+ </div>
+ </aside>
+ </div>
+</main>
+
+<div class="interrupt" id="interrupt" style="background-image:url('${img}')">
+ <span class="label">At-scale preview · 10 × 9 ft wall</span>
+ <span class="slider">scale <input type="range" min="120" max="400" value="220" id="scale-slider"></span>
+</div>
+
+<div class="buy-ribbon">
+ <button class="cta sample" onclick="addSample()">Add sample · $4.25</button>
+ <button class="cta buy" onclick="document.getElementById('rcw').focus()">Buy by the roll →</button>
+ <div class="calc">
+ <label>W</label><input id="rcw" type="number" min="1" max="80" step="0.5" value="10">
+ <label>× H</label><input id="rch" type="number" min="1" max="20" step="0.5" value="9">
+ <label>Roll</label>
+ <select id="rcrw" style="padding:6px 8px; border-radius:4px; border:1px solid rgba(255,255,255,.18); background:rgba(0,0,0,.25); color:#f0eada; font:inherit"><option value="24">24"</option><option value="36">36"</option><option value="52">52"</option></select>
+ <span class="summary" id="rsum">— rolls · $—</span>
+ </div>
+</div>
+
+<div class="footer-acc">
+ <details class="acc"><summary>Coordinate paints <span class="hint">SW · BM · F&B</span></summary><div class="acc-body"><div class="pill-list"><a class="pill" href="#">Sherwin-Williams</a><a class="pill" href="#">Dunn-Edwards</a><a class="pill" href="#">Benjamin Moore</a><a class="pill" href="#">Farrow & Ball</a><a class="pill" href="#">Behr</a><a class="pill" href="#">PPG</a></div></div></details>
+ <details class="acc"><summary>Coordinate stripes & plaids <span class="hint">24 generated</span></summary><div class="acc-body"><div class="pair-grid" style="grid-template-columns:repeat(6,1fr)">${Array.from({length: 12}).map(() => '<div class="pair"></div>').join('')}</div></div></details>
+ <details class="acc"><summary>See in a room <span class="hint">residential preview</span></summary><div class="acc-body"><div style="height:200px; border-radius:8px; background:var(--room); background-size:cover; background-position:center; ${roomImgUrl ? `background-image:url('${roomImgUrl}')` : ''}"></div></div></details>
+ <details class="acc"><summary>Image adjustments <span class="hint">hue/sat/bri/contrast</span></summary><div class="acc-body" style="font-size:12px; color:var(--muted)">Sliders for hue · saturation · brightness · contrast · sepia · blur (omitted in editorial; open in Classic view)</div></details>
+ <details class="acc"><summary>Share / Furnish / Trade tools <span class="hint">3 actions</span></summary><div class="acc-body"><div class="tile-btns"><a class="tile-btn" href="#" onclick="openShare();return false">Share</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Furnish</a><a class="tile-btn" href="#" onclick="openTrade();return false">Trade tools</a></div></div></details>
+ <details class="acc"><summary>Ratings & reviews <span class="hint">★★★★☆ · ${escapeHtml(design.user_vote_count || 0)}</span></summary><div class="acc-body"><div style="font-size:22px; letter-spacing:4px; color:#b89a4a">★★★★☆</div><div style="font-size:11.5px; color:var(--muted); margin-top:4px">Curated · 4.2</div></div></details>
+ <details class="acc"><summary>Generator + prompt <span class="hint">wallco.ai · seed ${escapeHtml(design.seed || '—')}</span></summary><div class="acc-body"><pre style="font:11px/1.5 ui-monospace,Menlo,monospace; background:var(--panel); padding:10px; border-radius:6px; max-height:200px; overflow:auto; margin:0; white-space:pre-wrap; word-break:break-word">${escapeHtml((design.prompt || '').slice(0, 1200))}${(design.prompt && design.prompt.length > 1200) ? '\n…[truncated]' : ''}</pre></div></details>
+ <details class="acc"><summary>Full specs ledger <span class="hint">12 fields</span></summary><div class="acc-body"><div class="specs-row"><span class="k">Category</span><span class="v">${escapeHtml(catDisplay)}</span></div><div class="specs-row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div><div class="specs-row"><span class="k">Width</span><span class="v">${w}"</span></div><div class="specs-row"><span class="k">Repeat</span><span class="v">${repeatLabel}</span></div><div class="specs-row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div><div class="specs-row"><span class="k">Material</span><span class="v">Type II vinyl</span></div><div class="specs-row"><span class="k">Designed by</span><span class="v">${escapeHtml(generator)}</span></div><div class="specs-row"><span class="k">Pattern No.</span><span class="v">${id}</span></div><div class="specs-row"><span class="k">Handle</span><span class="v">${escapeHtml(handle)}</span></div><div class="specs-row"><span class="k">Dominant hex</span><span class="v">${escapeHtml(dominant)}</span></div><div class="specs-row"><span class="k">Status</span><span class="v">${design.is_published ? 'Live' : 'Curating'}</span></div></div></details>
+</div>
+
+<footer class="site">
+ <div class="h">wallco.ai — Curated-original wallpaper & murals</div>
+ <div>A Designer Wallcoverings company · Sherman Oaks, CA</div>
+</footer>
+`;
+
+ // ribbon calc
+ const rcw = document.getElementById('rcw'), rch = document.getElementById('rch'), rcrw = document.getElementById('rcrw');
+ const rsum = document.getElementById('rsum');
+ function recalc() {
+ const ww = parseFloat(rcw.value) || 0, wh = parseFloat(rch.value) || 0;
+ const rwi = parseFloat(rcrw.value) || 24;
+ if (ww < 1 || wh < 1) { rsum.textContent = '— rolls · $—'; return; }
+ if (isMural) { const p = Math.ceil((ww * 12) / rwi); rsum.textContent = p + ' panels · $' + (p * 245).toLocaleString(); }
+ else { const wp = Math.ceil((ww * 12) / rwi); const r = wp * Math.ceil(wh / 27); rsum.textContent = r + ' rolls · $' + (r * 165).toLocaleString(); }
+ }
+ [rcw, rch, rcrw].forEach(el => el.addEventListener('input', recalc));
+ recalc();
+
+ // scale slider
+ const scale = document.getElementById('scale-slider');
+ const interrupt = document.getElementById('interrupt');
+ scale.addEventListener('input', () => { interrupt.style.backgroundSize = scale.value + 'px ' + scale.value + 'px'; });
+
+ document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
+ document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
+ document.getElementById('m-trade-body').innerHTML = '<p>Trade tools.</p><div class="tile-btns"><a class="tile-btn" href="/design/' + id + '/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email</a><a class="tile-btn" href="#">Compare</a><a class="tile-btn" href="#">Save</a></div>';
+})();
+
+function openShare() { document.getElementById('m-share').showModal(); }
+function openFurnish() { document.getElementById('m-furnish').showModal(); }
+function openTrade() { document.getElementById('m-trade').showModal(); }
+async function copyLink() { try { await navigator.clipboard.writeText(location.href.replace(/\/editorial$/, '')); alert('Link copied'); } catch { alert('Copy failed'); } }
+function addSample() { alert('Sample added (stub)'); }
+function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
+function escapeAttr(v) { return escapeHtml(v); }
+</script>
+</body>
+</html>
diff --git a/public/theme-variants/v8-spec.html b/public/theme-variants/v8-spec.html
new file mode 100644
index 0000000..09a19a7
--- /dev/null
+++ b/public/theme-variants/v8-spec.html
@@ -0,0 +1,342 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Compact Spec-Sheet PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3; --ink: #1a1611; --muted: #8a8276; --line: #e2dccd;
+ --panel: #f3efe5; --panel-2: #ebe5d6; --accent: #c9b07a; --accent-dk: #1a1611;
+ --hero-bg: #e6dfd2; --calc: #ddd4ec; --coord: #d3e3e5;
+ --share: #f3dcce; --room: #cfdac4; --rate: #f0e7c3;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+ --serif: "Cormorant Garamond", Georgia, serif;
+ --mono: ui-monospace, Menlo, "SF Mono", monospace;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; }
+ body { background: var(--bg); color: var(--ink); font: 13px/1.4 var(--sans); -webkit-font-smoothing: antialiased; }
+ a { color: inherit; } button { font: inherit; cursor: pointer; }
+
+ header.topbar {
+ position: sticky; top: 0; z-index: 30;
+ background: rgba(250,248,243,.92); backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--line); padding: 8px 24px;
+ display: flex; align-items: center; gap: 20px;
+ }
+ header .brand { font: 600 17px/1 var(--serif); letter-spacing: .05em; }
+ header nav { font-size: 12px; color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap; }
+ header nav a { text-decoration: none; }
+ header .grow { flex: 1; }
+ header .switcher { font-size: 11px; color: var(--muted); }
+ header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
+
+ .crumb { max-width: 1640px; margin: 0 auto; padding: 12px 24px 0; display: flex; gap: 14px; font-size: 11.5px; color: var(--muted); }
+ .crumb .grow { flex: 1; }
+ .crumb a { color: var(--ink); text-decoration: none; }
+
+ main.specsheet {
+ max-width: 1640px; margin: 0 auto; padding: 12px 24px 40px;
+ display: grid; grid-template-columns: minmax(0, 720px) minmax(0, 1fr);
+ gap: 20px; align-items: start;
+ }
+
+ .col-image { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
+ .image-main {
+ aspect-ratio: 1/1; background: var(--hero-bg);
+ border-radius: 8px; overflow: hidden; position: relative;
+ box-shadow: 0 1px 0 var(--line);
+ }
+ .image-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .image-main .pill {
+ position: absolute; top: 12px; right: 12px;
+ background: rgba(250,248,243,.94); color: var(--ink);
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em;
+ padding: 6px 10px; border-radius: 999px; text-transform: uppercase;
+ }
+ .thumbs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
+ .thumb {
+ aspect-ratio: 1/1; background: var(--panel); border: 1px solid var(--line);
+ border-radius: 6px; background-size: cover; background-position: center;
+ cursor: pointer; transition: border-color .12s;
+ }
+ .thumb:hover { border-color: var(--accent); }
+ .chip-strip {
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 6px; padding: 8px 12px;
+ font: 600 11px/1.4 var(--mono); color: var(--ink); letter-spacing: .02em;
+ display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
+ }
+ .chip-strip .sep { color: var(--muted); }
+
+ /* right column — dense spec table */
+ .col-spec { min-width: 0; }
+ .name-bar {
+ background: var(--bg); border: 1px solid var(--line);
+ border-radius: 8px; padding: 14px 18px; margin-bottom: 12px;
+ }
+ .name-bar h1 { font: 500 26px/1.15 var(--serif); margin: 0 0 4px; }
+ .name-bar .meta { color: var(--muted); font-size: 12px; }
+
+ .cta-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }
+ .cta {
+ display: flex; align-items: center; justify-content: center;
+ border-radius: 8px; padding: 14px;
+ font: 700 12px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase;
+ border: 1px solid transparent;
+ }
+ .cta.sample { background: var(--accent); color: var(--accent-dk); }
+ .cta.buy { background: var(--accent-dk); color: #f0eada; }
+
+ .spec-table {
+ background: var(--bg); border: 1px solid var(--line);
+ border-radius: 8px; overflow: hidden;
+ }
+ .spec-table .row {
+ display: grid; grid-template-columns: 200px 1fr;
+ align-items: center; padding: 7px 14px;
+ font-size: 12px; line-height: 1.4;
+ border-bottom: 1px solid var(--line);
+ }
+ .spec-table .row:last-child { border-bottom: 0; }
+ .spec-table .row:nth-child(odd) { background: var(--panel); }
+ .spec-table .row .k { color: var(--muted); font: 600 10.5px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spec-table .row .v { color: var(--ink); font-weight: 500; font-family: var(--mono); font-size: 12.5px; }
+
+ .calc-compact {
+ background: var(--calc); border-radius: 8px; padding: 12px 14px; margin-top: 12px;
+ }
+ .calc-compact h4 { margin: 0 0 8px; font: 600 10.5px/1.2 var(--sans); text-transform: uppercase; letter-spacing: .08em; color: #5a4f73; }
+ .calc-compact .row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
+ .calc-compact label { font-size: 10.5px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc-compact input, .calc-compact select { width: 100%; padding: 6px 8px; border: 1px solid var(--line); border-radius: 5px; font: inherit; background: #fff; font-size: 12px; }
+ .calc-compact .total { margin-top: 8px; padding: 8px 10px; background: rgba(255,255,255,.6); border-radius: 5px; font-size: 12.5px; }
+ .calc-compact .total b { font: 500 16px var(--serif); }
+
+ details.acc {
+ margin-top: 8px; background: #fff; border: 1px solid var(--line); border-radius: 6px;
+ padding: 8px 14px; font-size: 12px;
+ }
+ details.acc[open] { padding-bottom: 12px; }
+ details.acc summary { cursor: pointer; font-weight: 600; list-style: none; display: flex; justify-content: space-between; align-items: center; }
+ details.acc summary::after { content: '▾'; color: var(--muted); transition: transform .2s; font-size: 10px; }
+ details.acc[open] summary::after { transform: rotate(180deg); }
+ details.acc summary .hint { font-weight: 400; font-size: 11px; color: var(--muted); margin-left: auto; margin-right: 8px; }
+ details.acc .acc-body { margin-top: 10px; font-size: 12px; line-height: 1.55; }
+ .pill-list { display: flex; flex-wrap: wrap; gap: 5px; }
+ .pill { font-size: 10.5px; padding: 4px 8px; border-radius: 999px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 5px; }
+ .tile-btn { flex: 1; min-width: 90px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); padding: 8px 6px; border-radius: 6px; font: 600 11px/1.2 var(--sans); color: var(--ink); text-align: center; text-decoration: none; }
+ .pair-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 5px; }
+ .pair-grid .pair { aspect-ratio: 1/1; background: rgba(0,0,0,.06); border-radius: 4px; background-size: cover; background-position: center; }
+ .swatches { display: flex; gap: 6px; flex-wrap: wrap; }
+ .sw { width: 28px; height: 28px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+
+ footer.site { border-top: 1px solid var(--line); padding: 22px; text-align: center; color: var(--muted); font-size: 11.5px; background: var(--panel); }
+ footer.site .h { font: 500 14px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading-shell { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+
+ dialog.modal { border: 0; border-radius: 14px; padding: 0; max-width: 540px; width: 92vw; box-shadow: 0 24px 80px rgba(0,0,0,.25); background: var(--bg); }
+ dialog.modal::backdrop { background: rgba(20,17,14,.55); backdrop-filter: blur(4px); }
+ dialog.modal header { display: flex; justify-content: space-between; align-items: center; padding: 18px 22px; border-bottom: 1px solid var(--line); }
+ dialog.modal header h3 { margin: 0; font: 500 18px/1 var(--serif); }
+ dialog.modal .close { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ dialog.modal .body { padding: 18px 22px 24px; }
+
+ @media (max-width: 1024px) { main.specsheet { grid-template-columns: 1fr; } .calc-compact .row { grid-template-columns: 1fr 1fr; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.specsheet { padding: 12px 14px; } .thumbs { grid-template-columns: repeat(2, 1fr); } .cta-row { grid-template-columns: 1fr; } }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <a href="/" class="brand">wallco.ai</a>
+ <nav><a href="/designs?sort=newest">Designs</a><a href="/designs?cat=monterey-mural">Murals</a><a href="/samples">Samples</a><a href="/basket">Basket</a></nav>
+ <span class="grow"></span>
+ <span class="switcher">Theme: Compact Spec-Sheet (V8) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<div id="root" class="loading-shell">Loading design…</div>
+
+<dialog class="modal" id="m-share"><header><h3>Share</h3><button class="close" onclick="document.getElementById('m-share').close()">×</button></header><div class="body" id="m-share-body"></div></dialog>
+<dialog class="modal" id="m-furnish"><header><h3>Furnish</h3><button class="close" onclick="document.getElementById('m-furnish').close()">×</button></header><div class="body" id="m-furnish-body"></div></dialog>
+
+<script>
+(async function () {
+ const pathMatch = location.pathname.match(/\/design\/(\d+)/);
+ const idFromQuery = new URLSearchParams(location.search).get('id');
+ const id = pathMatch ? parseInt(pathMatch[1], 10) : (idFromQuery ? parseInt(idFromQuery, 10) : null);
+ if (!Number.isFinite(id) || id < 1) { document.getElementById('root').textContent = 'No id'; return; }
+ document.getElementById('back-to-classic').href = '/design/' + id;
+
+ let design;
+ try {
+ const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const j = await r.json();
+ design = j.design;
+ } catch (e) { document.getElementById('root').textContent = 'Failed: ' + e.message; return; }
+
+ const title = design.title || ('Design No.' + id);
+ const cat = design.category || 'wallpaper';
+ const catDisplay = (cat || '').replace(/^botanical-wonders · /, 'Botanical Wonders — ').replace(/^designer-zoo-calm/, 'Designer Zoo Calm').replace(/^cactus-pine-scenic/, 'Cactus Pine Scenic').replace(/^monterey-mural/, 'Coastal Scenic').replace(/^tree-mural/, 'Full-Room Scenic').replace(/^cactus-11ft-mural/, 'Single-Subject Mural').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : design.kind === 'mural_panel' ? 'Mural Panel' : design.kind === 'mural' ? 'Single-Subject Mural' : (design.kind || 'pattern');
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#888';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const w = Number(design.width_in) || 24, h = Number(design.height_in) || 24;
+ const repeatLabel = w + '"' + (w !== h ? ' × ' + h + '"' : '');
+ const rollOrPanel = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const paletteStr = palette.length ? palette.slice(0, 5).map(p => p.hex).join(' · ') : dominant;
+ const roomMockup = (design.room_mockups && (typeof design.room_mockups === 'object' && !Array.isArray(design.room_mockups) ? Object.keys(design.room_mockups)[0] : design.room_mockups[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<div class="crumb">
+ <a href="/designs">Designs</a><span>›</span><a href="/designs?cat=${encodeURIComponent(cat)}">${escapeHtml(catDisplay)}</a>
+ <span class="grow"></span>
+ <span><a href="/design/${id - 2}?theme=spec">← prev</a> · <a href="/design/${id + 2}?theme=spec">next →</a></span>
+</div>
+
+<main class="specsheet">
+ <div class="col-image">
+ <div class="image-main">
+ <img loading="eager" src="${img}" alt="${escapeAttr(title)}">
+ <span class="pill">${design.is_published ? 'Live' : 'Curating'}</span>
+ </div>
+ <div class="thumbs">
+ <div class="thumb" style="background-image:url('${img}')"></div>
+ <div class="thumb" style="background-image:url('${img}'); background-size: 60%;"></div>
+ <div class="thumb" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : ''}></div>
+ <div class="thumb" style="background-image:url('${img}'); background-size: 30%;"></div>
+ </div>
+ <div class="chip-strip">
+ <span>${escapeHtml(catDisplay)}</span><span class="sep">·</span>
+ <span>${escapeHtml(kindLabel)}</span><span class="sep">·</span>
+ <span>${w}×${h}"</span><span class="sep">·</span>
+ <span>${escapeHtml(dominant)}</span><span class="sep">·</span>
+ <span>DIG#${id}</span><span class="sep">·</span>
+ <span>edges PASS</span>
+ </div>
+ </div>
+
+ <div class="col-spec">
+ <div class="name-bar">
+ <h1>${escapeHtml(title)}</h1>
+ <div class="meta">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ </div>
+
+ <div class="cta-row">
+ <button class="cta sample" onclick="addSample()">Add sample · $4.25</button>
+ <button class="cta buy" onclick="document.getElementById('scw').focus()">Buy by the roll →</button>
+ </div>
+
+ <div class="spec-table">
+ <div class="row"><span class="k">Category</span><span class="v">${escapeHtml(catDisplay)}</span></div>
+ <div class="row"><span class="k">Width</span><span class="v">${w}" (${escapeHtml(rollOrPanel.toLowerCase())})</span></div>
+ <div class="row"><span class="k">Material</span><span class="v">Type II vinyl · water-resist · class A</span></div>
+ <div class="row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div>
+ <div class="row"><span class="k">Repeat</span><span class="v">${repeatLabel} straight match</span></div>
+ <div class="row"><span class="k">${isMural ? 'Panel size' : 'Roll length'}</span><span class="v">${isMural ? (w + '" × 11 ft') : '27 ft'}</span></div>
+ <div class="row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div>
+ <div class="row"><span class="k">Designed by</span><span class="v">${escapeHtml(generator)}</span></div>
+ <div class="row"><span class="k">Handle</span><span class="v">${escapeHtml(handle)}</span></div>
+ <div class="row"><span class="k">Pattern No.</span><span class="v">${id}</span></div>
+ <div class="row"><span class="k">Dominant hex</span><span class="v">${escapeHtml(dominant)}</span></div>
+ <div class="row"><span class="k">Palette</span><span class="v">${escapeHtml(paletteStr)}</span></div>
+ <div class="row"><span class="k">${isMural ? 'Orientation' : 'Mural / tile'}</span><span class="v">${isMural ? 'Pre-pasted, hangs L→R' : 'Tile · 24" repeat'}</span></div>
+ <div class="row"><span class="k">Walls compatible</span><span class="v">Smooth · textured · drywall</span></div>
+ <div class="row"><span class="k">Lead time</span><span class="v">3–5 business days</span></div>
+ <div class="row"><span class="k">Settlement</span><span class="v">OK (audit logged)</span></div>
+ <div class="row"><span class="k">Edges scan</span><span class="v">PASS · 6-lens ΔE<tol</span></div>
+ <div class="row"><span class="k">Ghost layer</span><span class="v">clean</span></div>
+ <div class="row"><span class="k">Rating (curated)</span><span class="v">★★★★☆ 4.2 · ${escapeHtml(design.user_vote_count || 0)} votes</span></div>
+ <div class="row"><span class="k">Status</span><span class="v">${design.is_published ? 'Live on wallco.ai' : 'Curating · is_published=FALSE'}</span></div>
+ </div>
+
+ <div class="calc-compact">
+ <h4>Calculator</h4>
+ <div class="row">
+ <div><label>Wall W (ft)</label><input id="scw" type="number" min="1" max="80" step="0.5" value="10"></div>
+ <div><label>Wall H (ft)</label><input id="sch" type="number" min="1" max="20" step="0.5" value="9"></div>
+ <div><label>Roll W</label><select id="scrw"><option value="24">24"</option><option value="36">36"</option><option value="52">52"</option></select></div>
+ <div><label>Repeat</label><input value='${repeatLabel}' readonly></div>
+ </div>
+ <div class="total" id="stotal">— rolls · $—</div>
+ </div>
+
+ <details class="acc"><summary>Palette + recolor <span class="hint">${palette.length || 1} colors · drag to recolor</span></summary>
+ <div class="acc-body">
+ <div class="swatches">
+ ${palette.length ? palette.slice(0, 5).map(p => `<div class="sw" title="${escapeAttr(p.hex)} · ${(p.pct || 0).toFixed(1)}%" style="background:${escapeAttr(p.hex)}"></div>`).join('') : `<div class="sw" style="background:${escapeAttr(dominant)}"></div>`}
+ </div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>Image adjustments <span class="hint">hue/sat/bri/contrast</span></summary>
+ <div class="acc-body" style="color:var(--muted)">Sliders for hue · saturation · brightness · contrast · sepia · blur (compact view — full controls in Classic)</div>
+ </details>
+
+ <details class="acc"><summary>See in a room <span class="hint">residential preview</span></summary>
+ <div class="acc-body"><div style="height:200px; border-radius:6px; background:var(--room); background-size:cover; background-position:center; ${roomImgUrl ? `background-image:url('${roomImgUrl}')` : ''}"></div></div>
+ </details>
+
+ <details class="acc"><summary>Pairs well with <span class="hint">6 designs</span></summary>
+ <div class="acc-body"><div class="pair-grid"><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div><div class="pair"></div></div></div>
+ </details>
+
+ <details class="acc"><summary>Coordinate paints + stripes & plaids <span class="hint">7 brands · 24 stripes</span></summary>
+ <div class="acc-body">
+ <div class="pill-list" style="margin-bottom:10px"><a class="pill" href="#">Sherwin-Williams</a><a class="pill" href="#">Dunn-Edwards</a><a class="pill" href="#">Benjamin Moore</a><a class="pill" href="#">Farrow & Ball</a><a class="pill" href="#">Behr</a><a class="pill" href="#">PPG</a><a class="pill" href="#">RAL</a></div>
+ <div class="pair-grid">${Array.from({length: 12}).map(() => '<div class="pair"></div>').join('')}</div>
+ </div>
+ </details>
+
+ <details class="acc"><summary>Share · Furnish · Trade <span class="hint">3 actions</span></summary>
+ <div class="acc-body"><div class="tile-btns"><a class="tile-btn" href="#" onclick="openShare();return false">Share</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Furnish</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Email client</a><a class="tile-btn" href="#">Save</a></div></div>
+ </details>
+
+ <details class="acc"><summary>Generator + prompt <span class="hint">wallco.ai · seed ${escapeHtml(design.seed || '—')}</span></summary>
+ <div class="acc-body"><pre style="font:11px/1.45 var(--mono); background:var(--panel); padding:10px; border-radius:5px; max-height:180px; overflow:auto; margin:0; white-space:pre-wrap; word-break:break-word">${escapeHtml((design.prompt || '').slice(0, 1000))}${(design.prompt && design.prompt.length > 1000) ? '\n…[truncated]' : ''}</pre></div>
+ </details>
+ </div>
+</main>
+
+<footer class="site">
+ <div class="h">wallco.ai</div>
+ <div>A Designer Wallcoverings company · Sherman Oaks, CA</div>
+</footer>
+`;
+
+ const scw = document.getElementById('scw'), sch = document.getElementById('sch'), scrw = document.getElementById('scrw');
+ const stotal = document.getElementById('stotal');
+ function recalc() {
+ const ww = parseFloat(scw.value) || 0, wh = parseFloat(sch.value) || 0;
+ const rwi = parseFloat(scrw.value) || 24;
+ if (ww < 1 || wh < 1) { stotal.textContent = '— rolls · $—'; return; }
+ if (isMural) { const p = Math.ceil((ww * 12) / rwi); stotal.innerHTML = '<b>' + p + '</b> panels · <b>$' + (p * 245).toLocaleString() + '</b>'; }
+ else { const wp = Math.ceil((ww * 12) / rwi); const r = wp * Math.ceil(wh / 27); stotal.innerHTML = '<b>' + r + '</b> rolls · <b>$' + (r * 165).toLocaleString() + '</b> · ' + wp + ' panels wide'; }
+ }
+ [scw, sch, scrw].forEach(el => el.addEventListener('input', recalc));
+ recalc();
+
+ document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
+ document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
+})();
+
+function openShare() { document.getElementById('m-share').showModal(); }
+function openFurnish() { document.getElementById('m-furnish').showModal(); }
+async function copyLink() { try { await navigator.clipboard.writeText(location.href.replace(/\/spec$/, '')); alert('Link copied'); } catch { alert('Copy failed'); } }
+function addSample() { alert('Sample added (stub)'); }
+function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
+function escapeAttr(v) { return escapeHtml(v); }
+</script>
+</body>
+</html>
diff --git a/public/theme-variants/v9-config.html b/public/theme-variants/v9-config.html
new file mode 100644
index 0000000..2f91a92
--- /dev/null
+++ b/public/theme-variants/v9-config.html
@@ -0,0 +1,376 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Configurator-First PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3; --ink: #1a1611; --muted: #8a8276; --line: #e2dccd;
+ --panel: #f3efe5; --panel-2: #ebe5d6; --accent: #c9b07a; --accent-dk: #1a1611;
+ --hero-bg: #e6dfd2; --calc: #ddd4ec; --coord: #d3e3e5;
+ --share: #f3dcce; --room: #cfdac4; --rate: #f0e7c3; --trade: #e6ddd2;
+ --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
+ --serif: "Cormorant Garamond", Georgia, serif;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin: 0; padding: 0; }
+ body { background: var(--bg); color: var(--ink); font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; }
+ a { color: inherit; } button { font: inherit; cursor: pointer; }
+
+ header.topbar {
+ position: sticky; top: 0; z-index: 30;
+ background: rgba(250,248,243,.92); backdrop-filter: blur(10px);
+ border-bottom: 1px solid var(--line); padding: 10px 28px;
+ display: flex; align-items: center; gap: 24px;
+ }
+ header .brand { font: 600 18px/1 var(--serif); letter-spacing: .05em; }
+ header nav { font-size: 12.5px; color: var(--muted); display: flex; gap: 18px; flex-wrap: wrap; }
+ header nav a { text-decoration: none; }
+ header .grow { flex: 1; }
+ header .switcher { font-size: 11px; color: var(--muted); }
+ header .switcher a { text-decoration: underline; color: var(--ink); margin-left: 8px; }
+
+ .crumb { max-width: 1640px; margin: 0 auto; padding: 16px 28px 0; display: flex; gap: 16px; font-size: 12px; color: var(--muted); }
+ .crumb .grow { flex: 1; }
+ .crumb a { color: var(--ink); text-decoration: none; }
+
+ main.config { max-width: 1640px; margin: 0 auto; padding: 18px 28px 60px; display: flex; flex-direction: column; gap: 22px; }
+
+ /* CONFIGURATOR up top */
+ .configurator {
+ background: var(--calc);
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ padding: 24px;
+ }
+ .configurator h2 { margin: 0 0 4px; font: 500 24px/1.1 var(--serif); color: #2c2640; }
+ .configurator .tag { font-size: 12px; color: #5a4f73; letter-spacing: .05em; }
+ .steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-top: 18px; }
+ .step {
+ background: var(--bg); border: 1px solid var(--line);
+ border-radius: 10px; padding: 14px 16px;
+ }
+ .step .num { font: 600 10.5px/1 var(--sans); color: var(--accent); letter-spacing: .1em; text-transform: uppercase; }
+ .step .lbl { font: 600 13px/1.3 var(--sans); margin: 4px 0 6px; color: var(--ink); }
+ .step .input { display: flex; gap: 6px; align-items: center; }
+ .step input, .step select { width: 100%; padding: 7px 8px; border: 1px solid var(--line); border-radius: 5px; font: inherit; background: #fff; font-size: 13px; }
+ .step .ghost { background: var(--panel); border-radius: 5px; padding: 7px 9px; font: 500 13px var(--serif); color: var(--ink); }
+ .big-cta {
+ margin-top: 18px;
+ background: var(--accent); color: var(--accent-dk);
+ border: 0; border-radius: 10px; padding: 22px;
+ font: 700 16px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase;
+ width: 100%; cursor: pointer;
+ display: flex; align-items: center; justify-content: center; gap: 16px;
+ }
+ .big-cta .est { font: 500 16px var(--serif); text-transform: none; letter-spacing: 0; opacity: .85; }
+
+ /* 3-col preview row */
+ .preview-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
+ .preview {
+ background: var(--hero-bg); border-radius: 12px; aspect-ratio: 4/5;
+ overflow: hidden; position: relative;
+ }
+ .preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .preview .badge {
+ position: absolute; top: 12px; left: 12px;
+ background: rgba(26,22,17,.8); color: #f0eada;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em;
+ padding: 6px 10px; border-radius: 999px; text-transform: uppercase;
+ }
+ .preview.room { background: var(--room); background-size: cover; background-position: center; }
+ .preview.atscale { background-repeat: repeat; background-size: 220px 220px; }
+ .preview.atscale .scale-pill { position: absolute; right: 12px; bottom: 12px; background: rgba(250,248,243,.94); border-radius: 999px; padding: 6px 12px; font: 500 11px var(--sans); display: flex; gap: 8px; align-items: center; }
+ .preview.atscale .scale-pill input[type=range] { width: 90px; accent-color: var(--accent); }
+ .preview .room-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--muted); font-size: 12px; font-style: italic; }
+
+ .title-strip {
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 10px; padding: 14px 18px;
+ display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
+ }
+ .title-strip h1 { font: 500 22px/1.15 var(--serif); margin: 0; }
+ .title-strip .meta { color: var(--muted); font-size: 12.5px; }
+ .title-strip .meta .sep { padding: 0 6px; opacity: .55; }
+
+ /* bottom 3-col helper row */
+ .bottom-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
+ .card {
+ background: var(--panel); border: 1px solid var(--line);
+ border-radius: 10px; padding: 16px;
+ }
+ .card h3 { margin: 0 0 12px; font: 600 11px/1.2 var(--sans); text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
+ .card.trade { background: var(--trade); }
+ .swatches { display: flex; gap: 10px; flex-wrap: wrap; }
+ .sw { width: 36px; height: 36px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .adjust .sl { display: grid; grid-template-columns: 80px 1fr 34px; align-items: center; gap: 8px; font-size: 11.5px; padding: 3px 0; }
+ .adjust .sl input[type=range] { accent-color: var(--accent); width: 100%; }
+ .adjust .sl .v { font-variant-numeric: tabular-nums; color: var(--muted); text-align: right; }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 90px; background: rgba(255,255,255,.7); border: 1px solid rgba(0,0,0,.06); padding: 10px 8px; border-radius: 8px; font: 600 11.5px/1.2 var(--sans); color: var(--ink); text-align: center; text-decoration: none; }
+ .tile-btn:hover { background: #fff; }
+
+ /* footer collapsibles */
+ .footer-acc { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
+ details.acc { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px 16px; font-size: 13px; }
+ details.acc[open] { padding-bottom: 14px; }
+ details.acc summary { cursor: pointer; font-weight: 600; list-style: none; display: flex; justify-content: space-between; align-items: center; }
+ details.acc summary::after { content: '▾'; color: var(--muted); transition: transform .2s; font-size: 11px; }
+ details.acc[open] summary::after { transform: rotate(180deg); }
+ details.acc summary .hint { font-weight: 400; font-size: 11.5px; color: var(--muted); margin-left: auto; margin-right: 8px; }
+ details.acc .acc-body { margin-top: 12px; font-size: 12.5px; line-height: 1.55; }
+ .specs-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .specs-row:last-child { border-bottom: 0; }
+ .specs-row .k { color: var(--muted); }
+ .specs-row .v { font-weight: 600; text-align: right; }
+ .pill-list { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 9px; border-radius: 999px; background: rgba(0,0,0,.04); border: 1px solid rgba(0,0,0,.06); }
+ .pair-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
+ .pair-grid .pair { aspect-ratio: 1/1; background: rgba(0,0,0,.06); border-radius: 6px; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 28px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); margin-top: 30px; }
+ footer.site .h { font: 500 14px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading-shell { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+
+ dialog.modal { border: 0; border-radius: 14px; padding: 0; max-width: 540px; width: 92vw; box-shadow: 0 24px 80px rgba(0,0,0,.25); background: var(--bg); }
+ dialog.modal::backdrop { background: rgba(20,17,14,.55); backdrop-filter: blur(4px); }
+ dialog.modal header { display: flex; justify-content: space-between; align-items: center; padding: 18px 22px; border-bottom: 1px solid var(--line); }
+ dialog.modal header h3 { margin: 0; font: 500 18px/1 var(--serif); }
+ dialog.modal .close { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ dialog.modal .body { padding: 18px 22px 24px; }
+
+ @media (max-width: 1024px) {
+ .steps { grid-template-columns: repeat(2, 1fr); }
+ .preview-row, .bottom-row { grid-template-columns: 1fr; }
+ }
+ @media (max-width: 640px) {
+ header.topbar { flex-wrap: wrap; padding: 10px 14px; }
+ main.config { padding: 14px; }
+ .steps { grid-template-columns: 1fr; }
+ .footer-acc { grid-template-columns: 1fr; }
+ }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+ <a href="/" class="brand">wallco.ai</a>
+ <nav><a href="/designs?sort=newest">Designs</a><a href="/designs?cat=monterey-mural">Murals</a><a href="/samples">Samples</a><a href="/basket">Basket</a></nav>
+ <span class="grow"></span>
+ <span class="switcher">Theme: Configurator-First (V9) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<div id="root" class="loading-shell">Loading design…</div>
+
+<dialog class="modal" id="m-share"><header><h3>Share</h3><button class="close" onclick="document.getElementById('m-share').close()">×</button></header><div class="body" id="m-share-body"></div></dialog>
+<dialog class="modal" id="m-furnish"><header><h3>Furnish</h3><button class="close" onclick="document.getElementById('m-furnish').close()">×</button></header><div class="body" id="m-furnish-body"></div></dialog>
+
+<script>
+(async function () {
+ const pathMatch = location.pathname.match(/\/design\/(\d+)/);
+ const idFromQuery = new URLSearchParams(location.search).get('id');
+ const id = pathMatch ? parseInt(pathMatch[1], 10) : (idFromQuery ? parseInt(idFromQuery, 10) : null);
+ if (!Number.isFinite(id) || id < 1) { document.getElementById('root').textContent = 'No id'; return; }
+ document.getElementById('back-to-classic').href = '/design/' + id;
+
+ let design;
+ try {
+ const r = await fetch('/api/design/' + id, { credentials: 'same-origin' });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ const j = await r.json();
+ design = j.design;
+ } catch (e) { document.getElementById('root').textContent = 'Failed: ' + e.message; return; }
+
+ const title = design.title || ('Design No.' + id);
+ const cat = design.category || 'wallpaper';
+ const catDisplay = (cat || '').replace(/^botanical-wonders · /, 'Botanical Wonders — ').replace(/^designer-zoo-calm/, 'Designer Zoo Calm').replace(/^cactus-pine-scenic/, 'Cactus Pine Scenic').replace(/^monterey-mural/, 'Coastal Scenic').replace(/^tree-mural/, 'Full-Room Scenic').replace(/^cactus-11ft-mural/, 'Single-Subject Mural').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : design.kind === 'mural_panel' ? 'Mural Panel' : design.kind === 'mural' ? 'Single-Subject Mural' : (design.kind || 'pattern');
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#888';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const w = Number(design.width_in) || 24, h = Number(design.height_in) || 24;
+ const repeatLabel = w + '"' + (w !== h ? ' × ' + h + '"' : '');
+ const rollOrPanel = isMural ? 'Per panel (36" × 11 ft)' : 'Per roll (27 ft)';
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const roomMockup = (design.room_mockups && (typeof design.room_mockups === 'object' && !Array.isArray(design.room_mockups) ? Object.keys(design.room_mockups)[0] : design.room_mockups[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<div class="crumb">
+ <a href="/designs">Designs</a><span>›</span><a href="/designs?cat=${encodeURIComponent(cat)}">${escapeHtml(catDisplay)}</a>
+ <span class="grow"></span>
+ <span><a href="/design/${id - 2}?theme=config">← prev</a> · <a href="/design/${id + 2}?theme=config">next →</a></span>
+</div>
+
+<main class="config">
+
+ <!-- CONFIGURATOR (UP TOP — primary action) -->
+ <div class="configurator">
+ <h2>Configure your order</h2>
+ <div class="tag">Step 1 — wall dimensions · Step 2 — roll width · Step 3 — repeat fit · Step 4 — total + lead time</div>
+
+ <div class="steps">
+ <div class="step">
+ <div class="num">Step 1</div>
+ <div class="lbl">Wall dimensions</div>
+ <div class="input"><input id="ccw" type="number" min="1" max="80" step="0.5" value="10"><span style="color:var(--muted)">×</span><input id="cch" type="number" min="1" max="20" step="0.5" value="9"></div>
+ </div>
+ <div class="step">
+ <div class="num">Step 2</div>
+ <div class="lbl">Roll / panel choice</div>
+ <div class="input"><select id="ccrw"><option value="24">24" residential</option><option value="36">36" wide-roll</option><option value="52">52" commercial</option></select></div>
+ </div>
+ <div class="step">
+ <div class="num">Step 3</div>
+ <div class="lbl">Pattern repeat fit</div>
+ <div class="ghost" id="cc-fit">5 panels wide × 9 repeats tall</div>
+ </div>
+ <div class="step">
+ <div class="num">Step 4</div>
+ <div class="lbl">Total + lead time</div>
+ <div class="ghost" id="cc-total">— rolls · $— · 3–5 days</div>
+ </div>
+ </div>
+
+ <button class="big-cta" id="big-cta" onclick="addBundle()">
+ <span>Add to basket</span>
+ <span class="est" id="big-est">— rolls · $—</span>
+ </button>
+ </div>
+
+ <!-- TITLE STRIP -->
+ <div class="title-strip">
+ <h1>${escapeHtml(title)}</h1>
+ <div class="meta">${escapeHtml(catDisplay)}<span class="sep">·</span>${escapeHtml(kindLabel)}<span class="sep">·</span>${rollOrPanel}<span class="sep">·</span>${escapeHtml(dominant)}<span class="sep">·</span>${escapeHtml(handle)}</div>
+ </div>
+
+ <!-- 3 PREVIEWS — hero ⅓, room ⅓, at-scale ⅓ -->
+ <div class="preview-row">
+ <div class="preview">
+ <img loading="eager" src="${img}" alt="${escapeAttr(title)}">
+ <span class="badge">Pattern · ${escapeHtml(title)}</span>
+ </div>
+ <div class="preview room" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : ''}>
+ <span class="badge">See in a room</span>
+ ${roomImgUrl ? '' : '<div class="room-placeholder">Room mockup not yet generated</div>'}
+ </div>
+ <div class="preview atscale" id="atscale-preview" style="background-image:url('${img}')">
+ <span class="badge">At-scale · 10 × 9 ft wall</span>
+ <span class="scale-pill">scale <input type="range" min="120" max="360" value="220" id="scale-slider"></span>
+ </div>
+ </div>
+
+ <!-- BOTTOM 3 -->
+ <div class="bottom-row">
+ <div class="card">
+ <h3>Palette · drag to recolor</h3>
+ <div class="swatches">
+ ${palette.length ? palette.slice(0, 5).map(p => `<div class="sw" title="${escapeAttr(p.hex)} · ${(p.pct || 0).toFixed(1)}%" style="background:${escapeAttr(p.hex)}"></div>`).join('') : `<div class="sw" style="background:${escapeAttr(dominant)}"></div>`}
+ </div>
+ <div style="font-size:11px; color:var(--muted); margin-top:8px">Dominant ${escapeHtml(dominant)}</div>
+ </div>
+ <div class="card adjust">
+ <h3>Image adjustments</h3>
+ <div class="sl"><label>Hue</label><input type="range" min="-180" max="180" value="0"><span class="v">0</span></div>
+ <div class="sl"><label>Saturation</label><input type="range" min="0" max="200" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Brightness</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ <div class="sl"><label>Contrast</label><input type="range" min="50" max="150" value="100"><span class="v">100</span></div>
+ </div>
+ <div class="card trade">
+ <h3>Sample + trade tools</h3>
+ <div class="tile-btns">
+ <a class="tile-btn" href="#" onclick="addSample();return false">Sample $4.25</a>
+ <a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a>
+ <a class="tile-btn" href="#" onclick="openShare();return false">Share</a>
+ <a class="tile-btn" href="#" onclick="openFurnish();return false">Furnish</a>
+ </div>
+ </div>
+ </div>
+
+ <!-- Collapsed footer rows -->
+ <div class="footer-acc">
+ <details class="acc"><summary>Specs <span class="hint">Kind · W · Repeat · Roll · Material</span></summary><div class="acc-body">
+ <div class="specs-row"><span class="k">Category</span><span class="v">${escapeHtml(catDisplay)}</span></div>
+ <div class="specs-row"><span class="k">Kind</span><span class="v">${escapeHtml(kindLabel)}</span></div>
+ <div class="specs-row"><span class="k">Width</span><span class="v">${w}"</span></div>
+ <div class="specs-row"><span class="k">Repeat</span><span class="v">${repeatLabel}</span></div>
+ <div class="specs-row"><span class="k">Sold by</span><span class="v">${rollOrPanel}</span></div>
+ <div class="specs-row"><span class="k">${isMural ? 'Panel size' : 'Roll length'}</span><span class="v">${isMural ? (w + '" × 11 ft') : '27 ft'}</span></div>
+ <div class="specs-row"><span class="k">Material</span><span class="v">Type II vinyl</span></div>
+ <div class="specs-row"><span class="k">Designed by</span><span class="v">${escapeHtml(generator)}</span></div>
+ <div class="specs-row"><span class="k">Pattern No.</span><span class="v">${id}</span></div>
+ <div class="specs-row"><span class="k">Handle</span><span class="v">${escapeHtml(handle)}</span></div>
+ </div></details>
+ <details class="acc"><summary>Pairs well with · coordinate paints · stripes <span class="hint">7 brands · 24 stripes</span></summary><div class="acc-body">
+ <div class="pill-list" style="margin-bottom:10px"><a class="pill" href="#">SW</a><a class="pill" href="#">Dunn-E</a><a class="pill" href="#">BM</a><a class="pill" href="#">F&B</a><a class="pill" href="#">Behr</a><a class="pill" href="#">PPG</a><a class="pill" href="#">RAL</a></div>
+ <div class="pair-grid">${Array.from({length: 12}).map(() => '<div class="pair"></div>').join('')}</div>
+ </div></details>
+ <details class="acc"><summary>Furnish this design <span class="hint">Res · Com · Hosp · Health</span></summary><div class="acc-body"><div class="tile-btns"><a class="tile-btn" href="#" onclick="openFurnish();return false">Residential</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Commercial</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Hospitality</a><a class="tile-btn" href="#" onclick="openFurnish();return false">Healthcare</a></div></div></details>
+ <details class="acc"><summary>Ratings & reviews + Generator + prompt <span class="hint">★★★★☆ · seed ${escapeHtml(design.seed || '—')}</span></summary><div class="acc-body">
+ <div style="font-size:22px; letter-spacing:4px; color:#b89a4a">★★★★☆</div>
+ <div style="font-size:11.5px; color:var(--muted); margin-top:4px; margin-bottom:14px">Curated · 4.2 · ${escapeHtml(design.user_vote_count || 0)} votes</div>
+ <pre style="font:11px/1.5 ui-monospace,Menlo,monospace; background:var(--panel); padding:10px; border-radius:6px; max-height:180px; overflow:auto; margin:0; white-space:pre-wrap; word-break:break-word">${escapeHtml((design.prompt || '').slice(0, 1000))}${(design.prompt && design.prompt.length > 1000) ? '\n…[truncated]' : ''}</pre>
+ </div></details>
+ </div>
+
+</main>
+
+<footer class="site">
+ <div class="h">wallco.ai</div>
+ <div>A Designer Wallcoverings company · Sherman Oaks, CA</div>
+</footer>
+`;
+
+ const ccw = document.getElementById('ccw'), cch = document.getElementById('cch'), ccrw = document.getElementById('ccrw');
+ const ccFit = document.getElementById('cc-fit'), ccTotal = document.getElementById('cc-total'), bigEst = document.getElementById('big-est');
+ function recalc() {
+ const ww = parseFloat(ccw.value) || 0, wh = parseFloat(cch.value) || 0;
+ const rwi = parseFloat(ccrw.value) || 24;
+ if (ww < 1 || wh < 1) { ccTotal.textContent = '— rolls · $— · 3–5 days'; bigEst.textContent = '—'; return; }
+ if (isMural) {
+ const p = Math.ceil((ww * 12) / rwi); const cost = p * 245;
+ ccFit.textContent = p + ' panels wide × 1 tall';
+ ccTotal.textContent = p + ' panels · $' + cost.toLocaleString() + ' · 3–5 days';
+ bigEst.textContent = p + ' panels · $' + cost.toLocaleString();
+ } else {
+ const wp = Math.ceil((ww * 12) / rwi);
+ const rep = Math.ceil(wh / (parseFloat(String(w)) / 12 || 2));
+ const rolls = wp * Math.ceil(wh / 27);
+ const cost = rolls * 165;
+ ccFit.textContent = wp + ' panels wide × ' + rep + ' repeats tall';
+ ccTotal.textContent = rolls + ' rolls · $' + cost.toLocaleString() + ' · 3–5 days';
+ bigEst.textContent = rolls + ' rolls · $' + cost.toLocaleString();
+ }
+ }
+ [ccw, cch, ccrw].forEach(el => el.addEventListener('input', recalc));
+ recalc();
+
+ const scale = document.getElementById('scale-slider');
+ const ascale = document.getElementById('atscale-preview');
+ scale.addEventListener('input', () => { ascale.style.backgroundSize = scale.value + 'px ' + scale.value + 'px'; });
+
+ document.querySelectorAll('.adjust .sl').forEach(row => {
+ const r = row.querySelector('input[type=range]'); const v = row.querySelector('.v');
+ r.addEventListener('input', () => { v.textContent = r.value; });
+ });
+
+ document.getElementById('m-share-body').innerHTML = '<p>Share <b>' + escapeHtml(title) + '</b></p><div class="tile-btns"><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a></div>';
+ document.getElementById('m-furnish-body').innerHTML = '<p>Generate AI furnishings.</p><div class="tile-btns"><a class="tile-btn" href="#">Residential</a><a class="tile-btn" href="#">Commercial</a><a class="tile-btn" href="#">Hospitality</a><a class="tile-btn" href="#">Healthcare</a></div>';
+})();
+
+function openShare() { document.getElementById('m-share').showModal(); }
+function openFurnish() { document.getElementById('m-furnish').showModal(); }
+async function copyLink() { try { await navigator.clipboard.writeText(location.href.replace(/\/config$/, '')); alert('Link copied'); } catch { alert('Copy failed'); } }
+function addSample() { alert('Sample added (stub)'); }
+function addBundle() { alert('Bundle added (stub)'); }
+function escapeHtml(v) { if (v == null) return ''; return String(v).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
+function escapeAttr(v) { return escapeHtml(v); }
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 34ef814..a891979 100644
--- a/server.js
+++ b/server.js
@@ -444,6 +444,28 @@ app.get('/design/:id/tabs', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'theme-variants', 'v3-tabs.html'));
});
+// ── Theme variants V4–V10. All mirror V1–V3: static HTML at
+// public/theme-variants/<slug>.html, fetches /api/design/:id client-side.
+// V4 hero — full-bleed hero + narrow buy column
+// V5 vertical — 720px stack + floating buy bar
+// V6 drawer — minimal page + side drawers
+// V7 editorial — magazine layout w/ marginalia
+// V8 spec — wine-list spec sheet
+// V9 config — configurator-first
+// V10 stack — single-column card stack
+function themeVariantSend(req, res, file) {
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id) || id < 1) return res.status(404).type('html').send('<h1>404</h1>');
+ res.sendFile(path.join(__dirname, 'public', 'theme-variants', file));
+}
+app.get('/design/:id/hero', (req, res) => themeVariantSend(req, res, 'v4-hero.html'));
+app.get('/design/:id/vertical', (req, res) => themeVariantSend(req, res, 'v5-vertical.html'));
+app.get('/design/:id/drawer', (req, res) => themeVariantSend(req, res, 'v6-drawer.html'));
+app.get('/design/:id/editorial', (req, res) => themeVariantSend(req, res, 'v7-editorial.html'));
+app.get('/design/:id/spec', (req, res) => themeVariantSend(req, res, 'v8-spec.html'));
+app.get('/design/:id/config', (req, res) => themeVariantSend(req, res, 'v9-config.html'));
+app.get('/design/:id/stack', (req, res) => themeVariantSend(req, res, 'v10-stack.html'));
+
// ── Lightweight in-memory search across the catalog.
// /api/designs/search?q=<term>&limit=20
// Scores each design on motif (3), title (2), category/handle (1) substring match.
@@ -11341,6 +11363,27 @@ try {
<a class="hb-menu-item" data-hb-theme="tabs" role="menuitem" href="/design/${design.id}/tabs">
<span class="hb-menu-icon" aria-hidden="true">▤</span> V3 · Sticky Hero + Tabs
</a>
+ <a class="hb-menu-item" data-hb-theme="hero" role="menuitem" href="/design/${design.id}/hero">
+ <span class="hb-menu-icon" aria-hidden="true">◆</span> V4 · Hero-First Storefront
+ </a>
+ <a class="hb-menu-item" data-hb-theme="vertical" role="menuitem" href="/design/${design.id}/vertical">
+ <span class="hb-menu-icon" aria-hidden="true">▮</span> V5 · Mobile-Up Vertical
+ </a>
+ <a class="hb-menu-item" data-hb-theme="drawer" role="menuitem" href="/design/${design.id}/drawer">
+ <span class="hb-menu-icon" aria-hidden="true">▷</span> V6 · Drawer Heavy
+ </a>
+ <a class="hb-menu-item" data-hb-theme="editorial" role="menuitem" href="/design/${design.id}/editorial">
+ <span class="hb-menu-icon" aria-hidden="true">§</span> V7 · Editorial Magazine
+ </a>
+ <a class="hb-menu-item" data-hb-theme="spec" role="menuitem" href="/design/${design.id}/spec">
+ <span class="hb-menu-icon" aria-hidden="true">≡</span> V8 · Compact Spec-Sheet
+ </a>
+ <a class="hb-menu-item" data-hb-theme="config" role="menuitem" href="/design/${design.id}/config">
+ <span class="hb-menu-icon" aria-hidden="true">⚙</span> V9 · Configurator-First
+ </a>
+ <a class="hb-menu-item" data-hb-theme="stack" role="menuitem" href="/design/${design.id}/stack">
+ <span class="hb-menu-icon" aria-hidden="true">▣</span> V10 · Card-Stack
+ </a>
</div>
${_isAdmin ? `
<button type="button" class="hb-menu-item" data-hb-action="edit" role="menuitem">
← 441d381 /designs: replace binary 'In a room' chip with a chip row —
·
back to Wallco Ai
·
docs: SHOPIFY-WEBHOOK-REGISTRATION one-pager (UI registratio 74ee628 →