[object Object]

← back to Wallco Ai

feat(styleguides): Brand Selector admin tool wired to generator + studio recolor

0fbb86a497306b64073a1f47bf2825c3b0eec932 · 2026-06-01 11:11:28 -0700 · Steve Abrams

- public/admin/styleguides.html: faithful dark-luxe rebuild of the Paper
  'Brand Selector' artboard — 20 fashion + 10 wallcovering houses, selectable
  cards w/ country/est chips + palette dots, select-all/clear/top-5 controls,
  selection footer.
- 3 live actions: Generate (POST /api/generator/batch seeded by selected
  houses), Apply to Studio (POST /api/design/:id/recolor), Export JSON/Figma.
- scripts/fashion_palettes.js: +country/est/cat; +8 fashion + 10 wallcovering
  houses so all 30 cards drive real generation/recolor.
- server.js: GET /admin/styleguides (admin), GET /api/styleguides/brands,
  host-aware '/' so the styleguides.wallco.ai vhost root serves the gated page.

Files touched

Diff

commit 0fbb86a497306b64073a1f47bf2825c3b0eec932
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 11:11:28 2026 -0700

    feat(styleguides): Brand Selector admin tool wired to generator + studio recolor
    
    - public/admin/styleguides.html: faithful dark-luxe rebuild of the Paper
      'Brand Selector' artboard — 20 fashion + 10 wallcovering houses, selectable
      cards w/ country/est chips + palette dots, select-all/clear/top-5 controls,
      selection footer.
    - 3 live actions: Generate (POST /api/generator/batch seeded by selected
      houses), Apply to Studio (POST /api/design/:id/recolor), Export JSON/Figma.
    - scripts/fashion_palettes.js: +country/est/cat; +8 fashion + 10 wallcovering
      houses so all 30 cards drive real generation/recolor.
    - server.js: GET /admin/styleguides (admin), GET /api/styleguides/brands,
      host-aware '/' so the styleguides.wallco.ai vhost root serves the gated page.
---
 public/admin/styleguides.html | 393 ++++++++++++++++++++++++++++++++++++++++++
 scripts/fashion_palettes.js   |  61 +++++--
 server.js                     |  38 ++++
 3 files changed, 473 insertions(+), 19 deletions(-)

