← Commit history
data/pages.json+3−2
@@ -62,7 +62,8 @@   "tags": [],   "updated_at": "2026-07-24T15:00:09.396Z",   "owner": "adom",-  "slug": "hero-studio"+  "slug": "hero-studio",+  "aiThread": "hero wizard dogfood"  },  {   "type": "app",@@ -229,7 +230,7 @@   "score": 539,   "stars": 0,   "installs30d": 82,-  "brief": "adom-lbr , the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON , symbol + footp",+  "brief": "adom-lbr , the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON , symbol + footp",   "tags": [    "eda",    "altium",
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.18.0",+  "version": "0.18.1",   "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.",   "license": "MIT",   "tags": [
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.18.0",+  "version": "0.18.1",   "description": "The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.",   "license": "MIT",   "tags": [
public/js/app.js+4
@@ -213,6 +213,10 @@ function render() {     if (sEl) sEl.classList.toggle('sim-fenced', simOverImport);   }   $('vitrine-img').src = (!vit.full && vit.src) || '';+  $('vitrine-img').onerror = () => {+    try { fetch('console', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ level: 'error', text: 'vitrine image failed to load: ' + String(vit.src).slice(0, 80) }) }); } catch {}+    handleToast({ text: 'The showcase image failed to load (see console); re-import it or pick another source.', kind: 'warn', seq: Date.now() });+  };   $('fullimport-img').src = (vit.full && vit.src) || '';   $('vitrine-full').checked = !!vit.full; 
public/js/wizard.js+137−46
@@ -5,6 +5,26 @@    patch, every render follows the same SSE loop the panel uses.    Loads after app.js (classic scripts share top-level bindings). */ +/* ---------- snappiness: optimistic writes + rebuild-shy rendering ----------+   Through a buffering proxy the SSE echo can arrive up to a poll-tick late;+   waiting for it made every click feel like molasses (Colby, dogfood run).+   wizSend applies the patch LOCALLY first (the mock's instant feel), then+   sends; the echo is idempotent confirmation. */+function wizMerge(dst, src) {+  for (const k of Object.keys(src)) {+    if (src[k] && typeof src[k] === 'object' && !Array.isArray(src[k]) && dst[k] && typeof dst[k] === 'object') {+      wizMerge(dst[k], src[k]);+    } else {+      dst[k] = src[k];+    }+  }+}+function wizSend(patch) {+  wizMerge(state, patch);+  render();+  send(patch);+}+ /* ---------- step engine ---------- */ let wizStep = 'package'; let wizExpanded = false;   // seasoned checklist expanded back to one-by-one@@ -15,6 +35,34 @@ function wp() { return pageByKey(state.page); } function wSubText() { return state.subtitle || firstSentence(wp().brief, 80); } function wSubWords() { return wSubText().split(/\s+/).filter(Boolean); } +function wizLayoutThumb(l) {+  const win = {+    'bleed-bottom':    { x:42, y:9,  w:52, h:58 },+    'bleed-right':     { x:52, y:9,  w:52, h:40 },+    'bleed-top':       { x:42, y:-8, w:48, h:38 },+    'bleed-corner-br': { x:50, y:20, w:54, h:48 },+    'bleed-corner-tr': { x:50, y:-8, w:54, h:40 },+    'contained':       { x:46, y:9,  w:46, h:30 },+    'mirror':          { x:4,  y:9,  w:52, h:58 },+    'showcase':        { x:30, y:4,  w:74, h:54 },+  }[l];+  const mirror = l === 'mirror';+  const tx = mirror ? 62 : 8;+  const scrim = l === 'showcase'+    ? '<rect x="0" y="0" width="46" height="62" fill="url(#scr' + l + ')"/>' : '';+  return '<svg viewBox="0 0 100 62" preserveAspectRatio="none">'+    + '<defs><linearGradient id="scr' + l + '" x1="0" y1="0" x2="1" y2="0">'+    + '<stop offset="0" stop-color="#05090e" stop-opacity=".95"/><stop offset="1" stop-color="#05090e" stop-opacity="0"/>'+    + '</linearGradient></defs>'+    + '<rect x="0" y="0" width="100" height="62" fill="#0d1117"/>'+    + '<rect x="' + win.x + '" y="' + win.y + '" width="' + win.w + '" height="' + win.h + '" rx="2" fill="#1c2530" stroke="#3a4552" stroke-width="1"/>'+    + scrim+    + '<rect x="' + tx + '" y="12" width="26" height="6" rx="1.5" fill="#e6edf3" opacity=".9"/>'+    + '<rect x="' + tx + '" y="22" width="22" height="3" rx="1.5" fill="#8b949e" opacity=".8"/>'+    + '<rect x="' + tx + '" y="28" width="18" height="3" rx="1.5" fill="#8b949e" opacity=".5"/>'+    + '</svg>';+}+ const WIZ_LAYOUTS = [   { v: 'bleed-bottom',     label: 'Off the bottom' },   { v: 'bleed-right',      label: 'Off the right' },@@ -26,16 +74,16 @@ const WIZ_LAYOUTS = [   { v: 'showcase',         label: 'Backdrop' }, ]; const WIZ_THEMES = [-  { v: 'midnight',  label: 'Midnight' },-  { v: 'pcb',       label: 'Circuit board' },-  { v: 'onion',     label: 'Onion' },-  { v: 'pill',      label: 'Pill' },-  { v: 'grid',      label: 'Grid' },-  { v: 'gradient',  label: 'Gradient' },-  { v: 'aurora',    label: 'Aurora' },-  { v: 'mesh',      label: 'Mesh' },-  { v: 'circuitry', label: 'Circuitry' },-  { v: 'image',     label: 'Your image' },+  { v: 'midnight',  label: 'Midnight',         sw: 'sw-midnight' },+  { v: 'pcb',       label: 'Circuit board',    sw: 'sw-pcb' },+  { v: 'onion',     label: 'Onion arcs',       sw: 'sw-onion' },+  { v: 'pill',      label: 'Pill field',       sw: 'sw-pill' },+  { v: 'grid',      label: 'Square grid',      sw: 'sw-grid' },+  { v: 'gradient',  label: 'Bold gradient',    sw: 'sw-grad' },+  { v: 'aurora',    label: 'Aurora',           sw: 'sw-aurora' },+  { v: 'mesh',      label: 'Color mesh',       sw: 'sw-mesh' },+  { v: 'circuitry', label: 'Generated traces', sw: 'sw-circuitry' },+  { v: 'image',     label: 'Your image',       sw: 'sw-image' }, ];  const WIZ_STEPS = [@@ -45,7 +93,7 @@ const WIZ_STEPS = [     summary: () => (wp().title || state.page),     options: () => pages.slice(0, 24).map(pg => ({ v: pg.owner + '/' + pg.slug, label: pg.title, sub: pg.owner + '/' + pg.slug })),     get: () => state.page,-    set: (v) => { if (!state.verify) send({ page: v }); else handleToast({ text: 'The page is locked during a review gate.', kind: 'warn', seq: Date.now() }); },+    set: (v) => { if (!state.verify) wizSend({ page: v }); else handleToast({ text: 'The page is locked during a review gate.', kind: 'warn', seq: Date.now() }); },   },   {     id: 'showcase', title: 'Showcase image',@@ -63,31 +111,31 @@ const WIZ_STEPS = [       return i >= 0 ? 'up:' + i : 'page';     },     set: (v) => {-      if (v === 'mock') { send({ vitrine: { src: '', full: false } }); return; }+      if (v === 'mock') { wizSend({ vitrine: { src: '', full: false } }); return; }       if (v === 'upload') { $('vitrine-file').click(); return; }       if (v === 'page') { post('vitrine-from-page?page=' + encodeURIComponent(state.page)).catch(() => {}); return; }-      if (v.startsWith('up:')) { send({ vitrine: { src: wizUploads[Number(v.slice(3))].data } }); }+      if (v.startsWith('up:')) { wizSend({ vitrine: { src: wizUploads[Number(v.slice(3))].data } }); }     },   },   {     id: 'placement', title: 'Placement',     hint: 'Where the screenshot sits. Arrow keys work too.',     summary: () => (WIZ_LAYOUTS.find(l => l.v === state.layout) || {}).label,-    options: () => WIZ_LAYOUTS.map(l => ({ v: l.v, label: l.label, pict: l.v })),+    options: () => WIZ_LAYOUTS.map(l => ({ v: l.v, label: l.label, thumb: wizLayoutThumb(l.v) })),     get: () => state.layout,-    set: (v) => send({ layout: v }),+    set: (v) => wizSend({ layout: v }),   },   {     id: 'background', title: 'Background',     hint: 'The scene behind everything.',     summary: () => (WIZ_THEMES.find(t => t.v === state.theme) || {}).label,-    options: () => WIZ_THEMES.map(t => ({ v: t.v, label: t.label })),+    options: () => WIZ_THEMES.map(t => ({ v: t.v, label: t.label, sw: t.sw })),     get: () => state.theme,     set: (v) => {       if (v === 'image' && !(state.bgImage && state.bgImage.src)) {         handleToast({ text: 'Pick a background file in Classic controls (bg section), or hero-studio bg --file <image>.', kind: 'info', seq: Date.now() });       }-      send({ theme: v });+      wizSend({ theme: v });     },   },   {@@ -109,7 +157,7 @@ const WIZ_STEPS = [       let ht = null;       hin.addEventListener('input', () => {         clearTimeout(ht);-        ht = setTimeout(() => { send({ headline: hin.value }); setTimeout(() => runFit(null), 300); }, 350);+        ht = setTimeout(() => { wizSend({ headline: hin.value }); setTimeout(() => runFit(null), 300); }, 350);       });       const fitb = document.createElement('button');       fitb.className = 'btn small'; fitb.textContent = 'Make it as big as fits';@@ -127,7 +175,7 @@ const WIZ_STEPS = [             const n = (sin.value || firstSentence(wp().brief, 80)).split(/\s+/).filter(Boolean).length;             patch.accentWords = state.accentWords.filter(i => i < n);           }-          send(patch);+          wizSend(patch);         }, 350);       });       const pick = document.createElement('div');@@ -142,7 +190,7 @@ const WIZ_STEPS = [           t.addEventListener('click', () => {             const set = new Set(Array.isArray(state.accentWords) ? state.accentWords : []);             set.has(i) ? set.delete(i) : set.add(i);-            send({ accentWords: [...set].sort((a, b) => a - b) });+            wizSend({ accentWords: [...set].sort((a, b) => a - b) });           });           pick.appendChild(t);         });@@ -169,7 +217,7 @@ const WIZ_STEPS = [         const c = document.createElement('div');         c.className = 'card wide' + (state.overlay[k] ? ' on' : '');         c.innerHTML = '<div class="card-label">' + lab + '</div><div class="card-sub">' + (state.overlay[k] ? 'Shown' : 'Hidden') + '</div>';-        c.addEventListener('click', () => send({ overlay: { [k]: !state.overlay[k] } }));+        c.addEventListener('click', () => wizSend({ overlay: { [k]: !state.overlay[k] } }));         wrap.appendChild(c);       });       host.appendChild(wrap);@@ -195,6 +243,7 @@ function wizAllConfirmed() { function wizGoTo(id) {   wizStep = id;   wizExpanded = false;+  wizLastSig = '';   wizardRender();   requestAnimationFrame(positionWizRing); }@@ -283,11 +332,22 @@ function wizRenderReview(host) {   vlab.className = 'wiz-lab'; vlab.textContent = 'See it as';   const seg = document.createElement('div');   seg.className = 'wseg';-  [{ v: false, label: 'Exactly my PNG' }, { v: true, label: 'Homepage preview' }].forEach(o => {+  const ctxOn = document.body.classList.contains('show-ctx');+  [+    { id: 'png',  label: 'Exactly my PNG' },+    { id: 'mq',   label: 'Homepage preview' },+    { id: 'ctx',  label: 'Everywhere it lives' },+  ].forEach(o => {     const b = document.createElement('button');     b.textContent = o.label;-    if (!!state.marqueePreview === o.v) b.classList.add('on');-    b.addEventListener('click', () => send({ marqueePreview: o.v }));+    const active = o.id === 'ctx' ? ctxOn : (!ctxOn && !!state.marqueePreview === (o.id === 'mq'));+    if (active) b.classList.add('on');+    b.addEventListener('click', () => {+      document.body.classList.toggle('show-ctx', o.id === 'ctx');+      if (o.id !== 'ctx') wizSend({ marqueePreview: o.id === 'mq' });+      else { wizardRender(); document.getElementById('contexts').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }+      if (o.id !== 'ctx') wizardRender();+    });     seg.appendChild(b);   });   const legend = document.createElement('div');@@ -309,19 +369,20 @@ function wizRenderReview(host) {     ex.textContent = 'Check one by one';     ex.addEventListener('click', () => {       wizExpanded = true;-      send({ confirm: { title: false, subtitle: false, vitrine: false, type: false } });+      wizSend({ confirm: { title: false, subtitle: false, vitrine: false, type: false } });     });     row.append(q, ex);     checks.appendChild(row);     const c = state.confirm || {};     if (!(c.title && c.subtitle && c.vitrine && c.type) && !Object.values(state.flags || {}).some(Boolean)) {+      wizMerge(state, { confirm: { title: true, subtitle: true, vitrine: true, type: true } });       send({ confirm: { title: true, subtitle: true, vitrine: true, type: true } });     }   } else {     const allb = document.createElement('button');     allb.className = 'btn small';     allb.textContent = 'All of it looks good';-    allb.addEventListener('click', () => send({+    allb.addEventListener('click', () => wizSend({       confirm: { title: true, subtitle: true, vitrine: true, type: true },       flags: { title: false, subtitle: false, vitrine: false, type: false },     }));@@ -334,11 +395,11 @@ function wizRenderReview(host) {       const ok = document.createElement('button');       ok.className = 'btn small';       ok.textContent = 'Looks good';-      ok.addEventListener('click', () => send({ confirm: { [c.key]: true }, flags: { [c.key]: false } }));+      ok.addEventListener('click', () => wizSend({ confirm: { [c.key]: true }, flags: { [c.key]: false } }));       const fl = document.createElement('button');       fl.className = 'btn small';       fl.textContent = 'Needs fixing';-      fl.addEventListener('click', () => send({ flags: { [c.key]: true }, confirm: { [c.key]: false } }));+      fl.addEventListener('click', () => wizSend({ flags: { [c.key]: true }, confirm: { [c.key]: false } }));       row.append(q, ok, fl);       checks.appendChild(row);     });@@ -360,20 +421,20 @@ function wizRenderReview(host) { }  /* ---------- confetti: the finale earns it ---------- */-function wizConfetti(fromEl) {+function wizConfetti() {+  // whole-screen rain, ABOVE the preview modal, timed to when it pops   const c = document.createElement('canvas');-  c.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999';+  c.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:100000';   c.width = innerWidth; c.height = innerHeight;   document.body.appendChild(c);   const g = c.getContext('2d');-  const b = fromEl.getBoundingClientRect();-  const ox = b.left + b.width / 2, oy = b.top + b.height / 2;   const colors = ['#00b8b0', '#7a5cff', '#3fb950', '#e6edf3', '#37d6cd'];-  const parts = Array.from({ length: 140 }, (_, i) => ({-    x: ox, y: oy,-    vx: Math.cos(i * 2.399) * (2 + (i % 7)) * (0.6 + (i % 5) / 5),-    vy: Math.sin(i * 2.399) * (2 + (i % 7)) * (0.6 + (i % 5) / 5) - 6,-    s: 3 + (i % 4), r: i * 0.7, vr: ((i % 9) - 4) * 0.12,+  const parts = Array.from({ length: 260 }, (_, i) => ({+    x: (i / 260) * innerWidth + ((i * 37) % 40) - 20,+    y: -20 - ((i * 53) % 160),+    vx: ((i % 11) - 5) * 0.7,+    vy: 5 + ((i * 29) % 50) / 10,+    s: 4 + (i % 5), r: i * 0.7, vr: ((i % 9) - 4) * 0.14,     col: colors[i % colors.length],   }));   let t = 0;@@ -381,31 +442,61 @@ function wizConfetti(fromEl) {     t++;     g.clearRect(0, 0, c.width, c.height);     for (const p of parts) {-      p.x += p.vx; p.y += p.vy; p.vy += 0.22; p.vx *= 0.99; p.r += p.vr;+      p.x += p.vx; p.y += p.vy; p.vy += 0.12; p.r += p.vr;       g.save(); g.translate(p.x, p.y); g.rotate(p.r);-      g.fillStyle = p.col; g.globalAlpha = Math.max(0, 1 - t / 90);-      g.fillRect(-p.s / 2, -p.s / 2, p.s, p.s * 0.6);+      g.fillStyle = p.col; g.globalAlpha = Math.max(0, 1.15 - t / 75);+      g.fillRect(-p.s / 2, -p.s / 2, p.s, p.s * 0.62);       g.restore();     }-    if (t < 95) requestAnimationFrame(tick);+    if (t < 85) requestAnimationFrame(tick);     else c.remove();   })(); }-window.wizardOnGenerated = () => { const b = $('wbtn-generate'); if (b) wizConfetti(b); };+window.wizardOnGenerated = () => wizConfetti();  /* ---------- dock render ---------- */ function wizCard(o, on, onPick) {   const c = document.createElement('div');   c.className = 'card' + (on ? ' on' : '') + (o.sub && o.sub.length > 24 ? ' wide' : '');-  c.innerHTML = '<div class="card-label"></div>' + (o.sub ? '<div class="card-sub"></div>' : '');-  c.querySelector('.card-label').textContent = o.label;-  if (o.sub) c.querySelector('.card-sub').textContent = o.sub;+  if (o.thumb) {+    const t = document.createElement('div');+    t.className = 'thumb'; t.innerHTML = o.thumb;+    c.appendChild(t);+  } else if (o.sw) {+    const t = document.createElement('div');+    t.className = 'swx ' + o.sw;+    c.appendChild(t);+  }+  const l = document.createElement('div');+  l.className = 'card-label'; l.textContent = o.label;+  c.appendChild(l);+  if (o.sub) {+    const sb = document.createElement('div');+    sb.className = 'card-sub'; sb.textContent = o.sub;+    c.appendChild(sb);+  }   c.addEventListener('click', onPick);   return c; }+let wizLastSig = '';+function wizSignature() {+  const l = state.lint || [];+  const v = state.vitrine || {};+  return JSON.stringify([wizStep, wizExpanded, state.page, state.layout, state.theme,+    state.headline, state.subtitle, state.accentWords, state.overlay, state.confirm,+    state.flags, state.marqueePreview, state.gatesDone, wizUploads.length,+    v.full, (v.src || '').length, l.filter(x => x.level === 'fail').length,+    l.filter(x => x.level === 'warn').length, document.body.classList.contains('show-ctx')]);+} function wizardRender() {   if (!document.body.classList.contains('wizard')) return;   if (!state || !pages.length || !$('wdock')) return;+  const sig = wizSignature();+  if (sig === wizLastSig) return;+  const focused = document.activeElement && $('wdock').contains(document.activeElement)+    && document.activeElement.tagName === 'INPUT';+  if (focused) { wizLastSig = sig; return; }+  wizLastSig = sig;   const s = wizActive();   wizRenderRail();   $('wintro-ref').textContent = state.page;@@ -477,7 +568,7 @@ $('vitrine-file').addEventListener('change', () => {     r.onload = () => {       wizUploads.push({ name: f.name, data: r.result });       if (wizUploads.length && f === files[files.length - 1]) {-        send({ vitrine: { src: r.result } });+        wizSend({ vitrine: { src: r.result } });       }     };     r.readAsDataURL(f);
public/studio.css+18
@@ -357,3 +357,21 @@ body.wizard main { flex:1 1 auto; min-height:0; } .wiz-review .rv-col { flex:0 1 440px; } /* render mode exports HERO PIXELS ONLY: the wizard is stage chrome */ body[data-render] #wdock, body[data-render] #weditring { display:none !important; }+/* card previews (the visuals are the point) */+#wcards .card .thumb { width:100%; aspect-ratio:100/62; border-radius:6px; overflow:hidden; border:1px solid var(--border); }+#wcards .card .thumb svg { width:100%; height:100%; display:block; }+#wcards .card .swx { width:100%; aspect-ratio:16/10; border-radius:6px; border:1px solid var(--border); }+.sw-midnight { background:linear-gradient(155deg,#0a0e14,#0d1117 45%,#071318); }+.sw-pcb { background:linear-gradient(155deg,#03211f,#04302c); }+.sw-onion { background:repeating-radial-gradient(circle at 90% 10%, #101a2c 0 4px, #16223a 4px 6px); }+.sw-pill { background:#0c3a3c; }+.sw-grid { background:#0c3a3c url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12"><rect x="2" y="2" width="8" height="8" fill="none" stroke="%23ffffff" stroke-opacity=".35" stroke-width="1"/></svg>'); }+.sw-grad { background:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7); }+.sw-aurora { background:radial-gradient(28px 20px at 30% 20%, rgba(0,230,220,.8), transparent), radial-gradient(34px 26px at 70% 80%, rgba(140,107,247,.7), transparent), #0a0f16; }+.sw-mesh { background:conic-gradient(from 155deg at 40% 40%, rgba(0,184,177,.5), transparent 40%), #0b1016; }+.sw-circuitry { background:repeating-linear-gradient(45deg, rgba(0,184,177,.5) 0 1.5px, transparent 1.5px 7px), #080d13; }+.sw-image { background:repeating-conic-gradient(#1c2530 0 25%, #0d1117 0 50%) 0 0/12px 12px; }+#wcards .card:has(.thumb), #wcards .card:has(.swx) { width:150px; }+/* context previews: Review-step opt-in, not standing furniture */+body.wizard #contexts { display:none; }+body.wizard.show-ctx #contexts { display:flex; }
server.js+2−2
@@ -169,10 +169,10 @@ function srcHash(src) { function leanState() {   const out = Object.assign({}, state);   if (state.vitrine && state.vitrine.src && state.vitrine.src.startsWith('data:')) {-    out.vitrine = Object.assign({}, state.vitrine, { src: '/vitrine.png?v=' + srcHash(state.vitrine.src) });+    out.vitrine = Object.assign({}, state.vitrine, { src: 'vitrine.png?v=' + srcHash(state.vitrine.src) });   }   if (state.bgImage && state.bgImage.src && state.bgImage.src.startsWith('data:')) {-    out.bgImage = Object.assign({}, state.bgImage, { src: '/bg.png?v=' + srcHash(state.bgImage.src) });+    out.bgImage = Object.assign({}, state.bgImage, { src: 'bg.png?v=' + srcHash(state.bgImage.src) });   }   return out; }