← back to Hollywood Wallcoverings
Add Hollywood-branded Custom Creator at /CustomCreator (made-to-measure murals)
90eedda6c424d03c9e59231127a00f21333354cb · 2026-06-20 11:19:43 -0700 · Steve
Ports the mural-pdp custom creator into the Hollywood Express app: gallery +
configurator + live-priced Shopify draft-order checkout ($83.38/m², recomputed
server-side) + bespoke quote flow. Uses the SHOPIFY_* tokens already in the prod
process env — no new secrets, no nginx change. Upstream vendor names (Rebel Walls,
Graham & Brown) neutralized to 'Made to Measure' so nothing leaks customer-facing.
Adds a Custom Creator nav link. Scrubs the philipperomano/wallco heritage:
wallco-page-theme -> hw-page-theme, package.json description.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A customcreator.jsA customcreator/configure.htmlA customcreator/gallery.htmlA customcreator/gb-murals.jsonA customcreator/gb/119057.jpgA customcreator/gb/142786.jpgA customcreator/gb/142795.jpgA customcreator/gb/142804.jpgA customcreator/gb/142813.jpgA customcreator/gb/142822.jpgA customcreator/gb/142831.jpgA customcreator/gb/142840.jpgA customcreator/gb/142849.jpgA customcreator/gb/142858.jpgA customcreator/gb/142867.jpgA customcreator/gb/142876.jpgA customcreator/gb/142885.jpgA customcreator/gb/142894.jpgA customcreator/gb/142903.jpgA customcreator/gb/142912.jpgA customcreator/gb/142921.jpgA customcreator/gb/142930.jpgA customcreator/gb/142939.jpgA customcreator/gb/145332.jpgA customcreator/gb/145341.jpgA customcreator/gb/145350.jpgA customcreator/gb/145359.jpgA customcreator/gb/145368.jpgA customcreator/gb/145767.jpgA customcreator/gb/145776.jpgA customcreator/gb/145785.jpgA customcreator/gb/145819.jpgA customcreator/gb/145828.jpgA customcreator/gb/145837.jpgA customcreator/gb/145846.jpgA customcreator/gb/145855.jpgA customcreator/gb/145864.jpgA customcreator/gb/145873.jpgA customcreator/gb/145882.jpgA customcreator/gb/145891.jpgA customcreator/rooms/bedroom-fg.pngA customcreator/rooms/bedroom.jpgA customcreator/rooms/dining-fg.pngA customcreator/rooms/dining.jpgA customcreator/rooms/hallway-fg.pngA customcreator/rooms/hallway.jpgA customcreator/rooms/hero.jpgA customcreator/rooms/living-fg.pngA customcreator/rooms/living.jpgA customcreator/rooms/lobby-fg.pngA customcreator/rooms/lobby.jpgA customcreator/rooms/nursery-fg.pngA customcreator/rooms/nursery.jpgA customcreator/rooms/office-fg.pngA customcreator/rooms/office.jpgM package.jsonM server.js
Diff
commit 90eedda6c424d03c9e59231127a00f21333354cb
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jun 20 11:19:43 2026 -0700
Add Hollywood-branded Custom Creator at /CustomCreator (made-to-measure murals)
Ports the mural-pdp custom creator into the Hollywood Express app: gallery +
configurator + live-priced Shopify draft-order checkout ($83.38/m², recomputed
server-side) + bespoke quote flow. Uses the SHOPIFY_* tokens already in the prod
process env — no new secrets, no nginx change. Upstream vendor names (Rebel Walls,
Graham & Brown) neutralized to 'Made to Measure' so nothing leaks customer-facing.
Adds a Custom Creator nav link. Scrubs the philipperomano/wallco heritage:
wallco-page-theme -> hw-page-theme, package.json description.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
customcreator.js | 205 ++++++++++++++++
customcreator/configure.html | 489 +++++++++++++++++++++++++++++++++++++
customcreator/gallery.html | 142 +++++++++++
customcreator/gb-murals.json | 38 +++
customcreator/gb/119057.jpg | Bin 0 -> 655346 bytes
customcreator/gb/142786.jpg | Bin 0 -> 1154066 bytes
customcreator/gb/142795.jpg | Bin 0 -> 309588 bytes
customcreator/gb/142804.jpg | Bin 0 -> 302198 bytes
customcreator/gb/142813.jpg | Bin 0 -> 426615 bytes
customcreator/gb/142822.jpg | Bin 0 -> 456786 bytes
customcreator/gb/142831.jpg | Bin 0 -> 451289 bytes
customcreator/gb/142840.jpg | Bin 0 -> 381028 bytes
customcreator/gb/142849.jpg | Bin 0 -> 449747 bytes
customcreator/gb/142858.jpg | Bin 0 -> 354068 bytes
customcreator/gb/142867.jpg | Bin 0 -> 447537 bytes
customcreator/gb/142876.jpg | Bin 0 -> 690336 bytes
customcreator/gb/142885.jpg | Bin 0 -> 713723 bytes
customcreator/gb/142894.jpg | Bin 0 -> 502081 bytes
customcreator/gb/142903.jpg | Bin 0 -> 534451 bytes
customcreator/gb/142912.jpg | Bin 0 -> 606170 bytes
customcreator/gb/142921.jpg | Bin 0 -> 774380 bytes
customcreator/gb/142930.jpg | Bin 0 -> 592262 bytes
customcreator/gb/142939.jpg | Bin 0 -> 516786 bytes
customcreator/gb/145332.jpg | Bin 0 -> 737835 bytes
customcreator/gb/145341.jpg | Bin 0 -> 725317 bytes
customcreator/gb/145350.jpg | Bin 0 -> 678745 bytes
customcreator/gb/145359.jpg | Bin 0 -> 850319 bytes
customcreator/gb/145368.jpg | Bin 0 -> 821211 bytes
customcreator/gb/145767.jpg | Bin 0 -> 543626 bytes
customcreator/gb/145776.jpg | Bin 0 -> 540258 bytes
customcreator/gb/145785.jpg | Bin 0 -> 557461 bytes
customcreator/gb/145819.jpg | Bin 0 -> 868207 bytes
customcreator/gb/145828.jpg | Bin 0 -> 1162528 bytes
customcreator/gb/145837.jpg | Bin 0 -> 1082734 bytes
customcreator/gb/145846.jpg | Bin 0 -> 943748 bytes
customcreator/gb/145855.jpg | Bin 0 -> 905549 bytes
customcreator/gb/145864.jpg | Bin 0 -> 857751 bytes
customcreator/gb/145873.jpg | Bin 0 -> 828907 bytes
customcreator/gb/145882.jpg | Bin 0 -> 720205 bytes
customcreator/gb/145891.jpg | Bin 0 -> 705416 bytes
customcreator/rooms/bedroom-fg.png | Bin 0 -> 113259 bytes
customcreator/rooms/bedroom.jpg | Bin 0 -> 43041 bytes
customcreator/rooms/dining-fg.png | Bin 0 -> 131011 bytes
customcreator/rooms/dining.jpg | Bin 0 -> 47874 bytes
customcreator/rooms/hallway-fg.png | Bin 0 -> 65678 bytes
customcreator/rooms/hallway.jpg | Bin 0 -> 65720 bytes
customcreator/rooms/hero.jpg | Bin 0 -> 209870 bytes
customcreator/rooms/living-fg.png | Bin 0 -> 174164 bytes
customcreator/rooms/living.jpg | Bin 0 -> 55936 bytes
customcreator/rooms/lobby-fg.png | Bin 0 -> 57537 bytes
customcreator/rooms/lobby.jpg | Bin 0 -> 81120 bytes
customcreator/rooms/nursery-fg.png | Bin 0 -> 237660 bytes
customcreator/rooms/nursery.jpg | Bin 0 -> 45913 bytes
customcreator/rooms/office-fg.png | Bin 0 -> 82604 bytes
customcreator/rooms/office.jpg | Bin 0 -> 62181 bytes
package.json | 2 +-
server.js | 16 +-
57 files changed, 886 insertions(+), 6 deletions(-)
diff --git a/customcreator.js b/customcreator.js
new file mode 100644
index 0000000..4bed175
--- /dev/null
+++ b/customcreator.js
@@ -0,0 +1,205 @@
+'use strict';
+/**
+ * Hollywood Wallcoverings — Custom Creator (made-to-measure murals), mounted at /CustomCreator.
+ *
+ * Ported from the Designer Wallcoverings mural-pdp app into the Hollywood Express storefront.
+ * Customer-facing brand is HOLLYWOOD WALLCOVERINGS only — every upstream vendor name
+ * (Rebel Walls, Graham & Brown) is neutralized to "Made to Measure" so it never reaches the page.
+ *
+ * Pricing: custom murals sell at a flat $83.38/m² (UNIFORM across the live "Sold Per Square Meter"
+ * variants). price = RATE_SQM * area_m², recomputed SERVER-SIDE from clamped dims so the browser
+ * can never set the price. Bespoke murals are quote-on-request (mailto, no priced checkout).
+ *
+ * Tokens come from the process env already present on the Hollywood prod process:
+ * SHOPIFY_ADMIN_TOKEN (reads), SHOPIFY_DRAFT_TOKEN (write_draft_orders checkout), SHOPIFY_STORE.
+ * No new secrets, no nginx change — it rides the existing / (port 9832) Express app.
+ */
+const fs = require('fs');
+const path = require('path');
+
+const CC_DIR = path.join(__dirname, 'customcreator');
+const STORE = (process.env.SHOPIFY_STORE || 'designer-laboratory-sandbox.myshopify.com').replace(/^https?:\/\//, '');
+const SHOP = 'https://' + STORE;
+const EP = SHOP + '/admin/api/2024-10/graphql.json';
+const READ_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const DRAFT_TOKEN = process.env.SHOPIFY_DRAFT_TOKEN || READ_TOKEN;
+
+const RATE_SQM = 83.38, SAMPLE = 4.25, CM_PER_IN = 2.54;
+const MIN_CM = 50, MAX_CM = 1200;
+const HOUSE = 'Made to Measure'; // customer-facing vendor label (never the real upstream)
+
+function ftIn(inch) { let f = Math.floor(inch / 12), i = Math.round(inch - f * 12); if (i === 12) { f++; i = 0; } return `${f}′ ${i}″`; }
+const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
+
+// Customer-safe spec map: label -> first matching metafield key (namespace.key).
+const SPEC_FIELDS = [
+ ['Material', ['specs.material', 'custom.material']],
+ ['Fire Rating', ['specs.fire_rating', 'custom.fire_rating']],
+ ['Pattern Match', ['specs.match_type', 'custom.match_type']],
+ ['Style', ['specs.style']],
+ ['Motif', ['specs.pattern']],
+ ['Sold By', ['specs.unit_of_measure']],
+ ['Country of Origin', ['custom.origin', 'global.Country-of-Origin']],
+ ['Shipping Weight', ['specs.weight']],
+ ['Standard Panel', ['custom.mural_ref_dimensions']],
+];
+// NEVER expose: upstream-vendor leak / internal analysis fields.
+const LEAK_KEYS = /manufacturer_sku|supplier_name|real_vendor|discount_trade|google_product_category|custom_product|migrated_at|title_tag|description_tag|mural_cost|hue_coverage|cov_/i;
+// Strip any "| Rebel Walls …" / "| Graham & Brown …" trailing vendor tag from titles.
+const VENDOR_TAIL = /\s*\|\s*(Rebel Walls|Graham\s*&?\s*Brown).*$/i;
+
+async function gql(query, variables, token) {
+ const r = await fetch(EP, { method: 'POST', headers: { 'X-Shopify-Access-Token': token, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) });
+ return r.json();
+}
+
+const PRODUCT_Q = `query($q:String!){ products(first:1, query:$q){ nodes{
+ title handle vendor productType descriptionHtml
+ images(first:30){ nodes{ url altText } }
+ metafields(first:80){ nodes{ namespace key value type } }
+ variants(first:5){ nodes{ sku price selectedOptions{ name value } } }
+} } }`;
+const LIST_Q = `query{ products(first:24, query:"vendor:\\"Rebel Walls\\" status:active"){ nodes{ handle title featuredImage{url} variants(first:1){nodes{sku}} } } }`;
+const COLLECTION_Q = `query($after:String){ products(first:50, query:"vendor:\\"Rebel Walls\\" status:active", after:$after){ pageInfo{hasNextPage endCursor} nodes{
+ handle title createdAt
+ images(first:5){nodes{url}}
+ variants(first:1){nodes{sku}}
+ style:metafield(namespace:"specs",key:"style"){value}
+ chex:metafield(namespace:"custom",key:"color_1_hex"){value}
+ cname:metafield(namespace:"custom",key:"color_1_name"){value}
+} } }`;
+
+async function collection(maxN) {
+ const out = []; let after = null;
+ for (let pg = 0; pg < 4 && out.length < maxN; pg++) {
+ const j = await gql(COLLECTION_Q, { after }, READ_TOKEN);
+ const c = j.data && j.data.products; if (!c) break;
+ for (const n of c.nodes) {
+ const imgs = (n.images.nodes || []).map(i => i.url);
+ const flat = imgs.find(u => /image1\.(jpg|jpeg|png|webp)/i.test(u)) || imgs[0];
+ out.push({
+ handle: n.handle, sku: (n.variants.nodes[0] && n.variants.nodes[0].sku || '').replace(/-(Sample|Complete Mural|Sold Per Square Meter)$/i, ''),
+ title: n.title.replace(VENDOR_TAIL, ''), image: flat, createdAt: n.createdAt,
+ style: n.style && n.style.value || '', colorHex: n.chex && n.chex.value || '', colorName: n.cname && n.cname.value || '',
+ });
+ }
+ if (!c.pageInfo.hasNextPage) break; after = c.pageInfo.endCursor;
+ }
+ return out.slice(0, maxN);
+}
+
+// Bespoke murals (showcase) — quote-on-request (no priced checkout, links never expose the source).
+let GB_MURALS = [];
+try {
+ GB_MURALS = JSON.parse(fs.readFileSync(path.join(CC_DIR, 'gb-murals.json'), 'utf8')).map(m => ({
+ handle: 'gb:' + m.handle, id: m.id, sku: m.id + '-MASTER', title: m.name, vendor: HOUSE,
+ image: '/CustomCreator/gb/' + m.id + '.jpg',
+ }));
+} catch (e) { console.log('[CustomCreator] gb-murals load:', e.message); }
+
+function cleanProduct(p) {
+ const mf = {}; p.metafields.nodes.forEach(m => { if (!LEAK_KEYS.test(m.namespace + '.' + m.key)) mf[m.namespace + '.' + m.key] = m.value; });
+ const get1 = keys => { for (const k of keys) if (mf[k] != null && String(mf[k]).trim()) return mf[k]; return null; };
+ const specs = [];
+ for (const [label, keys] of SPEC_FIELDS) { const v = get1(keys); if (v) specs.push({ label, value: String(v) }); }
+ const colors = [];
+ for (let i = 1; i <= 5; i++) { const n = mf['custom.color_' + i + '_name'], h = mf['custom.color_' + i + '_hex'], pc = mf['custom.color_' + i + '_percentage']; if (h) colors.push({ name: n || '', hex: h, pct: pc ? Math.round(parseFloat(pc)) : null }); }
+ const baseSku = (p.variants.nodes[0] && p.variants.nodes[0].sku || '').replace(/-(Sample|Complete Mural|Sold Per Square Meter)$/i, '');
+ const sampleVar = p.variants.nodes.find(v => /Sample/i.test(v.selectedOptions[0]?.value || ''));
+ const imgs = p.images.nodes.map(i => ({ url: i.url, alt: (i.altText || p.title).replace(VENDOR_TAIL, '') }));
+ const flat = imgs.find(i => /image1\.(jpg|jpeg|png|webp)/i.test(i.url)) || imgs[0];
+ return {
+ sku: baseSku, title: p.title.replace(VENDOR_TAIL, ''),
+ pattern: (mf['custom.pattern_name'] || p.title).replace(VENDOR_TAIL, ''),
+ vendor: HOUSE, descriptionHtml: p.descriptionHtml || '',
+ images: imgs, flatImage: flat ? flat.url : (imgs[0] && imgs[0].url),
+ specs, colors, rate: RATE_SQM, sample: SAMPLE,
+ samplePrice: sampleVar ? sampleVar.price : String(SAMPLE),
+ };
+}
+
+const CREATE = `mutation($i:DraftOrderInput!){ draftOrderCreate(input:$i){ draftOrder{ id name invoiceUrl totalPriceSet{shopMoney{amount}} } userErrors{field message} } }`;
+
+async function makeDraft(body) {
+ const mode = body.mode === 'sample' ? 'sample' : 'mural';
+ const sku = String(body.sku || '').slice(0, 40), pattern = String(body.pattern || 'Custom Mural').slice(0, 80);
+ let line;
+ if (mode === 'sample') {
+ line = { title: `${pattern} — Sample`, originalUnitPrice: SAMPLE.toFixed(2), quantity: 1, requiresShipping: true, taxable: true, customAttributes: [{ key: 'SKU', value: sku }, { key: 'Type', value: 'Sample' }] };
+ } else {
+ const f = body.unit === 'cm' ? 1 : CM_PER_IN;
+ const w_cm = clamp((parseFloat(body.width) || 0) * f, MIN_CM, MAX_CM);
+ const h_cm = clamp((parseFloat(body.height) || 0) * f, MIN_CM, MAX_CM);
+ const area = (w_cm / 100) * (h_cm / 100);
+ if (!(area > 0)) return { error: 'invalid dimensions' };
+ const price = +(RATE_SQM * area).toFixed(2);
+ const wDim = ftIn(w_cm / CM_PER_IN), hDim = ftIn(h_cm / CM_PER_IN);
+ line = { title: `${pattern} — Custom Mural ${wDim} × ${hDim}`, originalUnitPrice: price.toFixed(2), quantity: 1, requiresShipping: true, taxable: true,
+ customAttributes: [{ key: 'SKU', value: sku }, { key: 'Width', value: wDim }, { key: 'Height', value: hDim }, { key: 'Area', value: `${(area * 10.7639).toFixed(1)} sq ft` }, { key: 'Rate', value: `$${RATE_SQM}/m²` }] };
+ }
+ const j = await gql(CREATE, { i: { lineItems: [line], tags: ['custom-mural', 'hollywood-custom-creator'] } }, DRAFT_TOKEN);
+ const ue = j.data && j.data.draftOrderCreate && j.data.draftOrderCreate.userErrors;
+ if (ue && ue.length) return { error: ue.map(e => e.message).join('; ') };
+ if (j.errors) return { error: (j.errors[0] && j.errors[0].message) || 'graphql error', scopeHint: /access denied|write_draft_orders/i.test(JSON.stringify(j.errors)) };
+ const d = j.data.draftOrderCreate.draftOrder;
+ return { invoiceUrl: d.invoiceUrl, name: d.name, total: d.totalPriceSet.shopMoney.amount };
+}
+
+function mount(app, express) {
+ const sendHtml = file => (req, res) => { res.type('html'); res.send(fs.readFileSync(path.join(CC_DIR, file))); };
+
+ // Static room/bespoke imagery (long-cached).
+ app.use('/CustomCreator/rooms', express.static(path.join(CC_DIR, 'rooms'), { maxAge: '1d' }));
+ app.use('/CustomCreator/gb', express.static(path.join(CC_DIR, 'gb'), { maxAge: '1d' }));
+
+ // Pages.
+ app.get(['/CustomCreator', '/CustomCreator/'], sendHtml('gallery.html'));
+ app.get('/CustomCreator/configure', sendHtml('configure.html'));
+
+ // APIs.
+ app.get('/CustomCreator/api/collection', (req, res) => {
+ const gb = GB_MURALS.map(g => ({ handle: g.handle, sku: g.sku, title: g.title, image: g.image, createdAt: '2026-06-18T18:00:00Z', style: 'Bespoke Mural', colorHex: '', colorName: '', vendor: HOUSE }));
+ if (!READ_TOKEN) return res.json(gb);
+ collection(160).then(list => res.json([...gb, ...list])).catch(() => res.json(gb));
+ });
+
+ app.get('/CustomCreator/api/products', (req, res) => {
+ if (!READ_TOKEN) return res.status(503).json({ error: 'catalog unavailable' });
+ gql(LIST_Q, {}, READ_TOKEN)
+ .then(j => res.json((j.data.products.nodes || []).map(n => ({ handle: n.handle, title: n.title.replace(VENDOR_TAIL, ''), image: n.featuredImage && n.featuredImage.url }))))
+ .catch(e => res.status(500).json({ error: e.message }));
+ });
+
+ app.get('/CustomCreator/api/product', (req, res) => {
+ const handle = req.query.handle, sku = req.query.sku;
+ if (handle && handle.indexOf('gb:') === 0) {
+ const g = GB_MURALS.find(x => x.handle === handle);
+ if (!g) return res.status(404).json({ error: 'not found' });
+ return res.json({
+ sku: g.sku, title: g.title, pattern: g.title, vendor: HOUSE,
+ descriptionHtml: 'A made-to-measure ' + g.title + ' bespoke mural — printed to your exact wall size on premium non-woven.',
+ images: [{ url: g.image, alt: g.title }], flatImage: g.image,
+ specs: [{ label: 'Type', value: 'Bespoke Mural' }, { label: 'Material', value: 'Non-woven, paste-the-wall' }, { label: 'Print', value: 'Made to measure' }],
+ colors: [], rate: 96.88, bespoke: true, sample: SAMPLE, samplePrice: String(SAMPLE),
+ });
+ }
+ if (!READ_TOKEN) return res.status(503).json({ error: 'catalog unavailable' });
+ const q = handle ? `handle:${handle}` : sku ? `sku:${sku}` : `vendor:"Rebel Walls" status:active`;
+ gql(PRODUCT_Q, { q }, READ_TOKEN).then(j => {
+ const p = j.data && j.data.products.nodes[0];
+ if (!p) return res.status(404).json({ error: 'not found' });
+ res.json(cleanProduct(p));
+ }).catch(e => res.status(500).json({ error: e.message }));
+ });
+
+ app.post('/CustomCreator/api/mural-cart', express.json({ limit: '8kb' }), (req, res) => {
+ if (!DRAFT_TOKEN) return res.status(503).json({ error: 'checkout unavailable' });
+ makeDraft(req.body || {})
+ .then(out => res.status(out.error ? (out.scopeHint ? 403 : 400) : 200).json(out))
+ .catch(e => res.status(500).json({ error: e.message }));
+ });
+
+ console.log(`[CustomCreator] mounted at /CustomCreator (read:${READ_TOKEN ? 'ok' : 'MISSING'} draft:${DRAFT_TOKEN ? 'ok' : 'MISSING'} · ${GB_MURALS.length} bespoke)`);
+}
+
+module.exports = { mount };
diff --git a/customcreator/configure.html b/customcreator/configure.html
new file mode 100644
index 0000000..813549f
--- /dev/null
+++ b/customcreator/configure.html
@@ -0,0 +1,489 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Custom Mural Configurator — Hollywood Wallcoverings</title>
+<style>
+ :root{
+ --ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e2db; --bg:#fbfaf7; --paper:#fff;
+ --accent:#c9a865; --accent-ink:#a98b50; --radius:4px;
+ --serif:'Cormorant Garamond',Georgia,'Times New Roman',serif;
+ --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
+ }
+ *{box-sizing:border-box;margin:0;padding:0}
+ body{font-family:var(--sans);color:var(--ink);background:var(--bg);line-height:1.55;-webkit-font-smoothing:antialiased}
+ .wrap{max-width:1320px;margin:0 auto;padding:30px 28px 90px}
+ .crumb{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-bottom:22px}
+ .crumb a{color:var(--muted);text-decoration:none}
+ .pdp{display:grid;grid-template-columns:1.18fr .82fr;gap:46px;align-items:start}
+ @media(max-width:920px){.pdp{grid-template-columns:1fr;gap:30px}}
+
+ .stage{position:sticky;top:18px}
+ .tabs{display:inline-flex;border:1px solid var(--line);border-radius:22px;overflow:hidden;margin-bottom:12px}
+ .tabs button{border:0;background:var(--paper);padding:7px 16px;font-size:12px;letter-spacing:.05em;cursor:pointer;color:var(--muted)}
+ .tabs button.on{background:var(--ink);color:#fff}
+ .photos .hero-img{width:100%;aspect-ratio:4/5;object-fit:cover;background:#eee;border-radius:var(--radius);display:block}
+ .thumbs{display:flex;gap:9px;margin-top:11px;flex-wrap:wrap}
+ .thumbs img{width:64px;height:80px;object-fit:cover;border-radius:3px;cursor:pointer;opacity:.5;border:1px solid var(--line);transition:opacity .15s}
+ .thumbs img.active,.thumbs img:hover{opacity:1}
+ .hide{display:none!important}
+
+ /* technical wall preview */
+ .preview-frame{background:#f3f1ec;border:1px solid var(--line);border-radius:var(--radius);padding:22px;display:flex;align-items:center;justify-content:center;min-height:360px}
+ .preview{position:relative;max-width:100%;max-height:60vh;box-shadow:0 6px 26px rgba(0,0,0,.16);background-size:cover;background-position:center;outline:1px solid rgba(0,0,0,.12)}
+ .preview .seam{position:absolute;top:0;bottom:0;width:0;border-left:1px dashed rgba(255,255,255,.85);box-shadow:0 0 0 .5px rgba(0,0,0,.25)}
+ .preview .dropnum{position:absolute;top:8px;transform:translateX(-50%);font-size:11px;font-weight:600;color:#fff;background:rgba(0,0,0,.45);padding:1px 6px;border-radius:10px}
+ .preview .trim{position:absolute;border:1px dashed rgba(255,80,80,.95);pointer-events:none}
+ .preview .trim-lbl{position:absolute;font-size:10px;color:#c0392b;background:rgba(255,255,255,.85);padding:0 4px;border-radius:3px}
+ .ov{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
+ .ov button{border:1px solid var(--line);background:#fff;border-radius:20px;padding:5px 13px;font-size:11.5px;cursor:pointer;color:var(--muted)}
+ .ov button.on{border-color:var(--ink);background:var(--ink);color:#fff}
+
+ /* PHOTOREAL room visualizer */
+ .room-frame{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--bg)}
+ .room{position:relative;width:100%;aspect-ratio:3/2;overflow:hidden;background-size:cover;background-position:center;background-color:#e9e2d4}
+ .room-wall{position:absolute;overflow:hidden}
+ .room-mural{position:absolute;bottom:0;left:50%;transform:translateX(-50%);background-size:cover;background-position:center bottom;transition:width .1s,height .1s}
+ .room-fg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none}
+ .room-cap{position:absolute;left:12px;bottom:10px;font-size:11px;letter-spacing:.04em;color:#fff;background:rgba(0,0,0,.42);padding:3px 9px;border-radius:12px}
+ .room-sel{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
+ .room-sel button{border:1px solid var(--line);background:#fff;border-radius:20px;padding:6px 14px;font-size:12px;cursor:pointer;color:var(--muted)}
+ .room-sel button.on{border-color:var(--ink);background:var(--ink);color:#fff}
+ .rz{position:absolute;right:-10px;bottom:-10px;width:20px;height:20px;border-radius:50%;background:var(--accent);border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.35);cursor:nwse-resize;display:none;touch-action:none;z-index:3}
+ .room-mural.custom{cursor:grab;bottom:auto;background-position:center;box-shadow:0 0 0 1px rgba(255,255,255,.75),0 8px 24px rgba(0,0,0,.32);touch-action:none}
+ .room-mural.custom:active{cursor:grabbing}
+ .room-mural.custom .rz{display:block}
+ .cust-ctrl{display:flex;align-items:center;gap:16px;margin-top:11px;font-size:12px;color:var(--muted);flex-wrap:wrap}
+ .cust-ctrl label{display:flex;align-items:center;gap:7px}
+ .cust-ctrl input[type=range]{width:110px;-webkit-appearance:none;appearance:none;height:4px;border-radius:3px;background:var(--line)}
+ .cust-ctrl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:15px;height:15px;border-radius:50%;background:var(--accent);border:2px solid #fff;cursor:pointer}
+ .cust-ctrl button{border:1px solid var(--line);background:#fff;border-radius:3px;padding:5px 11px;font-size:12px;cursor:pointer;color:var(--ink)}
+ .room-sel button.upload{border-style:dashed;color:var(--accent-ink)}
+ .room-pick{display:flex;gap:8px;margin-top:10px;overflow-x:auto;padding-bottom:4px}
+ .room-pick button{flex:0 0 auto;width:52px;height:52px;border-radius:5px;border:2px solid transparent;background-size:cover;background-position:center;cursor:pointer;opacity:.8}
+ .room-pick button.on{border-color:var(--ink);opacity:1}
+
+ .panel{max-width:470px}
+ .vendor{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-ink);font-weight:600}
+ h1{font-family:var(--serif);font-weight:500;font-size:38px;line-height:1.05;margin:8px 0 6px}
+ .colors{display:flex;align-items:center;gap:10px;margin:12px 0 16px;flex-wrap:wrap}
+ .chip{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
+ .chip i{width:15px;height:15px;border-radius:50%;border:1px solid var(--line);display:inline-block}
+ .desc{font-size:14.5px;color:#3a3a3a;line-height:1.6;margin:0 0 22px}
+ .calc{background:var(--paper);border:1px solid var(--line);border-radius:var(--radius);padding:22px}
+ .calc h2{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:600;margin-bottom:14px}
+ .unit-tog{display:inline-flex;border:1px solid var(--line);border-radius:20px;overflow:hidden;margin-bottom:16px}
+ .unit-tog button{border:0;background:var(--paper);padding:5px 15px;font-size:12px;cursor:pointer;color:var(--muted)}
+ .unit-tog button.on{background:var(--ink);color:#fff}
+ .dims{display:grid;grid-template-columns:1fr auto 1fr;align-items:end;gap:10px}
+ .field label{display:block;font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-bottom:5px}
+ .field input{width:100%;border:1px solid var(--line);border-radius:3px;padding:11px 12px;font-size:16px;background:#fff;font-family:var(--sans)}
+ .field input:focus{outline:none;border-color:var(--accent)}
+ .times{padding-bottom:12px;color:var(--muted)}
+ .sliders{display:flex;flex-direction:column;gap:18px}
+ .srow .shead{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:8px}
+ .srow label{font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:var(--muted)}
+ .srow .sval{font-family:var(--serif);font-size:21px;color:var(--ink)}
+ input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:4px;border-radius:3px;background:var(--line);outline:none;cursor:pointer}
+ input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:21px;height:21px;border-radius:50%;background:var(--accent);cursor:pointer;border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.28)}
+ input[type=range]::-moz-range-thumb{width:21px;height:21px;border-radius:50%;background:var(--accent);cursor:pointer;border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.28)}
+ input[type=range]:focus{outline:none}
+ .presets{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 4px}
+ .presets button{border:1px solid var(--line);background:#fff;border-radius:3px;padding:6px 11px;font-size:12px;cursor:pointer;color:var(--ink)}
+ .presets button:hover{border-color:var(--ink)}
+ .hint{font-size:11.5px;color:var(--muted);margin:8px 2px 0;line-height:1.5}
+ .readout{margin:16px 0 4px;border-top:1px solid var(--line)}
+ .readout div{display:flex;justify-content:space-between;padding:8px 2px;font-size:13px;border-bottom:1px solid var(--line)}
+ .readout dt{color:var(--muted)} .readout dd{font-weight:600}
+ .price-block{display:flex;align-items:baseline;justify-content:space-between;margin:16px 0 2px}
+ .price-block .lbl{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
+ .price{font-family:var(--serif);font-size:40px;font-weight:500;line-height:1}
+ .price small{font-size:15px;color:var(--muted)}
+ .perm2{font-size:12px;color:var(--muted);text-align:right;margin-bottom:16px}
+ .cta{display:block;width:100%;text-align:center;border:0;border-radius:3px;padding:15px;font-size:14px;letter-spacing:.05em;cursor:pointer;font-family:var(--sans)}
+ .cta.primary{background:var(--accent);color:#fff;font-weight:600;margin-bottom:10px}
+ .cta.primary:hover{background:var(--accent-ink)} .cta.primary:disabled{opacity:.6;cursor:wait}
+ .cta.ghost{background:#fff;color:var(--ink);border:1px solid var(--line)} .cta.ghost:hover{border-color:var(--ink)}
+ .note{font-size:12px;color:var(--muted);margin-top:13px;line-height:1.5}
+ .sec-title{font-family:var(--serif);font-size:22px;margin:32px 0 4px}
+ .specs{border-top:1px solid var(--line)}
+ .specs div{display:flex;justify-content:space-between;padding:12px 2px;border-bottom:1px solid var(--line);font-size:13.5px}
+ .specs dt{color:var(--muted)} .specs dd{text-align:right;max-width:62%}
+ .loading{opacity:.4}
+
+ /* ===== Above-the-fold fit — whole configurator on one screen, no page scroll ===== */
+ html,body{height:100%}
+ body{overflow:hidden}
+ /* render the entire UI permanently at 80% (reduced 20% from original, like a browser zoom-out) */
+ :root{--page-zoom:.8}
+ html{zoom:var(--page-zoom)}
+ /* divide the fold height by the zoom so the 80%-scaled page still fills the screen (no bottom gap) */
+ .wrap{height:calc(100svh / var(--page-zoom));padding:14px 28px 16px;display:flex;flex-direction:column;overflow:hidden}
+ .crumb{margin-bottom:10px}
+ .pdp{flex:1;min-height:0;gap:34px;align-items:stretch}
+ /* left column: stack tabs + active media region (fills) + selectors */
+ .stage{position:static;min-height:0;display:flex;flex-direction:column;overflow:hidden}
+ #roomFrameWrap,#photos,#previewFrame{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}
+ .room-frame{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;overflow:hidden}
+ .room{width:100%;height:100%;aspect-ratio:auto;max-height:none;max-width:100%}
+ .preview-frame{padding:16px}
+ .preview{max-height:56svh}
+ .photos .hero-img{aspect-ratio:auto;height:100%;width:auto;max-width:100%;max-height:60svh;margin:0 auto}
+ /* right column: scrolls internally only as a last resort on very short screens */
+ .panel{min-height:0;overflow-y:auto;max-width:none;padding-right:6px}
+ h1{font-size:27px;margin:4px 0 4px}
+ .colors{margin:8px 0 10px}
+ .desc{font-size:13px;line-height:1.45;margin:0 0 10px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
+ .calc{padding:13px}
+ .calc h2{margin-bottom:9px}
+ .unit-tog{margin-bottom:10px}
+ .sliders{gap:10px}
+ .hint{display:none}
+ .note{display:none}
+ .srow .shead{margin-bottom:5px}
+ .srow .sval{font-size:18px}
+ .presets{gap:6px;margin:10px 0 2px}
+ .presets button{padding:5px 9px;font-size:11.5px}
+ .hint{font-size:11px;margin:6px 2px 0}
+ .readout{margin:10px 0 2px;display:grid;grid-template-columns:1fr 1fr;column-gap:20px}
+ .readout div{padding:5px 2px;font-size:12px}
+ .price-block{margin:10px 0 2px}
+ .price{font-size:31px}
+ .perm2{margin-bottom:10px}
+ .cta{padding:12px}
+ .cta.primary{margin-bottom:8px}
+ .note{margin-top:8px;font-size:11px;line-height:1.4}
+ /* specs as a compact disclosure so it's available without pushing the fold */
+ .specs-wrap{margin-top:14px;border-top:1px solid var(--line);padding-top:10px}
+ .specs-wrap>summary{font-family:var(--serif);font-size:20px;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between}
+ .specs-wrap>summary::-webkit-details-marker{display:none}
+ .specs-wrap>summary::after{content:'+';color:var(--muted);font-family:var(--sans);font-size:20px;font-weight:300}
+ .specs-wrap[open]>summary::after{content:'\2013'}
+ .specs-wrap .specs{margin-top:6px}
+ /* ===== user-resizable split — drag the corner hamburger grip to grow/shrink the image ===== */
+ .pdp{position:relative}
+ .stage{position:relative}
+ .grip{position:absolute;bottom:8px;width:28px;height:22px;border-radius:6px;background:var(--paper);
+ border:1px solid var(--line);box-shadow:0 1px 5px rgba(0,0,0,.16);cursor:ew-resize;
+ display:flex;flex-direction:column;justify-content:center;align-items:center;gap:3px;
+ z-index:6;touch-action:none;opacity:.5;transition:opacity .15s,border-color .15s;user-select:none}
+ .grip:hover,.grip.active{opacity:1;border-color:var(--accent)}
+ .grip span{display:block;width:15px;height:1.5px;border-radius:2px;background:var(--muted)}
+ .grip.active span{background:var(--accent-ink)}
+ .grip.left{right:8px} /* bottom-right corner of the image (left) panel */
+ .grip.right{right:0} /* bottom-right corner of the page = right panel corner */
+ @media(max-width:920px){.grip{display:none}}
+
+ /* on phones, stop locking the viewport — let it stack and scroll normally */
+ @media(max-width:920px){
+ body{overflow:auto}
+ .wrap{height:auto;overflow:visible;padding-bottom:60px}
+ .pdp{display:block}
+ .panel{overflow:visible;max-width:470px}
+ .room{max-height:none;height:auto;width:100%}
+ .photos .hero-img{height:auto;width:100%;max-height:none}
+ }
+</style>
+</head>
+<body>
+<div class="wrap">
+ <nav class="crumb"><a href="https://hollywoodwallcoverings.com">Hollywood Wallcoverings</a> / <a href="/">Custom Creator</a> / <a href="#">Made to Measure</a> / <span id="bcTitle">—</span></nav>
+ <div class="pdp" id="pdp">
+ <div class="stage">
+ <div class="tabs">
+ <button id="tabRoom" class="on">In a Room</button>
+ <button id="tabPhotos">Photos</button>
+ <button id="tabPreview">Wall Preview</button>
+ </div>
+
+ <div id="roomFrameWrap">
+ <div class="room-frame"><div class="room" id="room">
+ <div class="room-wall" id="roomWall"><div class="room-mural" id="roomMural"><div class="rz" id="rzHandle"></div></div></div>
+ <img class="room-fg" id="roomFg2" alt="">
+ <div class="room-cap" id="roomCap">—</div>
+ </div></div>
+ <div class="room-sel" id="roomSel"></div>
+ <input type="file" id="roomUpload" accept="image/*" capture="environment" style="display:none">
+ <div class="cust-ctrl hide" id="custCtrl">
+ <span>Drag to position · corner handle to resize</span>
+ <label>Opacity <input type="range" id="muralOpacity" min="0.4" max="1" step="0.05" value="1"></label>
+ <button id="reuploadBtn">Change photo</button>
+ </div>
+ <div class="room-pick" id="roomPick"></div>
+ </div>
+
+ <div class="photos hide" id="photos"><img class="hero-img" id="hero" alt=""><div class="thumbs" id="thumbs"></div></div>
+
+ <div class="preview-frame hide" id="previewFrame"><div class="preview" id="preview"></div></div>
+ <div class="ov hide" id="ovBar">
+ <button class="on" data-ov="seams">Roll seams</button>
+ <button class="on" data-ov="nums">Roll numbering</button>
+ <button class="on" data-ov="trim">Trim area</button>
+ <button data-ov="flip">Flip</button>
+ </div>
+ </div>
+
+ <div class="panel">
+ <div class="vendor" id="vendor">Made to Measure · Custom Mural</div>
+ <h1 id="title">Loading…</h1>
+ <div class="colors" id="colors"></div>
+ <div class="desc" id="desc"></div>
+ <div class="calc">
+ <h2>Configure Your Mural</h2>
+ <div class="unit-tog" id="unitTog"><button data-u="ft" class="on">Feet</button><button data-u="in">Inches</button></div>
+ <div class="sliders">
+ <div class="srow"><div class="shead"><label>Wall width</label><span class="sval" id="wv">13′ 1″</span></div><input id="w" type="range" min="20" max="470" step="1" value="157"></div>
+ <div class="srow"><div class="shead"><label>Wall height</label><span class="sval" id="hv">9′ 2″</span></div><input id="h" type="range" min="20" max="470" step="1" value="110"></div>
+ </div>
+ <div class="presets" id="presets"></div>
+ <p class="hint">Tip: add ~4″ to your true wall size for installation trim. <span id="trimEg"></span></p>
+ <dl class="readout" id="readout"></dl>
+ <div class="price-block"><span class="lbl">Your mural</span><span class="price" id="price">—</span></div>
+ <div class="perm2" id="perm2"></div>
+ <button class="cta primary" id="addMural">Add Custom Mural to Cart</button>
+ <button class="cta ghost" id="addSample">Order a Sample — $4.25</button>
+ <p class="note">Pick a pattern and room, set your wall size, and see it scale on a real wall live. Printed to your exact size on premium non-woven in <span id="dropW">20</span>″ drops; trade pricing at checkout.</p>
+ </div>
+ <details class="specs-wrap">
+ <summary>Specifications</summary>
+ <dl class="specs" id="specs"></dl>
+ </details>
+ </div>
+ </div>
+</div>
+
+<script>
+const CM_PER_IN=2.54, ROLL_W_CM=50, TRIM_CM=5;
+// Photoreal room templates: real photo (floor/sides/ceiling) + furniture cutout overlay (occlusion)
+// + the flat feature-wall rectangle (inset %, front-facing) it represents in cm.
+const ROOMS=[
+ {id:'living', name:'Living Room', bg:'/CustomCreator/rooms/living.jpg', fg:'/CustomCreator/rooms/living-fg.png', wall:{l:2,t:4,r:8,b:17}, realW:430, realH:250},
+ {id:'bedroom',name:'Bedroom', bg:'/CustomCreator/rooms/bedroom.jpg', fg:'/CustomCreator/rooms/bedroom-fg.png', wall:{l:1,t:3,r:1,b:20}, realW:440, realH:250},
+ {id:'dining', name:'Dining Room', bg:'/CustomCreator/rooms/dining.jpg', fg:'/CustomCreator/rooms/dining-fg.png', wall:{l:4,t:13,r:13,b:18},realW:430, realH:225},
+ {id:'office', name:'Office', bg:'/CustomCreator/rooms/office.jpg', fg:'/CustomCreator/rooms/office-fg.png', wall:{l:5,t:3,r:1,b:22}, realW:440, realH:250},
+ {id:'nursery',name:'Nursery', bg:'/CustomCreator/rooms/nursery.jpg', fg:'/CustomCreator/rooms/nursery-fg.png', wall:{l:1,t:2,r:1,b:20}, realW:440, realH:255},
+ {id:'hallway',name:'Entryway', bg:'/CustomCreator/rooms/hallway.jpg', fg:'/CustomCreator/rooms/hallway-fg.png', wall:{l:18,t:13,r:18,b:17},realW:380,realH:240},
+ {id:'lobby', name:'Lobby', bg:'/CustomCreator/rooms/lobby.jpg', fg:'/CustomCreator/rooms/lobby-fg.png', wall:{l:6,t:4,r:12,b:28}, realW:600, realH:300},
+];
+let RATE=83.38;
+let state={unit:'ft',product:null,view:'room',flip:false,ov:{seams:true,nums:true,trim:true},roomIdx:0,list:[],
+ custom:false,userRoom:null,cbox:{l:22,t:12,w:56,h:64},opacity:1};
+const $=id=>document.getElementById(id);
+const fmt=n=>'$'+n.toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});
+const val=id=>parseFloat($(id).value)||0;
+// sliders always hold INCHES; the unit toggle only changes how lengths read (feet-then-inches vs inches). No cm anywhere.
+const toCm=v=>v*CM_PER_IN;
+const ftIn=inch=>{let f=Math.floor(inch/12),i=Math.round(inch-f*12);if(i===12){f++;i=0;}return f+'′ '+i+'″';};
+const fmtLen=inch=>state.unit==='ft'?ftIn(inch):(Math.round(inch)+'″');
+const cmToLen=cm=>fmtLen(cm/CM_PER_IN);
+const muralUrl=()=>state.product?(state.product.flatImage||(state.product.images[0]&&state.product.images[0].url)||''):'';
+
+function metrics(){
+ const w=toCm(val('w')),h=toCm(val('h'));
+ const netA=(w/100)*(h/100), pw=w+2*TRIM_CM, ph=h+2*TRIM_CM, printA=(pw/100)*(ph/100);
+ const drops=w>0?Math.ceil(pw/ROLL_W_CM):0;
+ return {w,h,pw,ph,netA,printA,drops,price:netA>0?RATE*netA:0};
+}
+function updateSval(){ $('wv').textContent=fmtLen(val('w')); $('hv').textContent=fmtLen(val('h')); }
+function relabelPresets(){document.querySelectorAll('#presets button').forEach(b=>{const w=b._w,h=b._h;if(w==null)return;b.textContent=state.unit==='ft'?(Math.round(w/12)+'×'+Math.round(h/12)+' ft'):(w+'×'+h+'″');});}
+function recalc(){
+ updateSval();
+ const m=metrics();
+ $('readout').innerHTML='';
+ if(m.netA>0)[['Net wall area',(m.netA*10.7639).toFixed(1)+' sq ft'],
+ ['Printed area (incl. trim)',(m.printA*10.7639).toFixed(1)+' sq ft'],['Drops / panels',m.drops+' panels'],
+ ['Trim margin','2″ each edge']].forEach(([k,v])=>{const d=document.createElement('div');d.innerHTML='<dt>'+k+'</dt><dd>'+v+'</dd>';$('readout').appendChild(d);});
+ if(state.product&&state.product.bespoke){
+ $('price').innerHTML=m.netA>0?fmt(m.price)+' <small>USD</small>':'—';
+ $('perm2').textContent=m.netA>0?'Bespoke · '+fmt(RATE)+'/m² · '+cmToLen(m.w)+' × '+cmToLen(m.h)+' · inquire to order':'';
+ } else {
+ $('price').innerHTML=m.netA>0?fmt(m.price)+' <small>USD</small>':'—';
+ $('perm2').textContent=m.netA>0?fmt(RATE)+'/m² · '+cmToLen(m.w)+' × '+cmToLen(m.h)+' · '+m.drops+' drops':'';
+ }
+ renderPreview(m); renderRoom(m);
+}
+
+function renderPreview(m){
+ if(state.view!=='preview'||!m||m.netA<=0)return;
+ const box=$('preview'),frame=$('previewFrame');
+ const maxW=frame.clientWidth-44,maxH=Math.min(window.innerHeight*0.6,540),ar=m.pw/m.ph;
+ let dw=maxW,dh=dw/ar; if(dh>maxH){dh=maxH;dw=dh*ar;}
+ box.style.width=dw+'px';box.style.height=dh+'px';
+ box.style.backgroundImage='url("'+muralUrl()+'")';box.style.transform=state.flip?'scaleX(-1)':'none';
+ box.innerHTML='';
+ if(state.ov.trim){const tx=(TRIM_CM/m.pw)*100,ty=(TRIM_CM/m.ph)*100;const t=document.createElement('div');t.className='trim';t.style.left=tx+'%';t.style.top=ty+'%';t.style.right=tx+'%';t.style.bottom=ty+'%';box.appendChild(t);
+ const lbl=document.createElement('div');lbl.className='trim-lbl';lbl.style.left=tx+'%';lbl.style.top='calc('+ty+'% - 14px)';lbl.textContent='wall '+cmToLen(m.w)+'×'+cmToLen(m.h);box.appendChild(lbl);}
+ if(state.ov.seams||state.ov.nums)for(let i=0;i<m.drops;i++){const lp=((i*ROLL_W_CM)/m.pw)*100;
+ if(state.ov.seams&&i>0){const s=document.createElement('div');s.className='seam';s.style.left=lp+'%';box.appendChild(s);}
+ if(state.ov.nums){const cx=(((i+0.5)*ROLL_W_CM)/m.pw)*100;const n=document.createElement('div');n.className='dropnum';n.style.left=Math.min(cx,98)+'%';n.textContent=(i+1);box.appendChild(n);}}
+}
+
+function applyCustomMural(){
+ const mu=$('roomMural');
+ mu.style.left=state.cbox.l+'%'; mu.style.top=state.cbox.t+'%'; mu.style.right='auto';
+ mu.style.width=state.cbox.w+'%'; mu.style.height=state.cbox.h+'%';
+ mu.style.transform=state.flip?'scaleX(-1)':'none'; mu.style.opacity=state.opacity;
+ mu.style.backgroundImage='url("'+muralUrl()+'")';
+}
+function renderRoom(m){
+ if(state.view!=='room'||!m||m.netA<=0)return;
+ const mu=$('roomMural');
+ if(state.custom){
+ $('room').style.backgroundImage='url("'+state.userRoom+'")';
+ $('roomFg2').style.display='none';
+ const wall=$('roomWall'); wall.style.left='0';wall.style.top='0';wall.style.right='0';wall.style.bottom='0';wall.style.overflow='visible';
+ mu.classList.add('custom'); applyCustomMural();
+ $('roomCap').textContent='Your room · drag & resize the mural onto your wall';
+ return;
+ }
+ const room=ROOMS[state.roomIdx];
+ $('roomFg2').style.display=''; mu.classList.remove('custom'); mu.style.opacity=1; mu.style.left='50%'; mu.style.top='auto';
+ $('room').style.backgroundImage='url("'+room.bg+'")';
+ $('roomFg2').src=room.fg;
+ const wall=$('roomWall'); wall.style.overflow='hidden';
+ wall.style.left=room.wall.l+'%'; wall.style.top=room.wall.t+'%'; wall.style.right=room.wall.r+'%'; wall.style.bottom=room.wall.b+'%';
+ const wPct=Math.max(8,(m.w/room.realW)*100), hPct=Math.max(8,(m.h/room.realH)*100);
+ mu.style.width=wPct+'%'; mu.style.height=Math.min(hPct,135)+'%';
+ mu.style.backgroundImage='url("'+muralUrl()+'")';
+ mu.style.transform='translateX(-50%)'+(state.flip?' scaleX(-1)':'');
+ $('roomCap').textContent=cmToLen(m.w)+' × '+cmToLen(m.h)+' · shown to scale in the '+room.name.toLowerCase();
+}
+// pointer drag + resize for the uploaded-room mural (works mouse + touch on desktop/mobile/iPad)
+let _drag=null;
+function muralDown(e){
+ if(!state.custom)return;
+ const isRz=e.target.id==='rzHandle';
+ _drag={mode:isRz?'resize':'move',sx:e.clientX,sy:e.clientY,box:{...state.cbox}};
+ try{$('roomMural').setPointerCapture(e.pointerId);}catch(_){}
+ e.preventDefault();
+}
+function muralMove(e){
+ if(!_drag)return;
+ const r=$('room').getBoundingClientRect();
+ const dx=(e.clientX-_drag.sx)/r.width*100, dy=(e.clientY-_drag.sy)/r.height*100;
+ if(_drag.mode==='move'){ state.cbox.l=Math.max(-30,Math.min(95,_drag.box.l+dx)); state.cbox.t=Math.max(-30,Math.min(95,_drag.box.t+dy)); }
+ else { state.cbox.w=Math.max(8,Math.min(160,_drag.box.w+dx)); state.cbox.h=Math.max(8,Math.min(180,_drag.box.h+dy)); }
+ applyCustomMural(); e.preventDefault();
+}
+function muralUp(){ _drag=null; }
+
+function setView(v){
+ state.view=v;
+ $('tabRoom').classList.toggle('on',v==='room'); $('tabPhotos').classList.toggle('on',v==='photos'); $('tabPreview').classList.toggle('on',v==='preview');
+ $('roomFrameWrap').classList.toggle('hide',v!=='room');
+ $('photos').classList.toggle('hide',v!=='photos');
+ $('previewFrame').classList.toggle('hide',v!=='preview'); $('ovBar').classList.toggle('hide',v!=='preview');
+ recalc();
+}
+
+function render(p){
+ state.product=p; RATE=p.rate||83.38;
+ $('title').textContent=p.pattern; $('bcTitle').textContent=p.pattern;
+ $('vendor').textContent=(p.vendor||'Made to Measure')+' · Custom Mural';
+ const imgs=p.images.length?p.images:[{url:'',alt:''}];
+ $('hero').src=imgs[0].url; $('hero').alt=imgs[0].alt;
+ const tw=$('thumbs'); tw.innerHTML='';
+ imgs.forEach((im,k)=>{const t=new Image();t.src=im.url;t.alt=im.alt;t.className=k===0?'active':'';t.onclick=()=>{$('hero').src=im.url;[...tw.children].forEach(c=>c.classList.remove('active'));t.classList.add('active');};tw.appendChild(t);});
+ const cw=$('colors'); cw.innerHTML='';
+ (p.colors||[]).forEach(c=>{const s=document.createElement('span');s.className='chip';s.innerHTML='<i style="background:'+c.hex+'"></i>'+(c.name||c.hex)+(c.pct?' · '+c.pct+'%':'');cw.appendChild(s);});
+ $('desc').innerHTML=p.descriptionHtml||'';
+ const dl=$('specs'); dl.innerHTML='';
+ (p.specs||[]).forEach(s=>{const d=document.createElement('div');d.innerHTML='<dt>'+s.label+'</dt><dd>'+s.value+'</dd>';dl.appendChild(d);});
+ if(p.bespoke){ $('addMural').textContent='Request a Quote →'; $('addSample').style.display='none'; $('vendor').textContent='Bespoke Mural'; }
+ else { $('addMural').textContent='Add Custom Mural to Cart'; $('addSample').style.display=''; $('addSample').textContent='Order a Sample — '+fmt(parseFloat(p.samplePrice||4.25)); }
+ [...$('roomPick').children].forEach(b=>b.classList.toggle('on',b.dataset.h===(p.sku||'').toLowerCase()));
+ recalc();
+}
+
+async function load(handle){
+ $('pdp').classList.add('loading');
+ try{
+ const r=await fetch('/CustomCreator/api/product'+(handle?('?handle='+encodeURIComponent(handle)):''));
+ const p=await r.json(); if(p.error)throw new Error(p.error);
+ render(p);
+ history.replaceState(null,'',location.pathname+'?handle='+p.sku.toLowerCase());
+ }catch(e){ $('title').textContent='Could not load product'; console.error(e); }
+ $('pdp').classList.remove('loading');
+}
+async function postCart(mode){
+ const p=state.product; if(!p)return;
+ if(p.bespoke){ const wcm=toCm(val('w')), hcm=toCm(val('h')), area=((wcm/100)*(hcm/100)*10.7639).toFixed(1), sz=ftIn(val('w'))+' × '+ftIn(val('h'));
+ window.location.href='mailto:info@hollywoodwallcoverings.com?subject='+encodeURIComponent('Mural Quote: '+p.pattern)+'&body='+encodeURIComponent('I would like a quote for the '+p.pattern+' mural at '+sz+' (approx '+area+' sq ft).'); return; }
+ const btn=$(mode==='sample'?'addSample':'addMural'); const orig=btn.textContent; btn.disabled=true; btn.textContent='Creating checkout…';
+ try{
+ const r=await fetch('/CustomCreator/api/mural-cart',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode,sku:p.sku,pattern:p.pattern,unit:'in',width:val('w'),height:val('h')})});
+ const j=await r.json();
+ if(j.invoiceUrl){window.location.href=j.invoiceUrl;return;}
+ alert(j.scopeHint?'Checkout not yet enabled — token needs write_draft_orders.':'Could not create checkout: '+(j.error||'unknown'));
+ }catch(e){alert('Network error: '+e.message);}
+ btn.disabled=false; btn.textContent=orig;
+}
+
+// ===== resizable split: corner hamburger grips on both panels adjust the image size =====
+const DEFAULT_SPLIT=0.59; // matches the original 1.18fr : .82fr column ratio
+let split=Math.max(0.32,Math.min(0.80,parseFloat(localStorage.getItem('muralSplit'))||DEFAULT_SPLIT));
+function applySplit(){ // persists via localStorage; two fr values that always sum to 2 (like 1.18+.82)
+ $('pdp').style.gridTemplateColumns=(split*2).toFixed(3)+'fr '+((1-split)*2).toFixed(3)+'fr';
+}
+function makeGrip(cls,host){
+ const g=document.createElement('div'); g.className='grip '+cls;
+ g.title='Drag to resize the image · double-click to reset';
+ g.innerHTML='<span></span><span></span><span></span>';
+ let dragging=false;
+ g.addEventListener('pointerdown',e=>{dragging=true;g.classList.add('active');try{g.setPointerCapture(e.pointerId);}catch(_){}e.preventDefault();});
+ g.addEventListener('pointermove',e=>{ if(!dragging)return;
+ const r=$('pdp').getBoundingClientRect();
+ split=Math.max(0.32,Math.min(0.80,(e.clientX-r.left)/r.width));
+ applySplit(); renderPreview(metrics()); renderRoom(metrics()); e.preventDefault(); });
+ const end=()=>{ if(!dragging)return; dragging=false; g.classList.remove('active'); localStorage.setItem('muralSplit',split.toFixed(4)); };
+ g.addEventListener('pointerup',end); g.addEventListener('pointercancel',end);
+ g.addEventListener('dblclick',()=>{ split=DEFAULT_SPLIT; applySplit(); localStorage.setItem('muralSplit',split.toFixed(4)); renderPreview(metrics()); renderRoom(metrics()); });
+ host.appendChild(g);
+}
+function setupGrips(){ applySplit(); makeGrip('left',document.querySelector('.stage')); makeGrip('right',$('pdp')); }
+
+function init(){
+ $('dropW').textContent=ROLL_W_CM;
+ // room selector (predefined rooms + upload-your-own)
+ const rs=$('roomSel');
+ const clearSel=()=>[...rs.children].forEach(c=>c.classList.remove('on'));
+ ROOMS.forEach((rm,i)=>{const b=document.createElement('button');b.textContent=rm.name;b.className=i===0?'on':'';
+ b.onclick=()=>{state.custom=false;state.roomIdx=i;clearSel();b.classList.add('on');$('custCtrl').classList.add('hide');renderRoom(metrics());};rs.appendChild(b);});
+ const up=document.createElement('button'); up.textContent='+ Your Room'; up.className='upload'; up.onclick=()=>$('roomUpload').click(); rs.appendChild(up);
+ // upload handler — file input is camera-capable on mobile/iPad, gallery on desktop
+ $('roomUpload').onchange=e=>{const f=e.target.files&&e.target.files[0]; if(!f)return;
+ const rd=new FileReader(); rd.onload=()=>{ state.userRoom=rd.result; state.custom=true; state.cbox={l:22,t:12,w:56,h:64}; state.opacity=1; $('muralOpacity').value=1;
+ clearSel(); up.classList.add('on'); $('custCtrl').classList.remove('hide'); if(state.view!=='room')setView('room'); renderRoom(metrics()); };
+ rd.readAsDataURL(f); e.target.value=''; };
+ $('reuploadBtn').onclick=()=>$('roomUpload').click();
+ $('muralOpacity').oninput=()=>{state.opacity=parseFloat($('muralOpacity').value);applyCustomMural();};
+ // drag + resize (pointer events = mouse + touch)
+ const mu=$('roomMural');
+ mu.addEventListener('pointerdown',muralDown); mu.addEventListener('pointermove',muralMove); mu.addEventListener('pointerup',muralUp); mu.addEventListener('pointercancel',muralUp);
+ document.querySelectorAll('#unitTog button').forEach(btn=>btn.onclick=()=>{
+ const u=btn.dataset.u; if(u===state.unit)return;
+ // display-only toggle — the slider already holds inches, so just re-render the labels
+ state.unit=u; document.querySelectorAll('#unitTog button').forEach(b=>b.classList.toggle('on',b===btn)); relabelPresets(); recalc();
+ });
+ $('trimEg').textContent='[e.g. a 10′×8′ wall → add a few inches each side for trim]';
+ [[120,96],[144,108],[168,108],[96,96],[192,108],[144,120]].forEach(([w,h])=>{const b=document.createElement('button');b._w=w;b._h=h;b.onclick=()=>{$('w').value=w;$('h').value=h;recalc();};$('presets').appendChild(b);});
+ relabelPresets();
+ document.querySelectorAll('#ovBar button').forEach(b=>b.onclick=()=>{const k=b.dataset.ov;if(k==='flip'){state.flip=!state.flip;b.classList.toggle('on',state.flip);}else{state.ov[k]=!state.ov[k];b.classList.toggle('on',state.ov[k]);}renderPreview(metrics());renderRoom(metrics());});
+ $('tabRoom').onclick=()=>setView('room'); $('tabPhotos').onclick=()=>setView('photos'); $('tabPreview').onclick=()=>setView('preview');
+ $('w').oninput=recalc; $('h').oninput=recalc;
+ $('addMural').onclick=()=>postCart('mural'); $('addSample').onclick=()=>postCart('sample');
+ window.addEventListener('resize',()=>{renderPreview(metrics());renderRoom(metrics());});
+ setupGrips();
+ const params=new URLSearchParams(location.search), start=params.get('handle');
+ fetch('/CustomCreator/api/products').then(r=>r.json()).then(list=>{
+ state.list=list;
+ const pk=$('roomPick'); pk.innerHTML='';
+ list.slice(0,18).forEach(it=>{const b=document.createElement('button');b.dataset.h=it.handle;b.title=it.title;if(it.image)b.style.backgroundImage='url("'+it.image+'")';b.onclick=()=>load(it.handle);pk.appendChild(b);});
+ load(start||(list[0]&&list[0].handle));
+ }).catch(()=>load(start));
+}
+init();
+</script>
+</body>
+</html>
diff --git a/customcreator/gallery.html b/customcreator/gallery.html
new file mode 100644
index 0000000..c5a7335
--- /dev/null
+++ b/customcreator/gallery.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Custom Creator — Made-to-Measure Murals | Hollywood Wallcoverings</title>
+<style>
+ :root{
+ --ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e2db; --bg:#fbfaf7; --paper:#fff;
+ --accent:#c9a865; --accent-ink:#a98b50; --radius:4px; --card:230px;
+ --serif:'Cormorant Garamond',Georgia,'Times New Roman',serif;
+ --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
+ }
+ *{box-sizing:border-box;margin:0;padding:0}
+ body{font-family:var(--sans);color:var(--ink);background:var(--bg);line-height:1.55;-webkit-font-smoothing:antialiased}
+ a{color:inherit;text-decoration:none}
+ .top{display:flex;align-items:center;justify-content:space-between;padding:20px 30px;border-bottom:1px solid var(--line)}
+ .brand{font-family:var(--serif);font-size:20px;letter-spacing:.02em}
+ .top nav a{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-left:22px}
+
+ .hero{position:relative;min-height:76vh;display:flex;align-items:center;overflow:hidden;background:#1a140d}
+ .hero-bg{position:absolute;inset:0;background:url('/CustomCreator/rooms/hero.jpg') center 38%/cover no-repeat;transform:scale(1.03)}
+ .hero-scrim{position:absolute;inset:0;background:linear-gradient(96deg,rgba(18,13,7,.70) 0%,rgba(18,13,7,.40) 38%,rgba(18,13,7,.05) 66%,rgba(18,13,7,0) 100%)}
+ .hero-content{position:relative;z-index:2;max-width:1340px;width:100%;margin:0 auto;padding:0 40px}
+ .hero-inner{max-width:580px;color:#fff}
+ .hero .eyebrow{font-size:12px;letter-spacing:.24em;text-transform:uppercase;color:#ecdcb6;font-weight:600}
+ .hero h1{font-family:var(--serif);font-weight:500;font-size:76px;line-height:1;margin:14px 0 18px;color:#fff;text-shadow:0 2px 30px rgba(0,0,0,.35)}
+ @media(max-width:640px){.hero h1{font-size:46px} .hero-inner{max-width:100%}}
+ .hero p{font-size:18px;color:rgba(255,255,255,.93);line-height:1.62;max-width:490px;margin:0 0 28px}
+ .hero .cta{display:inline-block;background:var(--accent);color:#fff;font-weight:600;font-size:14px;letter-spacing:.04em;padding:16px 36px;border-radius:3px}
+ .hero .cta:hover{background:var(--accent-ink)}
+ .steps{display:flex;gap:24px;margin-top:32px;flex-wrap:wrap;color:rgba(255,255,255,.82);font-size:12.5px;letter-spacing:.04em}
+ .steps b{color:#fff;font-weight:700}
+
+ .wrap{max-width:1340px;margin:0 auto;padding:0 28px 90px}
+ .bar{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:16px 2px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5;flex-wrap:wrap}
+ .bar .count{font-size:13px;color:var(--muted)}
+ .ctrls{display:flex;align-items:center;gap:22px;flex-wrap:wrap}
+ .ctrl{display:flex;align-items:center;gap:9px}
+ .ctrl label{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
+ .ctrl select{font-family:var(--sans);font-size:13px;border:1px solid var(--line);border-radius:3px;padding:7px 26px 7px 10px;background:#fff;cursor:pointer}
+ .ctrl input[type=range]{-webkit-appearance:none;appearance:none;width:120px;height:4px;border-radius:3px;background:var(--line);outline:none;cursor:pointer}
+ .ctrl input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:17px;height:17px;border-radius:50%;background:var(--accent);border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.25);cursor:pointer}
+ .ctrl input[type=range]::-moz-range-thumb{width:17px;height:17px;border-radius:50%;background:var(--accent);border:2px solid #fff;cursor:pointer}
+
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(var(--card),1fr));gap:22px;margin-top:26px}
+ .card{display:block;cursor:pointer}
+ .card-img{width:100%;aspect-ratio:4/5;background:#eee var(--bgimg) center/cover no-repeat;border-radius:var(--radius);overflow:hidden;position:relative;transition:box-shadow .18s}
+ .card:hover .card-img{box-shadow:0 8px 26px rgba(0,0,0,.16)}
+ .card-img:after{content:"Customize →";position:absolute;left:0;right:0;bottom:0;padding:12px;font-size:12px;letter-spacing:.06em;color:#fff;background:linear-gradient(transparent,rgba(0,0,0,.5));opacity:0;transition:opacity .18s;text-align:center}
+ .card:hover .card-img:after{opacity:1}
+ .card-name{font-family:var(--serif);font-size:18px;margin:11px 2px 2px;line-height:1.2}
+ .card-sub{font-size:12px;color:var(--muted);margin:0 2px}
+ .empty{padding:60px;text-align:center;color:var(--muted)}
+</style>
+</head>
+<body>
+ <div class="top">
+ <div class="brand">Hollywood Wallcoverings</div>
+ <nav><a href="https://hollywoodwallcoverings.com">Shop</a><a href="#grid">Patterns</a><a href="/CustomCreator/configure">Start Creating</a></nav>
+ </div>
+
+ <section class="hero">
+ <div class="hero-bg"></div><div class="hero-scrim"></div>
+ <div class="hero-content"><div class="hero-inner">
+ <div class="eyebrow">Hollywood Wallcoverings · Made to Measure</div>
+ <h1>Custom Creator</h1>
+ <p>Design your own mural, printed to the exact size of your wall. Choose a pattern, size it with live pricing, and preview it in a real room.</p>
+ <a class="cta" id="startBtn" href="/CustomCreator/configure">Start Creating →</a>
+ <div class="steps"><span><b>1.</b> Pick a pattern</span><span><b>2.</b> Size your wall</span><span><b>3.</b> Preview in a room</span><span><b>4.</b> Order made-to-measure</span></div>
+ </div></div>
+ </section>
+
+ <div class="wrap">
+ <div class="bar" id="grid">
+ <div class="count" id="count">Loading patterns…</div>
+ <div class="ctrls">
+ <div class="ctrl"><label for="sort">Sort</label>
+ <select id="sort">
+ <option value="newest">Newest</option>
+ <option value="color">Color</option>
+ <option value="style">Style</option>
+ <option value="sku">SKU A→Z</option>
+ <option value="title">Title A→Z</option>
+ </select>
+ </div>
+ <div class="ctrl"><label for="density">Density</label><input id="density" type="range" min="160" max="340" step="10" value="230"></div>
+ </div>
+ </div>
+ <div class="grid" id="cards"></div>
+ </div>
+
+<script>
+let DATA=[];
+const $=id=>document.getElementById(id);
+const esc=s=>String(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
+
+function colorKey(hex){
+ if(!hex) return 500;
+ const m=hex.replace('#','').match(/.{2}/g); if(!m||m.length<3) return 500;
+ const [r,g,b]=m.map(x=>parseInt(x,16));
+ const mx=Math.max(r,g,b),mn=Math.min(r,g,b),l=(mx+mn)/510,d=mx-mn;
+ if(d<18) return l>0.8?0:(l<0.18?900:400+Math.round((1-l)*80)); // white..gray..black
+ let h=mx===r?((g-b)/d)%6:mx===g?(b-r)/d+2:(r-g)/d+4; h*=60; if(h<0)h+=360;
+ return 50+Math.round(h/5); // warm→cool by hue, between white(0) and dark(900)
+}
+function sortData(mode){
+ const a=[...DATA];
+ if(mode==='newest') a.sort((x,y)=>(y.createdAt||'').localeCompare(x.createdAt||''));
+ else if(mode==='color') a.sort((x,y)=>colorKey(x.colorHex)-colorKey(y.colorHex)||x.title.localeCompare(y.title));
+ else if(mode==='style') a.sort((x,y)=>(x.style||'~').localeCompare(y.style||'~')||x.title.localeCompare(y.title));
+ else if(mode==='sku') a.sort((x,y)=>(x.sku||'').localeCompare(y.sku||''));
+ else if(mode==='title') a.sort((x,y)=>x.title.localeCompare(y.title));
+ return a;
+}
+function render(){
+ const mode=$('sort').value; const rows=sortData(mode);
+ $('cards').innerHTML=rows.map(p=>`
+ <a class="card" href="/CustomCreator/configure?handle=${encodeURIComponent(p.handle)}">
+ <div class="card-img" style="--bgimg:url('${esc(p.image)}')"></div>
+ <div class="card-name">${esc(p.title)}</div>
+ <div class="card-sub">${esc(p.style||'Mural')}${p.colorName?' · '+esc(p.colorName):''}</div>
+ </a>`).join('');
+ $('count').textContent=rows.length+' mural patterns';
+}
+function init(){
+ // restore persisted controls
+ const sv=localStorage.getItem('cc-sort'); if(sv)$('sort').value=sv;
+ const dv=localStorage.getItem('cc-density'); if(dv){$('density').value=dv;document.documentElement.style.setProperty('--card',dv+'px');}
+ $('sort').onchange=()=>{localStorage.setItem('cc-sort',$('sort').value);render();};
+ $('density').oninput=()=>{document.documentElement.style.setProperty('--card',$('density').value+'px');localStorage.setItem('cc-density',$('density').value);};
+ // first pattern → personalize the hero CTA
+ fetch('/CustomCreator/api/collection').then(r=>r.json()).then(list=>{
+ DATA=list||[];
+ if(DATA[0]) $('startBtn').href='/CustomCreator/configure?handle='+encodeURIComponent(DATA[0].handle);
+ render();
+ }).catch(()=>{$('count').textContent='Could not load patterns.';});
+}
+init();
+</script>
+</body>
+</html>
diff --git a/customcreator/gb-murals.json b/customcreator/gb-murals.json
new file mode 100644
index 0000000..7017f65
--- /dev/null
+++ b/customcreator/gb-murals.json
@@ -0,0 +1,38 @@
+[
+{"id":"145855","handle":"azure-arboretum-creme-brulee-bespoke-mural","name":"Azure Arboretum Creme Brulee","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15693/images/110923/145855_TILE_01__02169.1778225649.386.513.jpg?c=1"},
+{"id":"145819","handle":"whispering-tropics-forest-panoramic-bespoke-mural","name":"Whispering Tropics Forest","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15692/images/110924/145819_TILE%2520_01__47484.1778225649.386.513.jpg?c=1"},
+{"id":"145828","handle":"whispering-tropics-willow-panoramic-bespoke-mural","name":"Whispering Tropics Willow","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15691/images/110927/145828_TILE_01__01914.1778225649.386.513.jpg?c=1"},
+{"id":"145846","handle":"azure-arboretum-sky-bespoke-mural","name":"Azure Arboretum Sky","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15690/images/110926/145846_TILE_01__29791.1778225649.386.513.jpg?c=1"},
+{"id":"145882","handle":"jade-pavilion-dawn-bespoke-mural","name":"Jade Pavilion Dawn","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15689/images/110903/145882_TILE_01__93193.1778224907.386.513.jpg?c=1"},
+{"id":"145873","handle":"azure-arboretum-whispering-rose-bespoke-mural","name":"Azure Arboretum Whispering Rose","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15688/images/110913/145873_TILE__40277.1778225644.386.513.jpg?c=1"},
+{"id":"145864","handle":"azure-arboretum-willow-bespoke-mural","name":"Azure Arboretum Willow","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15687/images/110914/145864_TILE__52358.1778225645.386.513.jpg?c=1"},
+{"id":"145837","handle":"whispering-tropics-ivory-panoramic-bespoke-mural","name":"Whispering Tropics Ivory","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15686/images/110921/145837_TILE_01__37693.1778225648.386.513.jpg?c=1"},
+{"id":"145891","handle":"jade-pavilion-dusk-bespoke-mural","name":"Jade Pavilion Dusk","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15685/images/110908/145891_TILE_01__20222.1778225177.386.513.jpg?c=1"},
+{"id":"145767","handle":"kate-berry-gem-espalier-sky-bespoke-mural","name":"Kate Berry Gem Espalier Sky","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15547/images/51378/145767_TILE_Gem_Espalier_Sky_Bespoke_Mural__78118.1776163197.386.513.jpg?c=1"},
+{"id":"145776","handle":"kate-berry-gem-espalier-clay-bespoke-mural","name":"Kate Berry Gem Espalier Clay","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15546/images/51081/145776_TILE_Gem_Espalier_Clay_Bespoke_Mural__53225.1775740370.386.513.jpg?c=1"},
+{"id":"145785","handle":"kate-berry-gem-espalier-moon-bespoke-mural","name":"Kate Berry Gem Espalier Moon","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15545/images/51088/145785_TILE_Gem_Espalier_Moon_Bespoke_Mural__07845.1775740386.386.513.jpg?c=1"},
+{"id":"145350","handle":"whispering-grove-bespoke-mural","name":"Whispering Grove","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15245/images/47573/145350_TILE_01__03450.1771333786.386.513.jpg?c=1"},
+{"id":"145332","handle":"lys-whispering-rose-bespoke-mural","name":"Lys Whispering Rose","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15244/images/47571/145332_TILE_01__72951.1771333786.386.513.jpg?c=1"},
+{"id":"145341","handle":"lys-petrol-bespoke-mural","name":"Lys Petrol","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15243/images/47570/145341_TILE_01__72902.1771333786.386.513.jpg?c=1"},
+{"id":"145359","handle":"verdure-noir-bespoke-mural","name":"Verdure Noir","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15239/images/47567/145359_TILE_01__36116.1771333785.386.513.jpg?c=1"},
+{"id":"145368","handle":"paisley-tapestry-teal-bespoke-mural","name":"Paisley Tapestry Teal","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15238/images/47566/145368_TILE_01__00642.1771333785.386.513.jpg?c=1"},
+{"id":"119057","handle":"crane-the-renaissance-of-venus-bespoke-mural","name":"Crane The Renaissance Of Venus","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/15142/images/46651/119057_TILE_01__31903.1770974311.386.513.jpg?c=1"},
+{"id":"142912","handle":"obassia-damson-bespoke-mural","name":"Obassia Damson","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14564/images/46629/142912_TILE_01__39092.1770974103.386.513.jpg?c=1"},
+{"id":"142804","handle":"allurium-damson-bespoke-mural","name":"Allurium Damson","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14563/images/46618/142804_TILE_01__58754.1770974026.386.513.jpg?c=1"},
+{"id":"142795","handle":"allurium-night-bespoke-mural","name":"Allurium Night","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14562/images/46567/142795_TILE_01__72449.1770973408.386.513.jpg?c=1"},
+{"id":"142849","handle":"allurium-soft-pink-bespoke-mural","name":"Allurium Soft Pink","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14561/images/46636/142849_TILE_01__18288.1770974137.386.513.jpg?c=1"},
+{"id":"142822","handle":"allurium-sky-bespoke-mural","name":"Allurium Sky","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14539/images/46661/142822_TILE_01__82643.1770974388.386.513.jpg?c=1"},
+{"id":"142840","handle":"allurium-vintage-bespoke-mural","name":"Allurium Vintage","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14538/images/46656/142840_TILE_01__01786.1770974346.386.513.jpg?c=1"},
+{"id":"142858","handle":"allurium-denim-bespoke-mural","name":"Allurium Denim","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14536/images/46606/142858_TILE_01__93700.1770973957.386.513.jpg?c=1"},
+{"id":"142921","handle":"obassia-opulence-bespoke-mural","name":"Obassia Opulence","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14535/images/46663/142921_TILE_01__44560.1770974398.386.513.jpg?c=1"},
+{"id":"142939","handle":"forest-in-bloom-soft-green-bespoke-mural","name":"Forest In Bloom Soft Green","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14534/images/46664/142939_TILE_01__10325.1770974399.386.513.jpg?c=1"},
+{"id":"142885","handle":"obassia-dawn-bespoke-mural","name":"Obassia Dawn","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14533/images/46625/142885_TILE_01__30107.1770974074.386.513.jpg?c=1"},
+{"id":"142786","handle":"eternal-citybespoke-mural","name":"Eternal City","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14531/images/46659/142786_TILE_01__73647.1770974377.386.513.jpg?c=1"},
+{"id":"142831","handle":"allurium-day-bespoke-mural","name":"Allurium Day","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14530/images/46572/142831_TILE_01__63455.1770973513.386.513.jpg?c=1"},
+{"id":"142894","handle":"obassia-sapphire-bespoke-mural","name":"Obassia Sapphire","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14488/images/46610/142894_TILE_01__50899.1770973980.386.513.jpg?c=1"},
+{"id":"142813","handle":"allurium-sage-bespoke-mural","name":"Allurium Sage","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14400/images/46600/142813_TILE_01__07074.1770973914.386.513.jpg?c=1"},
+{"id":"142867","handle":"allurium-pink-bespoke-mural","name":"Allurium Pink","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14399/images/46627/142867_TILE_01__96301.1770974079.386.513.jpg?c=1"},
+{"id":"142903","handle":"obassia-emerald-bespoke-mural","name":"Obassia Emerald","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14398/images/46611/142903_TILE_01__96849.1770973980.386.513.jpg?c=1"},
+{"id":"142876","handle":"obassia-sky-bespoke-mural","name":"Obassia Sky","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14397/images/46608/142876_TILE_01__17916.1770973973.386.513.jpg?c=1"},
+{"id":"142930","handle":"forest-in-bloom-duck-egg-bespoke-mural","name":"Forest In Bloom Duck Egg","img":"https://cdn11.bigcommerce.com/s-7psb023nit/products/14396/images/46630/142930_TILE_01__28340.1770974107.386.513.jpg?c=1"}
+]
diff --git a/customcreator/gb/119057.jpg b/customcreator/gb/119057.jpg
new file mode 100644
index 0000000..d9ed85c
Binary files /dev/null and b/customcreator/gb/119057.jpg differ
diff --git a/customcreator/gb/142786.jpg b/customcreator/gb/142786.jpg
new file mode 100644
index 0000000..d15343a
Binary files /dev/null and b/customcreator/gb/142786.jpg differ
diff --git a/customcreator/gb/142795.jpg b/customcreator/gb/142795.jpg
new file mode 100644
index 0000000..2b9027f
Binary files /dev/null and b/customcreator/gb/142795.jpg differ
diff --git a/customcreator/gb/142804.jpg b/customcreator/gb/142804.jpg
new file mode 100644
index 0000000..c9ce98c
Binary files /dev/null and b/customcreator/gb/142804.jpg differ
diff --git a/customcreator/gb/142813.jpg b/customcreator/gb/142813.jpg
new file mode 100644
index 0000000..744f528
Binary files /dev/null and b/customcreator/gb/142813.jpg differ
diff --git a/customcreator/gb/142822.jpg b/customcreator/gb/142822.jpg
new file mode 100644
index 0000000..fa6be27
Binary files /dev/null and b/customcreator/gb/142822.jpg differ
diff --git a/customcreator/gb/142831.jpg b/customcreator/gb/142831.jpg
new file mode 100644
index 0000000..475bacd
Binary files /dev/null and b/customcreator/gb/142831.jpg differ
diff --git a/customcreator/gb/142840.jpg b/customcreator/gb/142840.jpg
new file mode 100644
index 0000000..26165c4
Binary files /dev/null and b/customcreator/gb/142840.jpg differ
diff --git a/customcreator/gb/142849.jpg b/customcreator/gb/142849.jpg
new file mode 100644
index 0000000..d0697cf
Binary files /dev/null and b/customcreator/gb/142849.jpg differ
diff --git a/customcreator/gb/142858.jpg b/customcreator/gb/142858.jpg
new file mode 100644
index 0000000..b46327c
Binary files /dev/null and b/customcreator/gb/142858.jpg differ
diff --git a/customcreator/gb/142867.jpg b/customcreator/gb/142867.jpg
new file mode 100644
index 0000000..d2e0207
Binary files /dev/null and b/customcreator/gb/142867.jpg differ
diff --git a/customcreator/gb/142876.jpg b/customcreator/gb/142876.jpg
new file mode 100644
index 0000000..07f8eca
Binary files /dev/null and b/customcreator/gb/142876.jpg differ
diff --git a/customcreator/gb/142885.jpg b/customcreator/gb/142885.jpg
new file mode 100644
index 0000000..b478b9a
Binary files /dev/null and b/customcreator/gb/142885.jpg differ
diff --git a/customcreator/gb/142894.jpg b/customcreator/gb/142894.jpg
new file mode 100644
index 0000000..084680e
Binary files /dev/null and b/customcreator/gb/142894.jpg differ
diff --git a/customcreator/gb/142903.jpg b/customcreator/gb/142903.jpg
new file mode 100644
index 0000000..ed9fcf5
Binary files /dev/null and b/customcreator/gb/142903.jpg differ
diff --git a/customcreator/gb/142912.jpg b/customcreator/gb/142912.jpg
new file mode 100644
index 0000000..e796a25
Binary files /dev/null and b/customcreator/gb/142912.jpg differ
diff --git a/customcreator/gb/142921.jpg b/customcreator/gb/142921.jpg
new file mode 100644
index 0000000..54279ed
Binary files /dev/null and b/customcreator/gb/142921.jpg differ
diff --git a/customcreator/gb/142930.jpg b/customcreator/gb/142930.jpg
new file mode 100644
index 0000000..951bc33
Binary files /dev/null and b/customcreator/gb/142930.jpg differ
diff --git a/customcreator/gb/142939.jpg b/customcreator/gb/142939.jpg
new file mode 100644
index 0000000..aae744a
Binary files /dev/null and b/customcreator/gb/142939.jpg differ
diff --git a/customcreator/gb/145332.jpg b/customcreator/gb/145332.jpg
new file mode 100644
index 0000000..36c60a0
Binary files /dev/null and b/customcreator/gb/145332.jpg differ
diff --git a/customcreator/gb/145341.jpg b/customcreator/gb/145341.jpg
new file mode 100644
index 0000000..0fce54b
Binary files /dev/null and b/customcreator/gb/145341.jpg differ
diff --git a/customcreator/gb/145350.jpg b/customcreator/gb/145350.jpg
new file mode 100644
index 0000000..4cb315d
Binary files /dev/null and b/customcreator/gb/145350.jpg differ
diff --git a/customcreator/gb/145359.jpg b/customcreator/gb/145359.jpg
new file mode 100644
index 0000000..fd3c3e6
Binary files /dev/null and b/customcreator/gb/145359.jpg differ
diff --git a/customcreator/gb/145368.jpg b/customcreator/gb/145368.jpg
new file mode 100644
index 0000000..ffae47a
Binary files /dev/null and b/customcreator/gb/145368.jpg differ
diff --git a/customcreator/gb/145767.jpg b/customcreator/gb/145767.jpg
new file mode 100644
index 0000000..606e20c
Binary files /dev/null and b/customcreator/gb/145767.jpg differ
diff --git a/customcreator/gb/145776.jpg b/customcreator/gb/145776.jpg
new file mode 100644
index 0000000..9139363
Binary files /dev/null and b/customcreator/gb/145776.jpg differ
diff --git a/customcreator/gb/145785.jpg b/customcreator/gb/145785.jpg
new file mode 100644
index 0000000..c6adac0
Binary files /dev/null and b/customcreator/gb/145785.jpg differ
diff --git a/customcreator/gb/145819.jpg b/customcreator/gb/145819.jpg
new file mode 100644
index 0000000..a03d71e
Binary files /dev/null and b/customcreator/gb/145819.jpg differ
diff --git a/customcreator/gb/145828.jpg b/customcreator/gb/145828.jpg
new file mode 100644
index 0000000..ecd1933
Binary files /dev/null and b/customcreator/gb/145828.jpg differ
diff --git a/customcreator/gb/145837.jpg b/customcreator/gb/145837.jpg
new file mode 100644
index 0000000..86e0ccd
Binary files /dev/null and b/customcreator/gb/145837.jpg differ
diff --git a/customcreator/gb/145846.jpg b/customcreator/gb/145846.jpg
new file mode 100644
index 0000000..8dbc8e6
Binary files /dev/null and b/customcreator/gb/145846.jpg differ
diff --git a/customcreator/gb/145855.jpg b/customcreator/gb/145855.jpg
new file mode 100644
index 0000000..2d9c97a
Binary files /dev/null and b/customcreator/gb/145855.jpg differ
diff --git a/customcreator/gb/145864.jpg b/customcreator/gb/145864.jpg
new file mode 100644
index 0000000..d0c23e4
Binary files /dev/null and b/customcreator/gb/145864.jpg differ
diff --git a/customcreator/gb/145873.jpg b/customcreator/gb/145873.jpg
new file mode 100644
index 0000000..8861c8a
Binary files /dev/null and b/customcreator/gb/145873.jpg differ
diff --git a/customcreator/gb/145882.jpg b/customcreator/gb/145882.jpg
new file mode 100644
index 0000000..3dbfeb5
Binary files /dev/null and b/customcreator/gb/145882.jpg differ
diff --git a/customcreator/gb/145891.jpg b/customcreator/gb/145891.jpg
new file mode 100644
index 0000000..7c604cc
Binary files /dev/null and b/customcreator/gb/145891.jpg differ
diff --git a/customcreator/rooms/bedroom-fg.png b/customcreator/rooms/bedroom-fg.png
new file mode 100644
index 0000000..70d1abe
Binary files /dev/null and b/customcreator/rooms/bedroom-fg.png differ
diff --git a/customcreator/rooms/bedroom.jpg b/customcreator/rooms/bedroom.jpg
new file mode 100644
index 0000000..92fcbb4
Binary files /dev/null and b/customcreator/rooms/bedroom.jpg differ
diff --git a/customcreator/rooms/dining-fg.png b/customcreator/rooms/dining-fg.png
new file mode 100644
index 0000000..2e20a0b
Binary files /dev/null and b/customcreator/rooms/dining-fg.png differ
diff --git a/customcreator/rooms/dining.jpg b/customcreator/rooms/dining.jpg
new file mode 100644
index 0000000..e9217af
Binary files /dev/null and b/customcreator/rooms/dining.jpg differ
diff --git a/customcreator/rooms/hallway-fg.png b/customcreator/rooms/hallway-fg.png
new file mode 100644
index 0000000..64fa41b
Binary files /dev/null and b/customcreator/rooms/hallway-fg.png differ
diff --git a/customcreator/rooms/hallway.jpg b/customcreator/rooms/hallway.jpg
new file mode 100644
index 0000000..45fd620
Binary files /dev/null and b/customcreator/rooms/hallway.jpg differ
diff --git a/customcreator/rooms/hero.jpg b/customcreator/rooms/hero.jpg
new file mode 100644
index 0000000..a87a03e
Binary files /dev/null and b/customcreator/rooms/hero.jpg differ
diff --git a/customcreator/rooms/living-fg.png b/customcreator/rooms/living-fg.png
new file mode 100644
index 0000000..9278472
Binary files /dev/null and b/customcreator/rooms/living-fg.png differ
diff --git a/customcreator/rooms/living.jpg b/customcreator/rooms/living.jpg
new file mode 100644
index 0000000..6b578dd
Binary files /dev/null and b/customcreator/rooms/living.jpg differ
diff --git a/customcreator/rooms/lobby-fg.png b/customcreator/rooms/lobby-fg.png
new file mode 100644
index 0000000..f01cea1
Binary files /dev/null and b/customcreator/rooms/lobby-fg.png differ
diff --git a/customcreator/rooms/lobby.jpg b/customcreator/rooms/lobby.jpg
new file mode 100644
index 0000000..bf56986
Binary files /dev/null and b/customcreator/rooms/lobby.jpg differ
diff --git a/customcreator/rooms/nursery-fg.png b/customcreator/rooms/nursery-fg.png
new file mode 100644
index 0000000..220c490
Binary files /dev/null and b/customcreator/rooms/nursery-fg.png differ
diff --git a/customcreator/rooms/nursery.jpg b/customcreator/rooms/nursery.jpg
new file mode 100644
index 0000000..87fae97
Binary files /dev/null and b/customcreator/rooms/nursery.jpg differ
diff --git a/customcreator/rooms/office-fg.png b/customcreator/rooms/office-fg.png
new file mode 100644
index 0000000..36eb8f9
Binary files /dev/null and b/customcreator/rooms/office-fg.png differ
diff --git a/customcreator/rooms/office.jpg b/customcreator/rooms/office.jpg
new file mode 100644
index 0000000..6b7e52d
Binary files /dev/null and b/customcreator/rooms/office.jpg differ
diff --git a/package.json b/package.json
index a4491c6..8c05129 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "hollywood-wallcoverings",
"version": "0.1.0",
- "description": "hollywood-wallcoverings.com — Phillipe Romano private-label storefront",
+ "description": "hollywoodwallcoverings.com — Hollywood Wallcoverings, a Designer Wallcoverings house-brand storefront (incl. the /CustomCreator made-to-measure mural tool)",
"main": "server.js",
"scripts": {
"start": "node server.js"
diff --git a/server.js b/server.js
index 84a9d61..9ef4183 100644
--- a/server.js
+++ b/server.js
@@ -95,6 +95,11 @@ app.get(['/favicon.ico','/favicon.svg'], (req, res) => res.sendFile(require('pat
// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
app.use(helmet({ contentSecurityPolicy: false }));
+// Custom Creator — made-to-measure murals at /CustomCreator (Hollywood-branded,
+// uses the SHOPIFY_* tokens already in this process env; no new secrets).
+try { require('./customcreator').mount(app, express); }
+catch (e) { console.error(`[${__SITE}] CustomCreator mount failed (${e.message})`); }
+
// Disable Cloudflare HTML caching — origin must say no-store so edits propagate
// immediately. CF respects no-store; max-age=0 alone is still cacheable.
// (added 2026-05-07 fleet cache-header sweep)
@@ -388,21 +393,21 @@ const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
<!-- Theme toggle (theme1 / theme2) — query-param-gated preview surface
(Steve 2026-05-29). Mirror of dw-domain-fleet shared/render.js productPage()
block (commit 34fbe0e). No admin auth → security-by-obscurity: ?theme=2
- flips to theme2 and persists in localStorage 'wallco-page-theme'. Toggle UI
- hidden until ?theme=N or localStorage opt-in. Shares the wallco-page-theme
+ flips to theme2 and persists in localStorage 'hw-page-theme'. Toggle UI
+ hidden until ?theme=N or localStorage opt-in. Shares the hw-page-theme
localStorage key for cross-site continuity. -->
<script>
(function(){
var qs = new URLSearchParams(location.search);
var fromUrl = qs.get('theme');
- var saved = null; try { saved = localStorage.getItem('wallco-page-theme'); } catch(e){}
+ var saved = null; try { saved = localStorage.getItem('hw-page-theme'); } catch(e){}
var theme = null;
if (fromUrl === '2' || fromUrl === 'theme2') theme = 'theme2';
else if (fromUrl === '1' || fromUrl === 'theme1') theme = 'theme1';
else if (saved === 'theme2' || saved === 'theme1') theme = saved;
if (theme) {
document.documentElement.setAttribute('data-page-theme', theme);
- if (fromUrl) { try { localStorage.setItem('wallco-page-theme', theme); } catch(e){} }
+ if (fromUrl) { try { localStorage.setItem('hw-page-theme', theme); } catch(e){} }
}
/* No opt-in → no attribute set, toggle stays display:none (gate works). */
})();
@@ -451,7 +456,7 @@ const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
b.addEventListener('click', function(){
var t = b.dataset.theme;
document.documentElement.setAttribute('data-page-theme', t);
- try { localStorage.setItem('wallco-page-theme', t); } catch(e){}
+ try { localStorage.setItem('hw-page-theme', t); } catch(e){}
sync(t);
});
});
@@ -625,6 +630,7 @@ const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
</a>
<nav class="ns-nav" id="nsPrimaryNav" aria-label="Primary">
<a href="/">Home</a>
+ <a href="/CustomCreator">Custom Creator</a>
<a href="/about">About</a>
<button type="button" onclick="nsContactOpen()">Contact</button>
</nav>
← a3a2311 Scrub residual vendor names from products.json (title/vendor
·
back to Hollywood Wallcoverings
·
Custom Creator: re-source to wallco.ai designs ONLY (drop Re 16691ff →