← back to Atomic50 Onboard

public/index.html

267 lines

<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Atomic 50 — A Designer Wallcoverings Collection</title>
<meta name="description" content="Atomic 50 pressed-tin ceilings & wall panels — a Designer Wallcoverings collection.">
<link rel="canonical" href="https://atomic50.designerwallcoverings.com/">
<link rel="icon" href="/favicon.ico">
<style>
:root{
 --bg:#efece6;--paper:#fbf9f4;--ink:#20211d;--accent:#7a6a4c;--gold:#9a7c3f;--line:#ddd6c9;--taupe:#a89e8a;
 --cols:4;--fs:1;
 --white:#f4f4f2;--grey:#9aa0aa;--black:#1c1e24;--orange:#e08a3c;--gold-c:#c9a75a;--green:#5f8f5c;--blue:#4f7fb5;--purple:#8a6fb5;--pink:#f3a8c0;--red:#d0453e;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
 font:15px/1.55 "Georgia","Times New Roman",serif}
a{color:inherit}
.sans{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
/* ── house chrome ── */
header.house{position:sticky;top:0;z-index:20;background:var(--paper);border-bottom:1px solid var(--line);
 display:flex;align-items:center;justify-content:space-between;padding:14px 26px}
.wordmark{font-size:15px;font-weight:700;letter-spacing:.14em;text-transform:uppercase}
.wordmark small{display:block;font-size:9px;letter-spacing:.24em;color:var(--taupe);font-weight:600}
nav.house{display:flex;gap:22px;font-size:12px;letter-spacing:.06em;text-transform:uppercase}
nav.house a{text-decoration:none;color:var(--ink);opacity:.8}
nav.house a:hover{opacity:1;color:var(--accent)}
/* ── hero ── */
.hero{position:relative;min-height:52vh;display:flex;align-items:flex-end;
 background:#20211d center/cover no-repeat;color:#fff;padding:48px 30px 40px}
.hero::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,#0000 30%,#000000c9);z-index:0}
.hero .hz{position:relative;z-index:1;max-width:760px}
.hero .eyebrow{font-size:11px;letter-spacing:.28em;text-transform:uppercase;opacity:.9;margin:0 0 14px}
.hero h1{font-size:clamp(30px,5vw,54px);line-height:1.04;margin:0 0 12px;font-weight:400}
.hero .kicker{font-size:12px;letter-spacing:.18em;text-transform:uppercase;opacity:.85;margin:0 0 10px}
.hero .tag{font-size:17px;max-width:600px;opacity:.95;margin:0}
/* ── about ── */
.about{background:var(--paper);border-bottom:1px solid var(--line);padding:34px 30px}
.about .in{max-width:820px;margin:0 auto}
.about p{margin:0 0 14px;color:#3a3a34}
.about .collab{font-style:italic;color:var(--accent);border-top:1px solid var(--line);padding-top:14px;margin-top:6px}
/* ── controls bar ── */
.bar{position:sticky;top:53px;z-index:15;background:var(--bg);border-bottom:1px solid var(--line);
 display:flex;align-items:center;gap:18px;flex-wrap:wrap;padding:12px 26px}
.bar .count{font-family:-apple-system,sans-serif;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--taupe)}
.ctl{display:flex;align-items:center;gap:8px;font-family:-apple-system,sans-serif;font-size:12px}
.ctl label{letter-spacing:.06em;text-transform:uppercase;color:var(--taupe)}
select,input[type=text]{background:var(--paper);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:6px 9px;font-size:13px;font-family:-apple-system,sans-serif}
input[type=range]{accent-color:var(--accent)}
.chipf{display:flex;gap:6px;flex-wrap:wrap}
.chipf button{background:var(--paper);border:1px solid var(--line);border-radius:999px;padding:4px 12px;font-size:12px;
 font-family:-apple-system,sans-serif;cursor:pointer;color:#4a463c;letter-spacing:.02em}
.chipf button:hover{border-color:var(--accent)}
.chipf button.on{background:var(--ink);color:#fff;border-color:var(--ink)}
.chipf button .dot{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:5px;vertical-align:middle;border:1px solid #0003}
/* ── grid ── */
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px;padding:22px 26px 70px;max-width:1500px;margin:0 auto}
.card{background:var(--paper);border:1px solid var(--line);border-radius:3px;overflow:hidden;
 display:flex;flex-direction:column;cursor:pointer;transition:box-shadow .15s,border-color .15s}
.card:hover{border-color:var(--accent);box-shadow:0 6px 22px #0000001a}
.imgwrap{position:relative;width:100%;aspect-ratio:1/1;background:#e6e1d8}
.imgwrap img{width:100%;height:100%;object-fit:cover;display:block}
.imgwrap img.spin{position:absolute;inset:0;opacity:0;transition:opacity .2s}
.imgwrap:hover img.spin{opacity:1}
.imgwrap .spinbadge{position:absolute;top:8px;right:8px;background:#000a;color:#fff;font-size:9px;font-weight:700;
 border-radius:4px;padding:2px 7px;letter-spacing:.06em;font-family:-apple-system,sans-serif;z-index:2}
/* ── finish-swatch hover gallery ── */
.imgwrap .finishbadge{position:absolute;top:8px;left:8px;background:#000a;color:#fff;font-size:9px;font-weight:700;
 border-radius:4px;padding:2px 7px;letter-spacing:.06em;font-family:-apple-system,sans-serif;z-index:3;pointer-events:none}
.imgwrap .dots{position:absolute;left:0;right:0;bottom:7px;display:flex;justify-content:center;gap:5px;z-index:3;pointer-events:none}
.imgwrap .dots i{width:6px;height:6px;border-radius:50%;background:#fff;opacity:.45;box-shadow:0 0 0 1px #0006;transition:opacity .15s,transform .15s}
.imgwrap .dots i.on{opacity:1;transform:scale(1.25)}
.imgwrap.multi{cursor:pointer}
.imgwrap.noimg{background:repeating-linear-gradient(45deg,#e6e1d8,#e6e1d8 8px,#ded8cc 8px,#ded8cc 16px)}
.imgwrap.noimg::after{content:'no image';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
 color:var(--taupe);font-size:12px;font-family:-apple-system,sans-serif}
.card .b{padding:calc(11px*var(--fs)) calc(13px*var(--fs)) calc(13px*var(--fs));display:flex;flex-direction:column;gap:calc(4px*var(--fs))}
.card .kind{font-family:-apple-system,sans-serif;font-size:calc(9.5px*var(--fs));letter-spacing:.12em;text-transform:uppercase;color:var(--gold)}
.card .name{font-size:calc(16px*var(--fs));line-height:1.25}
.card .sub{font-family:-apple-system,sans-serif;font-size:calc(11px*var(--fs));color:var(--taupe)}
.card .cta{font-family:-apple-system,sans-serif;font-size:calc(11px*var(--fs));letter-spacing:.08em;text-transform:uppercase;color:var(--accent);margin-top:2px}
#empty{padding:70px;text-align:center;color:var(--taupe);font-family:-apple-system,sans-serif}
footer.house{background:var(--ink);color:#cfc9bb;padding:34px 30px;font-family:-apple-system,sans-serif;font-size:12.5px;text-align:center;letter-spacing:.04em}
footer.house a{color:#fff;text-decoration:none;border-bottom:1px solid #ffffff40}
@media(max-width:820px){.grid{grid-template-columns:repeat(2,1fr)}.ctl.dens{display:none}nav.house{display:none}}
</style></head>
<body>
<header class="house">
  <a href="/" style="text-decoration:none"><span class="wordmark" id="wm">Designer Wallcoverings<small id="wmsub">A Designer Wallcoverings Collection</small></span></a>
  <nav class="house" id="nav"></nav>
</header>

<section class="hero" id="hero">
  <div class="hz">
    <p class="eyebrow" id="eyebrow"></p>
    <h1 id="htitle"></h1>
    <p class="kicker" id="kicker"></p>
    <p class="tag" id="tag"></p>
  </div>
</section>

<section class="about" id="aboutsec"><div class="in" id="about"></div></section>

<div class="bar" id="collection">
  <span class="count" id="count">…</span>
  <div class="ctl"><label>Sort</label>
    <select id="sort">
      <option value="newest">Newest</option>
      <option value="color">Color</option>
      <option value="style">Style</option>
      <option value="type">Type</option>
      <option value="pattern">Pattern A→Z</option>
      <option value="sku">SKU A→Z</option>
      <option value="name">Name A→Z</option>
    </select></div>
  <div class="ctl dens"><label>Density</label>
    <input type="range" id="density" min="2" max="8" step="1"></div>
  <input type="text" id="q" placeholder="Search pattern / sku…" style="min-width:180px">
  <div class="chipf" id="typef"></div>
  <div class="chipf" id="colorf"></div>
</div>

<div class="grid" id="grid"></div>
<div id="empty" style="display:none">No products match.</div>

<footer class="house" id="footer"></footer>

<script>
const $=s=>document.querySelector(s);
const grid=$('#grid');
let ALL=[],CFG=null,FACETS=null,VIEW=[];
let FTYPE='',FCOLOR='';
const BUCKET={white:'#f4f4f2',grey:'#9aa0aa',black:'#1c1e24',orange:'#e08a3c',gold:'#c9a75a',green:'#5f8f5c',blue:'#4f7fb5',purple:'#8a6fb5',pink:'#f3a8c0',red:'#d0453e'};
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}

// persisted prefs (CLAUDE.md: sort + density persist)
$('#sort').value=localStorage.getItem('a50.sort')||'newest';
function setDensity(v){v=Number(v);
 const fs=Math.max(.72,Math.min(1.25, v<=4 ? 1+(4-v)*.08 : 1-(v-4)*.055));
 document.documentElement.style.setProperty('--cols',v);
 document.documentElement.style.setProperty('--fs',fs.toFixed(3));}
const dens=localStorage.getItem('a50.density')||'4';
$('#density').value=dens;setDensity(dens);
$('#density').oninput=()=>{setDensity($('#density').value);localStorage.setItem('a50.density',$('#density').value);};

// finish-swatch set for a product: capped `swatches` from the feed, else the
// single swatch. De-dupe defensively; cap to 12 so the cycle stays snappy.
function finishSet(p){
 const raw=(Array.isArray(p.swatches)&&p.swatches.length?p.swatches:(p.swatch?[p.swatch]:[]));
 const out=[];for(const u of raw){if(u&&!out.includes(u)){out.push(u);if(out.length===12)break;}}
 return out;
}
function card(p){
 const name=esc(p.display_name||p.sku||'(untitled)');
 const set=finishSet(p);
 if(!set.length) return `<div class="card" data-href="/product/${encodeURIComponent(p.handle)}">
   <div class="imgwrap noimg"></div><div class="b">
   <span class="kind">${esc(p.product_type==='Architectural Wallcoverings'?(p.collection||'Pressed Tin'):p.product_type)}</span>
   <div class="name">${name}</div><span class="sub">${esc(p.finish||'')}</span>
   <span class="cta">Request a Quote →</span></div></div>`;
 const multi=set.length>1;
 const dots=multi?`<div class="dots">${set.map((_,i)=>`<i class="${i===0?'on':''}"></i>`).join('')}</div>`:'';
 const badge=multi?`<span class="finishbadge">${set.length} FINISHES</span>`:'';
 return `<div class="card" data-href="/product/${encodeURIComponent(p.handle)}">
   <div class="imgwrap${multi?' multi':''}" data-swatches='${esc(JSON.stringify(set))}' data-i="0">
     <img loading="lazy" src="${esc(set[0])}" alt="${name}" onerror="this.style.display='none';this.parentNode.classList.add('noimg')">
     ${badge}${dots}
   </div><div class="b">
   <span class="kind">${esc(p.product_type==='Architectural Wallcoverings'?(p.collection||'Pressed Tin'):p.product_type)}</span>
   <div class="name">${name}</div>
   <span class="sub">${esc(p.finish||'')}</span>
   <span class="cta">${multi?'Cycle finishes · ':''}Request a Quote →</span>
 </div></div>`;
}

// ── swatch-cycle wiring ──
// Desktop: mouse-move across the image maps X-position → finish index (scrub).
// Touch: tap the image advances to the next finish (and never navigates).
// Non-image areas / non-multi cards navigate to the PDP as before.
const isTouch=matchMedia('(hover:none),(pointer:coarse)').matches;
function setSwatch(wrap,i){
 let set;try{set=JSON.parse(wrap.dataset.swatches||'[]');}catch{set=[];}
 if(!set.length)return;
 i=((i%set.length)+set.length)%set.length;
 if(String(i)===wrap.dataset.i)return;
 wrap.dataset.i=String(i);
 const img=wrap.querySelector('img');if(img){img.src=set[i];img.style.display='';wrap.classList.remove('noimg');}
 const dots=wrap.querySelectorAll('.dots i');dots.forEach((d,k)=>d.classList.toggle('on',k===i));
}
grid.addEventListener('mousemove',e=>{
 if(isTouch)return;
 const wrap=e.target.closest('.imgwrap.multi');if(!wrap)return;
 const r=wrap.getBoundingClientRect();let set;try{set=JSON.parse(wrap.dataset.swatches||'[]');}catch{set=[];}
 if(!set.length)return;
 const frac=Math.min(.999,Math.max(0,(e.clientX-r.left)/r.width));
 setSwatch(wrap,Math.floor(frac*set.length));
});
grid.addEventListener('mouseleave',e=>{
 const wrap=e.target&&e.target.closest?e.target.closest('.imgwrap.multi'):null;if(wrap)setSwatch(wrap,0);
},true);
grid.addEventListener('click',e=>{
 const wrap=e.target.closest('.imgwrap.multi');
 if(isTouch&&wrap){ // tap-to-advance on touch — do NOT navigate
   e.preventDefault();setSwatch(wrap,(Number(wrap.dataset.i)||0)+1);return;
 }
 const c=e.target.closest('.card[data-href]');if(c)location.href=c.dataset.href;
});

function apply(){
 const q=$('#q').value.trim().toLowerCase();
 VIEW=ALL.filter(p=>{
  if(FTYPE&&String(p.product_type||'')!==FTYPE)return false;
  if(FCOLOR&&String(p.color_bucket||'')!==FCOLOR)return false;
  if(q&&![p.display_name,p.sku,p.pattern,p.style,p.collection].some(v=>v&&String(v).toLowerCase().includes(q)))return false;
  return true;
 });
 const s=$('#sort').value,cmp={
  newest:(a,b)=>new Date(b.created_at||0)-new Date(a.created_at||0),
  color:(a,b)=>(a.hue??999)-(b.hue??999),
  style:(a,b)=>String(a.style||'~').localeCompare(String(b.style||'~')),
  type:(a,b)=>String(a.product_type||'').localeCompare(String(b.product_type||'')),
  pattern:(a,b)=>String(a.pattern||'~').localeCompare(String(b.pattern||'~')),
  sku:(a,b)=>String(a.sku||a.handle).localeCompare(String(b.sku||b.handle),undefined,{numeric:true}),
  name:(a,b)=>String(a.display_name||'').localeCompare(String(b.display_name||''),undefined,{numeric:true}),
 }[s];
 if(cmp)VIEW=[...VIEW].sort(cmp);
 grid.innerHTML=VIEW.map(card).join('');
 $('#count').textContent=VIEW.length+' of '+ALL.length+' designs';
 $('#empty').style.display=VIEW.length?'none':'block';
}
$('#sort').onchange=()=>{localStorage.setItem('a50.sort',$('#sort').value);apply();};
let t;$('#q').oninput=()=>{clearTimeout(t);t=setTimeout(apply,250);};

function renderChips(){
 $('#typef').innerHTML=(FACETS.types||[]).map(o=>
  `<button data-t="${esc(o.value)}" class="${FTYPE===o.value?'on':''}">${esc(o.value)} · ${o.count}</button>`).join('');
 $('#colorf').innerHTML=(FACETS.colors||[]).map(o=>
  `<button data-c="${esc(o.value)}" class="${FCOLOR===o.value?'on':''}"><span class="dot" style="background:${BUCKET[o.value]||'#999'}"></span>${esc(o.value)} · ${o.count}</button>`).join('');
 $('#typef').querySelectorAll('button').forEach(b=>b.onclick=()=>{FTYPE=FTYPE===b.dataset.t?'':b.dataset.t;renderChips();apply();});
 $('#colorf').querySelectorAll('button').forEach(b=>b.onclick=()=>{FCOLOR=FCOLOR===b.dataset.c?'':b.dataset.c;renderChips();apply();});
}

async function boot(){
 const [cfg,pd,fc]=await Promise.all([
  fetch(location.origin+'/api/config').then(r=>r.json()),
  fetch(location.origin+'/api/products').then(r=>r.json()),
  fetch(location.origin+'/api/facets').then(r=>r.json())]);
 CFG=cfg;ALL=pd.products;FACETS=fc;
 document.title=cfg.title;
 $('#wm').firstChild.textContent=cfg.wordmark;
 $('#wmsub').textContent=cfg.eyebrow;
 $('#eyebrow').textContent=cfg.eyebrow;
 $('#htitle').textContent=cfg.line;
 $('#kicker').textContent=cfg.kicker;
 $('#tag').textContent=cfg.tagline;
 $('#nav').innerHTML=(cfg.nav||[]).map(n=>`<a href="${esc(n.href)}">${esc(n.label)}</a>`).join('');
 $('#about').innerHTML=(cfg.about.paragraphs||[]).map(p=>`<p>${esc(p)}</p>`).join('')
   +`<p class="collab">${esc(cfg.about.collab)}</p>`;
 // hero image = first product with a swatch
 const heroP=ALL.find(p=>p.swatch);
 if(heroP)$('#hero').style.backgroundImage=`url("${heroP.swatch}")`;
 $('#footer').innerHTML=`Atomic 50 — a collection of <a href="${esc(cfg.houseUrl)}">Designer Wallcoverings</a>. `
   +`Pressed-tin ceilings & wall panels, to the trade. Request a quote or order a $${cfg.samplePrice.toFixed(2)} sample on any pattern.`;
 renderChips();apply();
}
boot();
</script>
</body></html>