← back to Wallco Ai
Add 6 room-left PDP theme variants V12-V17 (room setting left of pattern, adjustments in a hamburger selector) + routes + Theme submenu + gallery entries + admin-tabs nav link
79f45cc7166ecd63bd6a3f6ca497856acaf4739e · 2026-05-29 12:42:36 -0700 · Steve
Files touched
M public/admin/theme-gallery.htmlA public/theme-variants/v12-roomsplit.htmlA public/theme-variants/v13-roomdrawer.htmlA public/theme-variants/v14-roomzine.htmlA public/theme-variants/v15-roomsticky.htmlA public/theme-variants/v16-roommin.htmlA public/theme-variants/v17-roomshow.htmlA scripts/gen-theme-variants.jsA scripts/room-left-template.htmlM server.jsM src/admin.js
Diff
commit 79f45cc7166ecd63bd6a3f6ca497856acaf4739e
Author: Steve <steve@designerwallcoverings.com>
Date: Fri May 29 12:42:36 2026 -0700
Add 6 room-left PDP theme variants V12-V17 (room setting left of pattern, adjustments in a hamburger selector) + routes + Theme submenu + gallery entries + admin-tabs nav link
---
public/admin/theme-gallery.html | 6 +
public/theme-variants/v12-roomsplit.html | 310 ++++++++++++++++++++++++++++++
public/theme-variants/v13-roomdrawer.html | 310 ++++++++++++++++++++++++++++++
public/theme-variants/v14-roomzine.html | 310 ++++++++++++++++++++++++++++++
public/theme-variants/v15-roomsticky.html | 310 ++++++++++++++++++++++++++++++
public/theme-variants/v16-roommin.html | 310 ++++++++++++++++++++++++++++++
public/theme-variants/v17-roomshow.html | 310 ++++++++++++++++++++++++++++++
scripts/gen-theme-variants.js | 70 +++++++
scripts/room-left-template.html | 294 ++++++++++++++++++++++++++++
server.js | 24 +++
src/admin.js | 1 +
11 files changed, 2255 insertions(+)
diff --git a/public/admin/theme-gallery.html b/public/admin/theme-gallery.html
index 0e9da4c..26a8c49 100644
--- a/public/admin/theme-gallery.html
+++ b/public/admin/theme-gallery.html
@@ -84,6 +84,12 @@ const VARIANTS = [
['config', 'V9 · Configurator-First'],
['stack', 'V10 · Card-Stack'],
['best', 'V11 · Best (synthesis)'],
+ ['roomsplit', 'V12 · Room-Left Split'],
+ ['roomdrawer', 'V13 · Room-Left Drawer'],
+ ['roomzine', 'V14 · Room-Left Editorial'],
+ ['roomsticky', 'V15 · Room-Left Sticky'],
+ ['roommin', 'V16 · Room-Left Minimal'],
+ ['roomshow', 'V17 · Room-Left Showcase'],
];
const LOGICAL_H = { 1280:1700, 834:1180, 390:760 }; // logical iframe height per width
const $ = s => document.querySelector(s);
diff --git a/public/theme-variants/v12-roomsplit.html b/public/theme-variants/v12-roomsplit.html
new file mode 100644
index 0000000..5e9c8ed
--- /dev/null
+++ b/public/theme-variants/v12-roomsplit.html
@@ -0,0 +1,310 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Room-Left Split PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #faf8f3;
+ --ink: #1a1611;
+ --muted: #8a8276;
+ --panel: #eee9dd;
+ --bar: rgba(250,248,243,.92);
+ --accent: #16203c;
+ --on-accent: #fff;
+ --split: 1fr 1fr;
+ --radius: 12px;
+ --shadow: 0 1px 0 var(--line);
+ --brandfont: var(--serif);
+ --titlefont: 500 30px/1.15 var(--serif);
+ --paneltitle: 500 18px/1 var(--serif);
+ --room-ar: 4/3;
+ --pat-max: 420px;
+ --pat-border: 0;
+ --room-pos: static;
+ --line: rgba(0,0,0,.10);
+ --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: 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: 40; background: var(--bar);
+ backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
+ display: flex; align-items: center; gap: 20px; }
+ header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
+ header nav { font-size: 12.5px; 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; }
+ .adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
+ background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
+ font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
+ .adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
+
+ main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
+ display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
+
+ /* LEFT — room setting */
+ .col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
+ .room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
+ overflow: hidden; position: relative; box-shadow: var(--shadow); }
+ .room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
+ border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
+ .room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
+ background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
+ .room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
+
+ /* RIGHT — pattern + buy */
+ .col-pattern { min-width: 0; }
+ .pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
+ .pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ h1.title { font: var(--titlefont); margin: 18px 0 4px; }
+ .cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
+ .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
+ .cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
+ .cta.sample { background: var(--accent); color: var(--on-accent); }
+ .cta.buy { background: var(--ink); color: var(--bg); }
+ .mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
+ border-top: 1px solid var(--line); padding-top: 16px; }
+ .mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
+ .mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
+
+ /* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
+ .ov { position: fixed; inset: 0; z-index: 60; display: none; }
+ .ov.open { display: block; }
+ .ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
+ .menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
+ box-shadow: 0 24px 80px rgba(0,0,0,.28); }
+ .menu { border-radius: 14px; padding: 8px; min-width: 250px; }
+ .menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
+ .menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
+ text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
+ .menu button:hover, .menu button.on { background: var(--panel); }
+ .menu button .ic { width: 22px; text-align: center; opacity: .7; }
+ .panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
+ .panel header { display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
+ .panel header h3 { margin: 0; font: var(--paneltitle); }
+ .panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ .panel .pbody { padding: 18px 20px 24px; }
+
+ /* mode: dropdown — menu under the button, panel as small popover top-right */
+ [data-mode="dropdown"] .menu { top: 62px; right: 24px; }
+ [data-mode="dropdown"] .panel { top: 62px; right: 24px; }
+ /* mode: drawer — slide in from right */
+ [data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
+ [data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
+ /* mode: sheet — slide up from bottom */
+ [data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
+ display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
+ [data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
+ [data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
+ /* mode: modal — centered */
+ [data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ [data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ /* mode: palette — command-palette near top-center */
+ [data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
+ [data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
+ /* mode: fab — floating action button, menu pops above it */
+ [data-mode="fab"] .menu { right: 24px; bottom: 86px; }
+ [data-mode="fab"] .panel { right: 24px; bottom: 86px; }
+ .fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
+ border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
+ box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
+ body[data-mode="fab"] .fab-btn { display: flex; }
+ body[data-mode="fab"] header .adjust-btn { display: none; }
+
+ /* adjustment panel content */
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
+ .sliders input[type=range] { width: 100%; }
+ .roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
+ .pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
+ .pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
+ .calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
+ .calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
+ .calc .tot b { font: 500 17px var(--serif); }
+ .spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spectab .row .v { font-family: var(--mono); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
+ pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+ @media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
+</style>
+</head>
+<body data-mode="dropdown">
+
+<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>
+ <button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
+ <span class="switcher">Theme: Room-Left Split (V12) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
+
+<div id="root" class="loading">Loading design…</div>
+
+<div class="ov" id="ov">
+ <div class="scrim" id="ov-scrim"></div>
+ <div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
+ <div class="panel" id="adj-panel" role="dialog" hidden>
+ <header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
+ <div class="pbody" id="adj-panel-body"></div>
+ </div>
+</div>
+
+<script>
+(async function () {
+ const m = location.pathname.match(/\/design\/(\d+)/);
+ const qid = new URLSearchParams(location.search).get('id');
+ const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 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);
+ design = (await r.json()).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(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const created = design.created_at ? new Date(design.created_at) : null;
+ const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+ const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
+
+ // ambient tint from dominant (used by showcase mode via --amb)
+ document.documentElement.style.setProperty('--amb', dominant + '22');
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="roomleft">
+ <section class="col-room">
+ <div class="room-main">
+ <img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
+ <span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
+ </div>
+ <div class="room-thumbs">
+ <div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
+ <div class="t" style="background-image:url('${img}')"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:55%"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:30%"></div>
+ </div>
+ <div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
+ </section>
+
+ <section class="col-pattern">
+ <div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
+ <h1 class="title">${escapeHtml(title)}</h1>
+ <div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ <div class="cta-row">
+ <button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
+ <button class="cta buy" id="adjust-open-2">Adjust & buy</button>
+ </div>
+ <div class="mini-specs">
+ <div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
+ <div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
+ <div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
+ <div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
+ ${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
+ <div><div class="k">Edges</div><div class="v">PASS</div></div>
+ </div>
+ </section>
+</main>
+
+<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
+
+ // ---- adjustments: the hamburger menu of things to select from ----
+ const w = 24, h = 24;
+ const ADJ = [
+ ['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
+ ['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
+ ['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
+ ['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
+ ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
+ ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
+ ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
+ ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+ ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
+ ];
+
+ const menu = document.getElementById('adj-menu');
+ menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
+ `<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
+
+ const ov = document.getElementById('ov');
+ const panel = document.getElementById('adj-panel');
+ const menuEl = document.getElementById('adj-menu');
+ function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
+ function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
+ function showPanel(key){
+ const item = ADJ.find(a => a[0] === key); if(!item) return;
+ document.getElementById('adj-panel-title').textContent = item[2];
+ document.getElementById('adj-panel-body').innerHTML = item[3]();
+ panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
+ menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
+ if (key === 'calc') wireCalc();
+ }
+ ['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
+ document.getElementById('ov-scrim').addEventListener('click', closeOv);
+ document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
+ menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
+
+ function wireCalc(){
+ const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
+ if(!cw) return;
+ function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
+ if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
+ else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
+ [cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
+ }
+ // thumbnail → swap room main image
+ document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
+ const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
+ const u = bg.slice(5,-2); if(im && u) im.src = u;
+ }));
+})();
+window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
+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/v13-roomdrawer.html b/public/theme-variants/v13-roomdrawer.html
new file mode 100644
index 0000000..8282a94
--- /dev/null
+++ b/public/theme-variants/v13-roomdrawer.html
@@ -0,0 +1,310 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Room-Left Drawer PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #fbf7f4;
+ --ink: #211a16;
+ --muted: #8a8276;
+ --panel: #efe6df;
+ --bar: rgba(251,247,244,.93);
+ --accent: #b5572e;
+ --on-accent: #fff;
+ --split: 1.4fr 1fr;
+ --radius: 12px;
+ --shadow: 0 1px 0 var(--line);
+ --brandfont: var(--serif);
+ --titlefont: 500 31px/1.12 var(--serif);
+ --paneltitle: 500 18px/1 var(--serif);
+ --room-ar: 3/4;
+ --pat-max: 420px;
+ --pat-border: 0;
+ --room-pos: static;
+ --line: rgba(0,0,0,.10);
+ --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: 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: 40; background: var(--bar);
+ backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
+ display: flex; align-items: center; gap: 20px; }
+ header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
+ header nav { font-size: 12.5px; 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; }
+ .adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
+ background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
+ font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
+ .adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
+
+ main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
+ display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
+
+ /* LEFT — room setting */
+ .col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
+ .room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
+ overflow: hidden; position: relative; box-shadow: var(--shadow); }
+ .room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
+ border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
+ .room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
+ background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
+ .room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
+
+ /* RIGHT — pattern + buy */
+ .col-pattern { min-width: 0; }
+ .pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
+ .pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ h1.title { font: var(--titlefont); margin: 18px 0 4px; }
+ .cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
+ .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
+ .cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
+ .cta.sample { background: var(--accent); color: var(--on-accent); }
+ .cta.buy { background: var(--ink); color: var(--bg); }
+ .mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
+ border-top: 1px solid var(--line); padding-top: 16px; }
+ .mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
+ .mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
+
+ /* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
+ .ov { position: fixed; inset: 0; z-index: 60; display: none; }
+ .ov.open { display: block; }
+ .ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
+ .menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
+ box-shadow: 0 24px 80px rgba(0,0,0,.28); }
+ .menu { border-radius: 14px; padding: 8px; min-width: 250px; }
+ .menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
+ .menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
+ text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
+ .menu button:hover, .menu button.on { background: var(--panel); }
+ .menu button .ic { width: 22px; text-align: center; opacity: .7; }
+ .panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
+ .panel header { display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
+ .panel header h3 { margin: 0; font: var(--paneltitle); }
+ .panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ .panel .pbody { padding: 18px 20px 24px; }
+
+ /* mode: dropdown — menu under the button, panel as small popover top-right */
+ [data-mode="dropdown"] .menu { top: 62px; right: 24px; }
+ [data-mode="dropdown"] .panel { top: 62px; right: 24px; }
+ /* mode: drawer — slide in from right */
+ [data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
+ [data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
+ /* mode: sheet — slide up from bottom */
+ [data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
+ display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
+ [data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
+ [data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
+ /* mode: modal — centered */
+ [data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ [data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ /* mode: palette — command-palette near top-center */
+ [data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
+ [data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
+ /* mode: fab — floating action button, menu pops above it */
+ [data-mode="fab"] .menu { right: 24px; bottom: 86px; }
+ [data-mode="fab"] .panel { right: 24px; bottom: 86px; }
+ .fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
+ border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
+ box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
+ body[data-mode="fab"] .fab-btn { display: flex; }
+ body[data-mode="fab"] header .adjust-btn { display: none; }
+
+ /* adjustment panel content */
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
+ .sliders input[type=range] { width: 100%; }
+ .roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
+ .pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
+ .pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
+ .calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
+ .calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
+ .calc .tot b { font: 500 17px var(--serif); }
+ .spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spectab .row .v { font-family: var(--mono); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
+ pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+ @media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
+</style>
+</head>
+<body data-mode="drawer">
+
+<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>
+ <button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
+ <span class="switcher">Theme: Room-Left Drawer (V13) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
+
+<div id="root" class="loading">Loading design…</div>
+
+<div class="ov" id="ov">
+ <div class="scrim" id="ov-scrim"></div>
+ <div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
+ <div class="panel" id="adj-panel" role="dialog" hidden>
+ <header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
+ <div class="pbody" id="adj-panel-body"></div>
+ </div>
+</div>
+
+<script>
+(async function () {
+ const m = location.pathname.match(/\/design\/(\d+)/);
+ const qid = new URLSearchParams(location.search).get('id');
+ const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 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);
+ design = (await r.json()).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(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const created = design.created_at ? new Date(design.created_at) : null;
+ const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+ const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
+
+ // ambient tint from dominant (used by showcase mode via --amb)
+ document.documentElement.style.setProperty('--amb', dominant + '22');
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="roomleft">
+ <section class="col-room">
+ <div class="room-main">
+ <img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
+ <span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
+ </div>
+ <div class="room-thumbs">
+ <div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
+ <div class="t" style="background-image:url('${img}')"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:55%"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:30%"></div>
+ </div>
+ <div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
+ </section>
+
+ <section class="col-pattern">
+ <div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
+ <h1 class="title">${escapeHtml(title)}</h1>
+ <div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ <div class="cta-row">
+ <button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
+ <button class="cta buy" id="adjust-open-2">Adjust & buy</button>
+ </div>
+ <div class="mini-specs">
+ <div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
+ <div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
+ <div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
+ <div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
+ ${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
+ <div><div class="k">Edges</div><div class="v">PASS</div></div>
+ </div>
+ </section>
+</main>
+
+<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
+
+ // ---- adjustments: the hamburger menu of things to select from ----
+ const w = 24, h = 24;
+ const ADJ = [
+ ['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
+ ['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
+ ['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
+ ['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
+ ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
+ ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
+ ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
+ ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+ ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
+ ];
+
+ const menu = document.getElementById('adj-menu');
+ menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
+ `<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
+
+ const ov = document.getElementById('ov');
+ const panel = document.getElementById('adj-panel');
+ const menuEl = document.getElementById('adj-menu');
+ function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
+ function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
+ function showPanel(key){
+ const item = ADJ.find(a => a[0] === key); if(!item) return;
+ document.getElementById('adj-panel-title').textContent = item[2];
+ document.getElementById('adj-panel-body').innerHTML = item[3]();
+ panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
+ menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
+ if (key === 'calc') wireCalc();
+ }
+ ['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
+ document.getElementById('ov-scrim').addEventListener('click', closeOv);
+ document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
+ menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
+
+ function wireCalc(){
+ const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
+ if(!cw) return;
+ function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
+ if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
+ else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
+ [cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
+ }
+ // thumbnail → swap room main image
+ document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
+ const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
+ const u = bg.slice(5,-2); if(im && u) im.src = u;
+ }));
+})();
+window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
+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/v14-roomzine.html b/public/theme-variants/v14-roomzine.html
new file mode 100644
index 0000000..a054bbe
--- /dev/null
+++ b/public/theme-variants/v14-roomzine.html
@@ -0,0 +1,310 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Room-Left Editorial PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #f6f3ec;
+ --ink: #15120d;
+ --muted: #8a8276;
+ --panel: #e8e2d4;
+ --bar: rgba(246,243,236,.92);
+ --accent: #15120d;
+ --on-accent: #f6f3ec;
+ --split: 1.6fr 1fr;
+ --radius: 12px;
+ --shadow: 0 1px 0 var(--line);
+ --brandfont: var(--serif);
+ --titlefont: 500 42px/1.04 var(--serif);
+ --paneltitle: 500 24px/1 var(--serif);
+ --room-ar: 5/4;
+ --pat-max: 420px;
+ --pat-border: 0;
+ --room-pos: static;
+ --line: rgba(0,0,0,.10);
+ --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: 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: 40; background: var(--bar);
+ backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
+ display: flex; align-items: center; gap: 20px; }
+ header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
+ header nav { font-size: 12.5px; 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; }
+ .adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
+ background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
+ font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
+ .adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
+
+ main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
+ display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
+
+ /* LEFT — room setting */
+ .col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
+ .room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
+ overflow: hidden; position: relative; box-shadow: var(--shadow); }
+ .room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
+ border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
+ .room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
+ background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
+ .room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
+
+ /* RIGHT — pattern + buy */
+ .col-pattern { min-width: 0; }
+ .pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
+ .pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ h1.title { font: var(--titlefont); margin: 18px 0 4px; }
+ .cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
+ .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
+ .cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
+ .cta.sample { background: var(--accent); color: var(--on-accent); }
+ .cta.buy { background: var(--ink); color: var(--bg); }
+ .mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
+ border-top: 1px solid var(--line); padding-top: 16px; }
+ .mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
+ .mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
+
+ /* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
+ .ov { position: fixed; inset: 0; z-index: 60; display: none; }
+ .ov.open { display: block; }
+ .ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
+ .menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
+ box-shadow: 0 24px 80px rgba(0,0,0,.28); }
+ .menu { border-radius: 14px; padding: 8px; min-width: 250px; }
+ .menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
+ .menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
+ text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
+ .menu button:hover, .menu button.on { background: var(--panel); }
+ .menu button .ic { width: 22px; text-align: center; opacity: .7; }
+ .panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
+ .panel header { display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
+ .panel header h3 { margin: 0; font: var(--paneltitle); }
+ .panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ .panel .pbody { padding: 18px 20px 24px; }
+
+ /* mode: dropdown — menu under the button, panel as small popover top-right */
+ [data-mode="dropdown"] .menu { top: 62px; right: 24px; }
+ [data-mode="dropdown"] .panel { top: 62px; right: 24px; }
+ /* mode: drawer — slide in from right */
+ [data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
+ [data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
+ /* mode: sheet — slide up from bottom */
+ [data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
+ display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
+ [data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
+ [data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
+ /* mode: modal — centered */
+ [data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ [data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ /* mode: palette — command-palette near top-center */
+ [data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
+ [data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
+ /* mode: fab — floating action button, menu pops above it */
+ [data-mode="fab"] .menu { right: 24px; bottom: 86px; }
+ [data-mode="fab"] .panel { right: 24px; bottom: 86px; }
+ .fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
+ border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
+ box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
+ body[data-mode="fab"] .fab-btn { display: flex; }
+ body[data-mode="fab"] header .adjust-btn { display: none; }
+
+ /* adjustment panel content */
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
+ .sliders input[type=range] { width: 100%; }
+ .roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
+ .pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
+ .pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
+ .calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
+ .calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
+ .calc .tot b { font: 500 17px var(--serif); }
+ .spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spectab .row .v { font-family: var(--mono); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
+ pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+ @media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
+</style>
+</head>
+<body data-mode="sheet">
+
+<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>
+ <button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
+ <span class="switcher">Theme: Room-Left Editorial (V14) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
+
+<div id="root" class="loading">Loading design…</div>
+
+<div class="ov" id="ov">
+ <div class="scrim" id="ov-scrim"></div>
+ <div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
+ <div class="panel" id="adj-panel" role="dialog" hidden>
+ <header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
+ <div class="pbody" id="adj-panel-body"></div>
+ </div>
+</div>
+
+<script>
+(async function () {
+ const m = location.pathname.match(/\/design\/(\d+)/);
+ const qid = new URLSearchParams(location.search).get('id');
+ const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 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);
+ design = (await r.json()).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(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const created = design.created_at ? new Date(design.created_at) : null;
+ const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+ const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
+
+ // ambient tint from dominant (used by showcase mode via --amb)
+ document.documentElement.style.setProperty('--amb', dominant + '22');
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="roomleft">
+ <section class="col-room">
+ <div class="room-main">
+ <img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
+ <span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
+ </div>
+ <div class="room-thumbs">
+ <div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
+ <div class="t" style="background-image:url('${img}')"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:55%"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:30%"></div>
+ </div>
+ <div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
+ </section>
+
+ <section class="col-pattern">
+ <div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
+ <h1 class="title">${escapeHtml(title)}</h1>
+ <div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ <div class="cta-row">
+ <button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
+ <button class="cta buy" id="adjust-open-2">Adjust & buy</button>
+ </div>
+ <div class="mini-specs">
+ <div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
+ <div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
+ <div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
+ <div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
+ ${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
+ <div><div class="k">Edges</div><div class="v">PASS</div></div>
+ </div>
+ </section>
+</main>
+
+<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
+
+ // ---- adjustments: the hamburger menu of things to select from ----
+ const w = 24, h = 24;
+ const ADJ = [
+ ['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
+ ['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
+ ['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
+ ['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
+ ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
+ ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
+ ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
+ ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+ ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
+ ];
+
+ const menu = document.getElementById('adj-menu');
+ menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
+ `<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
+
+ const ov = document.getElementById('ov');
+ const panel = document.getElementById('adj-panel');
+ const menuEl = document.getElementById('adj-menu');
+ function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
+ function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
+ function showPanel(key){
+ const item = ADJ.find(a => a[0] === key); if(!item) return;
+ document.getElementById('adj-panel-title').textContent = item[2];
+ document.getElementById('adj-panel-body').innerHTML = item[3]();
+ panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
+ menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
+ if (key === 'calc') wireCalc();
+ }
+ ['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
+ document.getElementById('ov-scrim').addEventListener('click', closeOv);
+ document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
+ menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
+
+ function wireCalc(){
+ const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
+ if(!cw) return;
+ function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
+ if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
+ else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
+ [cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
+ }
+ // thumbnail → swap room main image
+ document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
+ const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
+ const u = bg.slice(5,-2); if(im && u) im.src = u;
+ }));
+})();
+window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
+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/v15-roomsticky.html b/public/theme-variants/v15-roomsticky.html
new file mode 100644
index 0000000..99b47c3
--- /dev/null
+++ b/public/theme-variants/v15-roomsticky.html
@@ -0,0 +1,310 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Room-Left Sticky PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #f7f8f4;
+ --ink: #16201a;
+ --muted: #8a8276;
+ --panel: #e4ebdf;
+ --bar: rgba(247,248,244,.93);
+ --accent: #2f5d3a;
+ --on-accent: #fff;
+ --split: 1fr 0.85fr;
+ --radius: 12px;
+ --shadow: 0 1px 0 var(--line);
+ --brandfont: var(--serif);
+ --titlefont: 500 30px/1.15 var(--serif);
+ --paneltitle: 500 18px/1 var(--serif);
+ --room-ar: 4/3;
+ --pat-max: 420px;
+ --pat-border: 0;
+ --room-pos: sticky;
+ --line: rgba(0,0,0,.10);
+ --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: 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: 40; background: var(--bar);
+ backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
+ display: flex; align-items: center; gap: 20px; }
+ header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
+ header nav { font-size: 12.5px; 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; }
+ .adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
+ background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
+ font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
+ .adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
+
+ main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
+ display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
+
+ /* LEFT — room setting */
+ .col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
+ .room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
+ overflow: hidden; position: relative; box-shadow: var(--shadow); }
+ .room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
+ border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
+ .room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
+ background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
+ .room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
+
+ /* RIGHT — pattern + buy */
+ .col-pattern { min-width: 0; }
+ .pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
+ .pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ h1.title { font: var(--titlefont); margin: 18px 0 4px; }
+ .cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
+ .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
+ .cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
+ .cta.sample { background: var(--accent); color: var(--on-accent); }
+ .cta.buy { background: var(--ink); color: var(--bg); }
+ .mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
+ border-top: 1px solid var(--line); padding-top: 16px; }
+ .mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
+ .mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
+
+ /* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
+ .ov { position: fixed; inset: 0; z-index: 60; display: none; }
+ .ov.open { display: block; }
+ .ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
+ .menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
+ box-shadow: 0 24px 80px rgba(0,0,0,.28); }
+ .menu { border-radius: 14px; padding: 8px; min-width: 250px; }
+ .menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
+ .menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
+ text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
+ .menu button:hover, .menu button.on { background: var(--panel); }
+ .menu button .ic { width: 22px; text-align: center; opacity: .7; }
+ .panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
+ .panel header { display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
+ .panel header h3 { margin: 0; font: var(--paneltitle); }
+ .panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ .panel .pbody { padding: 18px 20px 24px; }
+
+ /* mode: dropdown — menu under the button, panel as small popover top-right */
+ [data-mode="dropdown"] .menu { top: 62px; right: 24px; }
+ [data-mode="dropdown"] .panel { top: 62px; right: 24px; }
+ /* mode: drawer — slide in from right */
+ [data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
+ [data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
+ /* mode: sheet — slide up from bottom */
+ [data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
+ display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
+ [data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
+ [data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
+ /* mode: modal — centered */
+ [data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ [data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ /* mode: palette — command-palette near top-center */
+ [data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
+ [data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
+ /* mode: fab — floating action button, menu pops above it */
+ [data-mode="fab"] .menu { right: 24px; bottom: 86px; }
+ [data-mode="fab"] .panel { right: 24px; bottom: 86px; }
+ .fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
+ border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
+ box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
+ body[data-mode="fab"] .fab-btn { display: flex; }
+ body[data-mode="fab"] header .adjust-btn { display: none; }
+
+ /* adjustment panel content */
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
+ .sliders input[type=range] { width: 100%; }
+ .roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
+ .pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
+ .pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
+ .calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
+ .calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
+ .calc .tot b { font: 500 17px var(--serif); }
+ .spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spectab .row .v { font-family: var(--mono); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
+ pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+ @media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
+</style>
+</head>
+<body data-mode="modal">
+
+<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>
+ <button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
+ <span class="switcher">Theme: Room-Left Sticky (V15) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
+
+<div id="root" class="loading">Loading design…</div>
+
+<div class="ov" id="ov">
+ <div class="scrim" id="ov-scrim"></div>
+ <div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
+ <div class="panel" id="adj-panel" role="dialog" hidden>
+ <header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
+ <div class="pbody" id="adj-panel-body"></div>
+ </div>
+</div>
+
+<script>
+(async function () {
+ const m = location.pathname.match(/\/design\/(\d+)/);
+ const qid = new URLSearchParams(location.search).get('id');
+ const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 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);
+ design = (await r.json()).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(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const created = design.created_at ? new Date(design.created_at) : null;
+ const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+ const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
+
+ // ambient tint from dominant (used by showcase mode via --amb)
+ document.documentElement.style.setProperty('--amb', dominant + '22');
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="roomleft">
+ <section class="col-room">
+ <div class="room-main">
+ <img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
+ <span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
+ </div>
+ <div class="room-thumbs">
+ <div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
+ <div class="t" style="background-image:url('${img}')"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:55%"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:30%"></div>
+ </div>
+ <div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
+ </section>
+
+ <section class="col-pattern">
+ <div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
+ <h1 class="title">${escapeHtml(title)}</h1>
+ <div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ <div class="cta-row">
+ <button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
+ <button class="cta buy" id="adjust-open-2">Adjust & buy</button>
+ </div>
+ <div class="mini-specs">
+ <div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
+ <div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
+ <div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
+ <div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
+ ${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
+ <div><div class="k">Edges</div><div class="v">PASS</div></div>
+ </div>
+ </section>
+</main>
+
+<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
+
+ // ---- adjustments: the hamburger menu of things to select from ----
+ const w = 24, h = 24;
+ const ADJ = [
+ ['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
+ ['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
+ ['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
+ ['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
+ ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
+ ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
+ ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
+ ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+ ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
+ ];
+
+ const menu = document.getElementById('adj-menu');
+ menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
+ `<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
+
+ const ov = document.getElementById('ov');
+ const panel = document.getElementById('adj-panel');
+ const menuEl = document.getElementById('adj-menu');
+ function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
+ function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
+ function showPanel(key){
+ const item = ADJ.find(a => a[0] === key); if(!item) return;
+ document.getElementById('adj-panel-title').textContent = item[2];
+ document.getElementById('adj-panel-body').innerHTML = item[3]();
+ panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
+ menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
+ if (key === 'calc') wireCalc();
+ }
+ ['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
+ document.getElementById('ov-scrim').addEventListener('click', closeOv);
+ document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
+ menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
+
+ function wireCalc(){
+ const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
+ if(!cw) return;
+ function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
+ if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
+ else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
+ [cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
+ }
+ // thumbnail → swap room main image
+ document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
+ const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
+ const u = bg.slice(5,-2); if(im && u) im.src = u;
+ }));
+})();
+window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
+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/v16-roommin.html b/public/theme-variants/v16-roommin.html
new file mode 100644
index 0000000..4895872
--- /dev/null
+++ b/public/theme-variants/v16-roommin.html
@@ -0,0 +1,310 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Room-Left Minimal PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #fcfcfa;
+ --ink: #1f1f1d;
+ --muted: #8a8276;
+ --panel: #f0f0ec;
+ --bar: rgba(252,252,250,.92);
+ --accent: #1f1f1d;
+ --on-accent: #fcfcfa;
+ --split: 1fr 1fr;
+ --radius: 4px;
+ --shadow: none;
+ --brandfont: var(--sans);
+ --titlefont: 400 26px/1.25 var(--sans);
+ --paneltitle: 600 15px/1 var(--sans);
+ --room-ar: 4/3;
+ --pat-max: 420px;
+ --pat-border: 0;
+ --room-pos: static;
+ --line: rgba(0,0,0,.10);
+ --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: 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: 40; background: var(--bar);
+ backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
+ display: flex; align-items: center; gap: 20px; }
+ header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
+ header nav { font-size: 12.5px; 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; }
+ .adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
+ background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
+ font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
+ .adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
+
+ main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
+ display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
+
+ /* LEFT — room setting */
+ .col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
+ .room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
+ overflow: hidden; position: relative; box-shadow: var(--shadow); }
+ .room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
+ border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
+ .room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
+ background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
+ .room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
+
+ /* RIGHT — pattern + buy */
+ .col-pattern { min-width: 0; }
+ .pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
+ .pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ h1.title { font: var(--titlefont); margin: 18px 0 4px; }
+ .cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
+ .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
+ .cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
+ .cta.sample { background: var(--accent); color: var(--on-accent); }
+ .cta.buy { background: var(--ink); color: var(--bg); }
+ .mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
+ border-top: 1px solid var(--line); padding-top: 16px; }
+ .mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
+ .mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
+
+ /* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
+ .ov { position: fixed; inset: 0; z-index: 60; display: none; }
+ .ov.open { display: block; }
+ .ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
+ .menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
+ box-shadow: 0 24px 80px rgba(0,0,0,.28); }
+ .menu { border-radius: 14px; padding: 8px; min-width: 250px; }
+ .menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
+ .menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
+ text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
+ .menu button:hover, .menu button.on { background: var(--panel); }
+ .menu button .ic { width: 22px; text-align: center; opacity: .7; }
+ .panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
+ .panel header { display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
+ .panel header h3 { margin: 0; font: var(--paneltitle); }
+ .panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ .panel .pbody { padding: 18px 20px 24px; }
+
+ /* mode: dropdown — menu under the button, panel as small popover top-right */
+ [data-mode="dropdown"] .menu { top: 62px; right: 24px; }
+ [data-mode="dropdown"] .panel { top: 62px; right: 24px; }
+ /* mode: drawer — slide in from right */
+ [data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
+ [data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
+ /* mode: sheet — slide up from bottom */
+ [data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
+ display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
+ [data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
+ [data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
+ /* mode: modal — centered */
+ [data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ [data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ /* mode: palette — command-palette near top-center */
+ [data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
+ [data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
+ /* mode: fab — floating action button, menu pops above it */
+ [data-mode="fab"] .menu { right: 24px; bottom: 86px; }
+ [data-mode="fab"] .panel { right: 24px; bottom: 86px; }
+ .fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
+ border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
+ box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
+ body[data-mode="fab"] .fab-btn { display: flex; }
+ body[data-mode="fab"] header .adjust-btn { display: none; }
+
+ /* adjustment panel content */
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
+ .sliders input[type=range] { width: 100%; }
+ .roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
+ .pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
+ .pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
+ .calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
+ .calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
+ .calc .tot b { font: 500 17px var(--serif); }
+ .spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spectab .row .v { font-family: var(--mono); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
+ pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+ @media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
+</style>
+</head>
+<body data-mode="palette">
+
+<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>
+ <button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
+ <span class="switcher">Theme: Room-Left Minimal (V16) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
+
+<div id="root" class="loading">Loading design…</div>
+
+<div class="ov" id="ov">
+ <div class="scrim" id="ov-scrim"></div>
+ <div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
+ <div class="panel" id="adj-panel" role="dialog" hidden>
+ <header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
+ <div class="pbody" id="adj-panel-body"></div>
+ </div>
+</div>
+
+<script>
+(async function () {
+ const m = location.pathname.match(/\/design\/(\d+)/);
+ const qid = new URLSearchParams(location.search).get('id');
+ const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 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);
+ design = (await r.json()).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(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const created = design.created_at ? new Date(design.created_at) : null;
+ const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+ const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
+
+ // ambient tint from dominant (used by showcase mode via --amb)
+ document.documentElement.style.setProperty('--amb', dominant + '22');
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="roomleft">
+ <section class="col-room">
+ <div class="room-main">
+ <img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
+ <span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
+ </div>
+ <div class="room-thumbs">
+ <div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
+ <div class="t" style="background-image:url('${img}')"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:55%"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:30%"></div>
+ </div>
+ <div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
+ </section>
+
+ <section class="col-pattern">
+ <div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
+ <h1 class="title">${escapeHtml(title)}</h1>
+ <div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ <div class="cta-row">
+ <button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
+ <button class="cta buy" id="adjust-open-2">Adjust & buy</button>
+ </div>
+ <div class="mini-specs">
+ <div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
+ <div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
+ <div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
+ <div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
+ ${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
+ <div><div class="k">Edges</div><div class="v">PASS</div></div>
+ </div>
+ </section>
+</main>
+
+<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
+
+ // ---- adjustments: the hamburger menu of things to select from ----
+ const w = 24, h = 24;
+ const ADJ = [
+ ['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
+ ['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
+ ['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
+ ['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
+ ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
+ ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
+ ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
+ ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+ ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
+ ];
+
+ const menu = document.getElementById('adj-menu');
+ menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
+ `<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
+
+ const ov = document.getElementById('ov');
+ const panel = document.getElementById('adj-panel');
+ const menuEl = document.getElementById('adj-menu');
+ function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
+ function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
+ function showPanel(key){
+ const item = ADJ.find(a => a[0] === key); if(!item) return;
+ document.getElementById('adj-panel-title').textContent = item[2];
+ document.getElementById('adj-panel-body').innerHTML = item[3]();
+ panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
+ menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
+ if (key === 'calc') wireCalc();
+ }
+ ['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
+ document.getElementById('ov-scrim').addEventListener('click', closeOv);
+ document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
+ menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
+
+ function wireCalc(){
+ const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
+ if(!cw) return;
+ function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
+ if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
+ else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
+ [cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
+ }
+ // thumbnail → swap room main image
+ document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
+ const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
+ const u = bg.slice(5,-2); if(im && u) im.src = u;
+ }));
+})();
+window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
+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/v17-roomshow.html b/public/theme-variants/v17-roomshow.html
new file mode 100644
index 0000000..2c2dcf6
--- /dev/null
+++ b/public/theme-variants/v17-roomshow.html
@@ -0,0 +1,310 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — Room-Left Showcase PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ --bg: #f7f3ea;
+ --ink: #1a160d;
+ --muted: #8a8276;
+ --panel: #ece3d0;
+ --bar: rgba(247,243,234,.92);
+ --accent: #c2a14e;
+ --on-accent: #1a160d;
+ --split: 1.2fr 1fr;
+ --radius: 16px;
+ --shadow: 0 20px 55px rgba(0,0,0,.16);
+ --brandfont: var(--serif);
+ --titlefont: 500 32px/1.12 var(--serif);
+ --paneltitle: 500 18px/1 var(--serif);
+ --room-ar: 4/3;
+ --pat-max: 380px;
+ --pat-border: 10px solid #fff;
+ --room-pos: static;
+ --line: rgba(0,0,0,.10);
+ --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: 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: 40; background: var(--bar);
+ backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
+ display: flex; align-items: center; gap: 20px; }
+ header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
+ header nav { font-size: 12.5px; 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; }
+ .adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
+ background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
+ font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
+ .adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
+
+ main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
+ display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
+
+ /* LEFT — room setting */
+ .col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
+ .room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
+ overflow: hidden; position: relative; box-shadow: var(--shadow); }
+ .room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
+ border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
+ .room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
+ background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
+ .room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
+
+ /* RIGHT — pattern + buy */
+ .col-pattern { min-width: 0; }
+ .pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
+ .pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ h1.title { font: var(--titlefont); margin: 18px 0 4px; }
+ .cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
+ .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
+ .cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
+ .cta.sample { background: var(--accent); color: var(--on-accent); }
+ .cta.buy { background: var(--ink); color: var(--bg); }
+ .mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
+ border-top: 1px solid var(--line); padding-top: 16px; }
+ .mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
+ .mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
+
+ /* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
+ .ov { position: fixed; inset: 0; z-index: 60; display: none; }
+ .ov.open { display: block; }
+ .ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
+ .menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
+ box-shadow: 0 24px 80px rgba(0,0,0,.28); }
+ .menu { border-radius: 14px; padding: 8px; min-width: 250px; }
+ .menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
+ .menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
+ text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
+ .menu button:hover, .menu button.on { background: var(--panel); }
+ .menu button .ic { width: 22px; text-align: center; opacity: .7; }
+ .panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
+ .panel header { display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
+ .panel header h3 { margin: 0; font: var(--paneltitle); }
+ .panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ .panel .pbody { padding: 18px 20px 24px; }
+
+ /* mode: dropdown — menu under the button, panel as small popover top-right */
+ [data-mode="dropdown"] .menu { top: 62px; right: 24px; }
+ [data-mode="dropdown"] .panel { top: 62px; right: 24px; }
+ /* mode: drawer — slide in from right */
+ [data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
+ [data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
+ /* mode: sheet — slide up from bottom */
+ [data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
+ display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
+ [data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
+ [data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
+ /* mode: modal — centered */
+ [data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ [data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ /* mode: palette — command-palette near top-center */
+ [data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
+ [data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
+ /* mode: fab — floating action button, menu pops above it */
+ [data-mode="fab"] .menu { right: 24px; bottom: 86px; }
+ [data-mode="fab"] .panel { right: 24px; bottom: 86px; }
+ .fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
+ border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
+ box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
+ body[data-mode="fab"] .fab-btn { display: flex; }
+ body[data-mode="fab"] header .adjust-btn { display: none; }
+
+ /* adjustment panel content */
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
+ .sliders input[type=range] { width: 100%; }
+ .roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
+ .pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
+ .pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
+ .calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
+ .calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
+ .calc .tot b { font: 500 17px var(--serif); }
+ .spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spectab .row .v { font-family: var(--mono); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
+ pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+ @media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
+</style>
+</head>
+<body data-mode="fab">
+
+<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>
+ <button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
+ <span class="switcher">Theme: Room-Left Showcase (V17) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
+
+<div id="root" class="loading">Loading design…</div>
+
+<div class="ov" id="ov">
+ <div class="scrim" id="ov-scrim"></div>
+ <div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
+ <div class="panel" id="adj-panel" role="dialog" hidden>
+ <header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
+ <div class="pbody" id="adj-panel-body"></div>
+ </div>
+</div>
+
+<script>
+(async function () {
+ const m = location.pathname.match(/\/design\/(\d+)/);
+ const qid = new URLSearchParams(location.search).get('id');
+ const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 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);
+ design = (await r.json()).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(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const created = design.created_at ? new Date(design.created_at) : null;
+ const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+ const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
+
+ // ambient tint from dominant (used by showcase mode via --amb)
+ document.documentElement.style.setProperty('--amb', dominant + '22');
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="roomleft">
+ <section class="col-room">
+ <div class="room-main">
+ <img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
+ <span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
+ </div>
+ <div class="room-thumbs">
+ <div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
+ <div class="t" style="background-image:url('${img}')"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:55%"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:30%"></div>
+ </div>
+ <div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
+ </section>
+
+ <section class="col-pattern">
+ <div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
+ <h1 class="title">${escapeHtml(title)}</h1>
+ <div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ <div class="cta-row">
+ <button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
+ <button class="cta buy" id="adjust-open-2">Adjust & buy</button>
+ </div>
+ <div class="mini-specs">
+ <div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
+ <div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
+ <div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
+ <div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
+ ${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
+ <div><div class="k">Edges</div><div class="v">PASS</div></div>
+ </div>
+ </section>
+</main>
+
+<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
+
+ // ---- adjustments: the hamburger menu of things to select from ----
+ const w = 24, h = 24;
+ const ADJ = [
+ ['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
+ ['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
+ ['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
+ ['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
+ ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
+ ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
+ ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
+ ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+ ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
+ ];
+
+ const menu = document.getElementById('adj-menu');
+ menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
+ `<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
+
+ const ov = document.getElementById('ov');
+ const panel = document.getElementById('adj-panel');
+ const menuEl = document.getElementById('adj-menu');
+ function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
+ function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
+ function showPanel(key){
+ const item = ADJ.find(a => a[0] === key); if(!item) return;
+ document.getElementById('adj-panel-title').textContent = item[2];
+ document.getElementById('adj-panel-body').innerHTML = item[3]();
+ panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
+ menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
+ if (key === 'calc') wireCalc();
+ }
+ ['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
+ document.getElementById('ov-scrim').addEventListener('click', closeOv);
+ document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
+ menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
+
+ function wireCalc(){
+ const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
+ if(!cw) return;
+ function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
+ if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
+ else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
+ [cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
+ }
+ // thumbnail → swap room main image
+ document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
+ const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
+ const u = bg.slice(5,-2); if(im && u) im.src = u;
+ }));
+})();
+window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
+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/scripts/gen-theme-variants.js b/scripts/gen-theme-variants.js
new file mode 100644
index 0000000..b642796
--- /dev/null
+++ b/scripts/gen-theme-variants.js
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+/* Generate the V12–V17 "room-left + hamburger adjustments" PDP theme variants
+ * from scripts/room-left-template.html. Each shares the concept (room setting
+ * LEFT of the pattern, adjustments selected from a hamburger) but differs in
+ * hamburger MODE + palette + split ratio + typography. Emits into
+ * public/theme-variants/. Idempotent — safe to re-run. */
+'use strict';
+const fs = require('fs');
+const path = require('path');
+
+const TPL = fs.readFileSync(path.join(__dirname, 'room-left-template.html'), 'utf8');
+const OUT = path.join(__dirname, '..', 'public', 'theme-variants');
+
+const base = {
+ muted: '#8a8276', line: 'rgba(0,0,0,.10)',
+ brandfont: 'var(--serif)', paneltitle: '500 18px/1 var(--serif)',
+ radius: '12px', shadow: '0 1px 0 var(--line)', patMax: '420px', patBorder: '0', roomAr: '4/3', roomPos: 'static',
+};
+
+const CONFIGS = [
+ { code:'V12', slug:'roomsplit', name:'Room-Left Split', mode:'dropdown',
+ bg:'#faf8f3', ink:'#1a1611', panel:'#eee9dd', bar:'rgba(250,248,243,.92)',
+ accent:'#16203c', onAccent:'#fff', split:'1fr 1fr', titlefont:'500 30px/1.15 var(--serif)' },
+ { code:'V13', slug:'roomdrawer', name:'Room-Left Drawer', mode:'drawer',
+ bg:'#fbf7f4', ink:'#211a16', panel:'#efe6df', bar:'rgba(251,247,244,.93)',
+ accent:'#b5572e', onAccent:'#fff', split:'1.4fr 1fr', roomAr:'3/4', titlefont:'500 31px/1.12 var(--serif)' },
+ { code:'V14', slug:'roomzine', name:'Room-Left Editorial', mode:'sheet',
+ bg:'#f6f3ec', ink:'#15120d', panel:'#e8e2d4', bar:'rgba(246,243,236,.92)',
+ accent:'#15120d', onAccent:'#f6f3ec', split:'1.6fr 1fr', roomAr:'5/4',
+ titlefont:'500 42px/1.04 var(--serif)', paneltitle:'500 24px/1 var(--serif)' },
+ { code:'V15', slug:'roomsticky', name:'Room-Left Sticky', mode:'modal',
+ bg:'#f7f8f4', ink:'#16201a', panel:'#e4ebdf', bar:'rgba(247,248,244,.93)',
+ accent:'#2f5d3a', onAccent:'#fff', split:'1fr 0.85fr', roomPos:'sticky', titlefont:'500 30px/1.15 var(--serif)' },
+ { code:'V16', slug:'roommin', name:'Room-Left Minimal', mode:'palette',
+ bg:'#fcfcfa', ink:'#1f1f1d', panel:'#f0f0ec', bar:'rgba(252,252,250,.92)',
+ accent:'#1f1f1d', onAccent:'#fcfcfa', split:'1fr 1fr', radius:'4px', shadow:'none',
+ brandfont:'var(--sans)', titlefont:'400 26px/1.25 var(--sans)', paneltitle:'600 15px/1 var(--sans)' },
+ { code:'V17', slug:'roomshow', name:'Room-Left Showcase', mode:'fab',
+ bg:'#f7f3ea', ink:'#1a160d', panel:'#ece3d0', bar:'rgba(247,243,234,.92)',
+ accent:'#c2a14e', onAccent:'#1a160d', split:'1.2fr 1fr', radius:'16px',
+ shadow:'0 20px 55px rgba(0,0,0,.16)', patMax:'380px', patBorder:'10px solid #fff',
+ titlefont:'500 32px/1.12 var(--serif)' },
+];
+
+function cssVars(c) {
+ const v = { ...base, ...c };
+ const lines = [
+ ['--bg', v.bg], ['--ink', v.ink], ['--muted', v.muted], ['--panel', v.panel], ['--bar', v.bar],
+ ['--accent', v.accent], ['--on-accent', v.onAccent], ['--split', v.split],
+ ['--radius', v.radius], ['--shadow', v.shadow], ['--brandfont', v.brandfont],
+ ['--titlefont', v.titlefont], ['--paneltitle', v.paneltitle],
+ ['--room-ar', v.roomAr], ['--pat-max', v.patMax], ['--pat-border', v.patBorder], ['--room-pos', v.roomPos],
+ ];
+ return lines.map(([k, val]) => ` ${k}: ${val};`).join('\n');
+}
+
+let n = 0;
+for (const c of CONFIGS) {
+ const html = TPL
+ .replace(/%%CODE%%/g, c.code)
+ .replace(/%%NAME%%/g, c.name)
+ .replace(/%%MODE%%/g, c.mode)
+ .replace(/%%CSSVARS%%/g, cssVars(c));
+ const file = path.join(OUT, `${c.code.toLowerCase()}-${c.slug}.html`);
+ fs.writeFileSync(file, html);
+ console.log(`wrote ${path.basename(file)} (${c.name} · mode=${c.mode})`);
+ n++;
+}
+console.log(`[done] ${n} variants generated`);
+module.exports = { CONFIGS };
diff --git a/scripts/room-left-template.html b/scripts/room-left-template.html
new file mode 100644
index 0000000..ff29d3c
--- /dev/null
+++ b/scripts/room-left-template.html
@@ -0,0 +1,294 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>wallco.ai — %%NAME%% PDP</title>
+<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
+<style>
+ :root {
+ %%CSSVARS%%
+ --line: rgba(0,0,0,.10);
+ --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: 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: 40; background: var(--bar);
+ backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); padding: 10px 24px;
+ display: flex; align-items: center; gap: 20px; }
+ header .brand { font: 600 18px/1 var(--brandfont); letter-spacing: .04em; }
+ header nav { font-size: 12.5px; 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; }
+ .adjust-btn { display: inline-flex; align-items: center; gap: 8px; border: 1px solid var(--line);
+ background: var(--accent); color: var(--on-accent); border-radius: 999px; padding: 9px 16px;
+ font: 700 12px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .adjust-btn .bars { display: inline-flex; flex-direction: column; gap: 2.5px; }
+ .adjust-btn .bars i { width: 15px; height: 2px; background: currentColor; border-radius: 2px; display: block; }
+
+ main.roomleft { max-width: 1640px; margin: 0 auto; padding: 22px 24px 60px;
+ display: grid; grid-template-columns: var(--split); gap: 30px; align-items: start; }
+
+ /* LEFT — room setting */
+ .col-room { min-width: 0; position: var(--room-pos, static); top: 80px; }
+ .room-main { aspect-ratio: var(--room-ar, 4/3); background: var(--panel); border-radius: var(--radius);
+ overflow: hidden; position: relative; box-shadow: var(--shadow); }
+ .room-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .room-main .tag { position: absolute; left: 14px; bottom: 14px; background: var(--bar);
+ border: 1px solid var(--line); border-radius: 999px; padding: 6px 13px;
+ font: 600 10.5px/1 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .room-thumbs { display: flex; gap: 8px; margin-top: 12px; }
+ .room-thumbs .t { flex: 1; aspect-ratio: 1/1; border-radius: calc(var(--radius) - 4px);
+ background: var(--panel); border: 1px solid var(--line); background-size: cover; background-position: center; cursor: pointer; }
+ .room-cap { margin-top: 12px; font-size: 12px; color: var(--muted); }
+
+ /* RIGHT — pattern + buy */
+ .col-pattern { min-width: 0; }
+ .pattern-art { aspect-ratio: 1/1; max-width: var(--pat-max, 420px); background: var(--panel);
+ border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); border: var(--pat-border, 0); }
+ .pattern-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ h1.title { font: var(--titlefont); margin: 18px 0 4px; }
+ .cat { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
+ .cta-row { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
+ .cta { border-radius: 999px; padding: 13px 20px; font: 700 12px/1 var(--sans);
+ letter-spacing: .07em; text-transform: uppercase; border: 1px solid transparent; }
+ .cta.sample { background: var(--accent); color: var(--on-accent); }
+ .cta.buy { background: var(--ink); color: var(--bg); }
+ .mini-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; max-width: 460px;
+ border-top: 1px solid var(--line); padding-top: 16px; }
+ .mini-specs .k { font: 600 10px/1.3 var(--sans); letter-spacing: .07em; text-transform: uppercase; color: var(--muted); }
+ .mini-specs .v { font-family: var(--mono); font-size: 12.5px; margin-top: 1px; }
+
+ /* adjustments overlay (menu + panel) — placement driven by body[data-mode] */
+ .ov { position: fixed; inset: 0; z-index: 60; display: none; }
+ .ov.open { display: block; }
+ .ov .scrim { position: absolute; inset: 0; background: rgba(20,17,14,.45); backdrop-filter: blur(3px); }
+ .menu, .panel { position: absolute; background: var(--bg); border: 1px solid var(--line);
+ box-shadow: 0 24px 80px rgba(0,0,0,.28); }
+ .menu { border-radius: 14px; padding: 8px; min-width: 250px; }
+ .menu h4 { margin: 6px 12px 8px; font: 600 10.5px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
+ .menu button { display: flex; width: 100%; align-items: center; gap: 11px; border: 0; background: transparent;
+ text-align: left; padding: 10px 12px; border-radius: 9px; color: var(--ink); font-size: 13.5px; }
+ .menu button:hover, .menu button.on { background: var(--panel); }
+ .menu button .ic { width: 22px; text-align: center; opacity: .7; }
+ .panel { border-radius: 14px; width: min(440px, 92vw); max-height: 78vh; overflow: auto; }
+ .panel header { display: flex; justify-content: space-between; align-items: center;
+ padding: 16px 20px; border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--bg); }
+ .panel header h3 { margin: 0; font: var(--paneltitle); }
+ .panel header .x { background: transparent; border: 0; font-size: 22px; color: var(--muted); }
+ .panel .pbody { padding: 18px 20px 24px; }
+
+ /* mode: dropdown — menu under the button, panel as small popover top-right */
+ [data-mode="dropdown"] .menu { top: 62px; right: 24px; }
+ [data-mode="dropdown"] .panel { top: 62px; right: 24px; }
+ /* mode: drawer — slide in from right */
+ [data-mode="drawer"] .menu { top: 0; right: 0; height: 100%; border-radius: 0; min-width: 320px; padding: 18px; }
+ [data-mode="drawer"] .panel { top: 0; right: 0; height: 100%; border-radius: 0; width: min(440px,94vw); max-height: 100%; }
+ /* mode: sheet — slide up from bottom */
+ [data-mode="sheet"] .menu { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; min-width: 0;
+ display: grid; grid-template-columns: repeat(auto-fill,minmax(150px,1fr)); gap: 6px; padding: 18px 22px 28px; }
+ [data-mode="sheet"] .menu h4 { grid-column: 1/-1; }
+ [data-mode="sheet"] .panel { left: 0; right: 0; bottom: 0; border-radius: 18px 18px 0 0; width: auto; max-height: 70vh; }
+ /* mode: modal — centered */
+ [data-mode="modal"] .menu { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ [data-mode="modal"] .panel { top: 50%; left: 50%; transform: translate(-50%,-50%); }
+ /* mode: palette — command-palette near top-center */
+ [data-mode="palette"] .menu { top: 84px; left: 50%; transform: translateX(-50%); width: min(520px,92vw); }
+ [data-mode="palette"] .panel { top: 84px; left: 50%; transform: translateX(-50%); }
+ /* mode: fab — floating action button, menu pops above it */
+ [data-mode="fab"] .menu { right: 24px; bottom: 86px; }
+ [data-mode="fab"] .panel { right: 24px; bottom: 86px; }
+ .fab-btn { position: fixed; right: 24px; bottom: 24px; z-index: 55; width: 60px; height: 60px;
+ border-radius: 50%; background: var(--accent); color: var(--on-accent); border: 0;
+ box-shadow: 0 10px 30px rgba(0,0,0,.3); font-size: 22px; display: none; align-items: center; justify-content: center; }
+ body[data-mode="fab"] .fab-btn { display: flex; }
+ body[data-mode="fab"] header .adjust-btn { display: none; }
+
+ /* adjustment panel content */
+ .swatches { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
+ .sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 0 0 1px var(--line); }
+ .sliders label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 3px; }
+ .sliders input[type=range] { width: 100%; }
+ .roomprev { height: 220px; border-radius: 10px; background: var(--panel); background-size: cover; background-position: center; }
+ .pairg { display: grid; grid-template-columns: repeat(3,1fr); gap: 6px; }
+ .pairg .p { aspect-ratio: 1/1; border-radius: 6px; background: var(--panel); background-size: cover; background-position: center; }
+ .pills { display: flex; flex-wrap: wrap; gap: 6px; }
+ .pill { font-size: 11px; padding: 5px 10px; border-radius: 999px; background: var(--panel); border: 1px solid var(--line); text-decoration: none; }
+ .calc .r { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+ .calc label { font-size: 11px; color: var(--muted); display: block; margin-bottom: 2px; }
+ .calc input, .calc select { width: 100%; padding: 7px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit; background: #fff; }
+ .calc .tot { margin-top: 10px; padding: 10px 12px; background: var(--panel); border-radius: 8px; }
+ .calc .tot b { font: 500 17px var(--serif); }
+ .spectab .row { display: grid; grid-template-columns: 150px 1fr; padding: 6px 0; border-bottom: 1px solid var(--line); font-size: 12.5px; }
+ .spectab .row .k { color: var(--muted); font: 600 10px/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
+ .spectab .row .v { font-family: var(--mono); }
+ .tile-btns { display: flex; flex-wrap: wrap; gap: 6px; }
+ .tile-btn { flex: 1; min-width: 100px; background: var(--panel); border: 1px solid var(--line); padding: 10px; border-radius: 8px; font: 600 12px/1.2 var(--sans); text-align: center; text-decoration: none; }
+ pre.prompt { font: 11px/1.45 var(--mono); background: var(--panel); padding: 12px; border-radius: 8px; max-height: 200px; overflow: auto; margin: 0; white-space: pre-wrap; word-break: break-word; }
+
+ footer.site { border-top: 1px solid var(--line); padding: 24px; text-align: center; color: var(--muted); font-size: 12px; background: var(--panel); }
+ footer.site .h { font: 500 15px/1.4 var(--serif); color: var(--ink); margin-bottom: 4px; }
+ .loading { display: flex; align-items: center; justify-content: center; height: 60vh; color: var(--muted); font-style: italic; }
+ @media (max-width: 1000px) { main.roomleft { grid-template-columns: 1fr; } .col-room { position: static; } }
+ @media (max-width: 640px) { header.topbar { flex-wrap: wrap; padding: 8px 14px; } main.roomleft { padding: 16px 14px; } }
+</style>
+</head>
+<body data-mode="%%MODE%%">
+
+<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>
+ <button class="adjust-btn" id="adjust-open"><span class="bars"><i></i><i></i><i></i></span> Adjust</button>
+ <span class="switcher">Theme: %%NAME%% (%%CODE%%) <a id="back-to-classic" href="#">← Classic</a></span>
+</header>
+
+<button class="fab-btn" id="fab-open" aria-label="Adjust"><span style="font-size:24px">≡</span></button>
+
+<div id="root" class="loading">Loading design…</div>
+
+<div class="ov" id="ov">
+ <div class="scrim" id="ov-scrim"></div>
+ <div class="menu" id="adj-menu" role="menu" aria-label="Adjustments"></div>
+ <div class="panel" id="adj-panel" role="dialog" hidden>
+ <header><h3 id="adj-panel-title">Adjust</h3><button class="x" id="adj-panel-close">×</button></header>
+ <div class="pbody" id="adj-panel-body"></div>
+ </div>
+</div>
+
+<script>
+(async function () {
+ const m = location.pathname.match(/\/design\/(\d+)/);
+ const qid = new URLSearchParams(location.search).get('id');
+ const id = m ? parseInt(m[1], 10) : (qid ? parseInt(qid, 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);
+ design = (await r.json()).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(/ · /g, ' — ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
+ const isMural = design.kind === 'mural_panel' || design.kind === 'mural';
+ const kindLabel = design.kind === 'seamless_tile' ? 'Seamless Tile (24×24")' : isMural ? 'Mural Panel' : (design.kind || 'pattern');
+ const palette = Array.isArray(design.palette) ? design.palette : [];
+ const dominant = design.dominant_hex || (palette[0] && palette[0].hex) || '#999';
+ const img = design.image_url || ('/designs/img/by-id/' + id);
+ const handle = design.handle || ('wallco-' + id);
+ const generator = design.generator || 'wallco.ai';
+ const created = design.created_at ? new Date(design.created_at) : null;
+ const roomMockup = (design.room_mockups && (Array.isArray(design.room_mockups) ? design.room_mockups[0] : Object.keys(design.room_mockups)[0])) || null;
+ const roomImgUrl = roomMockup ? ('/designs/room/design_' + id + '_' + roomMockup + '.png') : null;
+ const roomLabel = roomMockup ? roomMockup.replace(/_/g, ' ') : 'room preview';
+
+ // ambient tint from dominant (used by showcase mode via --amb)
+ document.documentElement.style.setProperty('--amb', dominant + '22');
+
+ const root = document.getElementById('root');
+ root.className = '';
+ root.innerHTML = `
+<main class="roomleft">
+ <section class="col-room">
+ <div class="room-main">
+ <img loading="eager" src="${roomImgUrl || img}" alt="${escapeAttr(title)} in a room">
+ <span class="tag">${roomImgUrl ? 'See in a room — ' + escapeHtml(roomLabel) : 'Pattern preview'}</span>
+ </div>
+ <div class="room-thumbs">
+ <div class="t" ${roomImgUrl ? `style="background-image:url('${roomImgUrl}')"` : `style="background-image:url('${img}')"`}></div>
+ <div class="t" style="background-image:url('${img}')"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:55%"></div>
+ <div class="t" style="background-image:url('${img}'); background-size:30%"></div>
+ </div>
+ <div class="room-cap">Room setting shown left · the wallcovering applied to a styled interior. Tap a thumbnail to switch the view.</div>
+ </section>
+
+ <section class="col-pattern">
+ <div class="pattern-art"><img loading="eager" src="${img}" alt="${escapeAttr(title)}"></div>
+ <h1 class="title">${escapeHtml(title)}</h1>
+ <div class="cat">${escapeHtml(catDisplay)} · ${escapeHtml(handle)} · by ${escapeHtml(generator)}</div>
+ <div class="cta-row">
+ <button class="cta sample" onclick="alert('Sample added (stub)')">Add sample · $4.25</button>
+ <button class="cta buy" id="adjust-open-2">Adjust & buy</button>
+ </div>
+ <div class="mini-specs">
+ <div><div class="k">Kind</div><div class="v">${escapeHtml(kindLabel)}</div></div>
+ <div><div class="k">Dominant</div><div class="v">${escapeHtml(dominant)}</div></div>
+ <div><div class="k">Pattern No.</div><div class="v">${id}</div></div>
+ <div><div class="k">Status</div><div class="v">${design.is_published ? 'Live' : 'Curating'}</div></div>
+ ${created ? `<div><div class="k">Created</div><div class="v" title="${escapeAttr(design.created_at)}">${created.toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'})}</div></div>` : ''}
+ <div><div class="k">Edges</div><div class="v">PASS</div></div>
+ </div>
+ </section>
+</main>
+
+<footer class="site"><div class="h">wallco.ai</div><div>A Designer Wallcoverings company · Sherman Oaks, CA</div></footer>`;
+
+ // ---- adjustments: the hamburger menu of things to select from ----
+ const w = 24, h = 24;
+ const ADJ = [
+ ['palette','◐','Palette & recolor', () => `<div class="swatches">${(palette.length?palette.slice(0,6):[{hex:dominant}]).map(p=>`<div class="sw" style="background:${escapeAttr(p.hex)}" title="${escapeAttr(p.hex)}"></div>`).join('')}</div><p style="color:var(--muted);font-size:12.5px">Recolor this design — drag a swatch onto the pattern to remap that ink (full recolor in Classic).</p>`],
+ ['image','✦','Image adjustments', () => `<div class="sliders">${['Hue','Saturation','Brightness','Contrast','Sepia'].map(s=>`<label>${s}</label><input type="range" min="0" max="100" value="50">`).join('')}</div>`],
+ ['room','▣','See in a room', () => `<div class="roomprev" style="${roomImgUrl?`background-image:url('${roomImgUrl}')`:''}"></div><p style="color:var(--muted);font-size:12.5px;margin-top:10px">${roomImgUrl?('Residential preview — '+escapeHtml(roomLabel)):'No room render yet for this design.'}</p>`],
+ ['pairs','▥','Pairs well with', () => `<div class="pairg">${Array.from({length:6}).map(()=>`<div class="p" style="background-image:url('${img}');background-size:${40+Math.floor(Math.random()*40)}%"></div>`).join('')}</div>`],
+ ['paints','◇','Coordinate paints', () => `<div class="pills">${['Sherwin-Williams','Dunn-Edwards','Benjamin Moore','Farrow & Ball','Behr','PPG','RAL'].map(b=>`<a class="pill" href="#">${b}</a>`).join('')}</div>`],
+ ['specs','≡','Full specifications', () => `<div class="spectab">${[['Category',catDisplay],['Kind',kindLabel],['Width',(isMural?'36"':'24"')],['Sold by',isMural?'Per panel (36"×11ft)':'Per roll (27 ft)'],['Repeat',w+'" straight match'],['Material','Type II vinyl · class A'],['Handle',handle],['Designed by',generator],['Settlement','OK (logged)'],['Edges scan','PASS · 6-lens']].map(([k,v])=>`<div class="row"><span class="k">${escapeHtml(k)}</span><span class="v">${escapeHtml(v)}</span></div>`).join('')}</div>`],
+ ['calc','▦','Roll calculator', () => `<div class="calc"><div class="r"><div><label>Wall W (ft)</label><input id="cw" type="number" value="10" min="1"></div><div><label>Wall H (ft)</label><input id="ch" type="number" value="9" min="1"></div><div><label>Roll W</label><select id="crw"><option>24</option><option>36</option><option>52</option></select></div></div><div class="tot" id="ctot">— rolls · $—</div></div>`],
+ ['share','↗','Share · Furnish · Trade', () => `<div class="tile-btns"><a class="tile-btn" href="#" onclick="copyLink();return false">Copy link</a><a class="tile-btn" href="#">Pinterest</a><a class="tile-btn" href="#">Instagram</a><a class="tile-btn" href="/design/${id}/tearsheet" target="_blank">Tear sheet</a><a class="tile-btn" href="#">Furnish (AI)</a><a class="tile-btn" href="#">Email client</a></div>`],
+ ['gen','⌘','Generator + prompt', () => `<pre class="prompt">${escapeHtml((design.prompt||'wallco.ai original · seed '+(design.seed||'—')).slice(0,1200))}</pre>`],
+ ];
+
+ const menu = document.getElementById('adj-menu');
+ menu.innerHTML = '<h4>Adjust this design</h4>' + ADJ.map(([k,ic,label]) =>
+ `<button data-adj="${k}" role="menuitem"><span class="ic">${ic}</span> ${label}</button>`).join('');
+
+ const ov = document.getElementById('ov');
+ const panel = document.getElementById('adj-panel');
+ const menuEl = document.getElementById('adj-menu');
+ function openMenu(){ ov.classList.add('open'); panel.hidden = true; menuEl.style.display=''; }
+ function closeOv(){ ov.classList.remove('open'); panel.hidden = true; }
+ function showPanel(key){
+ const item = ADJ.find(a => a[0] === key); if(!item) return;
+ document.getElementById('adj-panel-title').textContent = item[2];
+ document.getElementById('adj-panel-body').innerHTML = item[3]();
+ panel.hidden = false; menuEl.style.display='none'; ov.classList.add('open');
+ menu.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.adj===key));
+ if (key === 'calc') wireCalc();
+ }
+ ['adjust-open','adjust-open-2','fab-open'].forEach(idn => { const el=document.getElementById(idn); if(el) el.addEventListener('click', openMenu); });
+ document.getElementById('ov-scrim').addEventListener('click', closeOv);
+ document.getElementById('adj-panel-close').addEventListener('click', () => { panel.hidden=true; menuEl.style.display=''; });
+ menu.addEventListener('click', e => { const b=e.target.closest('button[data-adj]'); if(b) showPanel(b.dataset.adj); });
+ document.addEventListener('keydown', e => { if(e.key==='Escape') closeOv(); });
+
+ function wireCalc(){
+ const cw=document.getElementById('cw'), ch=document.getElementById('ch'), crw=document.getElementById('crw'), tot=document.getElementById('ctot');
+ if(!cw) return;
+ function calc(){ const a=+cw.value||0,b=+ch.value||0,rw=+crw.value||24; if(a<1||b<1){tot.textContent='— rolls · $—';return;}
+ if(isMural){const p=Math.ceil((a*12)/rw);tot.innerHTML='<b>'+p+'</b> panels · <b>$'+(p*245).toLocaleString()+'</b>';}
+ else{const wp=Math.ceil((a*12)/rw),r=wp*Math.ceil(b/27);tot.innerHTML='<b>'+r+'</b> rolls · <b>$'+(r*165).toLocaleString()+'</b>';} }
+ [cw,ch,crw].forEach(el=>el.addEventListener('input',calc)); calc();
+ }
+ // thumbnail → swap room main image
+ document.querySelectorAll('.room-thumbs .t').forEach(t => t.addEventListener('click', () => {
+ const bg = t.style.backgroundImage; const im = document.querySelector('.room-main img');
+ const u = bg.slice(5,-2); if(im && u) im.src = u;
+ }));
+})();
+window.copyLink = async function(){ try{ await navigator.clipboard.writeText(location.href.replace(/\/[a-z0-9-]+$/,'/'+location.pathname.match(/design\/(\d+)/)[1])); alert('Link copied'); }catch{ alert('Copy failed'); } };
+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 0c667b2..156687a 100644
--- a/server.js
+++ b/server.js
@@ -466,6 +466,12 @@ app.get('/design/:id/spec', (req, res) => themeVariantSend(req, res, 'v8-sp
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'));
app.get('/design/:id/best', (req, res) => themeVariantSend(req, res, 'v11-best.html'));
+app.get('/design/:id/roomsplit', (req, res) => themeVariantSend(req, res, 'v12-roomsplit.html'));
+app.get('/design/:id/roomdrawer', (req, res) => themeVariantSend(req, res, 'v13-roomdrawer.html'));
+app.get('/design/:id/roomzine', (req, res) => themeVariantSend(req, res, 'v14-roomzine.html'));
+app.get('/design/:id/roomsticky', (req, res) => themeVariantSend(req, res, 'v15-roomsticky.html'));
+app.get('/design/:id/roommin', (req, res) => themeVariantSend(req, res, 'v16-roommin.html'));
+app.get('/design/:id/roomshow', (req, res) => themeVariantSend(req, res, 'v17-roomshow.html'));
// ── Lightweight in-memory search across the catalog.
// /api/designs/search?q=<term>&limit=20
@@ -11704,6 +11710,24 @@ try {
<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>
+ <a class="hb-menu-item" data-hb-theme="roomsplit" role="menuitem" href="/design/${design.id}/roomsplit">
+ <span class="hb-menu-icon" aria-hidden="true">▢</span> V12 · Room-Left Split
+ </a>
+ <a class="hb-menu-item" data-hb-theme="roomdrawer" role="menuitem" href="/design/${design.id}/roomdrawer">
+ <span class="hb-menu-icon" aria-hidden="true">◧</span> V13 · Room-Left Drawer
+ </a>
+ <a class="hb-menu-item" data-hb-theme="roomzine" role="menuitem" href="/design/${design.id}/roomzine">
+ <span class="hb-menu-icon" aria-hidden="true">❏</span> V14 · Room-Left Editorial
+ </a>
+ <a class="hb-menu-item" data-hb-theme="roomsticky" role="menuitem" href="/design/${design.id}/roomsticky">
+ <span class="hb-menu-icon" aria-hidden="true">◫</span> V15 · Room-Left Sticky
+ </a>
+ <a class="hb-menu-item" data-hb-theme="roommin" role="menuitem" href="/design/${design.id}/roommin">
+ <span class="hb-menu-icon" aria-hidden="true">▫</span> V16 · Room-Left Minimal
+ </a>
+ <a class="hb-menu-item" data-hb-theme="roomshow" role="menuitem" href="/design/${design.id}/roomshow">
+ <span class="hb-menu-icon" aria-hidden="true">◉</span> V17 · Room-Left Showcase
+ </a>
</div>
${_isAdmin ? `
<button type="button" class="hb-menu-item" data-hb-action="edit" role="menuitem">
diff --git a/src/admin.js b/src/admin.js
index 1e4e59f..e151bb0 100644
--- a/src/admin.js
+++ b/src/admin.js
@@ -162,6 +162,7 @@ function adminHeader(active) {
['/admin/inspiration', 'Inspiration'],
['/admin/leads', 'Leads'],
['/admin/golive', 'Go-Live'],
+ ['/admin/theme-gallery','Theme Gallery'],
['/', 'View site →']
];
return `<header class="admin-header">
← 0b5f501 cactus-curator: per-card Bad/Digital/Unpublish animate out +
·
back to Wallco Ai
·
fix(publish-gate): sudo-psql on Linux + image-path fallback cf75a5c →