← back to Greenland Onboard

public/index.html

500 lines

<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230c0d12'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%235eead4'%3E%F0%9F%97%84%3C/text%3E%3C/svg%3E">
<title>Cork · Phillipe Romano · Internal Tool</title>
<style>
:root{--cols:6;--fs:1;--bg:#0c0d12;--panel:#15171f;--card:#12141b;--ink:#eef0f5;--mut:#9aa0b0;--acc:#5eead4;--line:#262a36;
 --gold:#c9a75a;--ok:#4ea86b;--warn:#d9a441;--bad:#d0453e;--pricebg:#101c1a}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
header{position:sticky;top:0;z-index:9;background:linear-gradient(180deg,#0c0d12,#0c0d12e8);backdrop-filter:blur(8px);
 border-bottom:1px solid var(--line);padding:10px 16px;display:flex;gap:12px;align-items:center;flex-wrap:wrap}
h1{font-size:15px;margin:0;font-weight:700;letter-spacing:.2px}
h1 b{color:var(--acc)}
h1 .int{color:var(--gold);font-size:10px;font-weight:700;letter-spacing:.8px;border:1px solid var(--gold);border-radius:4px;padding:1px 6px;margin-left:4px;vertical-align:middle}
.pill{background:var(--acc);color:#052a24;font-weight:700;border-radius:999px;padding:2px 10px;font-size:12px}
.muted{color:var(--mut)}
select,input[type=text]{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:6px 9px;font-size:13px}
.ctl{display:flex;align-items:center;gap:7px}
.vt{background:var(--card,#14161c);border:1px solid var(--hair,#2a2d36);color:var(--mut,#9aa0aa);border-radius:7px;padding:5px 11px;cursor:pointer;font-size:13px}
.vt:hover{border-color:var(--acc)}
.vt.active{background:var(--acc);color:#052a24;border-color:var(--acc);font-weight:700}
input[type=range]{accent-color:var(--acc)}
.btn{background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:6px 12px;cursor:pointer;font-size:13px;text-decoration:none}
.btn:hover{border-color:var(--acc)}
#layout{display:flex;align-items:flex-start}
aside{position:sticky;width:238px;min-width:238px;max-height:calc(100vh - 52px);overflow:auto;
 padding:10px 10px 28px;border-right:1px solid var(--line);background:#0e0f15}
aside input[type=text]{width:100%;margin-bottom:7px;font-size:12.5px;padding:5px 8px}
aside details{border-bottom:1px solid var(--line)}
aside summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px;padding:8px 4px;
 font-size:10.5px;font-weight:700;color:var(--mut);text-transform:uppercase;letter-spacing:.6px;user-select:none}
aside summary::-webkit-details-marker{display:none}
aside summary::before{content:'▸';font-size:9px;color:var(--mut);transition:transform .12s}
aside details[open] summary::before{transform:rotate(90deg)}
aside summary:hover{color:var(--ink)}
aside summary .cur{margin-left:auto;color:var(--acc);text-transform:none;letter-spacing:0;font-weight:600;
 max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fbody{padding:2px 0 8px}
.frow{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:3px 8px;border-radius:6px;
 cursor:pointer;color:var(--mut);font-size:12.5px;user-select:none}
.frow:hover{background:var(--panel);color:var(--ink)}
.frow.active{background:var(--panel);color:var(--ink);outline:1px solid var(--acc);outline-offset:-1px}
.frow .fl{display:inline-flex;align-items:center;gap:7px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.frow .n{font-variant-numeric:tabular-nums;font-size:11px;color:var(--mut);flex:none}
.frow .dot{width:11px;height:11px;border-radius:50%;border:1px solid #0008;display:inline-block;flex:none}
.fmore{padding:3px 8px;font-size:11px;color:var(--mut);font-style:italic}
main{flex:1;min-width:0}
#burger{display:none;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:6px 12px;cursor:pointer;font-size:13px;font-weight:600;white-space:nowrap}
#fclose{display:none}
@media(max-width:880px){
 #layout{display:block}
 header{padding:8px 10px;gap:8px}
 #burger{display:inline-flex;align-items:center;gap:6px}
 .ctl:has(#density){display:none}
 aside{display:none;position:fixed;inset:0;top:0!important;z-index:40;width:auto;min-width:0;max-height:none!important;
  overflow:auto;border-right:0;padding:12px 12px 40px;background:var(--bg)}
 aside.open-mobile{display:block}
 #fclose{display:block;position:sticky;top:0;z-index:2;width:100%;background:var(--acc);color:#062a24;border:none;border-radius:8px;padding:11px;font-size:14px;font-weight:700;cursor:pointer;margin-bottom:10px}
 .grid{grid-template-columns:repeat(2,1fr)!important}
 #drawer{width:100%!important}
}
/* ── grid + cards ── */
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:8px;padding:10px 12px 60px}
.card{background:var(--card);border:1px solid var(--line);border-radius:2px;overflow:hidden;display:flex;flex-direction:column;position:relative;cursor:pointer}
.card:hover{border-color:#414a5e}
.imgwrap{position:relative;width:100%;aspect-ratio:1/1;background:#0a0b0f}
.imgwrap img{width:100%;height:100%;object-fit:cover;object-position:center;display:block}
.imgwrap.noimg{background:repeating-linear-gradient(45deg,#0a0b0f,#0a0b0f 8px,#101218 8px,#101218 16px)}
.imgwrap.noimg::after{content:'no image';position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#5b6373;font-size:11px;font-weight:600}
.stockbadge{position:absolute;top:6px;left:6px;font-size:9.5px;font-weight:700;letter-spacing:.3px;border-radius:999px;padding:2px 7px;background:#0b0d10cc;border:1px solid var(--line);backdrop-filter:blur(3px)}
.stockbadge.in{color:var(--ok);border-color:#2c5a3e}
.stockbadge.un{color:var(--warn);border-color:#5a4a2c}
.pricebadge{position:absolute;top:6px;right:6px;font-size:10px;font-weight:700;border-radius:999px;padding:2px 8px;background:#0b0d10cc;color:var(--acc);border:1px solid #2c5a54;backdrop-filter:blur(3px)}
.card .b{padding:calc(6px*var(--fs)) calc(8px*var(--fs)) calc(7px*var(--fs));display:flex;flex-direction:column;gap:calc(3px*var(--fs))}
.vend{font-size:calc(10px*var(--fs));color:var(--gold);font-weight:600;letter-spacing:.2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ttl{font-weight:600;font-size:calc(12.5px*var(--fs));line-height:1.3;letter-spacing:-.1px}
.sku{font-family:ui-monospace,Menlo,monospace;font-size:calc(11px*var(--fs));color:var(--mut)}
.skudw{color:var(--acc)}
.when{font-size:calc(9.5px*var(--fs));color:var(--mut)}
.skuline{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
.specrow{display:flex;flex-wrap:wrap;gap:4px 10px;font-size:calc(10.5px*var(--fs));color:var(--mut)}
.specrow b{color:#c4cad6;font-weight:600}
.chips{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
.chip{font-size:calc(10px*var(--fs));font-weight:600;border-radius:999px;padding:1px calc(8px*var(--fs));letter-spacing:.2px;white-space:nowrap;border:1px solid transparent}
.chip.cl{border-color:transparent;color:var(--mut);background:transparent;display:inline-flex;align-items:center;gap:5px;padding-left:0}
.chip.cl .dot{width:calc(10px*var(--fs));height:calc(10px*var(--fs));border-radius:50%;border:1px solid #0008;display:inline-block}
.chip.city{color:var(--gold);border-color:#4a3f24;background:#1a160c}
/* card-field visibility toggles → down to image-only */
body.f-off-vend .grid .vend{display:none}
body.f-off-ttl .grid .ttl{display:none}
body.f-off-sku .grid .skuline{display:none}
body.f-off-spec .grid .specrow{display:none}
body.f-off-chips .grid .chips{display:none}
body.f-off-price .grid .pricebadge{display:none}
body.f-off-stock .grid .stockbadge{display:none}
body.imgonly .grid .card .b{display:none}
#more{display:block;margin:8px auto 60px;background:var(--panel);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:9px 18px;cursor:pointer}
#clearf{display:none;margin-top:10px;width:100%}
#empty{padding:60px;text-align:center;color:var(--mut)}
/* ── detail drawer ── */
#scrim{position:fixed;inset:0;background:#000a;z-index:50;display:none}
#scrim.open{display:block}
#drawer{position:fixed;top:0;right:0;height:100vh;width:520px;max-width:100vw;background:#0e0f15;border-left:1px solid var(--line);
 z-index:51;transform:translateX(100%);transition:transform .18s ease;overflow:auto;display:flex;flex-direction:column}
#drawer.open{transform:translateX(0)}
#drawer .dhead{position:sticky;top:0;background:#0e0f15e8;backdrop-filter:blur(6px);border-bottom:1px solid var(--line);
 padding:12px 16px;display:flex;align-items:center;gap:10px;z-index:2}
#drawer .dhead h2{margin:0;font-size:15px;font-weight:700;flex:1;min-width:0}
#drawer .dhead h2 .city{color:var(--gold);font-weight:600}
#dclose{background:none;border:1px solid var(--line);color:var(--ink);border-radius:8px;width:30px;height:30px;cursor:pointer;font-size:16px;flex:none}
#dclose:hover{border-color:var(--acc)}
#drawer .dbody{padding:14px 16px 40px;display:flex;flex-direction:column;gap:16px}
#drawer .dimg{width:100%;border-radius:6px;border:1px solid var(--line);aspect-ratio:1/1;object-fit:cover;background:#0a0b0f}
.thumbs{display:flex;gap:6px;flex-wrap:wrap}
.sec{border:1px solid var(--line);border-radius:10px;padding:12px 14px;background:var(--card)}
.sec h3{margin:0 0 9px;font-size:10.5px;font-weight:700;color:var(--mut);text-transform:uppercase;letter-spacing:.7px}
.kv{display:grid;grid-template-columns:120px 1fr;gap:5px 12px;font-size:12.5px;align-items:baseline}
.kv .k{color:var(--mut)}
.kv .v{font-weight:500;word-break:break-word}
.kv .v.mono{font-family:ui-monospace,Menlo,monospace}
.kv .v.dw{color:var(--acc);font-family:ui-monospace,Menlo,monospace}
/* pricing card */
.pcard{background:var(--pricebg);border:1px solid #2c5a54}
.prow{display:flex;justify-content:space-between;align-items:baseline;padding:6px 0;border-bottom:1px dashed #223a37;font-size:13px}
.prow:last-child{border-bottom:none}
.prow .pl{color:var(--mut)}
.prow .pv{font-weight:700;font-variant-numeric:tabular-nums}
.prow .pv.big{font-size:18px;color:var(--acc)}
.prow .pv.unk{color:var(--warn);font-weight:600;font-size:12px}
.pnote{font-size:11px;color:var(--mut);margin-top:8px;line-height:1.5}
.pnote b{color:var(--warn)}
/* stock card */
.scard .stat{display:flex;align-items:center;gap:9px;margin-bottom:8px}
.dotbig{width:11px;height:11px;border-radius:50%;flex:none}
.dotbig.in{background:var(--ok);box-shadow:0 0 8px #4ea86b88}
.dotbig.un{background:var(--warn)}
.dotbig.chk{background:var(--acc);animation:pulse 1s infinite}
@keyframes pulse{50%{opacity:.4}}
.stat .lbl{font-weight:700}
.stat .sub{color:var(--mut);font-size:11.5px}
.acts{display:flex;flex-wrap:wrap;gap:7px;margin-top:4px}
.cardacts{padding:0 calc(8px*var(--fs,1)) calc(7px*var(--fs,1))}
#toast{position:fixed;left:50%;bottom:18px;transform:translateX(-50%) translateY(20px);max-width:min(560px,92vw);background:#1b1e24;color:var(--txt,#e8e6e1);border:1px solid var(--line,#2a2d33);border-radius:8px;padding:10px 16px;font-size:13px;box-shadow:0 8px 30px #0008;opacity:0;pointer-events:none;transition:.25s;z-index:60}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast.ok{border-color:#2a6a4a}#toast.err{border-color:#7a2a2a}
.act{font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:8px;padding:6px 12px;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px}
.act:hover{border-color:var(--acc)}
.act.go{border-color:#2c5a54;color:var(--acc)}
.pal{display:flex;gap:0;border-radius:6px;overflow:hidden;border:1px solid var(--line);height:26px}
.pal span{flex:1;position:relative}
.pal span::after{content:attr(data-pct);position:absolute;bottom:1px;left:0;right:0;text-align:center;font-size:8px;color:#000a;font-weight:700}
</style></head>
<body>
<header>
  <h1><b>Phillipe Romano</b> · Cork<span class="int">INTERNAL</span></h1>
  <button id="burger" type="button" aria-label="Open filters">☰ Filters</button>
  <span class="pill" id="total">…</span>
  <span class="muted" id="meta"></span>
  <div class="ctl" id="viewtoggle" style="gap:4px">
    <button id="tab-products" type="button" class="vt active">Products</button>
    <button id="tab-calendar" type="button" class="vt">🗓 Activation Calendar</button>
  </div>
  <div class="ctl"><label class="muted">Sort</label>
    <select id="sort">
      <option value="newest">Newest</option>
      <option value="dw">DW SKU A→Z</option>
      <option value="color">Color (hue)</option>
      <option value="city">City / Style</option>
      <option value="pattern">Pattern A→Z</option>
      <option value="sku">Mfr SKU A→Z</option>
    </select></div>
  <div class="ctl"><label class="muted">Density</label>
    <input type="range" id="density" min="3" max="18" step="1"></div>
</header>
<div id="layout">
<aside aria-label="filters">
  <button id="fclose" type="button">✕ Done — show products</button>
  <input type="text" id="q" placeholder="Search DW SKU / mfr code / color / city…">
  <input type="text" id="pattern" list="patternlist" placeholder="Pattern type-ahead…">
  <datalist id="patternlist"></datalist>
  <details id="fieldsbox"><summary>Card fields</summary>
    <div class="fbody">
      <label class="frow"><span class="fl"><input type="checkbox" data-fv="vend" checked>Brand line</span></label>
      <label class="frow"><span class="fl"><input type="checkbox" data-fv="ttl" checked>City · color</span></label>
      <label class="frow"><span class="fl"><input type="checkbox" data-fv="sku" checked>DW SKU · mfr code</span></label>
      <label class="frow"><span class="fl"><input type="checkbox" data-fv="spec" checked>Spec line</span></label>
      <label class="frow"><span class="fl"><input type="checkbox" data-fv="chips" checked>Hue · date chips</span></label>
      <label class="frow"><span class="fl"><input type="checkbox" data-fv="price" checked>Price badge</span></label>
      <label class="frow"><span class="fl"><input type="checkbox" data-fv="stock" checked>Stock badge</span></label>
    </div>
  </details>
  <div id="facetbox"></div>
  <button class="btn" id="clearf">✕ Clear filters</button>
</aside>
<main>
  <div class="grid" id="grid"></div>
  <button id="more" style="display:none">Load more</button>
  <div id="empty" style="display:none">No cork SKUs match.</div>
</main>
</div>

<div id="view-calendar" style="display:none"></div>
<div id="scrim"></div>
<aside id="drawer" aria-label="product detail" aria-hidden="true">
  <div class="dhead"><h2 id="dtitle">…</h2><button id="dclose" aria-label="Close">✕</button></div>
  <div class="dbody" id="dbody"></div>
</aside>

<script src="/activation-calendar.js"></script>
<script>
const $=s=>document.querySelector(s);
const grid=$('#grid');
let ALL=[], VIEW=[], shown=0, META={};
const PAGE=120;
// hue-bucket display colors keyed to enrichment colorFamily values
const FAM_HEX={White:'#f4f4f2',Neutral:'#b3b0a6',Warm:'#c9a75a',Red:'#c0564b',Green:'#5f8f5c',Blue:'#4f7fb5',Black:'#1c1e24',Grey:'#9aa0aa'};
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function fmtDate(s){ if(!s)return''; try{return new Date(s).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return'';} }
function fmt$(n){ return n==null?'—':'$'+Number(n).toFixed(2); }

const FIELDS=[
 ['collection','Collection'],['color_family','Colors'],['material','Material'],['use','Use'],
 ['width','Width'],['fire_rating','Fire Rating'],['backing','Backing'],['composition','Composition'],['city','City / Style'],
];
const FIL={}; FIELDS.forEach(([k])=>FIL[k]='');
let FCOUNTS={};
const FCAP=40;
$('#sort').value=localStorage.getItem('gl.sort')||'newest';

function setDensity(v){
  v=Number(v);
  const fs=Math.max(.55,Math.min(1.2, v<=6 ? 1+(6-v)*.06 : 1-(v-6)*.045));
  document.documentElement.style.setProperty('--cols',v);
  document.documentElement.style.setProperty('--fs',fs.toFixed(3));
}
const dens=localStorage.getItem('gl.density')||'6';
$('#density').value=dens; setDensity(dens);
$('#density').oninput=()=>{setDensity($('#density').value);localStorage.setItem('gl.density',$('#density').value);};

const asideEl=document.querySelector('aside');
$('#burger').onclick=()=>{asideEl.classList.add('open-mobile');document.body.style.overflow='hidden';};
$('#fclose').onclick=()=>{asideEl.classList.remove('open-mobile');document.body.style.overflow='';};
window.matchMedia('(max-width:880px)').addEventListener('change',e=>{ if(!e.matches){asideEl.classList.remove('open-mobile');document.body.style.overflow='';} });

const FV_DEFAULT={vend:true,ttl:true,sku:true,spec:true,chips:true,price:true,stock:true};
const FVIS=Object.assign({},FV_DEFAULT,JSON.parse(localStorage.getItem('gl.fieldvis')||'{}'));
function applyFieldVis(){
  for(const k of Object.keys(FV_DEFAULT))document.body.classList.toggle('f-off-'+k,!FVIS[k]);
  document.body.classList.toggle('imgonly',Object.keys(FV_DEFAULT).every(k=>!FVIS[k]));
  document.querySelectorAll('#fieldsbox input[data-fv]').forEach(cb=>cb.checked=!!FVIS[cb.dataset.fv]);
  localStorage.setItem('gl.fieldvis',JSON.stringify(FVIS));
}
document.querySelectorAll('#fieldsbox input[data-fv]').forEach(cb=>{ cb.onchange=()=>{FVIS[cb.dataset.fv]=cb.checked;applyFieldVis();}; });
applyFieldVis();

function card(p){
  const cityColor=[p.city&&`<span class="city">${esc(p.city)}</span>`,esc(p.color||p.pattern||p.dw_sku)].filter(Boolean).join(' · ');
  const img=p.image
    ?`<div class="imgwrap"><img loading="lazy" src="${esc(p.image)}" alt="${esc(p.title||p.dw_sku)}" onerror="this.style.display='none';this.parentNode.classList.add('noimg')">`
    :`<div class="imgwrap noimg">`;
  const stock=p.in_catalog
    ?`<span class="stockbadge in">● Available</span>`
    :`<span class="stockbadge un">○ Unlisted</span>`;
  const price=`<span class="pricebadge">$${Number(p.retail_per_yard).toFixed(2)}/yd</span>`;
  const dot=FAM_HEX[p.color_family]||p.hex||'#666';
  const spec=[p.width,p.backing,p.fire_rating&&('Fire '+p.fire_rating.split(',')[0])].filter(Boolean)
    .map(s=>`<span>${esc(s)}</span>`).join('');
  return `<div class="card" data-sku="${esc(p.dw_sku)}" data-mfr="${esc(p.mfr_sku||'')}" data-title="${esc(p.color||p.pattern||p.dw_sku||'')}">
    ${img}${stock}${price}</div>
    <div class="b">
    <span class="vend">Phillipe Romano · Cork</span>
    <div class="ttl">${cityColor}</div>
    <div class="skuline">
      <span class="sku skudw" title="DW SKU">${esc(p.dw_sku)}</span>
      <span class="sku" title="Mfr code (internal)">${esc(p.mfr_sku||'')}</span>
    </div>
    <div class="specrow">${spec}</div>
    <div class="chips">
      ${p.color_family?`<span class="chip cl"><span class="dot" style="background:${dot}"></span>${esc(p.color_family)}</span>`:''}
      ${p.city?`<span class="chip city">${esc(p.city)}</span>`:''}
      <span class="when" title="${esc(p.created_at||'')}">🕓 ${esc(fmtDate(p.created_at))}</span>
    </div>
    <div class="acts cardacts">
      <button class="act reqact" data-act="memo" title="Log a memo-sample request">Memo</button>
      <button class="act reqact" data-act="stock" title="Email the vendor a stock check">Stock</button>
      <button class="act reqact" data-act="price" title="Email the vendor for current price">Price</button>
    </div>
    </div>
  </div>`;
}
grid.addEventListener('click',async e=>{
  const rb=e.target.closest('.reqact');
  if(rb){
    e.stopPropagation();
    const card=rb.closest('.card'), type=rb.dataset.act, old=rb.textContent;
    rb.disabled=true; rb.textContent='…';
    try{
      const r=await fetch(location.origin+'/api/request',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({type,dw_sku:card.dataset.sku,mfr_sku:card.dataset.mfr,title:card.dataset.title})}).then(r=>r.json());
      toast(r.ok?(r.message||('Logged '+(r.req_no||''))):(r.error||'request failed'),!!r.ok);
    }catch(err){ toast('request failed: '+err.message,false); }
    rb.disabled=false; rb.textContent=old; return;
  }
  const c=e.target.closest('.card[data-sku]');
  if(c)openDrawer(c.dataset.sku);
});
let toastT=null;
function toast(msg,ok){
  let t=document.getElementById('toast');
  if(!t){ t=document.createElement('div'); t.id='toast'; document.body.appendChild(t); }
  t.textContent=msg; t.className='show '+(ok?'ok':'err');
  clearTimeout(toastT); toastT=setTimeout(()=>t.className='',6000);
}

// ── DETAIL DRAWER ──────────────────────────────────────────────────────────
const SHOPIFY_STORE='https://www.designerwallcoverings.com';
const SHOPIFY_ADMIN='https://admin.shopify.com/store/designer-laboratory-sandbox';
const ALLDW='https://all.designerwallcoverings.com';
function kv(k,v,cls){return v?`<div class="kv"><span class="k">${k}</span><span class="v ${cls||''}">${esc(v)}</span></div>`:'';}
function openDrawer(sku){
  const p=ALL.find(x=>x.dw_sku===sku); if(!p)return;
  $('#dtitle').innerHTML=(p.city?`<span class="city">${esc(p.city)}</span> · `:'')+esc(p.color||p.pattern||p.dw_sku);
  const pal=(p.top_colors||[]).slice(0,6).map(c=>`<span style="background:${esc(c.hex)}" data-pct="${Math.round(c.pct)}%" title="${esc(c.hex)} · ${c.pct}%"></span>`).join('');
  const specSec=`<div class="sec"><h3>Specifications</h3>
    ${kv('Material',p.material)}${kv('Use',p.use)}${kv('Width',p.width)}${kv('Width (in)',p.width_inches!=null?p.width_inches+'"':'')}
    ${kv('Backing',p.backing)}${kv('Composition',p.composition)}${kv('Pattern repeat',p.repeat||'None')}
    ${kv('Fire rating (US)',p.fire_rating)}${kv('Weight',p.weight?p.weight+' g/m²':'')}${kv('Minimum order',p.minimum_order)}
    ${kv('Product type',p.product_type)}${kv('Category',p.category)}${kv('Subcategory',p.subcategory)}
  </div>`;
  const idSec=`<div class="sec"><h3>Identity</h3>
    ${kv('DW SKU',p.dw_sku,'dw')}${kv('Mfr code',p.mfr_sku,'mono')}${kv('Mfr model',p.mfr_model,'mono')}
    ${kv('City / style',p.city)}${kv('Pattern',p.pattern)}${kv('Color',p.color)}${kv('Collection',p.collection)}
    ${kv('Collection code',p.collection_code,'mono')}${kv('Greenland gl_id',p.gl_id,'mono')}${kv('Created',fmtDate(p.created_at))}
    <div class="acts">
      <button class="act" data-copy="${esc(p.dw_sku)}" onclick="copyTxt(this)">📋 DW SKU</button>
      ${p.mfr_sku?`<button class="act" data-copy="${esc(p.mfr_sku)}" onclick="copyTxt(this)">📋 Mfr code</button>`:''}
    </div>
  </div>`;
  // PRICING — internal only. Retail canonical; net cost / discount / margin honestly unconfirmed.
  const margin = (p.net_cost!=null) ? (p.retail_per_yard-p.net_cost) : null;
  const marginPct = (p.net_cost!=null && p.retail_per_yard) ? Math.round((1-p.net_cost/p.retail_per_yard)*100) : null;
  const priceSec=`<div class="sec pcard"><h3>Pricing — internal only 🔒</h3>
    <div class="prow"><span class="pl">Retail (per yard)</span><span class="pv big">${fmt$(p.retail_per_yard)}</span></div>
    <div class="prow"><span class="pl">Sold in</span><span class="pv">${p.min_increment_yards}-yd increments · min ${p.min_increment_yards} yd</span></div>
    <div class="prow"><span class="pl">Memo sample</span><span class="pv">${fmt$(p.sample_price)}</span></div>
    <div class="prow"><span class="pl">Net cost</span><span class="pv ${p.net_cost!=null?'':'unk'}">${p.net_cost!=null?fmt$(p.net_cost):'unconfirmed'}</span></div>
    <div class="prow"><span class="pl">Trade discount</span><span class="pv ${p.trade_discount_pct!=null?'':'unk'}">${p.trade_discount_pct!=null?p.trade_discount_pct+'%':'unconfirmed'}</span></div>
    <div class="prow"><span class="pl">Margin / yd</span><span class="pv ${margin!=null?'':'unk'}">${margin!=null?fmt$(margin)+(marginPct!=null?` (${marginPct}%)`:''):'n/a until cost'}</span></div>
    <div class="pnote"><b>Net cost & discount not yet confirmed</b> — Greenland wholesale cost hasn't been supplied and the trade discount is unset in vendor_registry. Retail $59.50/yd is canonical. Margin computes automatically once a cost lands. Never expose these figures on any customer-facing surface.</div>
  </div>`;
  // STOCK — snapshot + live re-check ($0 open API)
  const stockSec=`<div class="sec scard"><h3>Stock / availability</h3>
    <div class="stat"><span class="dotbig ${p.in_catalog?'in':'un'}" id="dotstk"></span>
      <div><div class="lbl" id="stklbl">${p.in_catalog?'Available — in catalog':'Unlisted'}</div>
      <div class="sub" id="stksub">Snapshot from feed · ${fmtDate(META.generated_at)}</div></div></div>
    <div class="acts">
      <button class="act go" id="chklive" onclick="checkLive('${esc(p.dw_sku)}')">⟳ Check live ($0)</button>
    </div>
    <div class="pnote" style="margin-top:6px">Greenland's open API exposes availability (published-in-catalog), not a live inventory count. "Check live" re-hits that $0 open API — no metered scrape.</div>
  </div>`;
  const links=[];
  if(p.handle)links.push(`<a class="act go" href="${SHOPIFY_STORE}/products/${esc(p.handle)}" target="_blank" rel="noopener noreferrer">Shopify product ↗</a>`);
  if(p.shopify_pid)links.push(`<a class="act" href="${SHOPIFY_ADMIN}/products/${esc(p.shopify_pid)}" target="_blank" rel="noopener noreferrer">Shopify admin ↗</a>`);
  links.push(`<a class="act" href="${ALLDW}/?q=${encodeURIComponent(p.dw_sku)}" target="_blank" rel="noopener noreferrer">all.dw entry ↗</a>`);
  if(p.vendor_url)links.push(`<a class="act" href="${esc(p.vendor_url)}" target="_blank" rel="noopener noreferrer">Greenland source ↗</a>`);
  const linkSec=`<div class="sec"><h3>Links</h3><div class="acts">${links.join('')}</div>
    ${p.shop_status?`<div class="pnote" style="margin-top:8px">Shopify status: <b style="color:${p.shop_status==='ACTIVE'?'var(--ok)':'var(--warn)'}">${esc(p.shop_status)}</b>${p.online_published?' · online-store published':''}</div>`:''}</div>`;
  const descSec=p.description?`<div class="sec"><h3>Description</h3><div style="font-size:12.5px;line-height:1.55;color:#c8cdd8">${esc(p.description)}</div></div>`:'';
  $('#dbody').innerHTML=
    (p.image?`<img class="dimg" src="${esc(p.image)}" alt="${esc(p.title||p.dw_sku)}">`:'')+
    (pal?`<div class="pal">${pal}</div>`:'')+
    priceSec+stockSec+specSec+idSec+linkSec+descSec;
  $('#scrim').classList.add('open'); $('#drawer').classList.add('open'); $('#drawer').setAttribute('aria-hidden','false');
  document.body.style.overflow='hidden';
  location.hash=p.dw_sku;
}
function closeDrawer(){$('#scrim').classList.remove('open');$('#drawer').classList.remove('open');$('#drawer').setAttribute('aria-hidden','true');document.body.style.overflow='';if(location.hash)history.replaceState(null,'',location.pathname+location.search);}
$('#dclose').onclick=closeDrawer; $('#scrim').onclick=closeDrawer;
addEventListener('keydown',e=>{if(e.key==='Escape'&&$('#drawer').classList.contains('open'))closeDrawer();});

async function checkLive(sku){
  const btn=$('#chklive'), dot=$('#dotstk'), lbl=$('#stklbl'), sub=$('#stksub');
  btn.disabled=true; btn.textContent='⟳ checking…'; dot.className='dotbig chk';
  try{
    const r=await fetch(location.origin+'/api/stock/'+encodeURIComponent(sku));
    const j=await r.json();
    if(j.live){
      dot.className='dotbig '+(j.in_catalog?'in':'un');
      lbl.textContent=j.in_catalog?'Available — in catalog (live)':'Unlisted (live)';
      sub.textContent=`Live · ${fmtDate(j.checked_at)}${j.colorways_live!=null?' · '+j.colorways_live+' colorways':''}`;
      btn.textContent='✓ Live checked'; setTimeout(()=>{btn.textContent='⟳ Check live ($0)';btn.disabled=false;},1500);
    }else{ throw new Error(j.error||'no live data'); }
  }catch(e){
    dot.className='dotbig un'; sub.textContent='Live check failed: '+e.message;
    btn.textContent='⟳ Retry'; btn.disabled=false;
  }
}
window.checkLive=checkLive;
function copyTxt(btn){navigator.clipboard.writeText(btn.dataset.copy||'').then(()=>{const o=btn.textContent;btn.textContent='✓ copied';setTimeout(()=>{btn.textContent=o;},1200);});}
window.copyTxt=copyTxt;

// ── filtering / sort ──
function applyFilters(){
  const q=$('#q').value.trim().toLowerCase(), pat=$('#pattern').value.trim().toLowerCase();
  VIEW=ALL.filter(p=>{
    for(const [k] of FIELDS){ if(FIL[k]&&String(p[k]??'')!==FIL[k])return false; }
    if(pat&&!String(p.pattern||'').toLowerCase().includes(pat))return false;
    if(q&&![p.dw_sku,p.mfr_sku,p.color,p.pattern,p.city,p.collection].some(v=>v&&String(v).toLowerCase().includes(q)))return false;
    return true;
  });
  const HUEORD={White:0,Neutral:1,Warm:2,Red:3,Green:4,Blue:5,Black:6};
  const s=$('#sort').value, cmp={
    newest:(a,b)=>String(b.created_at||'').localeCompare(String(a.created_at||''))||String(a.dw_sku).localeCompare(String(b.dw_sku),undefined,{numeric:true}),
    dw:(a,b)=>String(a.dw_sku).localeCompare(String(b.dw_sku),undefined,{numeric:true}),
    color:(a,b)=>((HUEORD[a.color_family]??9)-(HUEORD[b.color_family]??9))||String(a.color||'').localeCompare(String(b.color||'')),
    city:(a,b)=>String(a.city||'~').localeCompare(String(b.city||'~'))||String(a.color||'').localeCompare(String(b.color||'')),
    pattern:(a,b)=>String(a.pattern||'').localeCompare(String(b.pattern||'')),
    sku:(a,b)=>String(a.mfr_sku||'').localeCompare(String(b.mfr_sku||'')),
  }[s];
  if(cmp)VIEW=[...VIEW].sort(cmp);
  shown=0; grid.innerHTML=''; renderMore();
  $('#total').textContent=VIEW.length.toLocaleString()+' cork SKUs';
  $('#empty').style.display=VIEW.length?'none':'block';
  renderSide();
}
function renderMore(){
  const slice=VIEW.slice(shown,shown+PAGE);
  grid.insertAdjacentHTML('beforeend',slice.map(card).join(''));
  shown+=slice.length;
  $('#meta').textContent=`${shown.toLocaleString()} of ${VIEW.length.toLocaleString()} shown`;
  $('#more').style.display=shown<VIEW.length?'block':'none';
}
function buildFacetCache(){
  FCOUNTS={};
  for(const [k] of FIELDS){
    const m={};
    for(const p of ALL){const v=p[k];if(v==null||v==='')continue;m[v]=(m[v]||0)+1;}
    FCOUNTS[k]=Object.entries(m).sort((a,b)=>b[1]-a[1]);
  }
  const pats=[...new Set(ALL.map(p=>p.pattern).filter(Boolean))].sort((a,b)=>String(a).localeCompare(String(b)));
  $('#patternlist').innerHTML=pats.map(s=>`<option value="${esc(s)}">`).join('');
}
function renderSide(){
  const openNow=new Set([...document.querySelectorAll('#facetbox details[open]')].map(d=>d.dataset.f));
  $('#facetbox').innerHTML=FIELDS.map(([k,label])=>{
    const rows=(FCOUNTS[k]||[]).slice(0,FCAP).map(([v,n])=>{
      const dot=k==='color_family'?`<span class="dot" style="background:${FAM_HEX[v]||'#666'}"></span>`:'';
      return `<div class="frow${FIL[k]===String(v)?' active':''}" onclick="pickF('${k}',this.dataset.v)" data-v="${esc(v)}">
        <span class="fl">${dot}${esc(v)}</span><span class="n">${n.toLocaleString()}</span></div>`;
    }).join('');
    const more=(FCOUNTS[k]||[]).length>FCAP?`<div class="fmore">+ ${(FCOUNTS[k].length-FCAP).toLocaleString()} more — use search</div>`:'';
    const cur=FIL[k]?`<span class="cur" title="${esc(FIL[k])}">${esc(FIL[k])}</span>`:'';
    return `<details data-f="${k}"${openNow.has(k)?' open':''}><summary>${label}${cur}</summary><div class="fbody">${rows}${more}</div></details>`;
  }).join('');
  $('#clearf').style.display=(Object.values(FIL).some(Boolean)||$('#q').value||$('#pattern').value)?'block':'none';
}
function pickF(k,v){FIL[k]=(FIL[k]===v?'':v);applyFilters();}
window.pickF=pickF;
$('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';$('#pattern').value='';applyFilters();};
$('#sort').onchange=()=>{localStorage.setItem('gl.sort',$('#sort').value);applyFilters();};
let t; $('#q').oninput=()=>{clearTimeout(t);t=setTimeout(applyFilters,250);};
let tp; $('#pattern').oninput=()=>{clearTimeout(tp);tp=setTimeout(applyFilters,250);};
$('#more').onclick=renderMore;
addEventListener('scroll',()=>{if(shown<VIEW.length&&innerHeight+scrollY>=document.body.offsetHeight-600)renderMore();});

async function loadAll(){
  const raw=await fetch(location.origin+'/api/products').then(r=>r.json());
  META=raw; ALL=raw.products||[];
  buildFacetCache(); applyFilters();
  // deep-link: open drawer if hash matches a SKU
  const h=decodeURIComponent(location.hash.replace('#','')); if(h&&ALL.some(p=>p.dw_sku===h))openDrawer(h);
  wireActivationTabs();
}

/* Products | Activation Calendar view toggle */
let _calRendered=false;
function showView(which){
  const cal=which==='calendar';
  document.getElementById('layout').style.display=cal?'none':'';
  document.getElementById('view-calendar').style.display=cal?'':'none';
  document.getElementById('tab-products').classList.toggle('active',!cal);
  document.getElementById('tab-calendar').classList.toggle('active',cal);
  if(cal && !_calRendered && window.ActivationCalendar){
    window.ActivationCalendar.render({
      mount:document.getElementById('view-calendar'),
      products:ALL,
      lineLabel:'Cork · Phillipe Romano',
      perDay:50,
    });
    _calRendered=true;
  }
}
function wireActivationTabs(){
  const tp=document.getElementById('tab-products'), tc=document.getElementById('tab-calendar');
  if(tp) tp.onclick=()=>showView('products');
  if(tc) tc.onclick=()=>showView('calendar');
}
loadAll();
</script>
</body></html>