← back to Quadrille House Site

public/index.html

417 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>The Quadrille House | Designer Wallcoverings</title>
<meta name="description" content="The full Quadrille house of hand-screened American wallcoverings — China Seas, Quadrille, Alan Campbell & more — curated by Designer Wallcoverings, to the trade.">
<meta property="og:type" content="website">
<meta property="og:title" content="The Quadrille House | Designer Wallcoverings">
<meta property="og:description" content="China Seas · Quadrille · Alan Campbell · Home Couture · Charles Burger · Cloth & Paper · Suncloth · Plains · Grasscloth.">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="The Quadrille House | Designer Wallcoverings">
<meta name="twitter:description" content="Hand-screened American wallcoverings & fabrics — curated by Designer Wallcoverings.">
<meta name="theme-color" content="#f5f1ea">
<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=Lora:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
</head>
<body>

<a class="corner ul on-hero wordmark" id="cUL" href="https://www.designerwallcoverings.com">Designer Wallcoverings</a>
<nav class="corner ur on-hero" id="cUR"></nav>

<!-- HERO -->
<header class="hero" id="hero">
  <div class="hero-inner">
    <div class="hero-eyebrow" id="heroEyebrow">A Designer Wallcoverings Collection</div>
    <h1 id="heroH1">The Quadrille House</h1>
    <div class="sub" id="heroSub">China Seas · Quadrille · Alan Campbell · Home Couture · Charles Burger · Cloth &amp; Paper · Suncloth · Plains · Grasscloth</div>
    <div class="hero-kicker" id="heroKicker">Hand-screened American textiles &amp; wallcoverings</div>
  </div>
  <a class="enter" href="#catalog">Explore the House</a>
</header>