diff --git a/public/admin/styleguides.html b/public/admin/styleguides.html
new file mode 100644
index 0000000..c71ff57
--- /dev/null
+++ b/public/admin/styleguides.html
@@ -0,0 +1,393 @@
+<!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}
+</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>
+      </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>
+</main>
+
+<div class="bar">
+  <div class="bar-in">
+    <span class="selpill"><b id="sel-count">0</b> selected</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>
+
+<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;
+  const jfetch = (p, opts) => fetch(authd(p), Object.assign({ credentials:'same-origin' }, opts || {}));
+
+  const state = { brands: { fashion: [], wallcovering: [] }, sel: new Set() };
+  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 ──────────────────────────────────────────────────────────
+  function cardFor(b){
+    const c = el('div','card'); c.dataset.key=b.key;
+    const dots = b.hex.slice(0,3).map(h=>`<span class="dot" style="background:${h}"></span>`).join('');
+    c.innerHTML =
+      `<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">${b.label}</div>`+
+      `<div class="meta"><span class="chip">${b.country||'—'}</span><span class="chip">est. ${b.est||'—'}</span></div>`+
+      `<div class="dots">${dots}</div>`;
+    c.addEventListener('click',()=>toggle(b.key,c));
+    return c;
+  }
+  function renderGrids(){
+    const gf=$('#grid-fashion'), gw=$('#grid-wallcovering');
+    gf.innerHTML=''; gw.innerHTML='';
+    state.brands.fashion.forEach(b=>gf.appendChild(cardFor(b)));
+    state.brands.wallcovering.forEach(b=>gw.appendChild(cardFor(b)));
+    $('#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';
+    ['btn-json','btn-export','btn-apply','btn-generate'].forEach(id=>$('#'+id).disabled = n===0);
+  }
+
+  // ── 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');
+      $('#gen-status').innerHTML='✓ Batch <b>#'+j.run_id+'</b> running — '+(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; }
+    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');
+      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; }
+  }
+
+  // ── boot ──────────────────────────────────────────────────────────────────
+  (async function(){
+    try{
+      const r=await jfetch('/api/styleguides/brands'); const j=await r.json();
+      state.brands.fashion=j.fashion||[]; state.brands.wallcovering=j.wallcovering||[];
+      [...state.brands.fashion,...state.brands.wallcovering].forEach(b=>byKey[b.key]=b);
+      renderGrids(); syncBar();
+    }catch(e){ $('#grid-fashion').innerHTML='<div class="skeleton">Failed to load houses: '+e.message+'</div>'; }
+  })();
+})();
+</script>
+</body>
+</html>
diff --git a/scripts/fashion_palettes.js b/scripts/fashion_palettes.js
index c65fdd0..9639458 100644
--- a/scripts/fashion_palettes.js
+++ b/scripts/fashion_palettes.js
@@ -22,25 +22,48 @@ const path = require('path');
 // categories pick only from the tone-on-tone subset; figure/scenic categories
 // (drunk-animals, murals) may use any palette since they skip the seam check.
 const FASHION_PALETTES = [
-  { brand: 'Hermès',          hex: ['#FF4F00', '#1a1410', '#FFFEFA', '#E5DDD0'], signature: 'burnt-orange accent, saddle-stitch warmth, generous ground', toneOnTone: false },
-  { brand: 'Bottega Veneta',  hex: ['#107A3B', '#0e1311', '#F4F0E6', '#1d2a23'], signature: 'BV green, woven intrecciato, craft-forward',                  toneOnTone: true },
-  { brand: 'Prada',           hex: ['#000000', '#FF0000', '#F5F2EB'],            signature: 'cream ground, scarlet accent, scientific austerity',          toneOnTone: false },
-  { brand: 'Saint Laurent',   hex: ['#000000', '#FFFFFF'],                       signature: 'pure monochrome, sharp ultra-minimal',                        toneOnTone: false },
-  { brand: 'Dior',            hex: ['#1A1A1A', '#FAF9F6', '#A98B5B', '#D8D2C5'], signature: 'refined gray, soft gold accent, couture restraint',           toneOnTone: true },
-  { brand: 'Chanel',          hex: ['#000000', '#FFFFFF'],                       signature: 'timeless black & white, high-contrast hierarchy',             toneOnTone: false },
-  { brand: 'Gucci',           hex: ['#006837', '#A41E1A', '#FFFEF7', '#1a1a1a'], signature: 'green & red maximalism, ornate, warm ivory ground',           toneOnTone: false },
-  { brand: 'Celine',          hex: ['#000000', '#FFFFFF', '#E0E0E0'],            signature: 'minimalist gallery monochrome, sharp',                        toneOnTone: false },
-  { brand: 'Loewe',           hex: ['#9B7B4F', '#F2EDE2', '#2C1F0F', '#C9BFA7'], signature: 'soft cream, hand-craft warmth, airy',                         toneOnTone: true },
-  { brand: 'Valentino',       hex: ['#D70000', '#FFFFFF', '#000000'],            signature: 'saturated romantic red, classical drama',                     toneOnTone: false },
-  { brand: 'Toteme',          hex: ['#1A1A1A', '#F5F2ED', '#D5CFC4'],            signature: 'quiet restrained neutral, understated',                       toneOnTone: true },
-  { brand: 'Khaite',          hex: ['#3A3530', '#F2EFE9', '#C9BFB4'],            signature: 'refined warm neutral, soft luxury',                           toneOnTone: true },
-  { brand: 'Balenciaga',      hex: ['#000000', '#F5F5F5', '#FF4F00'],            signature: 'brutalist, anti-luxury, oversized orange jolt',               toneOnTone: false },
-  { brand: 'Burberry',        hex: ['#A47C5A', '#FFFFFF', '#000000'],            signature: 'heritage tan, check-pattern motif, clean',                    toneOnTone: true },
-  { brand: 'Miu Miu',         hex: ['#FFB6C1', '#FFF8F2', '#000000', '#FFCFD8'], signature: 'playful editorial pink, magazine warmth',                     toneOnTone: false },
-  { brand: 'Jacquemus',       hex: ['#F4D24F', '#FFFCF5', '#1a1a1a', '#F2E5A8'], signature: 'sun-yellow accent, French Riviera, cinematic',                toneOnTone: false },
-  { brand: 'Skims',           hex: ['#C9B8A4', '#F5EFE6', '#3D352B', '#E8DDC9'], signature: 'nude flesh tones, soft conversion-led calm',                  toneOnTone: true },
-  { brand: 'Gymshark',        hex: ['#000000', '#1B5E20', '#FFFFFF'],            signature: 'bold black, deep-green energy, high contrast',                toneOnTone: false },
-  { brand: 'Phoebe Philo',    hex: ['#000000', '#F5F5F0', '#D8D2C5'],            signature: 'typography-led minimal, refined off-white',                   toneOnTone: true },
+  // ── Fashion houses ───────────────────────────────────────────────────────
+  { brand: 'Louis Vuitton',   country:'FR', est:1854, cat:'fashion', hex: ['#5A4632', '#C8A96A', '#F3ECDD', '#1a1a1a'], signature: 'monogram brown & tan, trunk-maker heritage, gilded restraint', toneOnTone: true },
+  { brand: 'Chanel',          country:'FR', est:1910, cat:'fashion', hex: ['#000000', '#FFFFFF'],                       signature: 'timeless black & white, high-contrast hierarchy',             toneOnTone: false },
+  { brand: 'Hermès',          country:'FR', est:1837, cat:'fashion', hex: ['#FF4F00', '#1a1410', '#FFFEFA', '#E5DDD0'], signature: 'burnt-orange accent, saddle-stitch warmth, generous ground',  toneOnTone: false },
+  { brand: 'Gucci',           country:'IT', est:1921, cat:'fashion', hex: ['#006837', '#A41E1A', '#FFFEF7', '#1a1a1a'], signature: 'green & red maximalism, ornate, warm ivory ground',           toneOnTone: false },
+  { brand: 'Prada',           country:'IT', est:1913, cat:'fashion', hex: ['#000000', '#FF0000', '#F5F2EB'],            signature: 'cream ground, scarlet accent, scientific austerity',          toneOnTone: false },
+  { brand: 'Dior',            country:'FR', est:1946, cat:'fashion', hex: ['#1A1A1A', '#FAF9F6', '#A98B5B', '#D8D2C5'], signature: 'refined gray, soft gold accent, couture restraint',           toneOnTone: true },
+  { brand: 'Versace',         country:'IT', est:1978, cat:'fashion', hex: ['#D4AF37', '#0a0a0a', '#A41E1A', '#F5EFE0'], signature: 'baroque gold on black, Medusa opulence, Greek-key maximalism', toneOnTone: false },
+  { brand: 'Burberry',        country:'UK', est:1856, cat:'fashion', hex: ['#A47C5A', '#FFFFFF', '#000000'],            signature: 'heritage tan, check-pattern motif, clean',                    toneOnTone: true },
+  { brand: 'Saint Laurent',   country:'FR', est:1961, cat:'fashion', hex: ['#000000', '#FFFFFF'],                       signature: 'pure monochrome, sharp ultra-minimal',                        toneOnTone: false },
+  { brand: 'Balenciaga',      country:'ES', est:1919, cat:'fashion', hex: ['#000000', '#F5F5F5', '#FF4F00'],            signature: 'brutalist, anti-luxury, oversized orange jolt',               toneOnTone: false },
+  { brand: 'Bottega Veneta',  country:'IT', est:1966, cat:'fashion', hex: ['#107A3B', '#0e1311', '#F4F0E6', '#1d2a23'], signature: 'BV green, woven intrecciato, craft-forward',                  toneOnTone: true },
+  { brand: 'Fendi',           country:'IT', est:1925, cat:'fashion', hex: ['#6B4F3A', '#2A211A', '#C9B79C', '#F2EADB'], signature: 'tobacco brown & cream, Roman pellicceria heritage',           toneOnTone: true },
+  { brand: 'Givenchy',        country:'FR', est:1952, cat:'fashion', hex: ['#0a0a0a', '#C8A24A', '#A41E1A', '#F4F1EA'], signature: 'parisian black with gold & oxblood, couture geometry',        toneOnTone: false },
+  { brand: 'Valentino',       country:'IT', est:1960, cat:'fashion', hex: ['#D70000', '#FFFFFF', '#000000'],            signature: 'saturated romantic red, classical drama',                     toneOnTone: false },
+  { brand: 'Armani',          country:'IT', est:1975, cat:'fashion', hex: ['#1C2A3A', '#8A8A86', '#15151A', '#E6E2DA'], signature: 'Armani greige & midnight navy, fluid minimal tailoring',      toneOnTone: true },
+  { brand: 'Ralph Lauren',    country:'US', est:1967, cat:'fashion', hex: ['#1B2A4A', '#9E1B32', '#F3ECDD', '#1a1a1a'], signature: 'ivy-league navy & polo crimson, equestrian heritage cream',   toneOnTone: false },
+  { brand: 'Tom Ford',        country:'US', est:2005, cat:'fashion', hex: ['#0a0a0a', '#6B4A2E', '#B08D4F', '#EDE6D8'], signature: 'glossy black, bronze & cognac, sensual modern glamour',       toneOnTone: true },
+  { brand: 'Celine',          country:'FR', est:1945, cat:'fashion', hex: ['#000000', '#FFFFFF', '#E0E0E0'],            signature: 'minimalist gallery monochrome, sharp',                        toneOnTone: false },
+  { brand: 'Loewe',           country:'ES', est:1846, cat:'fashion', hex: ['#9B7B4F', '#F2EDE2', '#2C1F0F', '#C9BFA7'], signature: 'soft cream, hand-craft warmth, airy',                         toneOnTone: true },
+  { brand: 'Alexander McQueen', country:'UK', est:1992, cat:'fashion', hex: ['#0a0a0a', '#7E1416', '#8C8C90', '#EDEAE3'], signature: 'gothic black & blood-red, romantic darkness, bone-grey',     toneOnTone: false },
+
+  // ── Extra fashion houses (palette pool only — not on the top-20 selector) ─
+  { brand: 'Toteme',          country:'SE', est:2014, cat:'fashion', hex: ['#1A1A1A', '#F5F2ED', '#D5CFC4'],            signature: 'quiet restrained neutral, understated',                       toneOnTone: true },
+  { brand: 'Khaite',          country:'US', est:2016, cat:'fashion', hex: ['#3A3530', '#F2EFE9', '#C9BFB4'],            signature: 'refined warm neutral, soft luxury',                           toneOnTone: true },
+  { brand: 'Miu Miu',         country:'IT', est:1993, cat:'fashion', hex: ['#FFB6C1', '#FFF8F2', '#000000', '#FFCFD8'], signature: 'playful editorial pink, magazine warmth',                     toneOnTone: false },
+  { brand: 'Jacquemus',       country:'FR', est:2009, cat:'fashion', hex: ['#F4D24F', '#FFFCF5', '#1a1a1a', '#F2E5A8'], signature: 'sun-yellow accent, French Riviera, cinematic',                toneOnTone: false },
+  { brand: 'Skims',           country:'US', est:2019, cat:'fashion', hex: ['#C9B8A4', '#F5EFE6', '#3D352B', '#E8DDC9'], signature: 'nude flesh tones, soft conversion-led calm',                  toneOnTone: true },
+  { brand: 'Gymshark',        country:'UK', est:2012, cat:'fashion', hex: ['#000000', '#1B5E20', '#FFFFFF'],            signature: 'bold black, deep-green energy, high contrast',                toneOnTone: false },
+  { brand: 'Phoebe Philo',    country:'UK', est:2023, cat:'fashion', hex: ['#000000', '#F5F5F0', '#D8D2C5'],            signature: 'typography-led minimal, refined off-white',                   toneOnTone: true },
+
+  // ── Wallcovering / heritage-textile houses ────────────────────────────────
+  { brand: 'Phillip Jeffries', country:'US', est:1976, cat:'wallcovering', hex: ['#C9B08A', '#6E563B', '#F1E9D8', '#A8916E'], signature: 'natural grasscloth — flax, sand & raffia neutrals, textural luxury', toneOnTone: true },
+  { brand: 'Schumacher',      country:'US', est:1889, cat:'wallcovering', hex: ['#A8322E', '#2E6B4F', '#C8A24A', '#F4ECDB'], signature: 'archival americana — coral red, jade & antique-gold chintz',         toneOnTone: false },
+  { brand: 'Cole & Son',      country:'UK', est:1875, cat:'wallcovering', hex: ['#1F3A52', '#C2A878', '#F2EBDA', '#2C3B30'], signature: 'english heritage — ink-navy & parchment, woods & hummingbird motifs', toneOnTone: true },
+  { brand: 'Thibaut',         country:'US', est:1886, cat:'wallcovering', hex: ['#3E6B4A', '#CDBE9A', '#F2ECDC', '#2A3A2C'], signature: 'classic botanical — leaf-green & wheat, american traditional',       toneOnTone: true },
+  { brand: 'Zoffany',         country:'UK', est:1895, cat:'wallcovering', hex: ['#6B4A33', '#B79A72', '#5E6E4E', '#EFE7D6'], signature: 'edwardian opulence — chestnut, gilt-sage & antique cream',           toneOnTone: true },
+  { brand: 'Farrow & Ball',   country:'UK', est:1946, cat:'wallcovering', hex: ['#2E5A55', '#EDE7D8', '#7A6A56', '#9FB0A6'], signature: 'chalky archival paint tones — inchyra teal, off-white & clay',       toneOnTone: true },
+  { brand: 'Brunschwig & Fils', country:'US', est:1909, cat:'wallcovering', hex: ['#A8322E', '#2E4A6B', '#C9B189', '#F2EBDA'], signature: 'document-print classicism — lacquer red, delft blue & linen',      toneOnTone: false },
+  { brand: 'de Gournay',      country:'UK', est:1986, cat:'wallcovering', hex: ['#C2A878', '#6E563B', '#5E7E5E', '#F1E9D8'], signature: 'hand-painted chinoiserie — celadon, tea-stain & gilt, scenic luxe', toneOnTone: true },
+  { brand: 'Fromental',       country:'UK', est:2005, cat:'wallcovering', hex: ['#274A6B', '#C8A24A', '#6B4A33', '#EEE6D4'], signature: 'embroidered silk panels — sapphire, antique gold & espresso',        toneOnTone: false },
+  { brand: 'Pierre Frey',     country:'FR', est:1935, cat:'wallcovering', hex: ['#B5322E', '#C8A24A', '#2E4A6B', '#F4ECDB'], signature: 'french painterly maximalism — vermilion, ochre & cobalt',            toneOnTone: false },
 ];
 
 // pickFashionPalette({ toneOnToneOnly: true }) restricts to the soft/archival
