[object Object]

← back to Dw Vendor Microsites

GA4: inject gtag into commercial (G-22TD8SY6L4) + hollywood (G-ZSCWGG7FZW) viewer index.html

0572b37a4311facc39dcef5f787d19bda178dcdc · 2026-08-17 13:19:33 -0700 · Steve Abrams

Files touched

Diff

commit 0572b37a4311facc39dcef5f787d19bda178dcdc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 17 13:19:33 2026 -0700

    GA4: inject gtag into commercial (G-22TD8SY6L4) + hollywood (G-ZSCWGG7FZW) viewer index.html
---
 vendors/commercial/viewer/public/index.html | 445 ++++++++++++++++++++++++++++
 vendors/hollywood/viewer/public/index.html  | 444 +++++++++++++++++++++++++++
 2 files changed, 889 insertions(+)

diff --git a/vendors/commercial/viewer/public/index.html b/vendors/commercial/viewer/public/index.html
new file mode 100644
index 0000000..b4c6317
--- /dev/null
+++ b/vendors/commercial/viewer/public/index.html
@@ -0,0 +1,445 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="robots" content="noindex, nofollow">
+<title>Trade Lines</title>
+<style>
+  :root{
+    --cols: 5;            /* density-driven grid columns */
+    --ink:#1c1a17; --muted:#6f6a62; --line:#e7e2da; --bg:#faf8f4; --card:#fff;
+    --accent:#7a6a52; --accent2:#3c5a4a;
+  }
+  *{box-sizing:border-box}
+  html,body{margin:0}
+  body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;color:var(--ink);background:var(--bg)}
+  a{color:inherit}
+  header.top{padding:22px 24px 12px;border-bottom:1px solid var(--line);background:#fff;position:sticky;top:0;z-index:30}
+  .brand{font-size:24px;letter-spacing:.04em;font-weight:600}
+  .brand small{display:block;font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);font-weight:500;margin-top:3px}
+  .count{color:var(--muted);font-size:13px;margin-left:auto}
+  .toprow{display:flex;align-items:flex-end;gap:18px;flex-wrap:wrap}
+  /* controls bar */
+  .controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid var(--line);background:#fff;position:sticky;top:73px;z-index:20}
+  .controls label{font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);display:flex;flex-direction:column;gap:4px}
+  select,input[type=search],input[type=text],input[type=email],textarea{font:inherit;padding:7px 9px;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--ink)}
+  input[type=range]{width:120px}
+  .grow{flex:1;min-width:180px}
+  .density{display:flex;flex-direction:column;gap:4px}
+  /* grid/list view toggle */
+  .seg{display:flex;flex-direction:column;gap:4px}
+  .seg .segbtns{display:flex;border:1px solid var(--line);border-radius:7px;overflow:hidden}
+  .seg button{border:none;background:#fff;color:var(--muted);padding:7px 11px;font:inherit;font-size:12.5px;cursor:pointer}
+  .seg button.on{background:var(--ink);color:#fff}
+  /* LIST view: single-column horizontal rows */
+  html.vlist .grid{grid-template-columns:1fr;gap:9px}
+  html.vlist .card{flex-direction:row;align-items:stretch}
+  html.vlist .card .imgwrap{flex:0 0 120px;width:120px;aspect-ratio:1/1}
+  html.vlist .card .reveal{position:static;transform:none;background:none;color:var(--ink);padding:11px 14px;justify-content:center;flex:1}
+  html.vlist .card .reveal .ptitle{font-size:14.5px}
+  html.vlist .card .reveal .rmeta{color:var(--muted)}
+  html.vlist .card .reveal .badge{background:var(--line);color:var(--muted)}
+  html.vlist .card .reveal .reqbtn{border-color:var(--line);color:var(--ink);align-self:flex-start;padding:6px 14px}
+  html.vlist .card .reveal .reqbtn.in{background:var(--accent2);border-color:var(--accent2);color:#fff}
+  html.vlist .card .dots{left:8px;bottom:8px}
+  .cartbtn{margin-left:auto;background:var(--ink);color:#fff;border:none;border-radius:8px;padding:9px 16px;font-weight:600;cursor:pointer;letter-spacing:.02em}
+  .cartbtn .n{display:inline-block;background:var(--accent2);border-radius:20px;padding:1px 8px;margin-left:6px;font-size:12px}
+  /* active-filter chips bar */
+  .activebar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:10px 24px;border-bottom:1px solid var(--line);background:#fcfbf8}
+  .activebar.empty{display:none}
+  .achip{display:inline-flex;align-items:center;gap:6px;background:var(--ink);color:#fff;border-radius:20px;padding:4px 6px 4px 11px;font-size:12.5px;line-height:1}
+  .achip .k{opacity:.6;text-transform:uppercase;letter-spacing:.06em;font-size:10px}
+  .achip .x{border:none;background:rgba(255,255,255,.2);color:#fff;border-radius:50%;width:18px;height:18px;cursor:pointer;font-size:13px;line-height:1;padding:0}
+  .clearall{margin-left:auto;border:1px solid var(--line);background:#fff;border-radius:7px;padding:6px 12px;cursor:pointer;font-size:12.5px}
+  /* layout */
+  .wrap{display:flex;align-items:flex-start}
+  aside{width:248px;flex:0 0 248px;padding:18px 16px 60px;border-right:1px solid var(--line);position:sticky;top:135px;max-height:calc(100vh - 135px);overflow:auto}
+  aside h3{font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:18px 0 8px}
+  aside h3:first-child{margin-top:0}
+  /* facet CHIPS (toggle, multi-select) */
+  .chips{display:flex;flex-wrap:wrap;gap:6px}
+  .chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:#fff;border-radius:18px;padding:4px 10px;font-size:12.5px;cursor:pointer;user-select:none}
+  .chip .cc{color:var(--muted);font-size:10.5px}
+  .chip.on{background:var(--ink);color:#fff;border-color:var(--ink)}
+  .chip.on .cc{color:rgba(255,255,255,.7)}
+  .chip.zero{opacity:.38;cursor:default;pointer-events:none}
+  .chip .swatch{width:11px;height:11px;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}
+  .facet-search{width:100%;margin-bottom:8px;padding:5px 8px;font-size:13px}
+  .facet-scroll{max-height:230px;overflow:auto;padding-right:4px}
+  .clearbtn{margin-top:14px;width:100%;padding:8px;border:1px solid var(--line);background:#fff;border-radius:7px;cursor:pointer;font-size:13px}
+  main{flex:1;padding:18px 24px 80px;min-width:0}
+  .grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:14px}
+  /* MINIMAL image-forward cards: image + hex dot(s) only; detail on hover/focus */
+  .card{position:relative;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
+  .card .imgwrap{aspect-ratio:1/1;background:#f1ede6;overflow:hidden;position:relative}
+  .card img{width:100%;height:100%;object-fit:cover;display:block}
+  .card .dots{position:absolute;left:8px;bottom:8px;display:flex;gap:4px;z-index:2}
+  .card .dot{width:14px;height:14px;border-radius:50%;border:1px solid rgba(255,255,255,.85);box-shadow:0 0 0 1px rgba(0,0,0,.18)}
+  /* reveal overlay (hover or .open via click) */
+  .reveal{position:absolute;inset:auto 0 0 0;background:linear-gradient(to top,rgba(20,18,15,.92),rgba(20,18,15,.78) 60%,rgba(20,18,15,0));color:#fff;padding:30px 11px 11px;transform:translateY(101%);transition:transform .18s ease;display:flex;flex-direction:column;gap:3px}
+  .card:hover .reveal,.card.open .reveal,.card:focus-within .reveal{transform:translateY(0)}
+  .reveal .ptitle{font-weight:600;font-size:13.5px;line-height:1.25}
+  .reveal .rmeta{font-size:11.5px;color:rgba(255,255,255,.82)}
+  .reveal .badge{display:inline-block;font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;background:rgba(255,255,255,.16);border-radius:20px;padding:2px 7px;align-self:flex-start;margin-bottom:1px}
+  .reqbtn{margin-top:6px;border:1px solid #fff;background:transparent;color:#fff;border-radius:7px;padding:6px;font-size:12px;font-weight:600;cursor:pointer}
+  .reqbtn.in{background:var(--accent2);border-color:var(--accent2)}
+  #sentinel{height:60px}
+  .status{text-align:center;color:var(--muted);padding:24px;font-size:13px}
+  /* modal */
+  .overlay{position:fixed;inset:0;background:rgba(28,26,23,.45);display:none;align-items:flex-start;justify-content:center;z-index:50;padding:40px 16px;overflow:auto}
+  .overlay.open{display:flex}
+  .modal{background:#fff;border-radius:12px;max-width:560px;width:100%;padding:22px 24px 26px}
+  .modal h2{margin:0 0 4px;font-size:20px}
+  .modal p.sub{margin:0 0 16px;color:var(--muted);font-size:13px}
+  .cartlist{max-height:230px;overflow:auto;border:1px solid var(--line);border-radius:8px;margin-bottom:16px}
+  .citem{display:flex;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid var(--line);font-size:13px}
+  .citem:last-child{border-bottom:none}
+  .citem img{width:42px;height:42px;object-fit:cover;border-radius:6px;background:#eee}
+  .citem .x{margin-left:auto;border:none;background:none;color:var(--muted);cursor:pointer;font-size:18px}
+  .field{display:flex;flex-direction:column;gap:4px;margin-bottom:11px}
+  .field label{font-size:12px;color:var(--muted)}
+  .row2{display:flex;gap:11px}.row2 .field{flex:1}
+  .submit{width:100%;background:var(--ink);color:#fff;border:none;border-radius:8px;padding:12px;font-weight:600;font-size:15px;cursor:pointer;margin-top:4px}
+  .submit:disabled{opacity:.5;cursor:default}
+  .closex{float:right;border:none;background:none;font-size:22px;cursor:pointer;color:var(--muted);line-height:1}
+  .ok{background:#eef5ef;border:1px solid #cfe3d4;color:#2c5d3c;padding:12px;border-radius:8px;font-size:14px}
+  @media(max-width:820px){aside{display:none}.controls{top:auto;position:static}}
+</style>
+<!-- Google tag (gtag.js) --><script async src="https://www.googletagmanager.com/gtag/js?id=G-22TD8SY6L4"></script><script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag("js",new Date());gtag("config","G-22TD8SY6L4");</script></head>
+<body>
+<header class="top">
+  <div class="toprow">
+    <div class="brand" id="brand">Trade Lines<small>Designer Wallcoverings</small></div>
+    <div class="count" id="count">loading…</div>
+  </div>
+</header>
+
+<div class="controls">
+  <label class="grow">Search
+    <input type="search" id="q" placeholder="pattern, color, SKU, collection…" autocomplete="off">
+  </label>
+  <label>Sort
+    <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="family">Color Family</option>
+      <option value="style">Style</option>
+      <option value="pattern">Pattern A→Z</option>
+      <option value="color_name">Color name A→Z</option>
+      <option value="collection">Collection A→Z</option>
+      <option value="line">Line</option>
+      <option value="product_type">Product Type</option>
+      <option value="material">Material</option>
+      <option value="light">Light → Dark</option>
+      <option value="dark">Dark → Light</option>
+      <option value="hue">Color Wheel (hue)</option>
+      <option value="sku">SKU A→Z</option>
+    </select>
+  </label>
+  <label class="density">Density
+    <input type="range" id="density" min="2" max="8" step="1" value="5">
+  </label>
+  <label class="seg">View
+    <span class="segbtns"><button type="button" id="vGrid" class="on">Grid</button><button type="button" id="vList">List</button></span>
+  </label>
+  <button class="cartbtn" id="cartbtn">Request Samples<span class="n" id="cartn">0</span></button>
+</div>
+
+<!-- active-filter chips -->
+<div class="activebar empty" id="activebar"></div>
+
+<div class="wrap">
+  <aside id="facets"></aside>
+  <main>
+    <div class="grid" id="grid"></div>
+    <div id="sentinel"></div>
+    <div class="status" id="status"></div>
+  </main>
+</div>
+
+<!-- sample request modal -->
+<div class="overlay" id="overlay">
+  <div class="modal">
+    <button class="closex" id="closex">&times;</button>
+    <h2>Request Samples</h2>
+    <p class="sub">No payment — we’ll mail trade samples of the items below.</p>
+    <div id="formArea">
+      <div class="cartlist" id="cartlist"></div>
+      <form id="reqform">
+        <div class="row2">
+          <div class="field"><label>Name *</label><input type="text" name="name" required></div>
+          <div class="field"><label>Email *</label><input type="email" name="email" required></div>
+        </div>
+        <div class="row2">
+          <div class="field"><label>Company / Studio</label><input type="text" name="company"></div>
+          <div class="field"><label>Phone</label><input type="text" name="phone"></div>
+        </div>
+        <div class="field"><label>Shipping address</label><textarea name="address" rows="2"></textarea></div>
+        <div class="field"><label>Notes</label><textarea name="notes" rows="2" placeholder="Optional"></textarea></div>
+        <button class="submit" id="submitbtn" type="submit">Submit sample request</button>
+      </form>
+    </div>
+    <div id="doneArea" style="display:none"></div>
+  </div>
+</div>
+
+<script>
+// per-vendor localStorage namespace (set after /api/config so two microsites don't collide)
+let NS = 'dwvm';
+const LS = {
+  get:(k,d)=>{try{const v=localStorage.getItem(NS+'.'+k);return v==null?d:JSON.parse(v);}catch{return d;}},
+  set:(k,v)=>{try{localStorage.setItem(NS+'.'+k,JSON.stringify(v));}catch{}}
+};
+const $ = (s)=>document.querySelector(s);
+const grid=$('#grid'), statusEl=$('#status'), countEl=$('#count');
+
+// approximate swatch color per family (for the chip dot)
+const FAMILY_SWATCH = {
+  'Red':'#c0392b','Orange/Brown':'#b5651d','Yellow':'#e1c340','Green':'#4a7c3f','Teal':'#2a9d8f',
+  'Blue':'#3a6ea5','Purple':'#7e57c2','Pink':'#e08aa8','Neutral/Beige':'#d8cbb3','Black':'#222',
+  'Grey':'#9a9a9a','White':'#f4f4f0'
+};
+// pretty labels for the active-chip key
+const FACET_LABEL = {family:'Color',style:'Style',material_bucket:'Material',enriched:'Status',line:'Line',product_type:'Type',collection:'Collection'};
+
+// ---- persisted UI state ----
+let sort, density, cart, active, view;
+function setView(v){
+  view = (v==='list') ? 'list' : 'grid';
+  document.documentElement.classList.toggle('vlist', view==='list');
+  const g=document.querySelector('#vGrid'), l=document.querySelector('#vList');
+  if(g) g.classList.toggle('on', view==='grid');
+  if(l) l.classList.toggle('on', view==='list');
+  LS.set('view',view);
+}
+let offset=0, total=0, loading=false, done=false;
+const LIMIT=60;
+
+function qs(){
+  const p=new URLSearchParams();
+  p.set('sort',sort); p.set('offset',offset); p.set('limit',LIMIT);
+  const q=$('#q').value.trim(); if(q) p.set('q',q);
+  for(const k of Object.keys(active)){ if(active[k] && active[k].length) p.set(k, active[k].join('|')); }
+  return p.toString();
+}
+function reset(){ offset=0; total=0; done=false; grid.innerHTML=''; statusEl.textContent=''; renderActive(); load(); }
+
+async function load(){
+  if(loading||done) return;
+  loading=true; statusEl.textContent='loading…';
+  try{
+    const r=await fetch('/api/skus?'+qs());
+    const d=await r.json();
+    total=d.total;
+    for(const row of d.rows) grid.appendChild(card(row));
+    offset += d.rows.length;
+    if(offset>=total || d.rows.length===0) done=true;
+    countEl.textContent = total.toLocaleString()+' of '+d.all.toLocaleString()+' products';
+    statusEl.textContent = done ? (total? 'End of results — '+total.toLocaleString()+' shown' : 'No products match your filters.') : '';
+  }catch(e){ statusEl.textContent='Error loading products.'; }
+  loading=false;
+}
+
+function inCart(sku){ return cart.some(c=>c.sku===sku); }
+function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
+
+// MINIMAL card: image + hex dot(s) only; everything else in hover/click reveal
+function card(row){
+  const el=document.createElement('div'); el.className='card'; el.tabIndex=0;
+  const img = row.image ? `<img loading="lazy" src="${row.image}" alt="${esc(row.pattern_name)}" onerror="this.style.opacity=.25">` : '';
+  let dots='';
+  if(row.color_hex) dots += `<span class="dot" style="background:${row.color_hex}" title="${row.color_hex}"></span>`;
+  if(!row.color_hex && row.family) dots += `<span class="dot" style="background:${FAMILY_SWATCH[row.family]||'#bbb'}" title="${esc(row.family)}"></span>`;
+  const lineBadge = row.line ? `<span class="badge">${esc(row.line)}</span>` : '';
+  el.innerHTML = `
+    <div class="imgwrap">
+      ${img}
+      <div class="dots">${dots}</div>
+      <div class="reveal">
+        ${lineBadge}
+        <div class="ptitle">${esc(row.pattern_name)}</div>
+        <div class="rmeta">${esc(row.color_name||'—')}${row.family?' · '+esc(row.family):''}</div>
+        ${row.collection?`<div class="rmeta">${esc(row.collection)}</div>`:''}
+        <button class="reqbtn ${inCart(row.sku)?'in':''}" data-sku="${esc(row.sku)}">${inCart(row.sku)?'✓ In sample request':'Request Sample'}</button>
+      </div>
+    </div>`;
+  // click anywhere on card (not the button) toggles the reveal open (touch/click reveal)
+  el.addEventListener('click',(e)=>{ if(e.target.closest('.reqbtn')) return; el.classList.toggle('open'); });
+  el.querySelector('.reqbtn').addEventListener('click',(e)=>{e.stopPropagation();toggleCart(row, e.currentTarget);});
+  return el;
+}
+
+// ---- cart ----
+function toggleCart(row, btn){
+  if(inCart(row.sku)){ cart=cart.filter(c=>c.sku!==row.sku); }
+  else cart.push({sku:row.sku,mfr_sku:row.mfr_sku,pattern_name:row.pattern_name,color_name:row.color_name,line:row.line,image:row.image});
+  LS.set('cart',cart);
+  if(btn){ btn.classList.toggle('in',inCart(row.sku)); btn.textContent=inCart(row.sku)?'✓ In sample request':'Request Sample'; }
+  $('#cartn').textContent=cart.length;
+}
+
+// ---- active-filter chip bar (removable) ----
+function renderActive(){
+  const bar=$('#activebar'); bar.innerHTML='';
+  let any=false;
+  for(const k of Object.keys(active)){
+    for(const v of (active[k]||[])){
+      any=true;
+      const c=document.createElement('span'); c.className='achip';
+      const label = k==='enriched' ? (v==='enriched'?'Enriched':'Unenriched') : v;
+      c.innerHTML=`<span class="k">${esc(FACET_LABEL[k]||k)}</span><span>${esc(label)}</span><button class="x" title="remove">&times;</button>`;
+      c.querySelector('.x').addEventListener('click',()=>{ active[k]=active[k].filter(x=>x!==v); if(!active[k].length) delete active[k]; LS.set('filters',active); loadFacets(); reset(); });
+      bar.appendChild(c);
+    }
+  }
+  if(any){
+    const clr=document.createElement('button'); clr.className='clearall'; clr.textContent='Clear all';
+    clr.addEventListener('click',()=>{active={};LS.set('filters',active);loadFacets();reset();});
+    bar.appendChild(clr);
+    bar.classList.remove('empty');
+  } else bar.classList.add('empty');
+}
+
+function toggleFacet(key,value){
+  active[key]=active[key]||[];
+  if(active[key].includes(value)) active[key]=active[key].filter(v=>v!==value);
+  else active[key].push(value);
+  if(!active[key].length) delete active[key];
+  LS.set('filters',active); loadFacets(); reset();
+}
+
+// ---- facets as CHIPS ----
+async function loadFacets(){
+  const r=await fetch('/api/facets'); const d=await r.json();
+  const f=d.facets;
+  const meta=[
+    ['family','Color Family',f.family,false,true],
+    ['style','Style',f.style,false,false],
+    ['material_bucket','Material',f.material_bucket,false,false],
+    ['enriched','Enrichment',f.enriched,false,false],
+    ['line','Line',f.line,false,false],
+    ['product_type','Product Type',f.product_type,false,false],
+    ['collection','Collection',f.collection,true,false],
+  ];
+  const root=$('#facets'); root.innerHTML='';
+  for(const [key,title,vals,searchable,swatch] of meta){
+    if(!vals || !vals.length) continue;
+    const h=document.createElement('h3'); h.textContent=title; root.appendChild(h);
+    let search=null;
+    if(searchable){
+      search=document.createElement('input'); search.type='text'; search.className='facet-search'; search.placeholder='Filter '+title.toLowerCase()+'…';
+      root.appendChild(search);
+    }
+    const box=document.createElement('div'); box.className=searchable?'chips facet-scroll':'chips';
+    for(const {value,count} of vals){
+      const on=(active[key]||[]).includes(value);
+      const chip=document.createElement('span');
+      chip.className='chip'+(on?' on':'')+(count===0&&!on?' zero':'');
+      chip.dataset.v=value.toLowerCase();
+      const label = key==='enriched' ? (value==='enriched'?'Enriched':'Unenriched') : value;
+      const sw = swatch ? `<span class="swatch" style="background:${FAMILY_SWATCH[value]||'#bbb'}"></span>` : '';
+      chip.innerHTML=`${sw}<span>${esc(label)}</span><span class="cc">${count}</span>`;
+      if(!(count===0&&!on)) chip.addEventListener('click',()=>toggleFacet(key,value));
+      box.appendChild(chip);
+    }
+    root.appendChild(box);
+    if(search) search.addEventListener('input',()=>{const t=search.value.toLowerCase();box.querySelectorAll('.chip').forEach(l=>l.style.display=l.dataset.v.includes(t)?'':'none');});
+  }
+  const clear=document.createElement('button'); clear.className='clearbtn'; clear.textContent='Clear all filters';
+  clear.addEventListener('click',()=>{active={};LS.set('filters',active);loadFacets();reset();});
+  root.appendChild(clear);
+}
+
+// ---- controls ----
+$('#sort').addEventListener('change',e=>{sort=e.target.value;LS.set('sort',sort);reset();});
+$('#density').addEventListener('input',e=>{density=+e.target.value;LS.set('density',density);document.documentElement.style.setProperty('--cols',density);});
+$('#vGrid').addEventListener('click',()=>setView('grid'));
+$('#vList').addEventListener('click',()=>setView('list'));
+let qt; $('#q').addEventListener('input',()=>{clearTimeout(qt);qt=setTimeout(reset,250);});
+
+// ---- infinite scroll ----
+new IntersectionObserver((ents)=>{ if(ents[0].isIntersecting) load(); },{rootMargin:'600px'}).observe($('#sentinel'));
+
+// ---- sample modal ----
+const overlay=$('#overlay');
+function openCart(){
+  renderCartList();
+  $('#formArea').style.display = cart.length? '' : 'none';
+  $('#doneArea').style.display='none'; $('#doneArea').innerHTML='';
+  overlay.classList.add('open');
+}
+function closeCart(){ overlay.classList.remove('open'); }
+$('#cartbtn').addEventListener('click',openCart);
+$('#closex').addEventListener('click',closeCart);
+overlay.addEventListener('click',e=>{if(e.target===overlay)closeCart();});
+
+function renderCartList(){
+  const list=$('#cartlist');
+  if(!cart.length){ list.innerHTML='<div class="status">No items yet. Click “Request Sample” on a product.</div>'; return; }
+  list.innerHTML='';
+  cart.forEach(it=>{
+    const row=document.createElement('div'); row.className='citem';
+    row.innerHTML=`${it.image?`<img src="${it.image}">`:''}
+      <div><strong>${esc(it.pattern_name||it.sku)}</strong><br><span style="color:#6f6a62">${esc(it.color_name||'')} ${it.line?'· '+esc(it.line):''}</span></div>
+      <button class="x" title="remove">&times;</button>`;
+    row.querySelector('.x').addEventListener('click',()=>{cart=cart.filter(c=>c.sku!==it.sku);LS.set('cart',cart);$('#cartn').textContent=cart.length;document.querySelectorAll('.reqbtn').forEach(b=>{if(b.dataset.sku===it.sku){b.classList.remove('in');b.textContent='Request Sample';}});openCart();});
+    list.appendChild(row);
+  });
+}
+
+$('#reqform').addEventListener('submit',async e=>{
+  e.preventDefault();
+  if(!cart.length) return;
+  const fd=new FormData(e.target);
+  const payload={items:cart};
+  for(const [k,v] of fd.entries()) payload[k]=v;
+  const btn=$('#submitbtn'); btn.disabled=true; btn.textContent='Submitting…';
+  // Shopify checkout path (real sample order) when wired; else no-payment request below.
+  if(window.DWV_SAMPLE && window.DWV_SAMPLE.available()){
+    try{ btn.textContent='Opening secure checkout…'; await window.DWV_SAMPLE.checkout(cart,{name:payload.name,email:payload.email}); return; }
+    catch(err){ /* fall through to the no-payment request flow */ }
+  }
+  try{
+    const r=await fetch('/api/sample-request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
+    const d=await r.json();
+    if(d.ok){
+      cart=[]; LS.set('cart',cart); $('#cartn').textContent=0;
+      document.querySelectorAll('.reqbtn.in').forEach(b=>{b.classList.remove('in');b.textContent='Request Sample';});
+      $('#formArea').style.display='none';
+      $('#doneArea').style.display='';
+      $('#doneArea').innerHTML=`<div class="ok">Thank you, ${esc(payload.name)} — your request for ${d.count} sample${d.count===1?'':'s'} was received. We’ll be in touch at ${esc(payload.email)}.</div>`;
+    } else { btn.disabled=false; btn.textContent='Submit sample request'; alert(d.error||'Could not submit.'); }
+  }catch(err){ btn.disabled=false; btn.textContent='Submit sample request'; alert('Network error.'); }
+});
+
+// ---- boot: read /api/config for title + per-vendor namespace, then hydrate state ----
+(async function boot(){
+  try{
+    const cfg=await (await fetch('/api/config')).json();
+    if(cfg.title){
+      document.title = cfg.title;
+      $('#brand').innerHTML = esc(cfg.title)+'<small>Designer Wallcoverings</small>';
+      NS = 'dwvm.'+cfg.title.replace(/[^a-z0-9]+/gi,'_').toLowerCase();
+    }
+    window.DWV_CFG = cfg.shopify || {};
+    if(window.DWV_CFG.storefrontToken && window.DWV_CFG.sampleVariantId){
+      await new Promise(res=>{const s=document.createElement('script');s.src='/buybutton.js';s.onload=res;s.onerror=res;document.head.appendChild(s);});
+    }
+  }catch{}
+  sort = LS.get('sort','newest');
+  density = LS.get('density',5);
+  cart = LS.get('cart',[]);
+  active = LS.get('filters',{});
+  view = LS.get('view','grid');
+  $('#sort').value=sort;
+  $('#density').value=density;
+  document.documentElement.style.setProperty('--cols',density);
+  setView(view);
+  $('#cartn').textContent=cart.length;
+  renderActive();
+  loadFacets();
+  load();
+})();
+</script>
+</body>
+</html>
diff --git a/vendors/hollywood/viewer/public/index.html b/vendors/hollywood/viewer/public/index.html
new file mode 100644
index 0000000..ec26aca
--- /dev/null
+++ b/vendors/hollywood/viewer/public/index.html
@@ -0,0 +1,444 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="robots" content="noindex, nofollow">
+<title>Trade Lines</title>
+<style>
+  :root{
+    --cols: 5;            /* density-driven grid columns */
+    --ink:#1c1a17; --muted:#6f6a62; --line:#e7e2da; --bg:#faf8f4; --card:#fff;
+    --accent:#7a6a52; --accent2:#3c5a4a;
+  }
+  *{box-sizing:border-box}
+  html,body{margin:0}
+  body{font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;color:var(--ink);background:var(--bg)}
+  a{color:inherit}
+  header.top{padding:22px 24px 12px;border-bottom:1px solid var(--line);background:#fff;position:sticky;top:0;z-index:30}
+  .brand{font-size:24px;letter-spacing:.04em;font-weight:600}
+  .brand small{display:block;font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);font-weight:500;margin-top:3px}
+  .count{color:var(--muted);font-size:13px;margin-left:auto}
+  .toprow{display:flex;align-items:flex-end;gap:18px;flex-wrap:wrap}
+  /* controls bar */
+  .controls{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid var(--line);background:#fff;position:sticky;top:73px;z-index:20}
+  .controls label{font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);display:flex;flex-direction:column;gap:4px}
+  select,input[type=search],input[type=text],input[type=email],textarea{font:inherit;padding:7px 9px;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--ink)}
+  input[type=range]{width:120px}
+  .grow{flex:1;min-width:180px}
+  .density{display:flex;flex-direction:column;gap:4px}
+  /* grid/list view toggle */
+  .seg{display:flex;flex-direction:column;gap:4px}
+  .seg .segbtns{display:flex;border:1px solid var(--line);border-radius:7px;overflow:hidden}
+  .seg button{border:none;background:#fff;color:var(--muted);padding:7px 11px;font:inherit;font-size:12.5px;cursor:pointer}
+  .seg button.on{background:var(--ink);color:#fff}
+  /* LIST view: single-column horizontal rows */
+  html.vlist .grid{grid-template-columns:1fr;gap:9px}
+  html.vlist .card{flex-direction:row;align-items:stretch}
+  html.vlist .card .imgwrap{flex:0 0 120px;width:120px;aspect-ratio:1/1}
+  html.vlist .card .reveal{position:static;transform:none;background:none;color:var(--ink);padding:11px 14px;justify-content:center;flex:1}
+  html.vlist .card .reveal .ptitle{font-size:14.5px}
+  html.vlist .card .reveal .rmeta{color:var(--muted)}
+  html.vlist .card .reveal .badge{background:var(--line);color:var(--muted)}
+  html.vlist .card .reveal .reqbtn{border-color:var(--line);color:var(--ink);align-self:flex-start;padding:6px 14px}
+  html.vlist .card .reveal .reqbtn.in{background:var(--accent2);border-color:var(--accent2);color:#fff}
+  html.vlist .card .dots{left:8px;bottom:8px}
+  .cartbtn{margin-left:auto;background:var(--ink);color:#fff;border:none;border-radius:8px;padding:9px 16px;font-weight:600;cursor:pointer;letter-spacing:.02em}
+  .cartbtn .n{display:inline-block;background:var(--accent2);border-radius:20px;padding:1px 8px;margin-left:6px;font-size:12px}
+  /* active-filter chips bar */
+  .activebar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:10px 24px;border-bottom:1px solid var(--line);background:#fcfbf8}
+  .activebar.empty{display:none}
+  .achip{display:inline-flex;align-items:center;gap:6px;background:var(--ink);color:#fff;border-radius:20px;padding:4px 6px 4px 11px;font-size:12.5px;line-height:1}
+  .achip .k{opacity:.6;text-transform:uppercase;letter-spacing:.06em;font-size:10px}
+  .achip .x{border:none;background:rgba(255,255,255,.2);color:#fff;border-radius:50%;width:18px;height:18px;cursor:pointer;font-size:13px;line-height:1;padding:0}
+  .clearall{margin-left:auto;border:1px solid var(--line);background:#fff;border-radius:7px;padding:6px 12px;cursor:pointer;font-size:12.5px}
+  /* layout */
+  .wrap{display:flex;align-items:flex-start}
+  aside{width:248px;flex:0 0 248px;padding:18px 16px 60px;border-right:1px solid var(--line);position:sticky;top:135px;max-height:calc(100vh - 135px);overflow:auto}
+  aside h3{font-size:11px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:18px 0 8px}
+  aside h3:first-child{margin-top:0}
+  /* facet CHIPS (toggle, multi-select) */
+  .chips{display:flex;flex-wrap:wrap;gap:14px}
+  .chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line);background:#fff;border-radius:18px;padding:4px 10px;font-size:12.5px;cursor:pointer;user-select:none}
+  .chip .cc{color:var(--muted);font-size:10.5px}
+  .chip.on{background:var(--ink);color:#fff;border-color:var(--ink)}
+  .chip.on .cc{color:rgba(255,255,255,.7)}
+  .chip.zero{opacity:.38;cursor:default;pointer-events:none}
+  .chip .swatch{width:11px;height:11px;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}
+  .facet-search{width:100%;margin-bottom:8px;padding:5px 8px;font-size:13px}
+  .facet-scroll{max-height:230px;overflow:auto;padding-right:4px}
+  .clearbtn{margin-top:14px;width:100%;padding:8px;border:1px solid var(--line);background:#fff;border-radius:7px;cursor:pointer;font-size:13px}
+  main{flex:1;padding:18px 24px 80px;min-width:0}
+  .grid{display:grid;grid-template-columns:repeat(var(--cols),minmax(0,1fr));gap:14px}
+  /* MINIMAL image-forward cards: image + hex dot(s) only; detail on hover/focus */
+  .card{position:relative;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column}
+  .card .imgwrap{aspect-ratio:1/1;background:#f1ede6;overflow:hidden;position:relative}
+  .card img{width:100%;height:100%;object-fit:cover;display:block}
+  .card .dots{position:absolute;left:8px;bottom:8px;display:flex;gap:4px;z-index:2}
+  .card .dot{width:14px;height:14px;border-radius:50%;border:1px solid rgba(255,255,255,.85);box-shadow:0 0 0 1px rgba(0,0,0,.18)}
+  /* reveal overlay (hover or .open via click) */
+  .reveal{position:absolute;inset:auto 0 0 0;background:linear-gradient(to top,rgba(20,18,15,.92),rgba(20,18,15,.78) 60%,rgba(20,18,15,0));color:#fff;padding:30px 11px 11px;transform:translateY(101%);transition:transform .18s ease;display:flex;flex-direction:column;gap:3px}
+  .card:hover .reveal,.card.open .reveal,.card:focus-within .reveal{transform:translateY(0)}
+  .reveal .ptitle{font-weight:600;font-size:13.5px;line-height:1.25}
+  .reveal .rmeta{font-size:11.5px;color:rgba(255,255,255,.82)}
+  .reveal .badge{display:inline-block;font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;background:rgba(255,255,255,.16);border-radius:20px;padding:2px 7px;align-self:flex-start;margin-bottom:1px}
+  .reqbtn{margin-top:6px;border:1px solid #fff;background:transparent;color:#fff;border-radius:7px;padding:6px;font-size:12px;font-weight:600;cursor:pointer}
+  .reqbtn.in{background:var(--accent2);border-color:var(--accent2)}
+  #sentinel{height:60px}
+  .status{text-align:center;color:var(--muted);padding:24px;font-size:13px}
+  /* modal */
+  .overlay{position:fixed;inset:0;background:rgba(28,26,23,.45);display:none;align-items:flex-start;justify-content:center;z-index:50;padding:40px 16px;overflow:auto}
+  .overlay.open{display:flex}
+  .modal{background:#fff;border-radius:12px;max-width:560px;width:100%;padding:22px 24px 26px}
+  .modal h2{margin:0 0 4px;font-size:20px}
+  .modal p.sub{margin:0 0 16px;color:var(--muted);font-size:13px}
+  .cartlist{max-height:230px;overflow:auto;border:1px solid var(--line);border-radius:8px;margin-bottom:16px}
+  .citem{display:flex;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid var(--line);font-size:13px}
+  .citem:last-child{border-bottom:none}
+  .citem img{width:42px;height:42px;object-fit:cover;border-radius:6px;background:#eee}
+  .citem .x{margin-left:auto;border:none;background:none;color:var(--muted);cursor:pointer;font-size:18px}
+  .field{display:flex;flex-direction:column;gap:4px;margin-bottom:11px}
+  .field label{font-size:12px;color:var(--muted)}
+  .row2{display:flex;gap:11px}.row2 .field{flex:1}
+  .submit{width:100%;background:var(--ink);color:#fff;border:none;border-radius:8px;padding:12px;font-weight:600;font-size:15px;cursor:pointer;margin-top:4px}
+  .submit:disabled{opacity:.5;cursor:default}
+  .closex{float:right;border:none;background:none;font-size:22px;cursor:pointer;color:var(--muted);line-height:1}
+  .ok{background:#eef5ef;border:1px solid #cfe3d4;color:#2c5d3c;padding:12px;border-radius:8px;font-size:14px}
+  @media(max-width:820px){aside{display:none}.controls{top:auto;position:static}}
+</style>
+<!-- Google tag (gtag.js) --><script async src="https://www.googletagmanager.com/gtag/js?id=G-ZSCWGG7FZW"></script><script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag("js",new Date());gtag("config","G-ZSCWGG7FZW");</script></head>
+<body>
+<header class="top">
+  <div class="toprow">
+    <div class="brand" id="brand">Trade Lines<small>Designer Wallcoverings</small></div>
+    <div class="count" id="count">loading…</div>
+  </div>
+</header>
+
+<div class="controls">
+  <label class="grow">Search
+    <input type="search" id="q" placeholder="pattern, color, SKU, collection…" autocomplete="off">
+  </label>
+  <label>Sort
+    <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="family">Color Family</option>
+      <option value="style">Style</option>
+      <option value="pattern">Pattern A→Z</option>
+      <option value="color_name">Color name A→Z</option>
+      <option value="collection">Collection A→Z</option>
+      <option value="line">Line</option>
+      <option value="product_type">Product Type</option>
+      <option value="material">Material</option>
+      <option value="light">Light → Dark</option>
+      <option value="dark">Dark → Light</option>
+      <option value="hue">Color Wheel (hue)</option>
+      <option value="sku">SKU A→Z</option>
+    </select>
+  </label>
+  <label class="density">Density
+    <input type="range" id="density" min="2" max="8" step="1" value="5">
+  </label>
+  <label class="seg">View
+    <span class="segbtns"><button type="button" id="vGrid" class="on">Grid</button><button type="button" id="vList">List</button></span>
+  </label>
+  <button class="cartbtn" id="cartbtn">Request Samples<span class="n" id="cartn">0</span></button>
+</div>
+
+<!-- active-filter chips -->
+<div class="activebar empty" id="activebar"></div>
+
+<div class="wrap">
+  <aside id="facets"></aside>
+  <main>
+    <div class="grid" id="grid"></div>
+    <div id="sentinel"></div>
+    <div class="status" id="status"></div>
+  </main>
+</div>
+
+<!-- sample request modal -->
+<div class="overlay" id="overlay">
+  <div class="modal">
+    <button class="closex" id="closex">&times;</button>
+    <h2>Request Samples</h2>
+    <p class="sub">No payment — we’ll mail trade samples of the items below.</p>
+    <div id="formArea">
+      <div class="cartlist" id="cartlist"></div>
+      <form id="reqform">
+        <div class="row2">
+          <div class="field"><label>Name *</label><input type="text" name="name" required></div>
+          <div class="field"><label>Email *</label><input type="email" name="email" required></div>
+        </div>
+        <div class="row2">
+          <div class="field"><label>Company / Studio</label><input type="text" name="company"></div>
+          <div class="field"><label>Phone</label><input type="text" name="phone"></div>
+        </div>
+        <div class="field"><label>Shipping address</label><textarea name="address" rows="2"></textarea></div>
+        <div class="field"><label>Notes</label><textarea name="notes" rows="2" placeholder="Optional"></textarea></div>
+        <button class="submit" id="submitbtn" type="submit">Submit sample request</button>
+      </form>
+    </div>
+    <div id="doneArea" style="display:none"></div>
+  </div>
+</div>
+
+<script>
+// per-vendor localStorage namespace (set after /api/config so two microsites don't collide)
+let NS = 'dwvm';
+const LS = {
+  get:(k,d)=>{try{const v=localStorage.getItem(NS+'.'+k);return v==null?d:JSON.parse(v);}catch{return d;}},
+  set:(k,v)=>{try{localStorage.setItem(NS+'.'+k,JSON.stringify(v));}catch{}}
+};
+const $ = (s)=>document.querySelector(s);
+const grid=$('#grid'), statusEl=$('#status'), countEl=$('#count');
+
+// approximate swatch color per family (for the chip dot)
+const FAMILY_SWATCH = {
+  'Red':'#c0392b','Orange/Brown':'#b5651d','Yellow':'#e1c340','Green':'#4a7c3f','Teal':'#2a9d8f',
+  'Blue':'#3a6ea5','Purple':'#7e57c2','Pink':'#e08aa8','Neutral/Beige':'#d8cbb3','Black':'#222',
+  'Grey':'#9a9a9a','White':'#f4f4f0'
+};
+// pretty labels for the active-chip key
+const FACET_LABEL = {family:'Color',style:'Style',material_bucket:'Material',enriched:'Status',line:'Line',product_type:'Type',collection:'Collection'};
+
+// ---- persisted UI state ----
+let sort, density, cart, active, view;
+function setView(v){
+  view = (v==='list') ? 'list' : 'grid';
+  document.documentElement.classList.toggle('vlist', view==='list');
+  const g=document.querySelector('#vGrid'), l=document.querySelector('#vList');
+  if(g) g.classList.toggle('on', view==='grid');
+  if(l) l.classList.toggle('on', view==='list');
+  LS.set('view',view);
+}
+let offset=0, total=0, loading=false, done=false;
+const LIMIT=60;
+
+function qs(){
+  const p=new URLSearchParams();
+  p.set('sort',sort); p.set('offset',offset); p.set('limit',LIMIT);
+  const q=$('#q').value.trim(); if(q) p.set('q',q);
+  for(const k of Object.keys(active)){ if(active[k] && active[k].length) p.set(k, active[k].join('|')); }
+  return p.toString();
+}
+function reset(){ offset=0; total=0; done=false; grid.innerHTML=''; statusEl.textContent=''; renderActive(); load(); }
+
+async function load(){
+  if(loading||done) return;
+  loading=true; statusEl.textContent='loading…';
+  try{
+    const r=await fetch('/api/skus?'+qs());
+    const d=await r.json();
+    total=d.total;
+    for(const row of d.rows) grid.appendChild(card(row));
+    offset += d.rows.length;
+    if(offset>=total || d.rows.length===0) done=true;
+    countEl.textContent = total.toLocaleString()+' of '+d.all.toLocaleString()+' products';
+    statusEl.textContent = done ? (total? 'End of results — '+total.toLocaleString()+' shown' : 'No products match your filters.') : '';
+  }catch(e){ statusEl.textContent='Error loading products.'; }
+  loading=false;
+}
+
+function inCart(sku){ return cart.some(c=>c.sku===sku); }
+function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
+
+// MINIMAL card: image + hex dot(s) only; everything else in hover/click reveal
+function card(row){
+  const el=document.createElement('div'); el.className='card'; el.tabIndex=0;
+  const img = row.image ? `<img loading="lazy" src="${row.image}" alt="${esc(row.pattern_name)}" onerror="this.style.opacity=.25">` : '';
+  let dots='';
+  if(row.color_hex) dots += `<span class="dot" style="background:${row.color_hex}" title="${row.color_hex}"></span>`;
+  if(!row.color_hex && row.family) dots += `<span class="dot" style="background:${FAMILY_SWATCH[row.family]||'#bbb'}" title="${esc(row.family)}"></span>`;
+  const lineBadge = row.line ? `<span class="badge">${esc(row.line)}</span>` : '';
+  el.innerHTML = `
+    <div class="imgwrap">
+      ${img}
+      <div class="dots">${dots}</div>
+      <div class="reveal">
+        ${lineBadge}
+        <div class="ptitle">${esc(row.pattern_name)}</div>
+        ${row.mfr_sku?`<div class="rmeta">${esc(row.mfr_sku)}</div>`:''}
+        <button class="reqbtn ${inCart(row.sku)?'in':''}" data-sku="${esc(row.sku)}">${inCart(row.sku)?'✓ In sample request':'Request Sample'}</button>
+      </div>
+    </div>`;
+  // click anywhere on card (not the button) toggles the reveal open (touch/click reveal)
+  el.addEventListener('click',(e)=>{ if(e.target.closest('.reqbtn')) return; el.classList.toggle('open'); });
+  el.querySelector('.reqbtn').addEventListener('click',(e)=>{e.stopPropagation();toggleCart(row, e.currentTarget);});
+  return el;
+}
+
+// ---- cart ----
+function toggleCart(row, btn){
+  if(inCart(row.sku)){ cart=cart.filter(c=>c.sku!==row.sku); }
+  else cart.push({sku:row.sku,mfr_sku:row.mfr_sku,pattern_name:row.pattern_name,color_name:row.color_name,line:row.line,image:row.image});
+  LS.set('cart',cart);
+  if(btn){ btn.classList.toggle('in',inCart(row.sku)); btn.textContent=inCart(row.sku)?'✓ In sample request':'Request Sample'; }
+  $('#cartn').textContent=cart.length;
+}
+
+// ---- active-filter chip bar (removable) ----
+function renderActive(){
+  const bar=$('#activebar'); bar.innerHTML='';
+  let any=false;
+  for(const k of Object.keys(active)){
+    for(const v of (active[k]||[])){
+      any=true;
+      const c=document.createElement('span'); c.className='achip';
+      const label = k==='enriched' ? (v==='enriched'?'Enriched':'Unenriched') : v;
+      c.innerHTML=`<span class="k">${esc(FACET_LABEL[k]||k)}</span><span>${esc(label)}</span><button class="x" title="remove">&times;</button>`;
+      c.querySelector('.x').addEventListener('click',()=>{ active[k]=active[k].filter(x=>x!==v); if(!active[k].length) delete active[k]; LS.set('filters',active); loadFacets(); reset(); });
+      bar.appendChild(c);
+    }
+  }
+  if(any){
+    const clr=document.createElement('button'); clr.className='clearall'; clr.textContent='Clear all';
+    clr.addEventListener('click',()=>{active={};LS.set('filters',active);loadFacets();reset();});
+    bar.appendChild(clr);
+    bar.classList.remove('empty');
+  } else bar.classList.add('empty');
+}
+
+function toggleFacet(key,value){
+  active[key]=active[key]||[];
+  if(active[key].includes(value)) active[key]=active[key].filter(v=>v!==value);
+  else active[key].push(value);
+  if(!active[key].length) delete active[key];
+  LS.set('filters',active); loadFacets(); reset();
+}
+
+// ---- facets as CHIPS ----
+async function loadFacets(){
+  const r=await fetch('/api/facets'); const d=await r.json();
+  const f=d.facets;
+  const meta=[
+    ['family','Color Family',f.family,false,true],
+    ['style','Style',f.style,false,false],
+    ['material_bucket','Material',f.material_bucket,false,false],
+    ['enriched','Enrichment',f.enriched,false,false],
+    ['line','Line',f.line,false,false],
+    ['product_type','Product Type',f.product_type,false,false],
+    ['collection','Collection',f.collection,true,false],
+  ];
+  const root=$('#facets'); root.innerHTML='';
+  for(const [key,title,vals,searchable,swatch] of meta){
+    if(!vals || !vals.length) continue;
+    const h=document.createElement('h3'); h.textContent=title; root.appendChild(h);
+    let search=null;
+    if(searchable){
+      search=document.createElement('input'); search.type='text'; search.className='facet-search'; search.placeholder='Filter '+title.toLowerCase()+'…';
+      root.appendChild(search);
+    }
+    const box=document.createElement('div'); box.className=searchable?'chips facet-scroll':'chips';
+    for(const {value,count} of vals){
+      const on=(active[key]||[]).includes(value);
+      const chip=document.createElement('span');
+      chip.className='chip'+(on?' on':'')+(count===0&&!on?' zero':'');
+      chip.dataset.v=value.toLowerCase();
+      const label = key==='enriched' ? (value==='enriched'?'Enriched':'Unenriched') : value;
+      const sw = swatch ? `<span class="swatch" style="background:${FAMILY_SWATCH[value]||'#bbb'}"></span>` : '';
+      chip.innerHTML=`${sw}<span>${esc(label)}</span><span class="cc">${count}</span>`;
+      if(!(count===0&&!on)) chip.addEventListener('click',()=>toggleFacet(key,value));
+      box.appendChild(chip);
+    }
+    root.appendChild(box);
+    if(search) search.addEventListener('input',()=>{const t=search.value.toLowerCase();box.querySelectorAll('.chip').forEach(l=>l.style.display=l.dataset.v.includes(t)?'':'none');});
+  }
+  const clear=document.createElement('button'); clear.className='clearbtn'; clear.textContent='Clear all filters';
+  clear.addEventListener('click',()=>{active={};LS.set('filters',active);loadFacets();reset();});
+  root.appendChild(clear);
+}
+
+// ---- controls ----
+$('#sort').addEventListener('change',e=>{sort=e.target.value;LS.set('sort',sort);reset();});
+$('#density').addEventListener('input',e=>{density=+e.target.value;LS.set('density',density);document.documentElement.style.setProperty('--cols',density);});
+$('#vGrid').addEventListener('click',()=>setView('grid'));
+$('#vList').addEventListener('click',()=>setView('list'));
+let qt; $('#q').addEventListener('input',()=>{clearTimeout(qt);qt=setTimeout(reset,250);});
+
+// ---- infinite scroll ----
+new IntersectionObserver((ents)=>{ if(ents[0].isIntersecting) load(); },{rootMargin:'600px'}).observe($('#sentinel'));
+
+// ---- sample modal ----
+const overlay=$('#overlay');
+function openCart(){
+  renderCartList();
+  $('#formArea').style.display = cart.length? '' : 'none';
+  $('#doneArea').style.display='none'; $('#doneArea').innerHTML='';
+  overlay.classList.add('open');
+}
+function closeCart(){ overlay.classList.remove('open'); }
+$('#cartbtn').addEventListener('click',openCart);
+$('#closex').addEventListener('click',closeCart);
+overlay.addEventListener('click',e=>{if(e.target===overlay)closeCart();});
+
+function renderCartList(){
+  const list=$('#cartlist');
+  if(!cart.length){ list.innerHTML='<div class="status">No items yet. Click “Request Sample” on a product.</div>'; return; }
+  list.innerHTML='';
+  cart.forEach(it=>{
+    const row=document.createElement('div'); row.className='citem';
+    row.innerHTML=`${it.image?`<img src="${it.image}">`:''}
+      <div><strong>${esc(it.pattern_name||it.sku)}</strong><br><span style="color:#6f6a62">${esc(it.color_name||'')} ${it.line?'· '+esc(it.line):''}</span></div>
+      <button class="x" title="remove">&times;</button>`;
+    row.querySelector('.x').addEventListener('click',()=>{cart=cart.filter(c=>c.sku!==it.sku);LS.set('cart',cart);$('#cartn').textContent=cart.length;document.querySelectorAll('.reqbtn').forEach(b=>{if(b.dataset.sku===it.sku){b.classList.remove('in');b.textContent='Request Sample';}});openCart();});
+    list.appendChild(row);
+  });
+}
+
+$('#reqform').addEventListener('submit',async e=>{
+  e.preventDefault();
+  if(!cart.length) return;
+  const fd=new FormData(e.target);
+  const payload={items:cart};
+  for(const [k,v] of fd.entries()) payload[k]=v;
+  const btn=$('#submitbtn'); btn.disabled=true; btn.textContent='Submitting…';
+  // Shopify checkout path (real sample order) when wired; else no-payment request below.
+  if(window.DWV_SAMPLE && window.DWV_SAMPLE.available()){
+    try{ btn.textContent='Opening secure checkout…'; await window.DWV_SAMPLE.checkout(cart,{name:payload.name,email:payload.email}); return; }
+    catch(err){ /* fall through to the no-payment request flow */ }
+  }
+  try{
+    const r=await fetch('/api/sample-request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
+    const d=await r.json();
+    if(d.ok){
+      cart=[]; LS.set('cart',cart); $('#cartn').textContent=0;
+      document.querySelectorAll('.reqbtn.in').forEach(b=>{b.classList.remove('in');b.textContent='Request Sample';});
+      $('#formArea').style.display='none';
+      $('#doneArea').style.display='';
+      $('#doneArea').innerHTML=`<div class="ok">Thank you, ${esc(payload.name)} — your request for ${d.count} sample${d.count===1?'':'s'} was received. We’ll be in touch at ${esc(payload.email)}.</div>`;
+    } else { btn.disabled=false; btn.textContent='Submit sample request'; alert(d.error||'Could not submit.'); }
+  }catch(err){ btn.disabled=false; btn.textContent='Submit sample request'; alert('Network error.'); }
+});
+
+// ---- boot: read /api/config for title + per-vendor namespace, then hydrate state ----
+(async function boot(){
+  try{
+    const cfg=await (await fetch('/api/config')).json();
+    if(cfg.title){
+      document.title = cfg.title;
+      $('#brand').innerHTML = esc(cfg.title)+'<small>Designer Wallcoverings</small>';
+      NS = 'dwvm.'+cfg.title.replace(/[^a-z0-9]+/gi,'_').toLowerCase();
+    }
+    window.DWV_CFG = cfg.shopify || {};
+    if(window.DWV_CFG.storefrontToken && window.DWV_CFG.sampleVariantId){
+      await new Promise(res=>{const s=document.createElement('script');s.src='/buybutton.js';s.onload=res;s.onerror=res;document.head.appendChild(s);});
+    }
+  }catch{}
+  sort = LS.get('sort','newest');
+  density = LS.get('density',5);
+  cart = LS.get('cart',[]);
+  active = LS.get('filters',{});
+  view = LS.get('view','grid');
+  $('#sort').value=sort;
+  $('#density').value=density;
+  document.documentElement.style.setProperty('--cols',density);
+  setView(view);
+  $('#cartn').textContent=cart.length;
+  renderActive();
+  loadFacets();
+  load();
+})();
+</script>
+</body>
+</html>

(oldest)  ·  back to Dw Vendor Microsites  ·  auto-data-snapshot: 2026-08-17T13:28:32 (1 data files) — .en 530a0bb →