← back to All Designerwallcoverings

public/microsites.html

492 lines

<!doctype html>
<html lang="en">
<head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
  var _fetch = window.fetch.bind(window);
  var cleanBase = function () { return location.origin + location.pathname; };
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
        input = new URL(input, cleanBase()).href;
      } else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
        input = new Request(new URL(input.url, cleanBase()).href, input);
      }
    } catch (_) { /* fall through to native */ }
    return _fetch(input, init);
  };
})();
</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Microsite Directory — Designer Wallcoverings</title>
<meta name="description" content="Every Designer Wallcoverings microsite, live — the vendor lookbooks and houses across the designerwallcoverings.com family, refreshed continuously.">
<style>
  :root { --cols: 4; --ink:#161412; --paper:#f7f5f1; --gold:#a08a5f; --line:#dedad2; }
  * { box-sizing: border-box; margin: 0; }
  body { background: var(--paper); color: var(--ink); font-family: "Optima", "Segoe UI", system-ui, sans-serif; }
  a { color: inherit; text-decoration: none; }

  header { display:flex; justify-content:space-between; align-items:center;
    padding: 22px 34px; position: sticky; top:0; background: rgba(247,245,241,.92);
    backdrop-filter: blur(8px); z-index: 20; border-bottom: 1px solid var(--line); }
  .wordmark { letter-spacing: .22em; font-size: 15px; text-transform: uppercase; font-weight: 600; }
  .wordmark small { display:block; letter-spacing:.34em; font-size:9px; color:var(--gold); margin-top:3px; }
  .burger { cursor:pointer; background:none; border:none; padding:8px; }
  .burger span { display:block; width:24px; height:1.5px; background:var(--ink); margin:5px 0; }
  nav.menu { display:none; position:fixed; top:0; right:0; bottom:0; width:290px; background:#141210;
    color:#eee9df; z-index:40; padding:80px 40px; flex-direction:column; gap:26px;
    font-size:13px; letter-spacing:.18em; text-transform:uppercase; }
  nav.menu.open { display:flex; }
  nav.menu .close { position:absolute; top:24px; right:28px; font-size:26px; cursor:pointer; background:none; border:none; color:#eee9df; }
  nav.menu a:hover { color: var(--gold); }

  .hero { padding: 74px 34px 58px; text-align:center; background:#141210; color:#f2eee6; }
  .hero h1 { font-family: Georgia, serif; font-weight: 400; font-size: clamp(30px, 5vw, 52px); letter-spacing:.04em; }
  .hero p { margin-top:14px; color:#b6ad9c; font-size:15px; letter-spacing:.08em; }
  .hero .stats { margin-top: 26px; display:flex; gap:44px; justify-content:center; font-size:12px; letter-spacing:.24em; text-transform:uppercase; color:var(--gold); flex-wrap:wrap; }
  .hero .stats b { display:block; font-size:26px; color:#f2eee6; font-family: Georgia, serif; font-weight:400; letter-spacing:.02em; }
  .hero .fresh { margin-top:20px; font-size:10px; letter-spacing:.24em; text-transform:uppercase; color:#7d766a; }

  .controls { display:flex; flex-wrap:wrap; gap:14px 22px; align-items:center;
    padding: 20px 34px; border-bottom:1px solid var(--line); background:var(--paper);
    position: sticky; top: 69px; z-index: 15; }
  .controls input[type=search] { flex:1 1 220px; max-width: 340px; padding:10px 14px; border:1px solid var(--line);
    background:#fff; font-size:14px; border-radius:2px; }
  .controls select { padding:10px 12px; border:1px solid var(--line); background:#fff; font-size:13px; border-radius:2px; }
  .seg { display:flex; border:1px solid var(--line); border-radius:2px; overflow:hidden; }
  .seg button { padding:9px 16px; background:#fff; border:none; font-size:12px; letter-spacing:.1em; text-transform:uppercase; cursor:pointer; color:#7d766a; }
  .seg button.on { background:var(--ink); color:#f2eee6; }
  .dens { display:flex; align-items:center; gap:10px; font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:#7d766a; }
  .dens input { accent-color: var(--gold); width: 130px; }

  /* Fields on/off menu — toggle any field on the card (down to image-only). */
  .fieldmenu { position:relative; }
  .fbtn { padding:9px 14px; border:1px solid var(--line); background:#fff; font-size:12px; letter-spacing:.1em;
    text-transform:uppercase; color:#7d766a; cursor:pointer; border-radius:2px; }
  .fbtn:hover { border-color:var(--gold); color:var(--ink); }
  .fpop { position:absolute; top:calc(100% + 8px); right:0; background:#fff; border:1px solid var(--line);
    border-radius:2px; box-shadow:0 14px 34px rgba(22,20,18,.16); padding:10px 6px; z-index:30; min-width:190px; }
  .fpop[hidden] { display:none; }
  .fpop label { display:flex; align-items:center; gap:10px; padding:7px 14px; font-size:12px; letter-spacing:.08em;
    text-transform:uppercase; color:#4b463d; cursor:pointer; white-space:nowrap; }
  .fpop label:hover { background:var(--paper); }
  .fpop input { accent-color: var(--gold); }

  .grid { display:grid; grid-template-columns: repeat(var(--cols), 1fr); gap: 26px; padding: 34px; }

  /* List view — cards become horizontal rows (thumbnail left, meta right). */
  .grid.list { display:block; padding: 20px 34px; }
  .grid.list .card { flex-direction:row; align-items:stretch; margin-bottom:14px; }
  .grid.list .card:hover { transform:none; }
  .grid.list .collage { flex:0 0 clamp(120px, 22vw, 210px); aspect-ratio:4/3; }
  .grid.list .meta { flex:1; display:flex; flex-direction:column; justify-content:center; }
  .grid.list .badge { display:none; }

  /* Extra record fields (off by default) — surfaced as labeled lines so each is toggleable. */
  .card .fline { margin-top:6px; font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:#9a927f; }
  .card .fline b { color:#6b6459; font-weight:600; }

  /* Per-field hide switches driven by body.hf-<field> classes (set from the Fields menu). */
  body.hf-image  .collage,
  body.hf-image  .card:not(.list) .collage { display:none; }
  body.hf-name   [data-f="name"]   { display:none; }
  body.hf-type   [data-f="type"]   { display:none; }
  body.hf-slug   [data-f="slug"]   { display:none; }
  body.hf-count  [data-f="count"]  { display:none; }
  body.hf-status [data-f="status"] { display:none; }
  body.hf-created [data-f="created"] { display:none; }
  body.hf-cta    [data-f="cta"]    { display:none; }
  body.hf-inquiry [data-f="inquiry"] { display:none; }
  .card { background:#fff; border:1px solid var(--line); overflow:hidden; display:flex; flex-direction:column;
    transition: box-shadow .25s, transform .25s; position:relative; }
  .card:hover { box-shadow: 0 14px 34px rgba(22,20,18,.14); transform: translateY(-3px); }
  .collage { display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; aspect-ratio: 4/3; gap:1px; background:var(--line); }
  .collage.single { grid-template-columns:1fr; grid-template-rows:1fr; }
  .collage img { width:100%; height:100%; object-fit:cover; display:block; }
  .collage .ph { display:flex; align-items:center; justify-content:center; background:#efece6; color:#c6bfb1;
    font-family:Georgia,serif; font-size:34px; }
  .badge { position:absolute; top:12px; left:12px; font-size:9px; letter-spacing:.2em; text-transform:uppercase;
    padding:5px 9px; border-radius:2px; background:rgba(20,18,16,.82); color:#f2eee6; }
  .badge.gated { background:rgba(160,138,95,.92); }
  .card .meta { padding: 16px 18px 18px; }
  .card h3 { font-family: Georgia, serif; font-weight:400; font-size:17px; letter-spacing:.03em; }
  .card .sub { margin-top:6px; font-size:11px; letter-spacing:.18em; text-transform:uppercase; color:#9a927f; }
  .card .cta { margin-top:12px; font-size:11px; letter-spacing:.22em; text-transform:uppercase; color:var(--gold); }
  /* per-microsite product count → drills into the catalog aggregator filtered to this vendor */
  .countdrill { cursor:pointer; border-bottom:1px dotted transparent; }
  .badge.countdrill:hover { background:var(--gold); color:#fff; }
  .sub .countdrill:hover, .card .sub .countdrill:hover { color:var(--gold); border-bottom-color:var(--gold); }

  /* per-card vendor inquiry button + page-level compose modal (Gmail DRAFT only, never auto-sends) */
  .card .mailv { margin-top:12px; align-self:flex-start; background:none; border:1px solid var(--line); border-radius:2px;
    padding:7px 12px; font-size:10px; letter-spacing:.18em; text-transform:uppercase; color:var(--gold); cursor:pointer; }
  .card .mailv:hover { border-color:var(--gold); }
  #mailModal { position:fixed; inset:0; background:rgba(22,20,18,.55); display:none; align-items:center; justify-content:center; z-index:60; }
  #mailModal.open { display:flex; }
  #mailModal .mbox { background:#fff; border:1px solid var(--line); width:min(560px,94vw); max-height:88vh; overflow:auto;
    padding:26px 28px; display:flex; flex-direction:column; gap:8px; }
  #mailModal h3 { font-family:Georgia,serif; font-weight:400; font-size:19px; letter-spacing:.03em; }
  #mailModal label { font-size:10px; letter-spacing:.18em; text-transform:uppercase; color:#9a927f; margin-top:6px; }
  #mailModal input, #mailModal textarea { width:100%; border:1px solid var(--line); background:#fdfcfa; padding:9px 11px;
    font:inherit; font-size:13px; border-radius:2px; }
  #mailModal textarea { min-height:180px; resize:vertical; line-height:1.5; }
  #mailModal .mrow { display:flex; gap:10px; margin-top:10px; }
  #mailModal .mgo { flex:1; padding:10px; border:1px solid var(--ink); background:var(--ink); color:#f2eee6;
    font-size:11px; letter-spacing:.18em; text-transform:uppercase; cursor:pointer; text-align:center; text-decoration:none; }
  #mailModal a.mgo { background:none; color:var(--ink); }
  #mailModal .mgo:hover { border-color:var(--gold); }
  #mailModal .mgo:disabled { opacity:.5; cursor:wait; }
  #mailModal .mstat { font-size:12px; margin-top:6px; }
  #mailModal .mstat.ok { color:#3d7a4a; } #mailModal .mstat.err { color:#a03c3c; }
  #mailModal .mclose { align-self:flex-end; background:none; border:none; font-size:22px; cursor:pointer; color:#9a927f; }

  .sentinel { height: 10px; }
  footer { padding: 44px 34px 60px; text-align:center; font-size:11px; letter-spacing:.2em; text-transform:uppercase; color:#9a927f; border-top:1px solid var(--line); }
  @media (max-width: 900px){ .grid{ --cols: 2 !important; } .controls{ top:63px } }
</style>
  <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
</head>
<body>

<header>
  <a class="wordmark" href="https://www.designerwallcoverings.com">Designer Wallcoverings<small>The Microsite Directory</small></a>
  <button class="burger" aria-label="Menu" onclick="menu.classList.add('open')"><span></span><span></span><span></span></button>
</header>

<nav class="menu" id="menu">
  <button class="close" aria-label="Close" onclick="menu.classList.remove('open')">&times;</button>
  <a href="/">All Products</a>
  <a href="/vendors.html">Vendor Index</a>
  <a href="https://www.designerwallcoverings.com">Shop the Store</a>
  <a href="https://www.designerwallcoverings.com/pages/contact-us">Contact a Designer</a>
</nav>

<section class="hero">
  <h1>Our Microsites, Live</h1>
  <p>Every house &amp; lookbook across the designerwallcoverings.com family — crawled continuously.</p>
  <div class="stats">
    <span><b id="statUp">—</b>Sites Live</span>
    <span><b id="statFeeds">—</b>With Catalogs</span>
    <span><b id="statProducts">—</b>Products Indexed</span>
    <span><b id="statPairs">—</b>Vendor Pairs</span>
    <span><b id="statComplete">—</b>Pairs Complete</span>
  </div>
  <div class="fresh" id="fresh">refreshed —</div>
</section>

<div class="controls">
  <input type="search" id="q" placeholder="Search microsites…" aria-label="Search microsites">
  <div class="seg" role="tablist">
    <button id="fFeeds" class="on">With Catalogs</button>
    <button id="fAll">All Live Sites</button>
  </div>
  <select id="sort" aria-label="Sort">
    <option value="products">Products: Most</option>
    <option value="products_asc">Products: Fewest</option>
    <option value="az">Name: A→Z</option>
    <option value="za">Name: Z→A</option>
    <option value="slug">Slug: A→Z</option>
    <option value="slug_desc">Slug: Z→A</option>
    <option value="type">Type: A→Z</option>
    <option value="type_desc">Type: Z→A</option>
    <option value="catalog">Has Catalog First</option>
    <option value="live">Live First</option>
    <option value="gated">Trade-Gated First</option>
  </select>
  <label class="dens">Density <input type="range" id="density" min="1" max="30" step="1" value="4"><span id="densN">4</span></label>
  <div class="seg" role="tablist" aria-label="View">
    <button id="vGrid" class="on">Grid</button>
    <button id="vList">List</button>
  </div>
  <div class="fieldmenu">
    <button id="fieldsBtn" class="fbtn" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
    <div class="fpop" id="fpop" hidden role="menu"></div>
  </div>
</div>

<main class="grid" id="grid"></main>
<div class="sentinel" id="sentinel"></div>

<!-- vendor-level stock & price inquiry — creates a Gmail DRAFT via George; never auto-sends -->
<div id="mailModal">
  <div class="mbox">
    <button class="mclose" aria-label="Close" onclick="mailModal.classList.remove('open')">&times;</button>
    <h3 id="mmTitle">Stock &amp; Price Inquiry</h3>
    <label>To — vendor rep email</label>
    <input type="text" id="mmTo" placeholder="vendor rep email — will be remembered">
    <label>Subject</label>
    <input type="text" id="mmSub">
    <label>Message</label>
    <textarea id="mmBody"></textarea>
    <div class="mrow">
      <button type="button" class="mgo" id="mmDraft">Create Gmail Draft</button>
      <a class="mgo" id="mmMailto" target="_blank" rel="noopener noreferrer">Open in Mail App</a>
    </div>
    <div class="mstat" id="mmStat" hidden></div>
  </div>
</div>

<footer>Designer Wallcoverings &amp; Fabrics — To The Trade &amp; Beyond</footer>

<script>
const grid = document.getElementById('grid'), sentinel = document.getElementById('sentinel');
const menu = document.getElementById('menu');
// Every field on a card is user-toggleable; [key, label, default-visible].
const FIELDS = [
  ['image',   'Image',      true],
  ['name',    'Name',       true],
  ['type',    'Type',       false],
  ['slug',    'Slug',       false],
  ['count',   'Products',   true],
  ['status',  'Status',     false],
  ['created', 'Created',    true],
  ['cta',     'Visit link', true],
  ['inquiry', 'Inquiry',    true],
];
function loadFields(){
  const def = {}; FIELDS.forEach(([k,,d]) => def[k] = d);
  try { return Object.assign(def, JSON.parse(localStorage.getItem('adwm_fields') || '{}')); }
  catch(e){ return def; }
}

const state = {
  all: [], view: [], rendered: 0, BATCH: 40,
  q: '',
  filter: localStorage.getItem('adwm_filter') || 'feeds',
  sort: localStorage.getItem('adwm_sort') || 'products',
  cols: localStorage.getItem('adwm_cols') || '4',
  viewMode: localStorage.getItem('adwm_view') || 'grid',
  fields: loadFields(),
};

document.documentElement.style.setProperty('--cols', state.cols);
density.value = state.cols;
densN.textContent = state.cols;
sort.value = state.sort;
setSeg();
setView();
buildFieldMenu();
applyFields();

function setView(){
  grid.classList.toggle('list', state.viewMode === 'list');
  vGrid.classList.toggle('on', state.viewMode === 'grid');
  vList.classList.toggle('on', state.viewMode === 'list');
}
function applyFields(){
  FIELDS.forEach(([k]) => document.body.classList.toggle('hf-' + k, !state.fields[k]));
}
function buildFieldMenu(){
  fpop.innerHTML = FIELDS.map(([k,label]) =>
    `<label><input type="checkbox" data-field="${k}"${state.fields[k] ? ' checked' : ''}> ${label}</label>`).join('');
  fpop.querySelectorAll('input').forEach(cb => cb.addEventListener('change', () => {
    state.fields[cb.dataset.field] = cb.checked;
    localStorage.setItem('adwm_fields', JSON.stringify(state.fields));
    applyFields();
  }));
}

function setSeg(){
  fFeeds.classList.toggle('on', state.filter==='feeds');
  fAll.classList.toggle('on', state.filter==='all');
}

// Prefer a display title; fall back to a title-cased slug so cards never read blank.
const titleCase = s => String(s||'').replace(/[-_]/g,' ').replace(/\b\w/g, c=>c.toUpperCase());
function displayName(s){
  const t = (s.title||'').trim();
  // The DW-family landings all report "Designer Wallcoverings" as <title>; that's not a name.
  if (t && !/^designer wallcoverings$/i.test(t) && !/^401 /.test(t)) return t.split('|')[0].trim();
  return s.vendor || titleCase(s.slug);
}

function ago(iso){
  if(!iso) return '—';
  const s = Math.max(0, (Date.now() - new Date(iso).getTime())/1000);
  if (s < 90) return 'moments ago';
  if (s < 5400) return Math.round(s/60)+' min ago';
  if (s < 129600) return Math.round(s/3600)+' hr ago';
  return Math.round(s/86400)+' days ago';
}

fetch('/api/microsites').then(r=>r.json()).then(d=>{
  state.all = (d.sites||[]).filter(s=>s.up);
  statUp.textContent = d.up != null ? d.up.toLocaleString() : state.all.length.toLocaleString();
  statFeeds.textContent = (d.with_feed!=null ? d.with_feed : state.all.filter(s=>s.feed).length).toLocaleString();
  statProducts.textContent = (d.products!=null ? d.products : 0).toLocaleString();
  statPairs.textContent = ((d.vendor_pairs && d.vendor_pairs.vendors) || 0).toLocaleString();
  statComplete.textContent = ((d.vendor_pairs && d.vendor_pairs.complete) || 0).toLocaleString();
  fresh.textContent = 'refreshed ' + ago(d.crawled_at);
  apply();
}).catch(()=>{ fresh.textContent='crawl snapshot not ready — check back shortly'; });

// Every field on a microsite record gets a sorter. Ties break on name so order is stable.
const nm = s => displayName(s);
const str = v => String(v == null ? '￿' : v);      // nulls sink to the end on A→Z
const gated = s => (s.status === 401 ? 1 : 0);
const SORTERS = {
  products:     (a,b)=> (b.productCount-a.productCount) || nm(a).localeCompare(nm(b)),
  products_asc: (a,b)=> (a.productCount-b.productCount) || nm(a).localeCompare(nm(b)),
  az:           (a,b)=> nm(a).localeCompare(nm(b)),
  za:           (a,b)=> nm(b).localeCompare(nm(a)),
  slug:         (a,b)=> str(a.slug).localeCompare(str(b.slug)),
  slug_desc:    (a,b)=> str(b.slug).localeCompare(str(a.slug)),
  type:         (a,b)=> str(a.type).localeCompare(str(b.type)) || nm(a).localeCompare(nm(b)),
  type_desc:    (a,b)=> str(b.type).localeCompare(str(a.type)) || nm(a).localeCompare(nm(b)),
  catalog:      (a,b)=> (b.feed-a.feed) || (b.productCount-a.productCount) || nm(a).localeCompare(nm(b)),
  live:         (a,b)=> (gated(a)-gated(b)) || (b.productCount-a.productCount) || nm(a).localeCompare(nm(b)),
  gated:        (a,b)=> (gated(b)-gated(a)) || nm(a).localeCompare(nm(b)),
};

function apply(){
  let v = state.all;
  if (state.filter==='feeds') v = v.filter(x=>x.feed);
  if (state.q){ const q=state.q.toLowerCase();
    v = v.filter(x=> (x.slug||'').includes(q) || nm(x).toLowerCase().includes(q) || (x.vendor||'').toLowerCase().includes(q)); }
  v = [...v].sort(SORTERS[state.sort] || SORTERS.products);
  state.view = v; state.rendered = 0; grid.innerHTML=''; renderMore();
}

function card(s){
  const el = document.createElement('a');
  el.className='card'; el.href=s.url; el.target='_blank'; el.rel='noopener noreferrer';
  const name = displayName(s);
  const gated = s.status===401;
  // Collage from up to 4 merged product images; else the site hero; else initial.
  const imgs = (s.products||[]).map(p=>p.image).filter(Boolean).slice(0,4);
  const pool = imgs.length ? imgs : (s.hero ? [s.hero] : []);
  const cells = pool.length
    ? pool.map(u=>`<img loading="lazy" src="${String(u).replace(/"/g,'')}" alt="${name} design">`).join('')
    : `<div class="ph">${name.charAt(0)}</div>`;
  // href-to-deeper-data: the per-microsite product COUNT links to the aggregator filtered to
  // this vendor's products (/?vendors=<vendor>). The tile itself is an <a> to the live microsite,
  // so the count is a <span class="countdrill"> with a delegated navigate (can't nest an <a>).
  // Only when we have a real vendor name that maps to the product grid's vendor facet.
  const aggUrl = s.vendor ? '/?vendors=' + encodeURIComponent(s.vendor) : '';
  const countTxt = s.productCount.toLocaleString() + ' designs';
  const badge = gated ? `<span class="badge gated" data-f="status">Trade Access</span>`
    : (s.productCount>0
        ? (aggUrl ? `<span class="badge countdrill" data-f="count" data-agg="${aggUrl.replace(/"/g,'&quot;')}" title="See all ${countTxt} in the catalog aggregator">${countTxt}</span>`
                  : `<span class="badge" data-f="count">${countTxt}</span>`)
        : '');
  const sub = gated ? 'By trade inquiry'
    : (s.productCount>0
        ? (aggUrl ? `<span class="countdrill" data-agg="${aggUrl.replace(/"/g,'&quot;')}" title="See all ${countTxt} in the catalog aggregator">${countTxt} →</span>`
                  : countTxt)
        : 'Editorial lookbook');
  const clean = v => String(v == null ? '' : v).replace(/[<>]/g,'');
  const created = s.created_at ? new Date(s.created_at) : null;
  const createdText = created && Number.isFinite(created.getTime())
    ? created.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' })
    : '—';
  el.innerHTML = `
    ${badge}
    <div class="collage${pool.length<=1?' single':''}" data-f="image">${cells}</div>
    <div class="meta">
      <h3 data-f="name"></h3>
      <div class="sub" data-f="count">${sub}</div>
      <div class="fline" data-f="type"><b>Type</b> ${clean(s.type) || '—'}</div>
      <div class="fline" data-f="slug"><b>Slug</b> ${clean(s.slug) || '—'}</div>
      <div class="fline" data-f="status"><b>Status</b> ${gated ? 'Trade-Gated' : 'Live'}</div>
      <div class="fline" data-f="created" title="${created ? created.toISOString() : ''}"><b>Created</b> 🕓 ${createdText}</div>
      <div class="cta" data-f="cta">Visit Microsite →</div>
      <button type="button" class="mailv" data-f="inquiry" title="Compose a stock &amp; price inquiry to this vendor — saves a Gmail DRAFT, never auto-sends">✉ Stock &amp; Price</button>
    </div>`;
  el.querySelector('h3').textContent = name;
  // The card itself is an <a> — the inquiry button must not navigate.
  el.querySelector('.mailv').addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); openMail(name); });
  // The count drill navigates to the aggregator (not the microsite) — intercept before the tile <a>.
  el.querySelectorAll('.countdrill').forEach((c) => c.addEventListener('click', (e) => {
    e.preventDefault(); e.stopPropagation(); window.location.href = c.dataset.agg;
  }));
  return el;
}

// ── vendor-level stock & price inquiry (every vendor card — Steve 2026-07-15 rule) ──
// Pre-fills the remembered rep email from /api/vendor-contact; "Create Gmail Draft" posts
// /api/vendor-inquiry (upserts the contact + drafts via George — NEVER an auto-send).
const mailModal = document.getElementById('mailModal');
const INQ_SIG = 'Thank you,\n\nDesigner Wallcoverings\nsteve@designerwallcoverings.com · 1-888-373-4564\n15442 Ventura Blvd, #102, Sherman Oaks, CA 91403';
const CONTACTS = {};
let mmVendor = '';
const mmTo = document.getElementById('mmTo'), mmSub = document.getElementById('mmSub'),
      mmBody = document.getElementById('mmBody'), mmStat = document.getElementById('mmStat'),
      mmDraft = document.getElementById('mmDraft'), mmMailto = document.getElementById('mmMailto');
function mmSync(){ mmMailto.href = 'mailto:' + encodeURIComponent(mmTo.value.trim())
  + '?subject=' + encodeURIComponent(mmSub.value) + '&body=' + encodeURIComponent(mmBody.value); }
[mmTo, mmSub, mmBody].forEach(el => el.addEventListener('input', mmSync));
async function openMail(vendor){
  mmVendor = vendor;
  document.getElementById('mmTitle').textContent = 'Stock & Price Inquiry — ' + vendor;
  mmTo.value = ''; mmStat.hidden = true; mmStat.textContent = '';
  mmSub.value = `Stock & price check — ${vendor} line (Designer Wallcoverings)`;
  mmBody.value = ['Hello,', '',
    `This is Designer Wallcoverings (trade account) — we'd like to confirm current stock and pricing on your ${vendor} line.`, '',
    'Specifically:',
    '  • Current stock / availability',
    '  • Lead time if not in stock',
    '  • Current net / trade price',
    '', INQ_SIG].join('\n');
  mailModal.classList.add('open');
  mmSync();
  try {
    const c = CONTACTS[vendor] || (CONTACTS[vendor] = await (await fetch('/api/vendor-contact?vendor=' + encodeURIComponent(vendor))).json());
    if (c && c.email && mmVendor === vendor && !mmTo.value) { mmTo.value = c.email; mmSync(); }
  } catch(e) { /* unknown contact — placeholder stands */ }
}
mailModal.addEventListener('click', (e) => { if (e.target === mailModal) mailModal.classList.remove('open'); });
mmDraft.addEventListener('click', async () => {
  const to = mmTo.value.trim();
  mmStat.hidden = false; mmStat.className = 'mstat';
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(to)) { mmStat.className = 'mstat err'; mmStat.textContent = 'Enter the vendor rep’s email address first.'; return; }
  if (!mmSub.value.trim() || !mmBody.value.trim()) { mmStat.className = 'mstat err'; mmStat.textContent = 'Subject and message are required.'; return; }
  mmDraft.disabled = true; const orig = mmDraft.textContent; mmDraft.textContent = 'Drafting…';
  mmStat.textContent = '◌ Saving contact + creating Gmail draft…';
  try {
    const r = await fetch('/api/vendor-inquiry', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({ vendor: mmVendor, to, subject: mmSub.value.trim(), body: mmBody.value, sku: '' }) });
    const j = await r.json();
    if (j.drafted) { mmStat.className = 'mstat ok'; mmStat.textContent = '✓ draft saved in Gmail (' + (j.account||'steve-office') + ') — review + send from there. Contact remembered.'; CONTACTS[mmVendor] = { email: to }; }
    else if (j.ok) { mmStat.className = 'mstat err'; mmStat.textContent = (j.contact_saved ? 'Contact saved, but the ' : 'The ') + 'Gmail draft failed: ' + (j.reason||'unknown') + ' — use "Open in Mail App" instead.'; if (j.contact_saved) CONTACTS[mmVendor] = { email: to }; }
    else { mmStat.className = 'mstat err'; mmStat.textContent = j.reason || 'Request failed — try again.'; }
  } catch(e) { mmStat.className = 'mstat err'; mmStat.textContent = 'Request failed — try again.'; }
  finally { mmDraft.disabled = false; mmDraft.textContent = orig; }
});

function renderMore(){
  const next = state.view.slice(state.rendered, state.rendered + state.BATCH);
  next.forEach(s=>grid.appendChild(card(s)));
  state.rendered += next.length;
}

new IntersectionObserver(es=>{ if(es[0].isIntersecting) renderMore(); }, {rootMargin:'800px'}).observe(sentinel);

q.addEventListener('input', e=>{ state.q=e.target.value.trim(); apply(); });
sort.addEventListener('change', e=>{ state.sort=e.target.value; localStorage.setItem('adwm_sort',state.sort); apply(); });
density.addEventListener('input', e=>{ state.cols=e.target.value; localStorage.setItem('adwm_cols',state.cols);
  densN.textContent=state.cols; document.documentElement.style.setProperty('--cols', state.cols); });
fFeeds.onclick=()=>{ state.filter='feeds'; localStorage.setItem('adwm_filter','feeds'); setSeg(); apply(); };
fAll.onclick=()=>{ state.filter='all'; localStorage.setItem('adwm_filter','all'); setSeg(); apply(); };

vGrid.onclick=()=>{ state.viewMode='grid'; localStorage.setItem('adwm_view','grid'); setView(); };
vList.onclick=()=>{ state.viewMode='list'; localStorage.setItem('adwm_view','list'); setView(); };

// Fields menu open/close (click-away closes).
fieldsBtn.addEventListener('click', (e)=>{ e.stopPropagation();
  const open = fpop.hidden; fpop.hidden = !open; fieldsBtn.setAttribute('aria-expanded', String(open)); });
document.addEventListener('click', (e)=>{ if (!fpop.hidden && !e.target.closest('.fieldmenu')) {
  fpop.hidden = true; fieldsBtn.setAttribute('aria-expanded','false'); } });
</script>
  <script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
</body>
</html>