[object Object]

← 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

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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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 →