← back to Wallco Ai

public/admin/styleguides.html

541 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brand Selector · Style Guides — wallco.ai</title>
<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=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
  :root{
    --bg:#0a0a0e; --bg2:#0d0d12; --card:#121218; --card-hi:#16161d;
    --line:#26262f; --line-soft:#1d1d25;
    --ink:#F4F4F5; --ink-soft:#b6b6c0; --muted:#8a8a96; --faint:#6a6a76;
    --gold:#E4C25A; --gold-soft:#caa84a;
    --serif:"Cormorant Garamond","Didot","Times New Roman",serif;
    --sans:"Inter",system-ui,-apple-system,sans-serif;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{background:var(--bg);color:var(--ink)}
  body{font:15px/1.5 var(--sans);-webkit-font-smoothing:antialiased;padding-bottom:96px}
  a{color:inherit}
  .wrap{max-width:1312px;margin:0 auto;padding:0 28px}

  /* ── Header ── */
  header{border-bottom:1px solid var(--line-soft);background:linear-gradient(180deg,#0c0c11,#0a0a0e);padding:42px 0 30px}
  .eyebrow{display:flex;align-items:center;gap:9px;font:600 11px var(--sans);letter-spacing:.22em;text-transform:uppercase;color:var(--muted);margin-bottom:14px}
  .eyebrow .dot{width:7px;height:7px;border-radius:50%;background:var(--gold);box-shadow:0 0 10px var(--gold)}
  .head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap}
  h1{font:500 64px/0.95 var(--serif);letter-spacing:-.01em}
  .lede{font:italic 400 18px/1.4 var(--serif);color:var(--ink-soft);margin-top:12px;max-width:680px}
  .counts{display:flex;gap:40px}
  .counts .c .n{font:500 46px/1 var(--serif);color:var(--gold)}
  .counts .c .l{font:600 10.5px var(--sans);letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin-top:6px}

  /* ── Section ── */
  section{padding:44px 0 8px}
  .sec-top{display:flex;justify-content:space-between;align-items:flex-end;gap:18px;flex-wrap:wrap;margin-bottom:26px}
  .sec-no{font:600 11px var(--sans);letter-spacing:.24em;color:var(--faint);margin-bottom:8px}
  .sec-h{font:500 38px/1 var(--serif)}
  .sec-sub{font:italic 400 14.5px var(--serif);color:var(--muted);margin-top:6px}
  .controls{display:flex;gap:10px;align-items:center}
  .ctl{font:500 12px var(--sans);color:var(--ink-soft);background:transparent;border:1px solid var(--line);border-radius:999px;padding:8px 16px;cursor:pointer;transition:.15s}
  .ctl:hover{border-color:var(--muted);color:var(--ink)}
  .ctl.gold{border-color:var(--gold-soft);color:var(--gold)}
  .ctl.gold:hover{background:rgba(228,194,90,.08)}

  /* ── Grid ── */
  .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
  @media(max-width:1100px){.grid{grid-template-columns:repeat(2,1fr)}}
  @media(max-width:640px){.grid{grid-template-columns:1fr}}
  .card{position:relative;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px 22px 20px;min-height:150px;cursor:pointer;transition:.16s;user-select:none}
  .card:hover{background:var(--card-hi);border-color:#34343f;transform:translateY(-1px)}
  .card.sel{border-color:var(--gold);background:#14130f;box-shadow:0 0 0 1px var(--gold),0 8px 30px rgba(228,194,90,.07)}
  .card .name{font:400 21px/1.1 var(--serif);color:var(--ink);padding-right:34px}
  .card .meta{display:flex;gap:6px;margin-top:13px}
  .chip{border:1px solid var(--line);border-radius:999px;padding:3px 9px;font:500 9.5px var(--sans);letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
  .dots{display:flex;gap:6px;margin-top:16px}
  .dot{width:17px;height:17px;border-radius:5px;border:1px solid rgba(255,255,255,.08)}
  .tick{position:absolute;top:16px;right:16px;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--line);display:flex;align-items:center;justify-content:center;transition:.15s}
  .card.sel .tick{background:var(--gold);border-color:var(--gold)}
  .tick svg{opacity:0;transition:.15s;width:12px;height:12px}
  .card.sel .tick svg{opacity:1}

  /* ── Footer bar ── */
  .bar{position:fixed;left:0;right:0;bottom:0;z-index:40;background:rgba(10,10,14,.92);backdrop-filter:blur(14px);border-top:1px solid var(--line)}
  .bar-in{max-width:1312px;margin:0 auto;padding:14px 28px;display:flex;align-items:center;gap:16px}
  .selpill{font:600 11.5px var(--sans);letter-spacing:.04em;color:var(--ink);background:#1b1b22;border:1px solid var(--line);border-radius:999px;padding:8px 15px;white-space:nowrap}
  .selnames{font:13px var(--sans);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
  .bar .actions{display:flex;gap:10px;align-items:center;margin-left:auto}
  .btn{font:600 12.5px var(--sans);border-radius:999px;padding:11px 20px;cursor:pointer;border:1px solid var(--line);background:transparent;color:var(--ink-soft);transition:.15s;white-space:nowrap}
  .btn:hover{border-color:var(--muted);color:var(--ink)}
  .btn.primary{background:var(--gold);color:#1a1505;border-color:var(--gold);font-weight:600}
  .btn.primary:hover{background:#eccd6e}
  .btn:disabled{opacity:.4;cursor:not-allowed}

  /* ── Modal ── */
  .scrim{position:fixed;inset:0;z-index:60;background:rgba(0,0,0,.66);display:none;align-items:center;justify-content:center;padding:32px}
  .scrim.on{display:flex}
  .modal{background:var(--bg2);border:1px solid var(--line);border-radius:16px;max-width:780px;width:100%;max-height:84vh;overflow:auto;padding:28px}
  .modal h3{font:500 26px var(--serif);margin-bottom:4px}
  .modal .mh-sub{color:var(--muted);font-size:13px;margin-bottom:18px}
  pre{background:#08080b;border:1px solid var(--line);border-radius:10px;padding:18px;font:12.5px/1.6 ui-monospace,Menlo,monospace;color:#cfcfd8;overflow:auto;white-space:pre-wrap;word-break:break-word}
  .modal-row{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap}
  .field{display:flex;align-items:center;gap:8px}
  .field label{font:600 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
  .field input[type=number]{width:74px;background:#08080b;border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:9px 11px;font:14px var(--sans)}
  .status{font:13px var(--sans);color:var(--ink-soft);margin-top:14px;min-height:20px}
  .picker{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:16px}
  .picker img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:9px;border:1px solid var(--line);cursor:pointer;transition:.15s}
  .picker .pi{position:relative}
  .picker img:hover{border-color:var(--gold);transform:scale(1.02)}
  .brandsel{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0}
  .brandsel button{font:500 12px var(--sans);border:1px solid var(--line);background:#101016;color:var(--ink-soft);border-radius:999px;padding:7px 13px;cursor:pointer}
  .brandsel button.on{border-color:var(--gold);color:var(--gold);background:rgba(228,194,90,.1)}
  .results{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:14px}
  .results .r{text-align:center}
  .results img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:9px;border:1px solid var(--line)}
  .results .cap{font:10px var(--sans);color:var(--muted);margin-top:5px}
  .x{float:right;font-size:22px;color:var(--muted);cursor:pointer;line-height:1;margin-top:-4px}
  .toast{position:fixed;left:50%;bottom:108px;transform:translateX(-50%);z-index:80;background:#16161d;border:1px solid var(--gold-soft);color:var(--ink);font:500 13px var(--sans);padding:12px 20px;border-radius:10px;opacity:0;transition:.25s;pointer-events:none}
  .toast.on{opacity:1}
  .skeleton{color:var(--faint);font:14px var(--sans);padding:40px 0;text-align:center}
  /* ── Add Style Guide form + custom-card delete ── */
  .addform{display:flex;flex-direction:column;gap:14px;margin:6px 0 4px}
  .afrow{display:flex;gap:14px;flex-wrap:wrap}
  .fld{display:flex;flex-direction:column;gap:6px}
  .fld.grow{flex:1;min-width:220px}
  .fld label{font:600 10px var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
  .fld input[type=text],.fld input[type=number],.fld select{background:#08080b;border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:10px 12px;font:14px var(--sans)}
  .fld input:focus,.fld select:focus{outline:none;border-color:var(--gold-soft)}
  .colors{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
  .colors .cw{position:relative}
  .colors input[type=color]{width:44px;height:44px;border:1px solid var(--line);border-radius:9px;background:#08080b;cursor:pointer;padding:2px}
  .colors .rm{position:absolute;top:-7px;right:-7px;width:18px;height:18px;border-radius:50%;border:1px solid var(--line);background:#16161d;color:var(--ink-soft);font:700 11px var(--sans);cursor:pointer;line-height:1;display:flex;align-items:center;justify-content:center}
  .colors .rm:hover{border-color:#d57;color:#f88}
  .card .del{position:absolute;top:14px;left:14px;width:22px;height:22px;border-radius:50%;border:1px solid var(--line);background:rgba(20,20,26,.85);color:var(--muted);font:700 13px var(--sans);cursor:pointer;display:none;align-items:center;justify-content:center;z-index:3}
  .card.custom:hover .del{display:flex}
  .card .del:hover{border-color:#d57;color:#f88}
</style>
</head>
<body>
<header>
  <div class="wrap">
    <div class="eyebrow"><span class="dot"></span> Style Guides · wallco.ai · V1</div>
    <div class="head-row">
      <div>
        <h1>Brand Selector</h1>
        <div class="lede">Pick the houses that source the next generative build · Top 20 fashion · Top 10 wallcovering</div>
        <a class="ctl gold" href="/admin/styleguide-beverly-hills.html" style="margin-top:10px;display:inline-block;text-decoration:none">↗ Beverly Hills Palette</a>
      </div>
      <div class="counts">
        <div class="c"><div class="n" id="cnt-fashion">20</div><div class="l">Fashion</div></div>
        <div class="c"><div class="n" id="cnt-wall">10</div><div class="l">Wallcovering</div></div>
      </div>
    </div>
  </div>
</header>

<main class="wrap">
  <section>
    <div class="sec-top">
      <div>
        <div class="sec-no">I.</div>
        <div class="sec-h">Fashion</div>
        <div class="sec-sub">Top 20 — Luxury &amp; Iconic Houses</div>
      </div>
      <div class="controls">
        <button class="ctl" data-all="fashion">Select all 20</button>
        <button class="ctl" data-clear="fashion">Clear</button>
        <button class="ctl gold" data-top5="fashion">Top 5 only</button>
      </div>
    </div>
    <div class="grid" id="grid-fashion"><div class="skeleton">Loading houses…</div></div>
  </section>

  <section>
    <div class="sec-top">
      <div>
        <div class="sec-no">II.</div>
        <div class="sec-h">Wallcovering</div>
        <div class="sec-sub">Top 10 — Heritage &amp; Designer Houses</div>
      </div>
      <div class="controls">
        <button class="ctl" data-all="wallcovering">Select all 10</button>
        <button class="ctl" data-clear="wallcovering">Clear</button>
      </div>
    </div>
    <div class="grid" id="grid-wallcovering"></div>
  </section>

  <section>
    <div class="sec-top">
      <div>
        <div class="sec-no">III.</div>
        <div class="sec-h">Your Style Guides</div>
        <div class="sec-sub">Custom houses you've added — usable by Generate &amp; Apply</div>
      </div>
      <div class="controls">
        <button class="ctl gold" id="btn-add-sg">+ Add Style Guide</button>
        <button class="ctl" data-all="custom">Select all</button>
        <button class="ctl" data-clear="custom">Clear</button>
      </div>
    </div>
    <div class="grid" id="grid-custom"><div class="skeleton">No custom guides yet — click “+ Add Style Guide”.</div></div>
  </section>
</main>

<div class="bar">
  <div class="bar-in">
    <span class="selpill"><b id="sel-count">0</b> selected</span>
    <span class="selpill" id="quota-pill" title="Free paid renders left for you today (per IP)" style="display:none"></span>
    <span class="selnames" id="sel-names">No houses selected</span>
    <div class="actions">
      <button class="btn" id="btn-clear">Clear all</button>
      <button class="btn" id="btn-json">Preview JSON</button>
      <button class="btn" id="btn-export">Export</button>
      <button class="btn" id="btn-apply">Apply to Studio →</button>
      <button class="btn primary" id="btn-generate">Generate Designs →</button>
    </div>
  </div>
</div>

<!-- JSON modal -->
<div class="scrim" id="scrim-json">
  <div class="modal">
    <span class="x" data-close>×</span>
    <h3>Style-Guide Payload</h3>
    <div class="mh-sub">Portable JSON for the generator, Figma build, or studio recolor.</div>
    <pre id="json-out"></pre>
    <div class="modal-row">
      <button class="btn" id="btn-copy">Copy JSON</button>
      <button class="btn" id="btn-copy-figma">Copy for Figma</button>
      <button class="btn" id="btn-dl">Download .json</button>
    </div>
  </div>
</div>

<!-- Generate modal -->
<div class="scrim" id="scrim-gen">
  <div class="modal">
    <span class="x" data-close>×</span>
    <h3>Generate Designs</h3>
    <div class="mh-sub">Renders new seamless tiles seeded from the selected houses' palettes &amp; signatures. Each render passes the settlement + seam gates.</div>
    <pre id="gen-prompt"></pre>
    <div class="modal-row">
      <div class="field"><label>Count</label><input type="number" id="gen-n" value="4" min="1" max="50"></div>
      <button class="btn primary" id="btn-gen-go">Render batch →</button>
      <a class="btn" href="/designs" target="_blank" rel="noopener noreferrer">Open Studio ↗</a>
    </div>
    <div class="status" id="gen-status"></div>
  </div>
</div>

<!-- Apply to Studio modal -->
<div class="scrim" id="scrim-apply">
  <div class="modal">
    <span class="x" data-close>×</span>
    <h3>Apply to Studio</h3>
    <div class="mh-sub">Recolor an existing design into a selected house palette. Pick a house, then click a design.</div>
    <div class="brandsel" id="apply-brands"></div>
    <div class="picker" id="apply-picker"><div class="skeleton">Loading recent designs…</div></div>
    <div class="status" id="apply-status"></div>
    <div class="results" id="apply-results"></div>
  </div>
</div>

<!-- Add Style Guide modal -->
<div class="scrim" id="scrim-add">
  <div class="modal">
    <span class="x" data-close>×</span>
    <h3>Add a Style Guide</h3>
    <div class="mh-sub">Define a custom brand palette. It joins the selector above and becomes usable by <b>Generate</b> (seeds the prompt) and <b>Apply</b> (recolors a design).</div>
    <div class="addform">
      <div class="afrow">
        <div class="fld grow"><label>Name</label><input id="af-label" type="text" maxlength="60" placeholder="e.g. Mid-Century Lounge"></div>
        <div class="fld"><label>Category</label>
          <select id="af-cat"><option value="custom">Custom</option><option value="fashion">Fashion</option><option value="wallcovering">Wallcovering</option></select>
        </div>
      </div>
      <div class="afrow">
        <div class="fld"><label>Country</label><input id="af-country" type="text" maxlength="4" placeholder="US"></div>
        <div class="fld"><label>Est.</label><input id="af-est" type="number" min="0" placeholder="1960"></div>
      </div>
      <div class="fld"><label>Signature</label><input id="af-sig" type="text" maxlength="200" placeholder="warm walnut &amp; ochre, atomic-era restraint"></div>
      <div class="fld"><label>Colors (1–6)</label>
        <div class="colors" id="af-colors"></div>
        <button class="ctl" id="af-addcolor" type="button" style="align-self:flex-start">+ color</button>
      </div>
    </div>
    <div class="modal-row">
      <button class="btn primary" id="btn-add-save">Save Style Guide</button>
    </div>
    <div class="status" id="add-status"></div>
  </div>
</div>

<div class="toast" id="toast"></div>

<script>
(function(){
  // ── admin-token passthrough (page may be opened with ?admin=<token>) ──────
  const QS = new URLSearchParams(location.search);
  const ADMIN = QS.get('admin') || '';
  const authd = (p) => ADMIN ? p + (p.includes('?') ? '&' : '?') + 'admin=' + encodeURIComponent(ADMIN) : p;
  // X-SG marks this page's paid calls so the server opens recolor to the
  // public here (capped) without opening it on the main storefront.
  const jfetch = (p, opts={}) => {
    const headers = Object.assign({ 'X-SG':'1' }, opts.headers || {});
    return fetch(authd(p), Object.assign({ credentials:'same-origin' }, opts, { headers }));
  };

  const state = { brands: { fashion: [], wallcovering: [], custom: [] }, sel: new Set(), quota: { admin:false, remaining:5, cap:5 } };
  const byKey = {};

  const $ = (s, r=document) => r.querySelector(s);
  const el = (t, c) => { const e=document.createElement(t); if(c)e.className=c; return e; };
  let toastT;
  function toast(msg){ const t=$('#toast'); t.textContent=msg; t.classList.add('on'); clearTimeout(toastT); toastT=setTimeout(()=>t.classList.remove('on'),2600); }

  // ── render cards ──────────────────────────────────────────────────────────
  const esc = s => String(s==null?'':s).replace(/[&<>"]/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]));
  function cardFor(b){
    const c = el('div','card'); c.dataset.key=b.key;
    if(b.custom) c.classList.add('custom');
    const dots = b.hex.slice(0,3).map(h=>`<span class="dot" style="background:${esc(h)}"></span>`).join('');
    const del = b.custom ? `<button class="del" title="Delete this style guide">×</button>` : '';
    c.innerHTML = del +
      `<div class="tick"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12l4.5 4.5L19 7" stroke="#1a1505" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>`+
      `<div class="name">${esc(b.label)}</div>`+
      `<div class="meta"><span class="chip">${esc(b.country||'—')}</span><span class="chip">est. ${esc(b.est||'—')}</span></div>`+
      `<div class="dots">${dots}</div>`;
    c.addEventListener('click',()=>toggle(b.key,c));
    if(b.custom){ const d=c.querySelector('.del'); d.addEventListener('click',e=>{ e.stopPropagation(); deleteGuide(b.key,b.label); }); }
    return c;
  }
  function renderGrids(){
    const gf=$('#grid-fashion'), gw=$('#grid-wallcovering'), gc=$('#grid-custom');
    gf.innerHTML=''; gw.innerHTML=''; gc.innerHTML='';
    state.brands.fashion.forEach(b=>gf.appendChild(cardFor(b)));
    state.brands.wallcovering.forEach(b=>gw.appendChild(cardFor(b)));
    if(state.brands.custom.length) state.brands.custom.forEach(b=>gc.appendChild(cardFor(b)));
    else gc.innerHTML='<div class="skeleton">No custom guides yet — click “+ Add Style Guide”.</div>';
    $('#cnt-fashion').textContent=state.brands.fashion.length;
    $('#cnt-wall').textContent=state.brands.wallcovering.length;
  }
  function cardEl(key){ return document.querySelector(`.card[data-key="${key}"]`); }

  function toggle(key,c){ c=c||cardEl(key); if(!c) return;
    if(state.sel.has(key)){state.sel.delete(key);c.classList.remove('sel');}
    else{state.sel.add(key);c.classList.add('sel');}
    syncBar();
  }
  function setSel(key,on){ const c=cardEl(key); if(!c)return; if(on){state.sel.add(key);c.classList.add('sel');}else{state.sel.delete(key);c.classList.remove('sel');} }

  function syncBar(){
    const n=state.sel.size;
    $('#sel-count').textContent=n;
    const names=[...state.sel].map(k=>byKey[k]&&byKey[k].label).filter(Boolean);
    $('#sel-names').textContent = n? names.join(' · ') : 'No houses selected';
    const noQuota = !state.quota.admin && (state.quota.remaining|0) <= 0;
    $('#btn-json').disabled = n===0;
    $('#btn-export').disabled = n===0;
    $('#btn-apply').disabled = n===0 || noQuota;
    $('#btn-generate').disabled = n===0 || noQuota;
    $('#btn-generate').textContent = noQuota ? 'Daily limit reached' : 'Generate Designs →';
  }
  function setQuota(q){ if(q) state.quota=q; renderQuota(); syncBar(); }
  function renderQuota(){
    const q=state.quota, pill=$('#quota-pill'); pill.style.display='';
    if(q.admin){ pill.innerHTML='✦ Admin · unlimited'; pill.style.color='var(--gold)'; return; }
    const r=Math.max(0,q.remaining|0);
    pill.innerHTML='<b style="color:'+(r?'var(--gold)':'#d57')+'">'+r+'</b> / '+q.cap+' free renders left';
  }
  async function refreshQuota(){
    try{ const r=await jfetch('/api/styleguides/quota'); const j=await r.json();
      if(j.ok) setQuota({admin:!!j.admin, remaining:j.unlimited?Infinity:j.remaining, cap:j.cap}); }catch(e){}
  }

  // ── payload ───────────────────────────────────────────────────────────────
  function selected(){ return [...state.sel].map(k=>byKey[k]).filter(Boolean); }
  function payload(){
    const houses=selected();
    const colors=[]; houses.forEach(h=>h.hex.slice(0,2).forEach(x=>{ if(!colors.includes(x)) colors.push(x); }));
    return {
      version:'wallco-styleguide-v1',
      generated_at:new Date().toISOString(),
      count:houses.length,
      houses:houses.map(h=>({key:h.key,label:h.label,category:h.cat,country:h.country,est:h.est,hex:h.hex,signature:h.signature})),
      combined_palette:colors,
      signatures:houses.map(h=>h.signature)
    };
  }
  function genPrompt(){
    const houses=selected();
    const colors=[]; houses.forEach(h=>h.hex.slice(0,2).forEach(x=>{ if(!colors.includes(x)) colors.push(x); }));
    const sigs=houses.map(h=>h.signature).join('; ');
    return {
      n: parseInt($('#gen-n').value,10)||4,
      colors: colors.slice(0,6).join(', '),
      patterns: houses.map(h=>({artist:h.label, title:h.label+' signature', dominant_hex:h.hex[0]})),
      styles: [],
      extra_text: 'Drawing on the house signatures of '+houses.map(h=>h.label).join(', ')+': '+sigs+'. Tone-on-tone, archival, no more than 4 colors.'
    };
  }

  // ── controls ────────────────────────────────────────────────────────────
  document.querySelectorAll('[data-all]').forEach(b=>b.addEventListener('click',()=>{ state.brands[b.dataset.all].forEach(x=>setSel(x.key,true)); syncBar(); }));
  document.querySelectorAll('[data-clear]').forEach(b=>b.addEventListener('click',()=>{ state.brands[b.dataset.clear].forEach(x=>setSel(x.key,false)); syncBar(); }));
  document.querySelectorAll('[data-top5]').forEach(b=>b.addEventListener('click',()=>{ const cat=b.dataset.top5; state.brands[cat].forEach(x=>setSel(x.key,false)); state.brands[cat].slice(0,5).forEach(x=>setSel(x.key,true)); syncBar(); }));
  $('#btn-clear').addEventListener('click',()=>{ state.sel.forEach(k=>{const c=cardEl(k);if(c)c.classList.remove('sel');}); state.sel.clear(); syncBar(); });

  // ── modals ──────────────────────────────────────────────────────────────
  function openModal(id){ $('#'+id).classList.add('on'); }
  function closeModal(s){ s.classList.remove('on'); }
  document.querySelectorAll('[data-close]').forEach(x=>x.addEventListener('click',e=>closeModal(e.target.closest('.scrim'))));
  document.querySelectorAll('.scrim').forEach(s=>s.addEventListener('click',e=>{ if(e.target===s) closeModal(s); }));
  document.addEventListener('keydown',e=>{ if(e.key==='Escape') document.querySelectorAll('.scrim.on').forEach(closeModal); });

  // Preview JSON
  $('#btn-json').addEventListener('click',()=>{ $('#json-out').textContent=JSON.stringify(payload(),null,2); openModal('scrim-json'); });
  $('#btn-copy').addEventListener('click',()=>{ navigator.clipboard.writeText(JSON.stringify(payload(),null,2)); toast('JSON copied'); });
  $('#btn-copy-figma').addEventListener('click',()=>{
    const p=payload();
    const txt='wallco.ai style guide — '+p.count+' houses\n'+p.houses.map(h=>`${h.label} (${h.country} · est. ${h.est}) — ${h.hex.join(' ')} — ${h.signature}`).join('\n');
    navigator.clipboard.writeText(txt); toast('Figma block copied');
  });
  $('#btn-export').addEventListener('click',()=>dlJson());
  $('#btn-dl').addEventListener('click',()=>dlJson());
  function dlJson(){
    const blob=new Blob([JSON.stringify(payload(),null,2)],{type:'application/json'});
    const a=el('a'); a.href=URL.createObjectURL(blob); a.download='wallco-styleguide-'+Date.now()+'.json'; a.click();
    setTimeout(()=>URL.revokeObjectURL(a.href),2000); toast('Downloaded');
  }

  // Generate
  $('#btn-generate').addEventListener('click',()=>{
    const p=genPrompt();
    $('#gen-prompt').textContent='palette: '+(p.colors||'(none)')+'\nlineage: '+p.patterns.map(x=>x.artist).join(', ')+'\n\n'+p.extra_text;
    $('#gen-status').textContent=''; openModal('scrim-gen');
  });
  $('#btn-gen-go').addEventListener('click',async()=>{
    const go=$('#btn-gen-go'); go.disabled=true;
    const p=genPrompt(); p.n=parseInt($('#gen-n').value,10)||4;
    $('#gen-status').textContent='Kicking off render of '+p.n+' design'+(p.n>1?'s':'')+'…';
    try{
      const r=await jfetch('/api/generator/batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});
      const j=await r.json();
      if(!j.ok) throw new Error(j.error||'failed');
      if(typeof j.remaining==='number') setQuota({admin:false,remaining:j.remaining,cap:state.quota.cap}); else refreshQuota();
      $('#gen-status').innerHTML='✓ Batch <b>#'+j.run_id+'</b> running ('+j.count+' image'+(j.count>1?'s':'')+') — '+(j.note||'')+' <a href="/designs" target="_blank" rel="noopener noreferrer" style="color:var(--gold)">watch the Studio ↗</a>';
      toast('Batch #'+j.run_id+' started');
    }catch(e){ $('#gen-status').textContent='✗ '+e.message; refreshQuota(); }
    finally{ go.disabled=false; }
  });

  // Apply to Studio
  let applyBrand=null;
  $('#btn-apply').addEventListener('click',async()=>{
    const houses=selected();
    const bs=$('#apply-brands'); bs.innerHTML='';
    applyBrand=houses[0]?houses[0].key:null;
    houses.forEach(h=>{ const b=el('button'); b.textContent=h.label; b.dataset.key=h.key; if(h.key===applyBrand)b.classList.add('on');
      b.addEventListener('click',()=>{ applyBrand=h.key; bs.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x.dataset.key===h.key)); });
      bs.appendChild(b); });
    $('#apply-results').innerHTML=''; $('#apply-status').textContent='';
    openModal('scrim-apply');
    const pk=$('#apply-picker'); pk.innerHTML='<div class="skeleton">Loading recent designs…</div>';
    try{
      const r=await jfetch('/api/designs/random?n=12'); const j=await r.json();
      const items=(j.items||j||[]); pk.innerHTML='';
      items.forEach(d=>{ const div=el('div','pi'); const img=el('img'); img.src=d.image_url; img.title='#'+d.id+(d.title?' · '+d.title:''); img.loading='lazy';
        img.addEventListener('click',()=>recolor(d.id)); div.appendChild(img); pk.appendChild(div); });
      if(!items.length) pk.innerHTML='<div class="skeleton">No designs available.</div>';
    }catch(e){ pk.innerHTML='<div class="skeleton">Failed to load: '+e.message+'</div>'; }
  });
  async function recolor(id){
    if(!applyBrand){ toast('Pick a house first'); return; }
    const label=byKey[applyBrand].label;
    $('#apply-status').textContent='→ Recoloring #'+id+' into '+label+'…';
    try{
      const r=await jfetch('/api/design/'+id+'/recolor',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({brand:applyBrand})});
      const j=await r.json();
      if(!j.ok) throw new Error(j.error||'failed');
      if(typeof j.remaining==='number') setQuota({admin:false,remaining:j.remaining,cap:state.quota.cap}); else refreshQuota();
      const div=el('div','r');
      div.innerHTML='<a href="/design/'+j.new_id+'" target="_blank" rel="noopener noreferrer"><img src="'+j.new_image_url+'"></a><div class="cap">'+label+' · #'+j.new_id+'</div>';
      $('#apply-results').prepend(div);
      $('#apply-status').textContent='✓ #'+id+' → '+label+' (#'+j.new_id+')';
      toast('Recolored → #'+j.new_id);
    }catch(e){ $('#apply-status').textContent='✗ '+e.message; refreshQuota(); }
  }

  // ── add / delete custom style guides ───────────────────────────────────────
  const colorsWrap = () => $('#af-colors');
  function addColorInput(val){
    if(colorsWrap().children.length>=6){ toast('Max 6 colors'); return; }
    const wrap=el('div','cw');
    const inp=el('input'); inp.type='color'; inp.value=val||'#8a7f6a';
    const rm=el('button','rm'); rm.type='button'; rm.textContent='×';
    rm.addEventListener('click',()=>{ if(colorsWrap().children.length>1) wrap.remove(); else toast('Need at least one color'); });
    wrap.appendChild(inp); wrap.appendChild(rm); colorsWrap().appendChild(wrap);
  }
  function resetAddForm(){
    ['af-label','af-country','af-sig'].forEach(id=>{const e=$('#'+id); if(e)e.value='';});
    $('#af-est').value=''; $('#af-cat').value='custom'; $('#add-status').textContent='';
    colorsWrap().innerHTML=''; addColorInput('#3a3a44'); addColorInput('#c8a96a'); addColorInput('#f3ecdd');
  }
  $('#btn-add-sg').addEventListener('click',()=>{ resetAddForm(); openModal('scrim-add'); setTimeout(()=>$('#af-label').focus(),50); });
  $('#af-addcolor').addEventListener('click',()=>addColorInput());
  $('#btn-add-save').addEventListener('click',async()=>{
    const label=$('#af-label').value.trim();
    if(!label){ $('#add-status').textContent='✗ Name is required'; $('#af-label').focus(); return; }
    const hex=[...colorsWrap().querySelectorAll('input[type=color]')].map(i=>i.value.toUpperCase());
    const body={ label, cat:$('#af-cat').value, country:$('#af-country').value.trim(),
                 est:$('#af-est').value.trim(), signature:$('#af-sig').value.trim(), hex };
    const save=$('#btn-add-save'); save.disabled=true; $('#add-status').textContent='Saving…';
    try{
      const r=await jfetch('/api/styleguides/add',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
      const j=await r.json();
      if(!j.ok) throw new Error(j.error||'failed');
      await loadBrands();
      const k=j.guide&&j.guide.key; if(k) setSel(k,true), syncBar();
      closeModal($('#scrim-add'));
      toast((j.updated?'Updated':'Added')+' “'+label+'”');
    }catch(e){ $('#add-status').textContent='✗ '+e.message; }
    finally{ save.disabled=false; }
  });
  async function deleteGuide(key,label){
    if(!confirm('Delete style guide “'+label+'”? This cannot be undone.')) return;
    try{
      const r=await jfetch('/api/styleguides/'+encodeURIComponent(key),{method:'DELETE'});
      const j=await r.json();
      if(!j.ok) throw new Error(j.error||'failed');
      state.sel.delete(key); await loadBrands(); syncBar();
      toast('Deleted “'+label+'”');
    }catch(e){ toast('✗ '+e.message); }
  }

  // ── load brands (built-ins + custom); preserves current selection ──────────
  async function loadBrands(){
    const r=await jfetch('/api/styleguides/brands'); const j=await r.json();
    state.brands.fashion=j.fashion||[]; state.brands.wallcovering=j.wallcovering||[]; state.brands.custom=j.custom||[];
    Object.keys(byKey).forEach(k=>delete byKey[k]);
    [...state.brands.fashion,...state.brands.wallcovering,...state.brands.custom].forEach(b=>byKey[b.key]=b);
    renderGrids();
    // re-apply selection classes for keys that still exist; drop the rest
    [...state.sel].forEach(k=>{ if(byKey[k]){ const c=cardEl(k); if(c)c.classList.add('sel'); } else state.sel.delete(k); });
    syncBar();
  }

  // ── boot ──────────────────────────────────────────────────────────────────
  (async function(){
    try{ await loadBrands(); refreshQuota(); }
    catch(e){ $('#grid-fashion').innerHTML='<div class="skeleton">Failed to load houses: '+e.message+'</div>'; }
  })();
})();
</script>
</body>
</html>