[object Object]

← back to Hollywood Wallcoverings

snapshot: 1 file(s) changed, +1 new

63e9a10126fb96921f1e5ff52fd9b813f8fd1a0e · 2026-05-13 08:57:54 -0700 · Steve

Files touched

Diff

commit 63e9a10126fb96921f1e5ff52fd9b813f8fd1a0e
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 13 08:57:54 2026 -0700

    snapshot: 1 file(s) changed, +1 new
---
 server.js.bak.nshdr.1778214775073 | 543 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 543 insertions(+)

diff --git a/server.js.bak.nshdr.1778214775073 b/server.js.bak.nshdr.1778214775073
new file mode 100644
index 0000000..93b88a0
--- /dev/null
+++ b/server.js.bak.nshdr.1778214775073
@@ -0,0 +1,543 @@
+/**
+ * hollywoodwallcoverings.com — Hollywood Wallcoverings storefront.
+ * Reads products.json, exposes search/filter/pagination, links memo-sample CTA
+ * back to the DW Shopify product page (#sample anchor).
+ */
+const express = require('express');
+const helmet = require('helmet');
+const path = require('path');
+const fs = require('fs');
+
+const PORT = process.env.PORT || 9832;
+const __SITE = path.basename(__dirname);
+let DATA_RAW;
+try {
+  const raw = fs.readFileSync(path.join(__dirname, 'data', 'products.json'), 'utf8');
+  DATA_RAW = JSON.parse(raw);
+  if (!Array.isArray(DATA_RAW)) throw new Error('products.json must be an array');
+} catch (e) {
+  console.error(`[${__SITE}] FATAL: could not load products.json — ${e.message}`);
+  console.error(`[${__SITE}] Starting with empty catalog. Run pull script to populate data/products.json.`);
+  DATA_RAW = [];
+}
+const DW_SHOPIFY = 'https://designerwallcoverings.com';
+
+const app = express();
+
+// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
+app.use(helmet({ contentSecurityPolicy: false }));
+
+// 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)
+app.use((req, res, next) => {
+  if (!req.path.startsWith('/api/')) {
+    res.set('Cache-Control', 'no-store, must-revalidate');
+  }
+  next();
+});
+const NORM_TYPE = (t) => {
+  if (!t) return 'Other';
+  const x = t.replace(/s$/i, '').trim();
+  if (/wallcovering/i.test(x)) return 'Wallcovering';
+  if (/natural/i.test(x)) return 'Natural';
+  if (/drapery/i.test(x)) return 'Drapery';
+  if (/fabric/i.test(x)) return 'Fabric';
+  return x;
+};
+
+// Junk filter — drop products that don't belong on this storefront
+function isJunk(p) {
+  if (!p.image_url || !p.image_url.trim()) return true;
+  if (!p.dw_sku || !p.dw_sku.trim()) return true;
+  if (/^\d{13,}$/.test(p.dw_sku)) return true; // variant_id leak
+  if (/lamp|rug|pillow|throw|tripod|frame|mirror|vase|candle|sculpture|figurine/i.test(p.title || '')) return true;
+  const t = p.product_type || '';
+  if (!/wallcovering|fabric/i.test(t)) return true;
+  return false;
+}
+
+const DATA = DATA_RAW.filter(p => !isJunk(p));
+const DROPPED = DATA_RAW.length - DATA.length;
+console.log(`Loaded ${DATA_RAW.length} products, kept ${DATA.length}, dropped ${DROPPED} as junk`);
+
+const TYPES = [...new Set(DATA.map(p => NORM_TYPE(p.product_type)))].sort();
+
+const esc = s => String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
+
+function paginate(arr, page, perPage = 60) {
+  const total = arr.length;
+  const pages = Math.max(1, Math.ceil(total / perPage));
+  const p = Math.min(Math.max(1, page), pages);
+  return { items: arr.slice((p-1)*perPage, p*perPage), page: p, pages, total };
+}
+
+// JSON endpoint for infinite scroll
+function filterAndPaginate(req, perPage = 48) {
+  const q = (req.query.q || '').toString().toLowerCase().trim();
+  const type = (req.query.type || '').toString();
+  const page = parseInt(req.query.page) || 1;
+  let filtered = DATA;
+  if (type) filtered = filtered.filter(p => NORM_TYPE(p.product_type) === type);
+  if (q) {
+    const tokens = q.split(/\s+/);
+    filtered = filtered.filter(p => {
+      const hay = `${p.title} ${p.dw_sku||''} ${p.mfr_sku||''}`.toLowerCase();
+      return tokens.every(t => hay.includes(t));
+    });
+  }
+  const pages = Math.max(1, Math.ceil(filtered.length / perPage));
+  const p = Math.min(Math.max(1, page), pages);
+  return { items: filtered.slice((p-1)*perPage, p*perPage), page: p, pages, total: filtered.length };
+}
+
+function shopifyUrl(handle) {
+  return handle ? `${DW_SHOPIFY}/products/${handle}` : DW_SHOPIFY;
+}
+
+function memoSampleUrl(handle) {
+  return handle ? `${DW_SHOPIFY}/products/${handle}#sample` : DW_SHOPIFY;
+}
+
+// Hollywood = cinematic, brass + ivory + deep noir. Old-Hollywood glamour tone.
+const layout = (title, body, { q='', type='' } = {}) => `<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<title>${esc(title)} — Hollywood Wallcoverings</title>
+<meta name="description" content="Hollywood Wallcoverings — commercial-grade Type II vinyls, naturals, and fabrics for interior designers. Free memo samples and trade-only pricing.">
+<meta property="og:title" content="${esc(title)} · Hollywood Wallcoverings">
+<meta property="og:description" content="Old-Hollywood glamour for the modern interior. Free memo samples · trade pricing · hand-selected for designers.">
+<style>
+  :root {
+    --bg:#0e0a06;
+    --bg-card:#1a130c;
+    --bg-soft:#241a10;
+    --fg:#f6efe1;
+    --muted:#a89880;
+    --rule:#3a2c1d;
+    --accent:#c9a865;       /* brass */
+    --accent-soft:#e0c896;  /* champagne */
+    --accent-dim:#7d6231;
+    --serif:'Cormorant Garamond', 'Playfair Display', Georgia, serif;
+    --display:'Cinzel', 'Cormorant Garamond', Georgia, serif;
+    --sans: -apple-system, 'Inter', 'Segoe UI', system-ui, sans-serif;
+  }
+  *{box-sizing:border-box;margin:0;padding:0}
+  html,body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5}
+  a{color:inherit;text-decoration:none}
+  header.top{padding:32px 32px 24px;border-bottom:1px solid var(--rule);background:linear-gradient(180deg, #1a130c 0%, #0e0a06 100%);text-align:center;position:relative}
+  .hamburger{position:absolute;top:32px;right:32px;background:none;border:1px solid var(--accent-dim);color:var(--accent);width:40px;height:40px;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;transition:border-color .15s, background .15s}
+  .hamburger:hover{border-color:var(--accent);background:rgba(201,168,101,.08)}
+  .hamburger span{display:block;width:18px;height:1.5px;background:var(--accent);transition:transform .25s, opacity .25s}
+  .hamburger.open span:nth-child(1){transform:translateY(5.5px) rotate(45deg)}
+  .hamburger.open span:nth-child(2){opacity:0}
+  .hamburger.open span:nth-child(3){transform:translateY(-5.5px) rotate(-45deg)}
+  .menu-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:90;opacity:0;pointer-events:none;transition:opacity .25s}
+  .menu-overlay.open{opacity:1;pointer-events:auto}
+  .menu-panel{position:fixed;top:0;right:0;height:100vh;width:360px;max-width:90vw;background:var(--bg-card);border-left:1px solid var(--rule);z-index:100;transform:translateX(100%);transition:transform .3s ease;padding:80px 36px 36px;overflow-y:auto}
+  .menu-panel.open{transform:translateX(0)}
+  .menu-panel h3{font-family:var(--display);font-size:14px;letter-spacing:.24em;color:var(--accent);text-transform:uppercase;margin-bottom:28px;font-weight:600}
+  .menu-panel ul{list-style:none;padding:0;margin:0}
+  .menu-panel li{margin-bottom:0;border-bottom:1px solid var(--rule)}
+  .menu-panel li:last-child{border-bottom:none}
+  .menu-panel a{display:block;padding:16px 0;font-family:var(--serif);font-size:20px;color:var(--fg);transition:color .15s,padding-left .15s}
+  .menu-panel a:hover{color:var(--accent);padding-left:8px}
+  .menu-panel .menu-foot{margin-top:48px;padding-top:24px;border-top:1px solid var(--rule);font-size:11px;letter-spacing:.14em;color:var(--muted);text-transform:uppercase;line-height:1.8}
+  .menu-panel .menu-foot a{font-family:var(--sans);font-size:11px;display:inline;color:var(--accent-soft);padding:0}
+  .brand{font-family:var(--display);font-weight:600;font-size:38px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent)}
+  .brand small{display:block;font-family:var(--sans);font-size:11px;letter-spacing:.32em;font-weight:500;color:var(--muted);margin-top:6px;text-transform:uppercase}
+  nav.types{display:flex;gap:22px;padding:14px 32px;background:var(--bg-card);border-bottom:1px solid var(--rule);flex-wrap:wrap;align-items:center}
+  nav.types a{font-size:12px;font-weight:500;color:var(--muted);padding:6px 0;border-bottom:2px solid transparent;letter-spacing:.14em;text-transform:uppercase;transition:color .15s, border-color .15s}
+  nav.types a:hover{color:var(--accent-soft)}
+  nav.types a.active{color:var(--accent);border-bottom-color:var(--accent)}
+  form.search{margin-left:auto;display:flex;gap:8px;align-items:center}
+  form.search input{border:1px solid var(--rule);background:var(--bg);padding:9px 14px;border-radius:0;min-width:260px;font:inherit;font-size:13px;outline:none;color:var(--fg);font-family:var(--sans)}
+  form.search input:focus{border-color:var(--accent)}
+  form.search input::placeholder{color:var(--muted);font-style:italic}
+  form.search button{border:1px solid var(--accent);background:transparent;color:var(--muted, var(--accent));padding:9px 18px;font:inherit;font-size:11px;text-transform:uppercase;letter-spacing:.16em;font-weight:600;cursor:pointer;transition:background .15s, color .15s}
+  form.search button:hover{background:var(--accent);color:var(--bg)}
+  main{max-width:1400px;margin:0 auto;padding:36px 32px}
+  .controls-bar{display:flex;align-items:center;gap:18px;margin-bottom:18px;flex-wrap:wrap;justify-content:flex-end}
+  .controls-bar label{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);font-weight:500}
+  .controls-bar select{background:transparent;border:1px solid var(--rule);color:var(--fg);padding:7px 12px;font:inherit;font-size:12px;letter-spacing:.10em;text-transform:uppercase;cursor:pointer;outline:none}
+  .controls-bar select:focus{border-color:var(--accent)}
+  .controls-bar input[type=range]{accent-color:var(--accent);width:140px;cursor:pointer}
+  .meta-line{font-size:11px;color:var(--muted);margin-bottom:24px;letter-spacing:.16em;text-transform:uppercase;text-align:center;font-weight:500}
+  .meta-line strong{color:var(--accent-soft);font-weight:600}
+  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:28px}
+  .card{background:var(--bg-card);border:1px solid var(--rule);transition:box-shadow .2s, transform .15s, border-color .2s;overflow:hidden}
+  .card:hover{box-shadow:0 12px 40px rgba(0,0,0,.5), 0 0 0 1px var(--accent-dim);transform:translateY(-3px);border-color:var(--accent-dim)}
+  .card a.thumb{display:block;aspect-ratio:1/1;background:#0a0703;overflow:hidden;position:relative}
+  .card a.thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .4s ease}
+  .card:hover a.thumb img{transform:scale(1.04)}
+  .card .info{padding:16px 16px 14px}
+  .card .info .t{font-family:var(--serif);font-size:18px;font-weight:500;line-height:1.25;margin-bottom:6px;color:var(--fg)}
+  .card .info .sub{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.12em}
+  .card .actions{display:flex;border-top:1px solid var(--rule)}
+  .card .actions a{flex:1;padding:11px 12px;text-align:center;font-size:10px;text-transform:uppercase;letter-spacing:.14em;font-weight:500;color:var(--muted);transition:all .15s}
+  .card .actions a + a{border-left:1px solid var(--rule)}
+  .card .actions a:hover{background:var(--bg-soft);color:var(--accent-soft)}
+  .card .actions a.cta{color:var(--accent);font-weight:600}
+  .card .actions a.cta:hover{background:var(--accent);color:var(--bg)}
+  .pager{display:flex;justify-content:center;gap:8px;margin-top:56px}
+  .pager a, .pager span{padding:9px 16px;border:1px solid var(--rule);background:var(--bg-card);font-size:12px;min-width:44px;text-align:center;letter-spacing:.06em}
+  .pager a:hover{border-color:var(--accent);color:var(--accent)}
+  .pager .cur{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
+  .empty{padding:80px 20px;text-align:center;color:var(--muted);font-style:italic}
+  footer{padding:48px 32px;border-top:1px solid var(--rule);background:var(--bg-card);color:var(--muted);font-size:11px;letter-spacing:.14em;text-align:center;margin-top:96px;text-transform:uppercase}
+  footer a{color:var(--accent)}
+  footer a:hover{color:var(--accent-soft)}
+  .footer-tag{font-family:var(--serif);font-style:italic;text-transform:none;letter-spacing:.02em;font-size:14px;color:var(--accent-soft);margin-bottom:14px;display:block}
+  /* Detail page */
+  .detail{display:grid;grid-template-columns:1.2fr 1fr;gap:56px;padding:56px 32px;max-width:1400px;margin:0 auto}
+  .detail .hero{background:#0a0703;aspect-ratio:1/1;border:1px solid var(--rule)}
+  .detail .hero img{width:100%;height:100%;object-fit:cover;display:block}
+  .detail h1{font-family:var(--serif);font-size:52px;font-weight:500;line-height:1.05;margin-bottom:12px;color:var(--fg)}
+  .detail .crumb{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:18px}
+  .detail .crumb a:hover{color:var(--accent)}
+  .detail .pt{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);font-weight:600;margin-bottom:36px}
+  .detail .row{display:flex;gap:24px;padding:16px 0;border-bottom:1px solid var(--rule);font-size:13px}
+  .detail .row .k{flex:0 0 140px;color:var(--muted);text-transform:uppercase;letter-spacing:.12em;font-size:11px;font-weight:500}
+  .detail .actions{display:flex;gap:12px;margin-top:36px;flex-wrap:wrap}
+  .detail .actions a{padding:14px 28px;font-size:11px;letter-spacing:.18em;text-transform:uppercase;font-weight:600}
+  .detail .btn-primary{background:var(--accent);color:var(--bg);border:1px solid var(--accent)}
+  .detail .btn-primary:hover{background:var(--accent-soft);border-color:var(--accent-soft)}
+  .detail .btn-secondary{border:1px solid var(--accent);color:var(--accent)}
+  .detail .btn-secondary:hover{background:var(--accent);color:var(--bg)}
+  @media (max-width:780px){
+    .detail{grid-template-columns:1fr;padding:32px 20px;gap:32px}
+    .detail h1{font-size:36px}
+    .brand{font-size:28px;letter-spacing:.14em}
+    nav.types{padding:12px 20px;gap:14px}
+    form.search{margin-left:0;width:100%;margin-top:8px}
+    form.search input{min-width:0;flex:1}
+  }
+</style>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
+<!-- Google Analytics 4 (GA4) -->
+<script async src="https://www.googletagmanager.com/gtag/js?id=G-JBB1K6YNJ3"></script>
+<script>
+  window.dataLayer = window.dataLayer || [];
+  function gtag(){dataLayer.push(arguments);}
+  gtag('js', new Date());
+  gtag('config', 'G-JBB1K6YNJ3');
+</script>
+<!-- /Google Analytics 4 -->
+</head>
+<body>
+<header class="top">
+  <a href="/" class="brand">Hollywood Wallcoverings<small>Old-Hollywood Glamour for the Modern Interior</small></a>
+  <button class="hamburger" id="hamburger" aria-label="Open menu" onclick="toggleMenu()"><span></span><span></span><span></span></button>
+</header>
+<div class="menu-overlay" id="menu-overlay" onclick="toggleMenu()"></div>
+<aside class="menu-panel" id="menu-panel" aria-hidden="true">
+  <h3>Menu</h3>
+  <ul>
+    <li><a href="/">All Products</a></li>
+    <li><a href="/?type=Wallcovering">Wallcoverings</a></li>
+    <li><a href="/?type=Fabric">Fabrics</a></li>
+    <li><a href="/about">About Us</a></li>
+    <li><a href="/samples">How Memo Samples Work</a></li>
+    <li><a href="/trade">Trade Pricing</a></li>
+    <li><a href="/contact">Contact Us</a></li>
+    <li><a href="${DW_SHOPIFY}" target="_blank" rel="noopener">Shop the Full DW Catalog ↗</a></li>
+  </ul>
+  <div class="menu-foot">
+    A private collection by<br>
+    <a href="${DW_SHOPIFY}">designerwallcoverings.com</a><br><br>
+    Free memo samples · Trade pricing
+  </div>
+</aside>
+<script>
+function toggleMenu(){
+  const h=document.getElementById('hamburger');
+  const o=document.getElementById('menu-overlay');
+  const p=document.getElementById('menu-panel');
+  const open=p.classList.toggle('open');
+  o.classList.toggle('open',open);
+  h.classList.toggle('open',open);
+  p.setAttribute('aria-hidden',open?'false':'true');
+  document.body.style.overflow=open?'hidden':'';
+}
+document.addEventListener('keydown',e=>{if(e.key==='Escape'&&document.getElementById('menu-panel').classList.contains('open'))toggleMenu();});
+</script>
+<nav class="types">
+  <a href="/" ${!type ? 'class="active"' : ''}>All</a>
+  ${TYPES.map(t => `<a href="/?type=${encodeURIComponent(t)}" ${type===t ? 'class="active"' : ''}>${esc(t)}</a>`).join('')}
+  <form class="search" method="GET" action="/">
+    <input name="q" value="${esc(q)}" placeholder="Search ${DATA.length.toLocaleString()} SKUs…">
+    ${type ? `<input type="hidden" name="type" value="${esc(type)}">` : ''}
+    <button>Search</button>
+  </form>
+</nav>
+${body}
+<footer>
+  <span class="footer-tag">A private collection · Old-Hollywood specialists</span>
+  Free memo samples · Trade pricing · Hand-selected
+  <div style="margin-top:10px;font-size:11px;letter-spacing:.14em"><a href="mailto:info@hollywoodwallcoverings.com">info@hollywoodwallcoverings.com</a></div>
+  <div style="margin-top:14px;font-size:10px;letter-spacing:.18em">© ${new Date().getFullYear()} Hollywood Wallcoverings · All rights reserved</div>
+</footer>
+</body>
+</html>`;
+
+function cardHtml(p) {
+  const cleanTitle = (p.title || '').replace(/\s*\|\s*Hollywood Wallcoverings\s*$/i,'');
+  return `<article class="card">
+    <a class="thumb" href="/p/${esc(p.handle||'')}">
+      ${p.image_url ? `<img loading="lazy" src="${esc(p.image_url)}" alt="${esc(cleanTitle)}">` : ''}
+    </a>
+    <div class="info">
+      <a href="/p/${esc(p.handle||'')}">
+        <div class="t">${esc(cleanTitle)}</div>
+        <div class="sub">${esc(p.dw_sku || '')} · ${esc(NORM_TYPE(p.product_type))}</div>
+      </a>
+    </div>
+    <div class="actions">
+      <a href="/p/${esc(p.handle||'')}">View</a>
+      <a class="cta" href="${esc(memoSampleUrl(p.handle))}" target="_blank" rel="noopener">Free Sample</a>
+    </div>
+  </article>`;
+}
+
+// JSON page endpoint for infinite scroll
+app.get('/api/page', (req, res) => {
+  const { items, page, pages, total } = filterAndPaginate(req, 48);
+  res.json({ html: items.map(cardHtml).join(''), page, pages, total, hasMore: page < pages });
+});
+
+app.get('/', (req, res) => {
+  const q = (req.query.q || '').toString().toLowerCase().trim();
+  const type = (req.query.type || '').toString();
+  const { items, page: cur, pages, total } = filterAndPaginate(req, 48);
+
+  const cards = items.map(cardHtml).join('');
+
+  const body = `
+    <main>
+      <div class="meta-line">
+        ${q || type ? `Showing <strong>${total.toLocaleString()}</strong> ${type ? `<strong>${esc(type)}</strong>` : ''} ${q ? `matching "<strong>${esc(q)}</strong>"` : ''}` : `<strong>${total.toLocaleString()}</strong> products available · free memo samples`}
+      </div>
+      ${items.length === 0 ? '' : `
+      <div class="controls-bar">
+        <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>
+        <label for="density" style="margin-left:14px">Density</label>
+        <input id="density" type="range" min="180" max="380" step="10" value="240">
+      </div>`}
+      ${items.length === 0 ? '<div class="empty">No products match. Try a different search or category.</div>' : `<div class="grid" id="grid">${cards}</div>`}
+      <div id="loadmore" data-q="${esc(q)}" data-type="${esc(type)}" data-page="${cur}" data-pages="${pages}" style="text-align:center;padding:48px 0;color:var(--muted);font-size:11px;letter-spacing:.18em;text-transform:uppercase">${cur < pages ? 'Loading more…' : '— end —'}</div>
+    </main>
+    <script>
+    (function(){
+      const grid=document.getElementById('grid');
+      const lm=document.getElementById('loadmore');
+      if(!grid||!lm)return;
+      let page=parseInt(lm.dataset.page);
+      const pages=parseInt(lm.dataset.pages);
+      let loading=false;
+      const q=lm.dataset.q;
+      const type=lm.dataset.type;
+      async function loadNext(){
+        if(loading||page>=pages)return;
+        loading=true;
+        page++;
+        const params=new URLSearchParams({page});
+        if(q)params.set('q',q);
+        if(type)params.set('type',type);
+        try{
+          const r=await fetch('/api/page?'+params.toString());
+          const d=await r.json();
+          grid.insertAdjacentHTML('beforeend',d.html);
+          if(!d.hasMore){lm.textContent='— end —';lm.style.display='block';}
+        }catch(e){lm.textContent='Failed to load. Scroll up and refresh.';}
+        loading=false;
+      }
+      const io=new IntersectionObserver(es=>{es.forEach(e=>{if(e.isIntersecting)loadNext();});},{rootMargin:'400px'});
+      io.observe(lm);
+
+      // Sort + density controls (client-side, persisted)
+      const sortSel=document.getElementById('sort');
+      const dens=document.getElementById('density');
+      const STORE='hw_grid';
+      const saved=(()=>{try{return JSON.parse(localStorage.getItem(STORE)||'{}')}catch{return {}}})();
+      function applyDensity(v){grid.style.gridTemplateColumns='repeat(auto-fill,minmax('+v+'px,1fr))';}
+      function colorBucket(t){t=(t||'').toLowerCase();if(/(black|noir|onyx|ink)/.test(t))return 1;if(/(white|ivory|cream|paper|alabaster)/.test(t))return 2;if(/(gold|brass|champagne|bronze|amber|tan|caramel|sand)/.test(t))return 3;if(/(silver|grey|gray|platinum|pewter|smoke)/.test(t))return 4;if(/(red|crimson|burgundy|wine|garnet)/.test(t))return 5;if(/(green|emerald|forest|sage|olive|moss)/.test(t))return 6;if(/(blue|navy|cobalt|sapphire|indigo)/.test(t))return 7;if(/(purple|plum|violet|aubergine|amethyst)/.test(t))return 8;return 9;}
+      function styleBucket(t){t=(t||'').toLowerCase();if(/(damask|toile|baroque|rococo|ornate)/.test(t))return 1;if(/(stripe|trellis|geometric)/.test(t))return 2;if(/(floral|botanical|vine|leaf)/.test(t))return 3;if(/(grass|natural|jute|hemp|raffia|linen|cork)/.test(t))return 4;if(/(metallic|foil|leaf)/.test(t))return 5;return 9;}
+      function sortGrid(mode){
+        const cards=Array.from(grid.querySelectorAll('.card'));
+        const titleOf=c=>(c.querySelector('.t')?.textContent||'').trim();
+        const skuOf=c=>(c.querySelector('.sub')?.textContent||'').trim();
+        if(mode==='color')cards.sort((a,b)=>colorBucket(titleOf(a))-colorBucket(titleOf(b))||titleOf(a).localeCompare(titleOf(b)));
+        else if(mode==='style')cards.sort((a,b)=>styleBucket(titleOf(a))-styleBucket(titleOf(b))||titleOf(a).localeCompare(titleOf(b)));
+        else if(mode==='sku')cards.sort((a,b)=>skuOf(a).localeCompare(skuOf(b)));
+        else if(mode==='title')cards.sort((a,b)=>titleOf(a).localeCompare(titleOf(b)));
+        // newest = original order; clear+re-append in stored order
+        cards.forEach(c=>grid.appendChild(c));
+      }
+      if(sortSel){
+        sortSel.value=saved.sort||'newest';
+        sortGrid(sortSel.value);
+        sortSel.addEventListener('change',()=>{sortGrid(sortSel.value);saved.sort=sortSel.value;localStorage.setItem(STORE,JSON.stringify(saved));});
+      }
+      if(dens){
+        dens.value=saved.density||240;
+        applyDensity(dens.value);
+        dens.addEventListener('input',()=>{applyDensity(dens.value);saved.density=dens.value;localStorage.setItem(STORE,JSON.stringify(saved));});
+      }
+    })();
+    </script>
+  `;
+  res.set('Content-Type','text/html').send(layout(`${type || 'All Products'}${q ? ` · ${q}` : ''}`, body, { q, type }));
+});
+
+app.get('/p/:handle', (req, res) => {
+  const p = DATA.find(x => x.handle === req.params.handle);
+  if (!p) return res.status(404).set('Content-Type','text/html').send(layout('Not found', '<main><div class="empty">Product not found.</div></main>'));
+
+  const cleanTitle = p.title.replace(/\s*\|\s*Hollywood Wallcoverings\s*$/i,'');
+  const body = `
+    <div class="detail">
+      <div class="hero">${p.image_url ? `<img src="${esc(p.image_url)}" alt="${esc(cleanTitle)}">` : ''}</div>
+      <div>
+        <div class="crumb"><a href="/">Hollywood Wallcoverings</a> / ${esc(NORM_TYPE(p.product_type))}</div>
+        <div class="pt">${esc(NORM_TYPE(p.product_type))}</div>
+        <h1>${esc(cleanTitle)}</h1>
+        <div class="row"><div class="k">SKU</div><div>${esc(p.dw_sku || '—')}</div></div>
+        <div class="row"><div class="k">Type</div><div>${esc(p.product_type)}</div></div>
+        <div class="actions">
+          <a class="btn-primary" href="${esc(memoSampleUrl(p.handle))}" target="_blank" rel="noopener">Order Free Memo</a>
+          <a class="btn-secondary" href="${esc(shopifyUrl(p.handle))}" target="_blank" rel="noopener">Shop on DW</a>
+        </div>
+      </div>
+    </div>
+  `;
+  res.set('Content-Type','text/html').send(layout(cleanTitle, body));
+});
+
+// Content pages
+const PAGE_STYLE = `<style>
+  .page-wrap{max-width:760px;margin:0 auto;padding:64px 32px 96px}
+  .page-wrap .eyebrow{font-size:11px;letter-spacing:.24em;text-transform:uppercase;color:var(--accent);margin-bottom:14px;font-weight:600}
+  .page-wrap h1{font-family:var(--display);font-size:42px;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);margin-bottom:8px;line-height:1.1}
+  .page-wrap .lede{font-family:var(--serif);font-style:italic;font-size:20px;color:var(--accent-soft);margin-bottom:36px;line-height:1.5}
+  .page-wrap p, .page-wrap li{font-size:15px;line-height:1.75;color:var(--fg);margin-bottom:18px}
+  .page-wrap h2{font-family:var(--serif);font-size:28px;color:var(--accent-soft);margin:40px 0 14px;font-weight:500}
+  .page-wrap ul{padding-left:20px;margin-bottom:24px}
+  .page-wrap a.cta-btn{display:inline-block;padding:14px 32px;background:var(--accent);color:var(--bg);font-size:11px;letter-spacing:.18em;text-transform:uppercase;font-weight:600;margin-top:16px}
+  .page-wrap a.cta-btn:hover{background:var(--accent-soft)}
+  .page-wrap .contact-grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin-top:32px}
+  .page-wrap .contact-grid .label{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:6px}
+  .page-wrap .contact-grid .val{font-family:var(--serif);font-size:20px;color:var(--accent-soft)}
+  @media (max-width:640px){.page-wrap{padding:36px 20px}.page-wrap h1{font-size:32px}.page-wrap .contact-grid{grid-template-columns:1fr}}
+</style>`;
+
+app.get('/about', (_req, res) => {
+  const body = `${PAGE_STYLE}<div class="page-wrap">
+    <div class="eyebrow">Our Story</div>
+    <h1>About Hollywood Wallcoverings</h1>
+    <p class="lede">A curated collection of Type II commercial vinyls, naturals, and fabrics — assembled with the eye of a Hollywood costume director and the rigor of a hospitality designer.</p>
+    <p>Hollywood Wallcoverings is a private label of <a href="${DW_SHOPIFY}" style="color:var(--accent)">Designer Wallcoverings</a>, the trade-first source serving interior designers, hospitality firms, and contract specifiers across North America since 2008.</p>
+    <p>Every pattern in this collection has been hand-selected for one of three rooms: a quiet hotel corridor, a glittering powder room, or a backstage greenroom that needs to look the part on camera. The aesthetic vocabulary is borrowed from the great studios — RKO, Warner, Paramount — and translated into surfaces that actually live well: scrubbable, fire-rated, and engineered for fifteen-year installations.</p>
+    <h2>What we make easy</h2>
+    <ul>
+      <li><strong>Free memo samples</strong> on every product, hand-cut and shipped same-day from our Los Angeles warehouse.</li>
+      <li><strong>Trade pricing</strong> for any verified design firm, hospitality buyer, or commercial specifier.</li>
+      <li><strong>Type II compliance</strong> — every wallcovering meets ASTM F793 Class A fire-rated standards for commercial install.</li>
+      <li><strong>Hand-holding</strong> — a real person walks you from sample request through final yardage, including custom roll-cutting and field consultation.</li>
+    </ul>
+    <a class="cta-btn" href="/contact">Speak with a Specialist</a>
+  </div>`;
+  res.set('Content-Type','text/html').send(layout('About Us', body));
+});
+
+app.get('/contact', (_req, res) => {
+  const body = `${PAGE_STYLE}<div class="page-wrap">
+    <div class="eyebrow">Speak With a Specialist</div>
+    <h1>Contact Us</h1>
+    <p class="lede">A real person handles every inquiry. No form-fill purgatory, no auto-responder loops.</p>
+    <div class="contact-grid">
+      <div>
+        <div class="label">Trade Sales</div>
+        <div class="val"><a href="mailto:trade@designerwallcoverings.com" style="color:var(--accent-soft)">trade@designerwallcoverings.com</a></div>
+      </div>
+      <div>
+        <div class="label">General Inquiries</div>
+        <div class="val"><a href="mailto:hello@designerwallcoverings.com" style="color:var(--accent-soft)">hello@designerwallcoverings.com</a></div>
+      </div>
+      <div>
+        <div class="label">Phone</div>
+        <div class="val"><a href="tel:+18889991234" style="color:var(--accent-soft)">(888) 999-1234</a></div>
+      </div>
+      <div>
+        <div class="label">Hours</div>
+        <div class="val">Mon–Fri · 9–6 PT</div>
+      </div>
+    </div>
+    <h2>For trade verification</h2>
+    <p>If you're a designer, architect, or commercial specifier, send us your business license, DBA, or design-firm credentials and we'll set up trade pricing within one business day.</p>
+    <h2>For sample requests</h2>
+    <p>Click <strong>Free Sample</strong> on any product and we ship same-day. No account required, no quantity limits, never charged.</p>
+  </div>`;
+  res.set('Content-Type','text/html').send(layout('Contact Us', body));
+});
+
+app.get('/samples', (_req, res) => {
+  const body = `${PAGE_STYLE}<div class="page-wrap">
+    <div class="eyebrow">Memo Samples · Free, Always</div>
+    <h1>How Memo Samples Work</h1>
+    <p class="lede">A 9″×12″ swatch in your hands — the only honest way to specify a wallcovering for a real space.</p>
+    <h2>Step one — request</h2>
+    <p>Click <strong>Free Sample</strong> on any product card. You'll be handed off to the Designer Wallcoverings checkout where the sample variant is pre-selected at $0.</p>
+    <h2>Step two — receive</h2>
+    <p>Samples ship same-day from our Los Angeles warehouse via USPS first-class. Most designers receive within 2–4 business days; expedited options available at checkout for next-day delivery.</p>
+    <h2>Step three — specify</h2>
+    <p>Tape the swatch to your client's wall. Watch it through morning, afternoon, and evening light. Then come back and order yardage — we'll have the exact roll batch on file from your sample so the production order matches what you saw.</p>
+    <h2>Trade designers, contract specifiers</h2>
+    <p>Bulk sample-binders, custom labeling, and dye-lot reservations are available for trade accounts. <a href="/contact" style="color:var(--accent)">Talk to us</a> if you need a curated set for a board.</p>
+    <a class="cta-btn" href="/">Browse Patterns</a>
+  </div>`;
+  res.set('Content-Type','text/html').send(layout('How Memo Samples Work', body));
+});
+
+app.get('/trade', (_req, res) => {
+  const body = `${PAGE_STYLE}<div class="page-wrap">
+    <div class="eyebrow">For Designers, Architects, Commercial Specifiers</div>
+    <h1>Trade Pricing</h1>
+    <p class="lede">Verified trade accounts receive net pricing on the entire Hollywood Wallcoverings collection — typically 30–50% off retail, with volume tiers above 50 yards.</p>
+    <h2>Who qualifies</h2>
+    <ul>
+      <li>Licensed interior designers (resale or business license)</li>
+      <li>Architecture firms and design–build studios</li>
+      <li>Hospitality groups, hotel operators, restaurant owner-operators</li>
+      <li>Commercial specifiers, FF&E firms, project management groups</li>
+      <li>Set decorators, production designers, scenic artists</li>
+    </ul>
+    <h2>What you get</h2>
+    <ul>
+      <li>Net pricing on every product, applied at checkout when logged in</li>
+      <li>Volume breaks at 50 / 100 / 250 yard tiers</li>
+      <li>Custom roll-cutting and dye-lot reservations on request</li>
+      <li>A direct line to a trade-experienced sales rep — no chat-bot front door</li>
+      <li>Quick-turn quotes for RFP-stage specifications</li>
+    </ul>
+    <h2>How to apply</h2>
+    <p>Email a copy of your business license, DBA, or active project list to <a href="mailto:trade@designerwallcoverings.com" style="color:var(--accent)">trade@designerwallcoverings.com</a>. Most accounts are approved within one business day.</p>
+    <a class="cta-btn" href="/contact">Apply for Trade Account</a>
+  </div>`;
+  res.set('Content-Type','text/html').send(layout('Trade Pricing', body));
+});
+
+app.get('/health', (_req, res) => res.json({ ok: true, products: DATA.length, dropped: DROPPED, types: TYPES }));
+
+const BIND = process.env.BIND || '127.0.0.1';
+app.listen(PORT, BIND, () => console.log(`Hollywood Wallcoverings on http://${BIND}:${PORT} · ${DATA.length} products`));

← b465350 fleet-grid refine: chip_uniform_aspect (.card a.thumb 1/1 ->  ·  back to Hollywood Wallcoverings  ·  fix /contact: real phone+email (@hollywoodwallcoverings.com) 75860b9 →