← 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 & Paper · Suncloth · Plains · Grasscloth</div>
<div class="hero-kicker" id="heroKicker">Hand-screened American textiles & 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 & 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 & 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,'&').replace(/</g,'<').replace(/"/g,'"');
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,'"')} — ${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,'"')}" 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>