<!-- 3D SHOWROOM EXPERIENCE (hero band, below top hero) -->
<style>
  .showroom-hero{position:relative;width:100%;height:78vh;min-height:520px;overflow:hidden;background:#14110d;display:flex;align-items:center;justify-content:center}
  .showroom-hero .sh-poster{position:absolute;inset:0;background:radial-gradient(120% 120% at 50% 30%,#3a3026 0%,#14110d 70%);background-size:cover;background-position:center}
  .showroom-hero .sh-overlay{position:relative;z-index:2;text-align:center;color:#f5efe4;max-width:760px;padding:0 24px}
  .showroom-hero .sh-eyebrow{letter-spacing:.32em;text-transform:uppercase;font-size:12px;opacity:.8;margin-bottom:14px}
  .showroom-hero .sh-h2{font-size:clamp(34px,6vw,68px);font-weight:600;margin:0 0 16px;line-height:1.02}
  .showroom-hero .sh-sub{font-size:clamp(15px,2vw,19px);opacity:.85;margin:0 auto 28px;max-width:560px}
  .showroom-hero .sh-enter{display:inline-block;border:1px solid rgba(245,239,228,.6);color:#f5efe4;background:rgba(0,0,0,.18);border-radius:999px;padding:14px 30px;font-size:15px;font-weight:600;cursor:pointer;backdrop-filter:blur(2px);transition:background .2s}
  .showroom-hero .sh-enter:hover{background:rgba(245,239,228,.14)}
  .showroom-hero iframe{position:absolute;inset:0;width:100%;height:100%;border:0;z-index:3}
</style>
<section class="showroom-hero" id="showroom" aria-label="3D Showroom Experience">
  <div class="sh-poster" id="shPoster"></div>
  <div class="sh-overlay" id="shOverlay">
    <div class="sh-eyebrow">Step Inside</div>
    <h2 class="sh-h2">3D Showroom Experience</h2>
    <p class="sh-sub">Walk the Quadrille House in an interactive 3D room — see every line dressed on the wall.</p>
    <a class="sh-enter" id="shEnter" href="https://quadrilleshowroom.designerwallcoverings.com/" target="_blank" rel="noopener" aria-label="Enter the 3D Showroom (opens quadrilleshowroom.designerwallcoverings.com)">Enter the Showroom ▸</a>
  </div>
</section>

<!-- BRAND / LINE FILTER -->
<section class="books" id="collections">
  <div class="eyebrow" id="booksEyebrow">The House</div>
  <h2 id="booksHeading">The Quadrille House</h2>
  <div class="chips" id="brandChips"></div>
</section>

<!-- CONTROLS -->
<div class="controls" id="catalog">
  <div class="controls-in">
    <a href="https://www.designerwallcoverings.com" class="bar-wm">Designer Wallcoverings</a>
    <div class="count"><b id="cnt">—</b> designs</div>
    <a class="chip sr-btn" href="https://quadrilleshowroom.designerwallcoverings.com" target="_blank" rel="noopener">3D Showroom ▸</a>
    <div class="ctl">
      <label for="pattern">Pattern</label>
      <select id="pattern"><option value="">All patterns</option></select>
    </div>
    <div class="ctl">
      <label for="sort">Sort</label>
      <select id="sort">
        <option value="newest">Newest</option>
        <option value="color">Color</option>
        <option value="series">Pattern</option>
        <option value="sku">SKU A→Z</option>
        <option value="title">Title A→Z</option>
      </select>
    </div>
    <div class="ctl">
      <label for="density">Density</label>
      <input type="range" id="density" min="2" max="20" step="1" value="4">
    </div>
    <button class="chip view-btn" id="view" type="button">▤ List view</button>
  </div>
</div>

<!-- COLOR FILTER -->
<div class="colorbar" id="colorbar"></div>

<!-- GRID -->
<main class="grid-wrap">
  <div class="list-head" id="listHead"></div>
  <div class="grid" id="grid"></div>
</main>

<!-- LOOKBOOK -->
<section class="lookbook" id="lookbook">
  <div class="lb-in">
    <div class="eyebrow">The Lookbook</div>
    <h2>Rooms &amp; lookbooks from the house</h2>
    <p class="lb-lead">Editorial rooms by leading designers, and the house's own lookbook library.</p>
    <div class="lb-rooms" id="lbRooms"></div>
    <h3 class="lb-sub">Lookbook library</h3>
    <div class="lb-pdfs" id="lbPdfs"></div>
  </div>
</section>

<!-- ABOUT -->
<section class="about" id="aboutSection" hidden>
  <div class="about-in">
    <div class="eyebrow" id="aboutEyebrow">About the House</div>
    <h2 id="aboutHeading">About</h2>
    <div class="about-body" id="aboutBody"></div>
    <p class="dw-collab" id="aboutCollab"></p>
  </div>
</section>

<footer>
  <div class="foot-in">
    <div>
      <a class="wm" href="https://www.designerwallcoverings.com">Designer Wallcoverings</a>
      <div style="margin-top:8px">Designer Wallcoverings &amp; Fabrics · To the Trade</div>
      <div id="footLine" style="margin-top:4px;opacity:.8">The Quadrille House — one of 200+ lines we carry.</div>
      <div class="share" id="shareCollection" style="margin-top:18px"></div>
    </div>
    <div class="gate" id="footGate">Hand-screened, sold to the trade. Order a memo sample before specifying. <a id="footGateLink" href="https://www.designerwallcoverings.com/collections/china-seas" style="color:var(--gold)">Shop China Seas →</a></div>
  </div>
</footer>

<a class="shopbar" id="shopbar" href="https://www.designerwallcoverings.com/collections/china-seas" target="_blank" rel="noopener">Shop the Quadrille house at <b>Designer Wallcoverings</b> →</a>

<script>
const $=s=>document.querySelector(s), grid=$('#grid');
let ALL=[], activeBrand='all', activeColor=null, activeSeries=null, FACETS=null, CFG=null;
const LS={sort:'qhouse.sort', dens:'qhouse.density', brand:'qhouse.brand', view:'qhouse.view'};
// LIST-VIEW columns — every non-always-null data field a product carries
// (match_type / finish / application / coverage are always null in house.json → skipped;
//  brand === book for all 3,717 products → one "Line" column).
const COLS=[
  {k:'brand',        l:'Line'},
  {k:'series',       l:'Pattern'},
  {k:'display_name', l:'Name'},
  {k:'color',        l:'Color'},
  {k:'color_bucket', l:'Color Family'},
  {k:'sku',          l:'DW SKU', mono:true},
  {k:'product_type', l:'Type'},
  {k:'composition',  l:'Material'},
  {k:'width',        l:'Width'},
  {k:'repeat',       l:'Repeat'},
  {k:'sample_price', l:'Sample', num:true},
  {k:'published_at', l:'Published', date:true},
];
let listSort=null; // {k, dir:1|-1}
const BUCKET_HEX={white:'#efeae0',grey:'#9e978c',black:'#2a2722',red:'#a23b2e',orange:'#c8763a',brown:'#7b5a3c',gold:'#b8923f',green:'#5d7150',teal:'#3f8a86',blue:'#5a7494',purple:'#7a5f86',pink:'#bb8aa0'};

// hero rotation from REAL interior room-setting photos (lookbook rooms), not
// fabric-group shots. Falls back to product room shots if the lookbook is empty.
async function startHero(items){
  let pool=[];
  try{ const lb=await (await fetch(location.origin+'/api/lookbook')).json(); pool=(lb.rooms||[]).map(r=>r.src).filter(Boolean); }catch{}
  if(pool.length<3){ pool=pool.concat(items.filter(p=>p.room&&p.images&&p.images.length>1).map(p=>p.room)); }
  pool=pool.slice(0,8);
  const h=$('#hero');
  pool.forEach((src,i)=>{
    const d=document.createElement('div');
    d.className='slide'+(i===0?' on':'');
    d.style.backgroundImage=`url("${src}")`;
    h.insertBefore(d,h.firstChild);
  });
  const slides=[...document.querySelectorAll('.hero .slide')];
  let i=0; if(slides.length>1) setInterval(()=>{slides[i].classList.remove('on');i=(i+1)%slides.length;slides[i].classList.add('on');},5200);
}

function colorKey(p){
  const b=p.color_bucket, hue=p.hue??999;
  if(b==='white')return [0, p.val??1];
  if(b==='grey') return [1, p.val??.5];
  if(b==='black')return [2, -(p.val??0)];
  return [3, hue];
}
function sortItems(items,mode){
  const a=[...items];
  switch(mode){
    case 'color': a.sort((x,y)=>{const kx=colorKey(x),ky=colorKey(y);return kx[0]-ky[0]||kx[1]-ky[1];});break;
    case 'series': a.sort((x,y)=>(x.series||'').localeCompare(y.series||'')|| (x.color||'').localeCompare(y.color||''));break;
    case 'sku': a.sort((x,y)=>(x.sku||'').localeCompare(y.sku||''));break;
    case 'title': a.sort((x,y)=>(x.title||'').localeCompare(y.title||''));break;
    default: a.sort((x,y)=> new Date(y.published_at||0)-new Date(x.published_at||0));
  }
  return a;
}

// ── LIST VIEW helpers ──
const isList=()=>localStorage.getItem(LS.view)==='list';
const LC_FMT={
  sample_price:v=>'$'+Number(v).toFixed(2),
  published_at:v=>new Date(v).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}),
};
const esc=s=>String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
function lcCell(p,c){
  const v=p[c.k];
  if(v==null||v==='') return `<span class="lc${c.mono?' mono':''}">—</span>`;
  const txt=esc(LC_FMT[c.k]?LC_FMT[c.k](v):v);
  const dot=c.k==='color_bucket'&&p.hex?`<span class="dot" style="background:${p.hex}"></span>`:'';
  return `<span class="lc${c.mono?' mono':''}" title="${txt}">${dot}${txt}</span>`;
}
function sortVal(p,c){
  const v=p[c.k];
  if(v==null||v==='') return null;
  if(c.num) return Number(v);
  if(c.date) return new Date(v).getTime();
  if(c.k==='width'||c.k==='repeat'){const n=parseFloat(String(v)); if(!isNaN(n)) return n;}
  return String(v).toLowerCase();
}
function listCompare(a,b){
  const c=COLS.find(x=>x.k===listSort.k);
  const va=sortVal(a,c), vb=sortVal(b,c);
  if(va==null&&vb==null) return 0;
  if(va==null) return 1;              // nulls always last
  if(vb==null) return -1;
  const r=(typeof va==='number'&&typeof vb==='number')?va-vb:String(va).localeCompare(String(vb),undefined,{numeric:true});
  return r*listSort.dir;
}
function renderListHead(){
  const lh=$('#listHead');
  lh.innerHTML='<span class="hc" style="cursor:default" aria-hidden="true"></span>'+COLS.map(c=>{
    const on=listSort&&listSort.k===c.k;
    return `<button class="hc ${on?'active':''}" data-k="${c.k}" type="button" title="Sort by ${c.l}">${c.l}${on?(listSort.dir===1?' ▲':' ▼'):''}</button>`;
  }).join('');
  lh.querySelectorAll('.hc[data-k]').forEach(b=>b.onclick=()=>{
    const k=b.dataset.k;
    listSort=(listSort&&listSort.k===k)?{k,dir:-listSort.dir}:{k,dir:1};
    renderListHead(); render();
  });
}
function placeListHead(){ // sticky offset = live height of the sticky controls bar
  const c=document.querySelector('.controls');
  $('#listHead').style.top=((c?c.offsetHeight:60)+2)+'px';
}
addEventListener('resize',placeListHead);
function applyView(){
  const list=isList();
  grid.classList.toggle('list',list);
  $('#listHead').classList.toggle('on',list);
  $('#view').textContent=list?'▦ Grid view':'▤ List view';
  if(list){ renderListHead(); placeListHead(); }
}
$('#view').addEventListener('click',()=>{
  localStorage.setItem(LS.view,isList()?'grid':'list');
  applyView(); render();
});

function render(){
  let items = ALL;
  if(activeColor) items = items.filter(p=>p.color_bucket===activeColor);
  if(activeSeries) items = items.filter(p=>p.series===activeSeries);
  items = sortItems(items, $('#sort').value);
  if(isList()&&listSort) items.sort(listCompare); // column sort over the FULL filtered set
  $('#cnt').textContent=items.length;
  grid.innerHTML = items.map(p=>`
    <div class="card" data-h="${p.handle}">
      <img class="ph" loading="lazy" decoding="async" width="400" height="400" alt="${(p.title||'').replace(/"/g,'&quot;')} — ${p.brand}" src="${p.swatch||p.room||''}">
      <div class="body">
        <div class="ser">${p.display_eyebrow||p.series||''}</div>
        <div class="nm">${p.display_name||p.color||p.title}</div>
        <div class="meta">
          <span class="col">${p.hex?`<span class="dot" style="background:${p.hex}"></span>`:''}${p.brand}</span>
          <span class="pr view">${p.cta_mode==='live'?'View →':p.cta_mode==='sample'?('Sample $'+Number(p.sample_price||4.25).toFixed(2)+' →'):'Request →'}</span>
        </div>
        ${COLS.map(c=>lcCell(p,c)).join('')}
      </div>
    </div>`).join('');
}
grid.addEventListener('click',e=>{const c=e.target.closest('.card');if(c)location.href='/product/'+c.dataset.h;});

// BRAND / LINE filter chips
function brandChips(){
  const wrap=$('#brandChips');
  const brands=CFG.brands||[];
  const total=brands.reduce((s,[,n])=>s+n,0);
  const all=`<div class="chip ${activeBrand==='all'?'active':''}" data-b="all">All Lines<span class="n">${total}</span></div>`;
  wrap.innerHTML=all+brands.map(([b,n])=>`<div class="chip ${activeBrand===b?'active':''}" data-b="${b}">${b}<span class="n">${n}</span></div>`).join('');
  wrap.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>{
    activeBrand=ch.dataset.b; activeSeries=null; activeColor=null;
    localStorage.setItem(LS.brand,activeBrand);
    loadBrand(); $('#catalog').scrollIntoView({behavior:'smooth'});
  });
}
function colorbar(facets){
  const wrap=$('#colorbar');
  if(!facets.colors||!facets.colors.length){wrap.style.display='none';return;}
  wrap.style.display='';
  const all=`<button class="cdot all ${!activeColor?'active':''}" data-c="" title="All colors">All</button>`;
  wrap.innerHTML=all+facets.colors.map(([c,n])=>`<button class="cdot ${activeColor===c?'active':''}" data-c="${c}" title="${c} (${n})"><span style="background:${BUCKET_HEX[c]||'#ccc'}"></span>${c}</button>`).join('');
  wrap.querySelectorAll('.cdot').forEach(b=>b.onclick=()=>{activeColor=b.dataset.c||null;colorbar(facets);render();});
}
function fillPatterns(facets){
  const sel=$('#pattern');
  sel.innerHTML='<option value="">All patterns</option>'+facets.series.map(([s,n])=>`<option value="${s}">${s} (${n})</option>`).join('');
}

function applyDensity(v){document.documentElement.style.setProperty('--cols',v);}
$('#density').addEventListener('input',e=>{applyDensity(e.target.value);localStorage.setItem(LS.dens,e.target.value);});
$('#sort').addEventListener('change',()=>{localStorage.setItem(LS.sort,$('#sort').value);render();});
$('#pattern').addEventListener('change',()=>{activeSeries=$('#pattern').value||null;render();$('#catalog').scrollIntoView({behavior:'smooth'});});

addEventListener('scroll',()=>{
  const was=document.body.classList.contains('scrolled');
  const now=scrollY>innerHeight*0.82;
  document.body.classList.toggle('scrolled',now);
  if(was!==now&&isList()) placeListHead(); // controls bar height changes when the wordmark row appears
});

// LOOKBOOK
async function loadLookbook(){
  try{
    const lb=await (await fetch(location.origin+'/api/lookbook')).json();
    $('#lbRooms').innerHTML=(lb.rooms||[]).slice(0,40).map(r=>`
      <figure class="lb-room"><img loading="lazy" decoding="async" alt="${(r.caption||'').replace(/"/g,'&quot;')}" src="${r.src}"></figure>`).join('');
    $('#lbPdfs').innerHTML=(lb.lookbooks||[]).map(p=>`
      <a class="lb-pdf" href="${p.file}" target="_blank" rel="noopener">
        <span class="lb-ico">PDF</span><span class="lb-t">${p.title}</span></a>`).join('');
  }catch(e){ $('#lookbook').style.display='none'; }
}

// ABOUT — house-level + active brand
function applyAbout(){
  const sec=$('#aboutSection'); if(!CFG.about) return;
  let blk = (activeBrand!=='all' && CFG.about.brands && CFG.about.brands[activeBrand]) || CFG.about.house;
  if(!blk||!(blk.paragraphs||[]).length){ sec.hidden=true; return; }
  $('#aboutHeading').textContent = activeBrand!=='all' ? `About ${activeBrand}` : (blk.heading||'About the House');
  $('#aboutBody').innerHTML = blk.paragraphs.map(p=>`<p>${p}</p>`).join('');
  $('#aboutCollab').textContent = blk.collab||'';
  sec.hidden=false;
}

function applyConfig(cfg){
  CFG=cfg;
  if(cfg.house){ $('#cUL').textContent=cfg.house; if(cfg.houseUrl)$('#cUL').href=cfg.houseUrl; }
  if(cfg.nav&&cfg.nav.length){
    $('#cUR').innerHTML=cfg.nav.map(n=>{const ext=/^https?:/.test(n.href);return `<a href="${n.href}"${ext?' target="_blank" rel="noopener"':''}>${n.label}</a>`;}).join('');
  }
  if(cfg.eyebrow) $('#heroEyebrow').textContent=cfg.eyebrow;
  if(cfg.line)    $('#heroH1').textContent=cfg.line;
  if(cfg.tagline) $('#heroSub').textContent=cfg.tagline;
  if(cfg.kicker)  $('#heroKicker').textContent=cfg.kicker;
  if(cfg.title)   document.title=cfg.title;
  if(cfg.metaDescription){ const m=document.querySelector('meta[name=description]'); if(m)m.content=cfg.metaDescription; }
  if(cfg.collectionUrl){ let c=document.querySelector('link[rel=canonical]'); if(!c){c=document.createElement('link');c.rel='canonical';document.head.appendChild(c);} c.href=cfg.collectionUrl; }
  if(cfg.slug) document.body.dataset.line=cfg.slug;
  if(cfg.palette){ const rs=document.documentElement.style, pl=cfg.palette; ['bg','paper','ink','accent','gold','line','taupe'].forEach(k=>{ if(pl[k]) rs.setProperty('--'+k, pl[k]); }); }
  if(cfg.booksHeading) $('#booksHeading').textContent=cfg.booksHeading;
  if(cfg.collectionUrl){ const sb=$('#shopbar'); if(sb)sb.href=cfg.collectionUrl; const fg=$('#footGateLink'); if(fg)fg.href=cfg.collectionUrl; }
  renderShare(cfg.collectionUrl||cfg.storeBase, cfg.title||'The Quadrille House | Designer Wallcoverings');
  // JSON-LD CollectionPage
  const ld={"@context":"https://schema.org","@type":"CollectionPage",name:cfg.title,description:cfg.metaDescription,url:cfg.collectionUrl};
  const s=document.createElement('script');s.type='application/ld+json';s.textContent=JSON.stringify(ld);document.head.appendChild(s);
}

function renderShare(url, title){
  const el=$('#shareCollection'); if(!el||!url)return;
  const U=encodeURIComponent(url), T=encodeURIComponent(title);
  const ic={
    pin:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12 2C6.5 2 4 5.8 4 8.9c0 1.9.7 3.6 2.3 4.2.3.1.5 0 .6-.3l.2-.9c.1-.3 0-.4-.2-.6-.4-.5-.7-1.2-.7-2.1 0-2.7 2-5.1 5.3-5.1 2.9 0 4.5 1.8 4.5 4.1 0 3.1-1.4 5.7-3.4 5.7-1.1 0-1.9-.9-1.7-2 .3-1.3.9-2.7.9-3.6 0-.8-.4-1.5-1.4-1.5-1.1 0-2 1.1-2 2.7 0 1 .3 1.6.3 1.6l-1.4 5.7c-.4 1.6 0 3.7 0 3.9 0 .1.2.2.3.1.1-.1 1.6-2 2.1-3.8l.8-3c.4.8 1.5 1.4 2.7 1.4 3.6 0 6-3.3 6-7.6C20.4 5 17 2 12 2z"/></svg>',
    fb:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M22 12a10 10 0 10-11.6 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.2c-1.2 0-1.6.8-1.6 1.6V12h2.7l-.4 2.9h-2.3v7A10 10 0 0022 12z"/></svg>',
    x:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M18.9 2H22l-7.3 8.3L23.3 22h-6.8l-5.3-7-6.1 7H2l7.8-8.9L1.2 2H8l4.8 6.3L18.9 2zm-1.2 18h1.9L7.1 4H5.1l12.6 16z"/></svg>',
    mail:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm8 7L4 6.5V18h16V6.5L12 11z"/></svg>',
    link:'<svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor"><path d="M10.6 13.4a1 1 0 001.4 0l4-4a3 3 0 00-4.2-4.2l-1.3 1.3a1 1 0 101.4 1.4l1.3-1.3a1 1 0 011.4 1.4l-4 4a1 1 0 000 1.4zm2.8-2.8a1 1 0 00-1.4 0l-4 4a3 3 0 004.2 4.2l1.3-1.3a1 1 0 10-1.4-1.4l-1.3 1.3a1 1 0 01-1.4-1.4l4-4a1 1 0 000-1.4z"/></svg>',
  };
  el.innerHTML=`<span class="share-lbl">Share</span>
    <a class="sbtn pin" title="Pin on Pinterest" target="_blank" rel="noopener" href="https://pinterest.com/pin/create/button/?url=${U}&description=${T}">${ic.pin}</a>
    <a class="sbtn fb" title="Share on Facebook" target="_blank" rel="noopener" href="https://www.facebook.com/sharer/sharer.php?u=${U}">${ic.fb}</a>
    <a class="sbtn x" title="Share on X" target="_blank" rel="noopener" href="https://twitter.com/intent/tweet?url=${U}&text=${T}">${ic.x}</a>
    <a class="sbtn mail" title="Email" href="mailto:?subject=${T}&body=${T}%20${U}">${ic.mail}</a>
    <button class="sbtn copy" title="Copy link" type="button">${ic.link}</button>`;
  el.querySelector('.copy').addEventListener('click',async()=>{
    const b=el.querySelector('.copy');
    try{ if(navigator.share){await navigator.share({title,url});return;}
      await navigator.clipboard.writeText(url); b.classList.add('done'); b.title='Copied!';
      setTimeout(()=>{b.classList.remove('done');b.title='Copy link';},1600);
    }catch(e){}
  });
}

// Load (or reload) a brand's products + facets
async function loadBrand(){
  const q = activeBrand==='all' ? '' : '?brand='+encodeURIComponent(activeBrand);
  const [pd,fc]=await Promise.all([
    fetch(location.origin+'/api/products'+q).then(r=>r.json()),
    fetch(location.origin+'/api/facets'+q).then(r=>r.json())]);
  ALL=pd.products; FACETS=fc;
  // collection-aware shopbar
  const sb=$('#shopbar'); if(sb) sb.innerHTML=`Shop ${activeBrand==='all'?'the Quadrille house':activeBrand} at <b>Designer Wallcoverings</b> →`;
  brandChips(); colorbar(fc); fillPatterns(fc); render(); applyAbout();
}

(async()=>{
  const savedSort=localStorage.getItem(LS.sort); if(savedSort)$('#sort').value=savedSort;
  const savedDens=localStorage.getItem(LS.dens)||'4'; $('#density').value=savedDens; applyDensity(savedDens);
  const savedBrand=localStorage.getItem(LS.brand); if(savedBrand)activeBrand=savedBrand;
  applyView();
  const cfg=await fetch(location.origin+'/api/config').then(r=>r.json());
  applyConfig(cfg);
  await loadBrand();
  startHero(ALL.length?ALL:[]);
  loadLookbook();
})();
</script>
</body>
</html>