diff --git a/server.js b/server.js
index 4287b22..d106449 100644
--- a/server.js
+++ b/server.js
@@ -1128,6 +1128,44 @@ setInterval(() => load(true), 30000);
 // check as a CRITICAL bypass (anyone could mint a token with {role:"admin"}).
 const { isAdmin, requireAdmin } = require('./src/admin-gate');
 
+// ── Style Guides — Brand Selector (admin) ──────────────────────────────────
+// A curated picker of fashion + wallcovering houses whose palettes/signatures
+// seed the generative pipeline. Three live actions on the page:
+//   • Generate  → POST /api/generator/batch (new SDXL designs in the houses' look)
+//   • Apply     → POST /api/design/:id/recolor (restyle an existing studio design)
+//   • Export    → portable style-guide JSON / Figma block (client-side)
+// Lives behind the same admin gate as the curators. Reachable two ways:
+//   /admin/styleguides  AND  the bare root of the styleguides.wallco.ai vhost.
+app.get('/', (req, res, next) => {
+  if (/^styleguides\./i.test(req.hostname || '')) {
+    if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+    return res.sendFile(path.join(__dirname, 'public', 'admin', 'styleguides.html'));
+  }
+  next();
+});
+app.get('/admin/styleguides', (req, res) => {
+  if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+  res.sendFile(path.join(__dirname, 'public', 'admin', 'styleguides.html'));
+});
+app.get('/api/styleguides/brands', (req, res) => {
+  try {
+    const { FASHION_PALETTES } = require('./scripts/fashion_palettes.js');
+    // Same kebab transform RECOLOR_BRANDS uses, so the page's data-brand keys
+    // match the recolor endpoint's server-side keys exactly.
+    const kebab = s => s.normalize('NFD').replace(/[̀-ͯ]/g, '')
+      .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
+    // Curated display order — mirrors the Brand Selector design (Paper file).
+    const FASHION_TOP = ['Louis Vuitton','Chanel','Hermès','Gucci','Prada','Dior','Versace','Burberry','Saint Laurent','Balenciaga','Bottega Veneta','Fendi','Givenchy','Valentino','Armani','Ralph Lauren','Tom Ford','Celine','Loewe','Alexander McQueen'];
+    const WALL_TOP = ['Phillip Jeffries','Schumacher','Cole & Son','Thibaut','Zoffany','Farrow & Ball','Brunschwig & Fils','de Gournay','Fromental','Pierre Frey'];
+    const byBrand = {}; FASHION_PALETTES.forEach(p => { byBrand[p.brand] = p; });
+    const pick = names => names.map(n => byBrand[n]).filter(Boolean).map(p => ({
+      key: kebab(p.brand), label: p.brand, cat: p.cat || 'fashion',
+      country: p.country || null, est: p.est || null, hex: p.hex, signature: p.signature,
+    }));
+    res.json({ ok: true, fashion: pick(FASHION_TOP), wallcovering: pick(WALL_TOP) });
+  } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
 // ── Global PDP theme takeover (Steve 2026-05-31, /admin/theme-gallery "Set Live").
 // The admin can promote one of the 17 theme-variant templates to BE the live
 // customer PDP. When set, the bare /design/:id route serves that static template

← 6250128 generator_tick: retry psql on boot-race PG-socket-not-ready  ·  back to Wallco Ai  ·  feat(styleguides): open to public with per-IP render cap (5 5a812ec →