← back to Wallco Ai

public/admin/elements.html

627 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>Elements — wallco.ai admin</title>
<style>
  :root {
    --ink:#1f1808; --line:#d8d0c0; --gold:#c9a14b;
    --bg:#fbf8f1; --card:#fff; --faint:#7a6e5a;
    --green:#1f6a2c; --red:#b3261e; --orange:#c0660b;
    --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
    --serif:'Playfair Display','Didot',Georgia,serif;
    --card-min: 220px;
  }
  * { box-sizing:border-box; }
  body { margin:0; background:var(--bg); color:var(--ink); font:14px var(--sans); }

  header {
    padding:14px 24px; border-bottom:1px solid var(--line); background:#fff;
    display:flex; gap:16px; align-items:baseline; position:sticky; top:0; z-index:10;
  }
  header h1 { font:400 24px/1.2 var(--serif); margin:0; letter-spacing:.5px; }
  header .meta { color:var(--faint); font-size:12px; letter-spacing:.04em; }
  header .stats { margin-left:auto; display:flex; gap:18px; font:600 12px ui-monospace,Menlo,monospace; }
  header .stats b { color:var(--ink); }
  header .shuffle-btn {
    margin-left:18px; padding:8px 16px; background:var(--ink); color:#fff;
    border:none; border-radius:4px; cursor:pointer;
    font:600 11.5px var(--sans); letter-spacing:.12em; text-transform:uppercase;
  }
  header .shuffle-btn:hover { background:var(--gold); }

  main { padding:16px 24px 80px; }

  /* Controls */
  .controls {
    display:flex; gap:14px; align-items:center; padding:11px 14px;
    background:#fff; border:1px solid var(--line); border-radius:8px;
    margin-bottom:8px; flex-wrap:wrap; font-size:12px;
  }
  .controls label { font:600 10.5px var(--sans); letter-spacing:.16em;
                    text-transform:uppercase; color:var(--faint); }
  .controls select, .controls input[type=text] {
    padding:6px 10px; border:1px solid var(--line); border-radius:4px;
    background:#fff; font:12px var(--sans); min-width:160px;
  }
  .controls input[type=range] { width:160px; }
  .controls .density-val { font:600 11px ui-monospace,Menlo,monospace;
                           color:var(--faint); min-width:36px; }
  .controls .toggle { display:flex; align-items:center; gap:6px; cursor:pointer; }
  .controls .toggle input { margin:0; }

  /* Active filter chips */
  .chips { display:flex; gap:6px; flex-wrap:wrap; padding:0 14px 12px; }
  .chip {
    background:var(--ink); color:#fff;
    font:600 10.5px var(--sans); letter-spacing:.06em;
    padding:4px 8px 4px 10px; border-radius:999px;
    display:inline-flex; align-items:center; gap:6px;
    cursor:pointer;
  }
  .chip:hover { background:var(--red); }
  .chip .x { font-size:14px; line-height:1; }

  /* Empty state */
  .empty-state {
    text-align:center; padding:40px 20px;
    background:#fff; border:1px dashed var(--line); border-radius:8px;
    margin:14px 0;
  }
  .empty-state h3 { font:400 18px var(--serif); margin:0 0 6px; }
  .empty-state p  { color:var(--faint); margin:0 0 12px; font-size:13px; }
  .empty-state button {
    background:var(--gold); color:#fff; border:0; padding:8px 16px;
    border-radius:4px; cursor:pointer; font:600 11.5px var(--sans);
    letter-spacing:.12em; text-transform:uppercase;
  }

  /* Grid */
  .grid {
    display:grid;
    grid-template-columns:repeat(auto-fill, minmax(var(--card-min), 1fr));
    gap:14px; padding:14px 0;
  }
  .element-card {
    background:#fff; border:1px solid var(--line); border-radius:8px;
    overflow:hidden; transition:transform .12s, box-shadow .12s;
    cursor:pointer; position:relative;
  }
  .element-card.selected {
    border-color:var(--gold); border-width:2px;
    box-shadow:0 0 0 1px var(--gold);
  }
  .element-card:hover {
    transform:translateY(-2px);
    box-shadow:0 6px 16px rgba(0,0,0,.12);
  }
  .card-thumb {
    aspect-ratio:1; background:#f3eee2 url('/admin/elements-placeholder.svg') center/40% no-repeat;
    background-size:cover;
    display:block; width:100%;
    position:relative; cursor:pointer;
  }
  .card-pick {
    position:absolute; left:0; right:0; bottom:0;
    padding:7px 10px; text-align:center;
    background:linear-gradient(transparent, rgba(0,0,0,.55));
    color:#fff; font:700 10.5px var(--sans); letter-spacing:.1em; text-transform:uppercase;
    opacity:0; transition:opacity .12s;
    pointer-events:none;
  }
  .element-card:hover .card-pick { opacity:1; }
  .card-pick.on {
    opacity:1; background:rgba(31,106,44,.92);
  }
  .card-id { text-decoration:none; }
  .card-id:hover { color:var(--gold); }
  .card-meta {
    padding:10px 12px;
    display:flex; flex-direction:column; gap:6px;
    font-size:12px;
  }
  .card-cat {
    display:inline-block;
    background:#f3eee2; color:var(--ink);
    padding:2px 8px; border-radius:999px;
    font:600 10px var(--sans); letter-spacing:.08em;
    text-transform:uppercase; align-self:flex-start;
    max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .card-color {
    display:flex; align-items:center; gap:6px;
    color:var(--faint); font-size:11px;
  }
  .card-color .dot {
    width:10px; height:10px; border-radius:50%;
    border:1px solid rgba(0,0,0,.15);
  }
  .card-id-row {
    display:flex; justify-content:space-between; align-items:center;
    border-top:1px solid var(--line); padding-top:6px; margin-top:2px;
  }
  .card-id {
    font:600 11px ui-monospace,Menlo,monospace; color:var(--faint);
  }
  .card-use-btn {
    background:none; border:1px solid var(--line); color:var(--ink);
    padding:3px 10px; border-radius:4px; cursor:pointer;
    font:600 10px var(--sans); letter-spacing:.12em; text-transform:uppercase;
  }
  .card-use-btn:hover { background:var(--ink); color:#fff; border-color:var(--ink); }

  .card-when {
    color:var(--faint); font-size:10.5px;
    font-variant-numeric:tabular-nums;
  }

  .card-tier {
    position:absolute; top:8px; right:8px;
    font:700 8.5px var(--sans); letter-spacing:.16em; text-transform:uppercase;
    padding:3px 7px; border-radius:3px;
    color:#fff; background:rgba(31,106,44,.92);
  }
  .card-tier.passed { background:rgba(122,110,90,.92); }

  /* Loading */
  .loading { padding:40px; text-align:center; color:var(--faint); }

  /* Compose drawer */
  .compose-drawer {
    position:fixed; bottom:0; left:0; right:0;
    background:#fff; border-top:2px solid var(--gold);
    padding:14px 24px;
    transform:translateY(100%); transition:transform .2s;
    z-index:20;
    box-shadow:0 -8px 24px rgba(0,0,0,.12);
  }
  .compose-drawer.open { transform:translateY(0); }
  .drawer-inner {
    max-width:1500px; margin:0 auto;
    display:flex; gap:14px; align-items:center;
  }
  .drawer-thumbs { display:flex; gap:8px; flex:1; overflow-x:auto; }
  .drawer-thumb {
    width:64px; height:64px; border-radius:4px; flex-shrink:0;
    background-size:cover; background-position:center;
    border:1px solid var(--line); position:relative;
  }
  .drawer-thumb .remove {
    position:absolute; top:-6px; right:-6px;
    width:18px; height:18px; border-radius:50%;
    background:var(--red); color:#fff; border:none;
    font-size:12px; line-height:1; cursor:pointer;
  }
  .drawer-cta {
    background:var(--ink); color:#fff; border:0;
    padding:12px 22px; border-radius:4px; cursor:pointer;
    font:600 12px var(--sans); letter-spacing:.16em; text-transform:uppercase;
  }
  .drawer-cta:hover { background:var(--gold); }
  .drawer-close {
    background:none; border:none; color:var(--faint); cursor:pointer;
    font:14px var(--sans); padding:6px 10px;
  }
</style>
</head>
<body>
<header>
  <h1>Elements</h1>
  <span class="meta">curated solid-opaque-middle building blocks</span>
  <div class="stats">
    <span>baked <b id="stat-baked">—</b></span>
    <span>clean <b id="stat-clean">—</b></span>
    <span>showing <b id="stat-showing">—</b></span>
  </div>
  <button class="shuffle-btn" id="shuffle-btn">Shuffle 12</button>
</header>

<main>
  <div class="controls">
    <label>Category</label>
    <select id="f-category"><option value="">All</option></select>

    <label>Colorway</label>
    <select id="f-colorway"><option value="">All</option></select>

    <label>Density</label>
    <select id="f-density">
      <option value="">All</option>
      <option value="sparse">Sparse</option>
      <option value="medium">Medium</option>
      <option value="dense">Dense</option>
    </select>

    <label class="toggle" title="Solid screen-print only = baked elements. Off = also include candidates that passed seamless-scan but haven't been baked yet.">
      <input type="checkbox" id="f-quality" checked>
      Solid screen-print only
    </label>

    <label>Search</label>
    <input type="text" id="f-search" placeholder="id, category, or hex…" style="min-width:200px">

    <label>Sort</label>
    <select id="f-sort">
      <option value="shuffle">Shuffled</option>
      <option value="newest">Newest</option>
      <option value="oldest">Oldest</option>
      <option value="category">Category A→Z</option>
      <option value="color">Color (by hue)</option>
      <option value="id">ID ↑</option>
      <option value="id_desc">ID ↓</option>
    </select>

    <label style="margin-left:auto">Density</label>
    <input id="density-slider" type="range" min="140" max="380" step="20" value="220">
    <span class="density-val" id="density-val">220</span>
  </div>

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

  <div id="grid" class="grid"><div class="loading">loading elements…</div></div>
</main>

<div class="compose-drawer" id="drawer">
  <div class="drawer-inner">
    <span style="font:600 11.5px var(--sans); letter-spacing:.16em; text-transform:uppercase; color:var(--faint);">
      Selected
    </span>
    <div class="drawer-thumbs" id="drawer-thumbs"></div>
    <button class="drawer-cta" id="drawer-cta">Use →</button>
    <button class="drawer-close" id="drawer-close" title="Close drawer (selection persists)">×</button>
  </div>
</div>

<script>
(function(){
  function fmtDate(iso){
    if(!iso) return '—';
    const d = new Date(iso); if(isNaN(d)) return '—';
    return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
  }
  const PER = 200;
  const state = {
    items: [],
    filtered: [],
    selected: [],
    filter: { category:'', colorway:'', density:'', quality:true, search:'' },
    cardMin: parseInt(localStorage.getItem('el-card-min') || '220', 10),
    shuffleSeed: 0,
    sort: localStorage.getItem('el-sort') || 'shuffle',
  };

  // Sort the view (Steve standing rule). dominant_hex exists → color-by-hue is available.
  function hexHue(hex) {
    const m = /^#?([0-9a-fA-F]{6})$/.exec(hex || ''); if (!m) return 1e6;
    const n = parseInt(m[1], 16), r = (n >> 16 & 255) / 255, g = (n >> 8 & 255) / 255, b = (n & 255) / 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min; let h = 0;
    if (d === 0) h = 0; else if (max === r) h = ((g - b) / d) % 6; else if (max === g) h = (b - r) / d + 2; else h = (r - g) / d + 4;
    h *= 60; if (h < 0) h += 360;
    const s = max === 0 ? 0 : d / max;
    if (s < 0.12) return 1e5 + (1 - max);
    return h;
  }
  function sortView(arr) {
    const r = arr.slice(), m = state.sort;
    if (m === 'newest')        r.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
    else if (m === 'oldest')   r.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')));
    else if (m === 'category') r.sort((a, b) => categoryOf(a).localeCompare(categoryOf(b)) || (b.id - a.id));
    else if (m === 'color')    r.sort((a, b) => hexHue(a.dominant_hex) - hexHue(b.dominant_hex) || (b.id - a.id));
    else if (m === 'id')       r.sort((a, b) => a.id - b.id);
    else if (m === 'id_desc')  r.sort((a, b) => b.id - a.id);
    return r;
  }

  function applyCardMin(px) {
    document.documentElement.style.setProperty('--card-min', px + 'px');
    document.getElementById('density-val').textContent = px;
  }

  function categoryOf(item) {
    return (item.category || '').split(' · ')[0] || 'misc';
  }
  function colorwayOf(item) {
    const parts = (item.category || '').split(' · ');
    return parts.length >= 2 ? parts[1] : '';
  }
  function densityOf(item) {
    if (item.bg_dominance == null) return null;
    if (item.bg_dominance < 0.30) return 'dense';
    if (item.bg_dominance < 0.70) return 'medium';
    return 'sparse';
  }

  function applyFilters() {
    const f = state.filter;
    state.filtered = state.items.filter(it => {
      if (f.quality && !it.element_motif_path) return false;
      if (f.category && categoryOf(it) !== f.category) return false;
      if (f.colorway && colorwayOf(it) !== f.colorway) return false;
      if (f.density && densityOf(it) !== f.density) return false;
      if (f.search) {
        const s = f.search.toLowerCase();
        const cat = (it.category || '').toLowerCase();
        const hex = (it.dominant_hex || '').toLowerCase();
        if (!cat.includes(s) && !hex.includes(s) && String(it.id) !== s) return false;
      }
      return true;
    });
    document.getElementById('stat-showing').textContent = state.filtered.length;
    renderChips();
    render();
  }

  function renderChips() {
    const f = state.filter;
    const chips = [];
    if (f.category)  chips.push({ k:'category',  v:f.category });
    if (f.colorway)  chips.push({ k:'colorway',  v:f.colorway });
    if (f.density)   chips.push({ k:'density',   v:f.density });
    if (!f.quality)  chips.push({ k:'quality',   v:'show all passed-seamless' });
    if (f.search)    chips.push({ k:'search',    v:f.search });
    const c = document.getElementById('chips');
    c.innerHTML = chips.map(ch =>
      `<span class="chip" data-k="${ch.k}">${ch.k}: ${ch.v} <span class="x">×</span></span>`
    ).join('');
    c.querySelectorAll('.chip').forEach(el => {
      el.addEventListener('click', () => {
        const k = el.dataset.k;
        if (k === 'category') document.getElementById('f-category').value = '';
        if (k === 'colorway') document.getElementById('f-colorway').value = '';
        if (k === 'density')  document.getElementById('f-density').value  = '';
        if (k === 'quality')  document.getElementById('f-quality').checked = true;
        if (k === 'search')   document.getElementById('f-search').value   = '';
        state.filter[k] = k === 'quality' ? true : '';
        applyFilters();
      });
    });
  }

  function shuffleList(arr, seed) {
    // Deterministic shuffle on seed so the same Shuffle press doesn't re-roll
    // on every render. Mulberry32 PRNG.
    let s = seed | 0;
    const a = arr.slice();
    for (let i = a.length - 1; i > 0; i--) {
      s = (s + 0x6D2B79F5) | 0;
      let t = Math.imul(s ^ (s >>> 15), 1 | s);
      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
      const j = ((t ^ (t >>> 14)) >>> 0) % (i + 1);
      [a[i], a[j]] = [a[j], a[i]];
    }
    return a;
  }

  function render() {
    const grid = document.getElementById('grid');
    let view = state.filtered;
    if (state.shuffleSeed) view = shuffleList(view, state.shuffleSeed);
    else if (state.sort !== 'shuffle') view = sortView(view);
    view = view.slice(0, PER);

    if (view.length === 0) {
      // 0-results — auto-suggest relaxing the most-constraining filter
      let suggest = '';
      const f = state.filter;
      if (f.density) suggest = 'remove the Density filter';
      else if (f.colorway) suggest = 'remove the Colorway filter';
      else if (f.category) suggest = 'remove the Category filter';
      else if (f.search) suggest = 'clear the Search box';
      else if (f.quality) suggest = 'turn off "Solid screen-print only"';
      grid.innerHTML = `
        <div class="empty-state" style="grid-column: 1 / -1;">
          <h3>No elements match these filters</h3>
          <p>Try this: ${suggest || 'broaden the filters'}</p>
          <button onclick="document.querySelectorAll('.chip').forEach(c=>c.click())">Clear all chips</button>
        </div>`;
      return;
    }

    grid.innerHTML = view.map(item => {
      const selected = state.selected.includes(item.id);
      const cat  = categoryOf(item);
      const cw   = colorwayOf(item);
      const hex  = item.dominant_hex || '#cccccc';
      const tier = item.element_motif_path ? 'solid' : 'passed';
      // Prefer the baked 512 reference thumb if present; fall back to live tile
      const img  = item.element_ref_path
        ? `/elements/img/${item.id}_ref512.png`
        : `/designs/img/by-id/${item.id}`;
      return `
        <div class="element-card ${selected ? 'selected' : ''}" data-id="${item.id}">
          <span class="card-tier ${tier}">${tier === 'solid' ? 'Solid' : 'Passed'}</span>
          <div class="card-thumb" data-id="${item.id}" title="Click to use this element in a new pattern" style="background-image:url('${img}')">
            <span class="card-pick ${selected ? 'on' : ''}">${selected ? '✓ Selected' : '+ Use in new pattern'}</span>
          </div>
          <div class="card-meta">
            <span class="card-cat" title="${item.category || ''}">${cat}</span>
            ${cw ? `<span class="card-color"><span class="dot" style="background:${hex}"></span>${cw}</span>` : `<span class="card-color"><span class="dot" style="background:${hex}"></span>${hex}</span>`}
            <div class="card-id-row">
              <a class="card-id" href="/design/${item.id}" target="_blank" rel="noopener" title="Open this design's page">#${item.id} ↗</a>
              <button class="card-use-btn" data-id="${item.id}">${selected ? '✓ Selected' : '+ Use'}</button>
            </div>
            <div class="card-when" title="created ${item.created_at || ''}">🕓 ${fmtDate(item.created_at)}</div>
          </div>
        </div>`;
    }).join('');

    // Clicking the element IMAGE selects it for composing a NEW pattern
    // (opens the compose drawer). The #id ↗ link still opens the PDP.
    grid.querySelectorAll('.card-thumb').forEach(t => {
      t.addEventListener('click', e => {
        e.preventDefault();
        toggleSelect(parseInt(t.dataset.id, 10));
      });
    });
    grid.querySelectorAll('.card-use-btn').forEach(b => {
      b.addEventListener('click', e => {
        e.stopPropagation();
        e.preventDefault();
        toggleSelect(parseInt(b.dataset.id, 10));
      });
    });
    grid.querySelectorAll('.card-id').forEach(a => {
      a.addEventListener('click', e => { e.stopPropagation(); });
    });
  }

  function toggleSelect(id) {
    const i = state.selected.indexOf(id);
    if (i >= 0) state.selected.splice(i, 1);
    else state.selected.push(id);
    renderDrawer();
    render();
  }

  function renderDrawer() {
    const d = document.getElementById('drawer');
    const cta = document.getElementById('drawer-cta');
    if (state.selected.length === 0) {
      d.classList.remove('open');
      return;
    }
    d.classList.add('open');
    cta.textContent =
      state.selected.length === 1 ? 'Use as reference →' :
      state.selected.length === 2 ? 'Blend 2 →' :
      `Compose ${state.selected.length} →`;
    document.getElementById('drawer-thumbs').innerHTML = state.selected.map(id => {
      const item = state.items.find(it => it.id === id);
      if (!item) return '';
      const img = item.element_ref_path
        ? `/elements/img/${item.id}_ref512.png`
        : `/designs/img/by-id/${item.id}`;
      return `<div class="drawer-thumb" style="background-image:url('${img}')">
        <button class="remove" data-id="${id}">×</button>
      </div>`;
    }).join('');
    document.getElementById('drawer-thumbs').querySelectorAll('.remove').forEach(b => {
      b.addEventListener('click', () => toggleSelect(parseInt(b.dataset.id, 10)));
    });
  }

  function populateFilterOptions() {
    const cats = new Set();
    const cws  = new Set();
    state.items.forEach(it => {
      const c = categoryOf(it);
      if (c) cats.add(c);
      const cw = colorwayOf(it);
      if (cw) cws.add(cw);
    });
    const catSel = document.getElementById('f-category');
    const cwSel  = document.getElementById('f-colorway');
    [...cats].sort().forEach(c =>
      catSel.insertAdjacentHTML('beforeend', `<option value="${c}">${c}</option>`));
    [...cws].sort().forEach(c =>
      cwSel.insertAdjacentHTML('beforeend', `<option value="${c}">${c}</option>`));
  }

  // Wire controls
  ['f-category','f-colorway','f-density'].forEach(id => {
    document.getElementById(id).addEventListener('change', e => {
      const key = id.split('-')[1];
      state.filter[key] = e.target.value;
      state.shuffleSeed = 0;
      applyFilters();
    });
  });
  document.getElementById('f-quality').addEventListener('change', e => {
    state.filter.quality = e.target.checked;
    state.shuffleSeed = 0;
    applyFilters();
  });
  document.getElementById('f-search').addEventListener('input', e => {
    state.filter.search = e.target.value.trim();
    state.shuffleSeed = 0;
    applyFilters();
  });
  document.getElementById('density-slider').addEventListener('input', e => {
    state.cardMin = parseInt(e.target.value, 10);
    localStorage.setItem('el-card-min', String(state.cardMin));
    applyCardMin(state.cardMin);
  });
  document.getElementById('f-sort').addEventListener('change', e => {
    state.sort = e.target.value;
    localStorage.setItem('el-sort', state.sort);
    state.shuffleSeed = 0;           // an explicit sort overrides the random shuffle
    render();
  });
  document.getElementById('shuffle-btn').addEventListener('click', () => {
    state.sort = 'shuffle';
    document.getElementById('f-sort').value = 'shuffle';
    localStorage.setItem('el-sort', 'shuffle');
    state.shuffleSeed = Date.now() & 0xffffffff;
    render();
  });
  document.getElementById('drawer-cta').addEventListener('click', async () => {
    const ids = state.selected.slice();
    if (ids.length === 0) return;
    const verb = ids.length === 1 ? 'use as reference' : ids.length === 2 ? 'blend' : `compose ${ids.length}`;
    if (!confirm(`Generate 1 NEW design (${verb}) from element(s) ${ids.map(i => '#' + i).join(', ')}?\n\nThis spends one paid Gemini image call. The result lands UNPUBLISHED in the curator queue for review.`)) return;
    const hint = (prompt('Optional art-director note (leave blank for none):', '') || '').trim();

    const cta = document.getElementById('drawer-cta');
    const orig = cta.textContent;
    cta.disabled = true;
    cta.textContent = 'Composing… (Gemini)';
    try {
      const r = await fetch('/api/admin/elements/compose', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ids, hint }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok || !j.ok) {
        alert('Compose failed: ' + (j.error || ('HTTP ' + r.status)));
        return;
      }
      state.selected = [];
      renderDrawer();
      render();
      alert(`✓ Generated #${j.new_id} (${j.mode}, ${j.elapsed_s}s).\nUNPUBLISHED — review it in the curator queue before it goes live.\n\nOpening the new design…`);
      window.open('/design/' + j.new_id, '_blank', 'noopener');
    } catch (e) {
      alert('Compose error: ' + (e && e.message || e));
    } finally {
      cta.disabled = false;
      cta.textContent = orig;
    }
  });
  document.getElementById('drawer-close').addEventListener('click', () => {
    document.getElementById('drawer').classList.remove('open');
  });

  // Boot
  applyCardMin(state.cardMin);
  document.getElementById('density-slider').value = state.cardMin;
  document.getElementById('f-sort').value = state.sort;

  fetch('/api/admin/elements/items')
    .then(r => r.json())
    .then(data => {
      if (!data.ok) {
        document.getElementById('grid').innerHTML =
          `<div class="empty-state" style="grid-column:1/-1"><h3>Error</h3><p>${data.error || 'unknown'}</p></div>`;
        return;
      }
      state.items = data.items || [];
      document.getElementById('stat-baked').textContent = state.items.filter(i => i.element_motif_path).length;
      document.getElementById('stat-clean').textContent = state.items.length;
      populateFilterOptions();
      // Cold-start shuffle only when no explicit sort is persisted (default 'shuffle').
      if (state.sort === 'shuffle') state.shuffleSeed = Date.now() & 0xffffffff;
      applyFilters();
    })
    .catch(err => {
      document.getElementById('grid').innerHTML =
        `<div class="empty-state" style="grid-column:1/-1"><h3>Fetch failed</h3><p>${err.message}</p></div>`;
    });
})();
</script>
  <script src="/admin/admin-modal-kit.js" defer></script>
</body>
</html>