← Commit history
SKILL.md+55−17
@@ -16,7 +16,7 @@ gate, which opens the studio in front of the human and BLOCKS until they click Generate:  ```bash-hero-studio pose <owner>/<slug> --surface webview|pup [--title "T"] [--brief "one-liner"] [--type app,service] [--timeout 900] [--port N]+hero-studio pose <owner>/<slug> --surface webview|pup [--title "T"] [--brief "one-liner"] [--type app,service] [--headline "H"] [--timeout 900] [--port N] # blocks; exits 0 with {file, path, port, latest} JSON on stdout when the HUMAN generates, # exit 2 on timeout, exit 1 on setup errors, exit 3 when the pup surface failed # (the reason is printed; there is NO silent webview fallback)@@ -149,7 +149,13 @@ clears its flag. The studio also runs a measured LAYOUT LINTER after every change: pill vs vitrine overlap (a scrim is auto-applied so the pill reads as a badge), image text intruding into the dynamic marquee band, text clipped by the hero edge-(HARD FAIL: locks Generate), near-edge insets, and 4+ line title wraps.+(HARD FAIL: locks Generate), near-edge insets, 4+ line title wraps, TEXT+COLLIDING WITH THE VITRINE (the fix is ALWAYS to move or resize the vitrine,+never to shrink the title; a title under 6.5cqw draws its own warning because+dock thumbnails render it illegibly), OPTICAL alignment (the title's ink edge+vs the subtitle's, canvas-measured, because glyph side bearing shifts pixels+off the box edge), and an informational published-headroom line (export px+between the lowest text and the marquee band). Findings render in the gate and publish to `state.lint` ([{id, level: warn|fail, msg}]); the AI should fix warns without being asked. FULL-HERO IMPORTS are never reported all-clear: the linter discloses that@@ -192,9 +198,12 @@ follow before letting the human generate:  1. Capture the app's REAL UI at the slot size pose printed (also live in the    left panel, click-to-copy), so nothing crops. In pup:-   `adom-desktop browser_open_window '{"sessionId":"vitrine-shot","profile":"vitrine-shot","url":"<app url>"}'`+   `adom-bridge-cli pup_open_window '{"sessionId":"vitrine-shot","profile":"vitrine-shot","url":"<app url>"}'`    then size the viewport to the slot aspect and-   `adom-desktop browser_screenshot '{"sessionId":"vitrine-shot"}'`.+   `adom-bridge-cli pup_screenshot '{"sessionId":"vitrine-shot"}'`.+   (Legacy boxes still ship `adom-desktop` with `browser_*` verb names; the+   studio's own pup ladder auto-resolves whichever generation the box has,+   hero-studio#31, and doctor prints which one answered.) 2. Load it: `hero-studio vitrine --file <shot.png>` (the open studio updates    live within a second). @@ -208,14 +217,28 @@ so every command lands in front of the human as you run it: ```bash hero-studio set theme=aurora gradAngle=220     # or: set '{"layout":"showcase"}' hero-studio set fontSize.title=9.5             # dot-paths build nested patches+hero-studio set fontSize.leading=0.96 fontSize.gap=1.2   # title line-height + text-block gap+hero-studio set headline="Pose. Approve. Ship."  # what BAKES as the title; page title stays metadata hero-studio set --list                         # the valid enum tokens (also in GET /state as `enums`)-hero-studio vitrine --file shot.png [--full] [--cover] [--chrome none|fake]+hero-studio preset bridge-family               # apply a family design system in one call; no name lists them+hero-studio vitrine --file shot.png [--full] [--cover] [--chrome none|fake|bare] hero-studio bg --file tile.png [--cover]       # YOUR brand texture as theme 'image'; composed text + linting still apply hero-studio state                              # read everything, incl. slotSize + enums+hero-studio compose preview.png                # headless compose of the CURRENT pose: verify staging without a browser (no provenance stamp; never publishable) hero-studio toast "trying the showcase layout" --kind info hero-studio reroll                             # new procedural circuitry pattern+hero-studio instances                          # every live studio on ports 8846-8866 (port, page, gate open?)+hero-studio stop --port N                      # polite shutdown of one instance; no zombie servers ``` +HEADLINE vs TITLE (hero-studio#33): the baked hero text is the HEADLINE+(`state.headline`, `--headline`, or the Headline field at the gate); when+empty, the page title is used. Punching up the hero copy therefore never+mutates page metadata, and Apply/changelog only carry title edits the human+made on purpose. The gate always shows the LIVE page: pose and the page+dropdown re-fetch title/brief from the wiki (#30), so a stale fixture row+cannot be ratified.+ Invalid enum values are REJECTED with the valid list (400 from /state); an ok:true means the value really landed. state.lint is computed by the OPEN studio page, so it is empty until a browser surface has rendered the pose;@@ -225,8 +248,10 @@ band scan) mirrors there for the driving AI to fix before review. The vitrine's FAKE titlebar is auto-off whenever an imported screenshot is present: a real window shot carries its own chrome, and stacking the fake bar above it reads as fake (hero-studio#11). The CSS mock keeps the fake bar.-Override either way with `--chrome none|fake`, the "Draw the fake window-titlebar" checkbox, or `set '{"vitrine":{"chrome":"none"}}'`.+Override either way with `--chrome none|fake|bare`, the "Draw the fake window+titlebar" checkbox, or `set '{"vitrine":{"chrome":"none"}}'`. `bare` goes+further than `none`: no titlebar, no frame border, no background, no shadow;+the import is placed as-is with its alpha preserved (the bridge-family look).  Layouts now include `showcase` (the vitrine gets ~65% of the hero behind a gradient scrim, Adom-USB style). Themes add `aurora`, `mesh`, and `circuitry`,@@ -236,10 +261,18 @@ the seed rides the provenance stamp and `reroll` re-rolls it. Full-hero import is the ratification path for an existing image: import the image, check "Use the imported image as the FULL hero", ratify the checklist as the human, Generate, and the export is that image with a legitimate studio-provenance stamp (the type pill and slug bake on top). This is the official-mechanism for keeping a good pre-gate hero through a deliberate hero change;-the registry accepts any studio-stamped PNG, so no server blessing exists or is-needed.+provenance stamp. This is the official mechanism for keeping a good pre-gate+hero through a deliberate hero change; the registry accepts any studio-stamped+PNG, so no server blessing exists or is needed.++Turning full-import ON auto-disables ALL composed overlays (pill, scores,+author, slug): imports usually carry their own, and the sim-on-top double+pill was a whole rejection class (hero-studio#32). Re-enabling one afterwards+is allowed; it renders with an amber dashed fence in the studio marking it as+a simulation (the fence never bakes into the export). When a full import has+every overlay off, Generate BYPASSES the browser entirely (provenance+`renderer: passthrough`): your pixels are decoded, normalized to 2000x1250,+and stamped, with zero recompression drift (hero-studio#36).  ## What the human controls @@ -253,11 +286,14 @@ needed.  ## What exports -The exported PNG carries: background art, title, subtitle, screenshot, the type-pill (top right), and the full wiki slug (bottom right). The pill and slug BAKE-into the image; the wiki page shows the hero exactly as exported, with no overlay-band. The only surface that overlays dynamic info (stats, author) is the homepage-marquee, and it brings its own, so keep the lower band of the image calm.+The exported PNG carries: background art, headline (or title), subtitle,+screenshot, and the type pill (top right). The pill BAKES into the image; the+wiki page shows the hero exactly as exported, with no overlay band. The baked+bottom-right slug is now OFF by default (hero-studio#29: it doubled with the+marquee's live slug and read as debug text); `set overlay.slug=true` bakes it+deliberately. The only surface that overlays dynamic info (stats, author,+slug) is the homepage marquee, and it brings its own, so keep the lower band+of the image calm.  ## Drive the posing state (posing aid, not generation) @@ -277,7 +313,9 @@ State fields: `page` (owner/slug), `layout`, `theme`, `gradAngle`, `titleMode`, (imported content; src is a data URL; cover crops instead of letterboxing), `marqueePreview` (composite today's homepage-marquee overlay for judging composition and title redundancy; never exported), `fontSize.title/sub/desc`-(cqw; author-adjustable image-zone type, defaults tuned for dock legibility),+(cqw) plus `fontSize.leading` (title line-height) and `fontSize.gap` (scales+the gaps between title/subtitle/detail), `headline` (what bakes as the title;+empty = page title), `approved` ({page, file, ts}: the persistent gate-closed record, set on Generate and shown as the green Approved banner), `guides`. 
bin/hero-studio+30−1
@@ -7,6 +7,35 @@ DIR="$(cd "$(dirname "$SELF")/.." && pwd)" case "$1" in   pose) shift; exec node "$DIR/pose.mjs" "$@" ;;   doctor) shift; exec node "$DIR/doctor.mjs" "$@" ;;-  vitrine|set|state|toast|reroll|changes|bg) exec node "$DIR/drive.mjs" "$@" ;;+  vitrine|set|preset|state|toast|reroll|changes|bg|compose|instances|stop) exec node "$DIR/drive.mjs" "$@" ;;+  -h|--help|help)+    # hero-studio#36: --help used to fall through to the server catch-all and+    # silently START an instance; help must never have side effects.+    cat <<'HELP'+hero-studio: pose package heroes in front of a human, generate approved PNGs++  hero-studio                          start the studio server (foreground; --port <n>)+  hero-studio pose <owner/slug> --surface webview|pup+                                       open the pose gate and block for approval+        [--title T] [--brief B] [--type app|skill|component[,more]]+        [--headline H] [--fresh-vitrine] [--steal] [--no-fallback]+        [--port <n>] [--timeout SECS]+  hero-studio doctor                   verify both render paths, print exact fixes+  hero-studio vitrine --file <png> [--full] [--cover] [--chrome none|fake|bare]+  hero-studio bg --file <image> [--cover]+  hero-studio set '<json>' | key=value [...] | set --list+  hero-studio preset [name]            apply a family design system; no name lists them+  hero-studio state                    print live state (includes enums + lint)+  hero-studio changes                  this review session's ledger (page + pose edits)+  hero-studio compose [out.png]        headless compose of the current pose (no stamp)+  hero-studio toast "text" [--kind info|success|warn]+  hero-studio reroll                   re-roll the circuitry theme pattern+  hero-studio instances                list live studios on ports 8846-8866+  hero-studio stop [--port <n>]        shut one instance down politely++All drive verbs take --port <n> (or HERO_STUDIO_PORT) to target one of several+concurrent instances. Full reference: the package SKILL.md on the wiki.+HELP+    exit 0 ;;   *) exec node "$DIR/server.js" "$@" ;; esac
doctor.mjs+9−5
@@ -150,16 +150,20 @@ export async function runChecks() {   // desktop relay answering plus a proxy URL the host can reach the studio on.   let pupOk = false;   const proxied = !!process.env.VSCODE_PROXY_URI;-  const ad = await run('adom-desktop', ['browser_status', '{}'], 12000);-  const adAnswered = ad.ok && !ad.stdout.includes('"error"');+  // Post-rename boxes answer via adom-bridge-cli with pup_* verbs (#31); the+  // adapter resolves whichever generation this box has.+  const { makeBridge } = await import('./bridge.mjs');+  const bridge = await makeBridge(process.env.ADOM_TARGET || '');+  const ad = await bridge.call('status', {}, 12000);+  const adAnswered = ad.ok && !ad.out.includes('"error"') && !ad.out.includes('_adom_use_cli_instead');   if (adAnswered && proxied) {     pupOk = true;-    pass('pup fallback', 'Adom Desktop answers; the host browser can render');+    pass('pup fallback', bridge.generation + ' answers (' + bridge.verbs.status + '); the host browser can render');   } else {     fail('pup fallback', !adAnswered-      ? 'Adom Desktop did not answer browser_status (no host relay from this container)'+      ? 'the desktop bridge did not answer (' + bridge.generation + ', ' + bridge.verbs.status + '); no host relay from this container'       : 'VSCODE_PROXY_URI is not set, so the host browser cannot reach the studio',-      'optional when the local renderer works; otherwise start Adom Desktop on the host and retry');+      'optional when the local renderer works; otherwise start the desktop bridge app on the host and retry');   }    // 6. python3 + PIL stamp the provenance metadata after the render (and crop
drive.mjs+52−3
@@ -3,7 +3,7 @@ // The open studio follows within a second (SSE + poll), so the human watches // the pose change as commands land. //-//   hero-studio vitrine --file <png> [--full] [--cover] [--chrome none|fake]   load a real screenshot+//   hero-studio vitrine --file <png> [--full] [--cover] [--chrome none|fake|bare] load a real screenshot //   hero-studio set '<json>' | key=value [...]            patch posing state //   hero-studio state                                     print current state //   hero-studio toast "<text>" [--kind info|success|warn] narrate to the human@@ -37,7 +37,7 @@ try {     const file = path.resolve(args[fi + 1]);     const ci = args.indexOf('--chrome');     const chrome = ci >= 0 ? args[ci + 1] : '';-    if (chrome && chrome !== 'none' && chrome !== 'fake') die('--chrome takes none|fake (default: auto, which is none for imported screenshots)');+    if (chrome && !['none', 'fake', 'bare'].includes(chrome)) die('--chrome takes none|fake|bare (bare: no frame, bg, or shadow at all; default: auto, which is none for imported screenshots)');     const out = await jf('vitrine-file', {       path: file,       full: args.includes('--full'),@@ -101,8 +101,57 @@ try {     console.log('{"ok":true}');   } else if (verb === 'reroll') {     console.log(JSON.stringify(await jf('reroll', {})));+  } else if (verb === 'preset') {+    // named family design systems (#34.2); no arg lists what exists+    const name = args.find((a) => !a.startsWith('--'));+    const r = await fetch(BASE + 'preset', {+      method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name || '' }),+    });+    const out = await r.json().catch(() => ({}));+    if (!r.ok || out.ok === false) {+      if (out.valid) { console.log(JSON.stringify({ presets: out.valid }, null, 2)); process.exit(name ? 1 : 0); }+      die('ERROR: ' + (out.error || 'preset rejected'));+    }+    console.log(JSON.stringify(out));+    console.error('preset ' + out.applied + ' applied; the open studio updates within a second');+  } else if (verb === 'instances') {+    // every live studio on this box (#26): scan the port neighborhood+    const ports = new Set([PORT]);+    for (let p2 = 8846; p2 <= 8866; p2++) ports.add(p2);+    const rows = [];+    await Promise.all([...ports].map(async (p2) => {+      try {+        const ac = new AbortController();+        const t = setTimeout(() => ac.abort(), 900);+        const r = await fetch('http://localhost:' + p2 + '/state', { signal: ac.signal });+        clearTimeout(t);+        if (!r.ok) return;+        const st = await r.json();+        if (!st || !st.page) return;+        rows.push({ port: p2, page: st.page, gateOpen: !!st.verify, posedAt: st.posedAt || null, approved: st.approved ? st.approved.file : null });+      } catch {}+    }));+    rows.sort((a, b) => a.port - b.port);+    console.log(JSON.stringify({ instances: rows }, null, 2));+    if (!rows.length) console.error('no live studio instances on ports 8846-8866; `hero-studio` or `hero-studio pose ...` starts one');+  } else if (verb === 'stop') {+    // polite shutdown of one instance (#26); refuses nothing, targets by --port+    const out = await jf('shutdown', {});+    console.log(JSON.stringify(out));+    console.error('studio on :' + (out.port || PORT) + ' is shutting down');+  } else if (verb === 'compose') {+    // headless compose of the CURRENT pose (#36.2): verify staging without a+    // browser. Not the gate: no provenance stamp, never publishable.+    const fsm = await import('fs');+    const outFile = path.resolve(args.find((a) => !a.startsWith('--')) || 'compose.png');+    const r = await fetch(BASE + 'compose.png');+    if (!r.ok) die('ERROR: compose render failed (' + r.status + '): ' + (await r.text()).slice(0, 300));+    const buf = Buffer.from(await r.arrayBuffer());+    fsm.writeFileSync(outFile, buf);+    console.log(JSON.stringify({ ok: true, file: outFile, bytes: buf.length }));+    console.error('composed preview written (no provenance stamp; the pose gate + Generate remain the only publish path)');   } else {-    die('verbs: pose | vitrine | set | state | toast | reroll');+    die('verbs: pose | vitrine | set | preset | state | toast | reroll | changes | bg | compose | instances | stop');   } } catch (e) {   die('ERROR: ' + e.message + '\n  hint: is the studio server up? `hero-studio` starts it; `hero-studio pose <ref> --surface ...` does everything');
package.json+2−2
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.15.0",+  "version": "0.16.0",   "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": [@@ -91,4 +91,4 @@     "render-pup.mjs",     "screenshots/hero.png"   ]-}+}\ No newline at end of file
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.15.0",+  "version": "0.16.0",   "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": [
pose.mjs+25−22
@@ -15,6 +15,7 @@ import { fileURLToPath } from 'url'; import path from 'path'; import fs from 'fs'; import { runChecks, printReport } from './doctor.mjs';+import { makeBridge } from './bridge.mjs';  const DIR = path.dirname(fileURLToPath(import.meta.url)); let BASE = 'http://localhost:8846/';@@ -66,23 +67,13 @@ const jf = async (p, opts) => {   if (!r.ok) throw new Error(p + ' -> ' + r.status);   return r.json(); };-// Multiple Adom Desktops on one relay need addressing (#24 ask 3):-// --target <name> or ADOM_TARGET rides every adom-desktop call.+// Multiple desktops on one relay need addressing (#24 ask 3): --target+// <name> or ADOM_TARGET. All desktop-bridge calls go through the adapter+// (hero-studio#31: the Adom Bridge v2 rename changed the binary AND the verb+// prefix; the adapter resolves both at runtime). const AD_TARGET = flags.target || process.env.ADOM_TARGET || '';-const adArgs = (a) => (AD_TARGET ? [...a, '--target', AD_TARGET] : a); const sh = (cmd, a, timeout = 20000) => new Promise((res) => {-  execFile(cmd, cmd === 'adom-desktop' ? adArgs(a) : a, { timeout }, (err, stdout) => res(err ? null : stdout));-});-// Verbose variant: keeps the output and failure mode, so a pup failure can be-// DIAGNOSED instead of swallowed (hero-studio#12: PUP_UNREACHABLE fired while-// the same relay demonstrably worked, and nothing said why).-const shv = (cmd, a, timeout = 20000) => new Promise((res) => {-  execFile(cmd, cmd === 'adom-desktop' ? adArgs(a) : a, { timeout }, (err, stdout, stderr) => res({-    ok: !err,-    out: String(stdout || ''),-    errOut: String(stderr || ''),-    fail: err ? (err.killed ? 'timed out after ' + timeout + 'ms' : 'exit ' + (err.code ?? '?')) : '',-  }));+  execFile(cmd, a, { timeout }, (err, stdout) => res(err ? null : stdout)); });  // 1. pick an instance (hero-studio#15: concurrent studios). Explicit@@ -158,6 +149,9 @@ await jf('state', {     confirm: { title: false, subtitle: false, vitrine: false, type: false },     flags: { title: false, subtitle: false, vitrine: false, type: false },     lint: [],+    // #33: the baked headline is pose state, distinct from the page title;+    // --headline sets it, otherwise reset so the previous page's cannot leak+    headline: flags.headline || '',     posedAt: Date.now(),     // Clear the previous package's vitrine so a failed auto-pull can never     // leave sibling A's screenshot in sibling B's hero -- but PRESERVE staged@@ -169,6 +163,13 @@ await jf('state', {   }), }); +// 2a-live. re-pull title/brief/type from the wiki (#30/#33): the seeded row+// above is only a fallback; the LIVE page wins when it exists.+try {+  const lv = await jf('refresh-page?page=' + ref, { method: 'POST' });+  if (lv && lv.ok) console.error('live page: "' + lv.title + '"');+} catch {}+ // 2b. fresh change ledger for THIS review session (#20): everything the human // edits at the gate (page fields + pose deltas) accumulates from here and // rides back in this command's final JSON as `changes`.@@ -195,21 +196,23 @@ if (surface === 'pup') {   // Adom Desktop pup window. Reuse an existing session for this instance;   // never spawn a duplicate window (browser_status first, then   // navigate-or-open). Every step's real output is kept for diagnosis.+  const bridge = await makeBridge(AD_TARGET);+  console.error('desktop bridge: ' + bridge.generation + ' (verbs: ' + bridge.verbs.open_window + ' family)');   const steps = [];-  const status = await shv('adom-desktop', ['browser_status', '{}']);-  steps.push(['browser_status', status]);+  const status = await bridge.call('status', {}, 15000);+  steps.push([bridge.verbs.status, status]);   const hasSession = status.ok && status.out.includes(SESSION);-  const looksOk = (r) => r.ok && !r.out.includes('"error"');+  const looksOk = (r) => r.ok && !r.out.includes('"error"') && !r.out.includes('_adom_use_cli_instead');   let opened = hasSession-    ? await shv('adom-desktop', ['browser_navigate', JSON.stringify({ sessionId: SESSION, url: proxyBase })], 30000)-    : await shv('adom-desktop', ['browser_open_window', JSON.stringify({ sessionId: SESSION, profile: SESSION, url: proxyBase })], 60000);-  steps.push([hasSession ? 'browser_navigate' : 'browser_open_window', opened]);+    ? await bridge.call('navigate', { sessionId: SESSION, url: proxyBase }, 30000)+    : await bridge.call('open_window', { sessionId: SESSION, profile: SESSION, url: proxyBase }, 60000);+  steps.push([hasSession ? bridge.verbs.navigate : bridge.verbs.open_window, opened]);   if (!looksOk(opened) && !hasSession) {     // A first open on a cold profile can outlive its exec timeout while the     // window is still coming up; poll for the session before calling it dead.     for (let i = 0; i < 6 && !looksOk(opened); i++) {       await new Promise((r) => setTimeout(r, 5000));-      const st = await shv('adom-desktop', ['browser_status', '{}']);+      const st = await bridge.call('status', {}, 15000);       if (st.ok && st.out.includes(SESSION)) opened = { ok: true, out: st.out, errOut: '', fail: '' };     }   }
public/index.html+99−8
@@ -202,11 +202,11 @@ label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:po           mask:url('adom-logo.svg') no-repeat left center / contain; } .h-title {-  font-family:'Familjen Grotesk'; font-weight:700; font-size:var(--fs-title, 8cqw); line-height:1.02;-  letter-spacing:-.14cqw; color:#e6edf3; margin-bottom:1.8cqw;+  font-family:'Familjen Grotesk'; font-weight:700; font-size:var(--fs-title, 8cqw); line-height:var(--fs-leading, 1.02);+  letter-spacing:-.14cqw; color:#e6edf3; margin-bottom:calc(1.8cqw * var(--fs-gap, 1)); } .h-title { max-width:30cqw; }-.h-sub { font-size:var(--fs-sub, 2.5cqw); line-height:1.45; color:#e8eef4; margin-bottom:1.2cqw;+.h-sub { font-size:var(--fs-sub, 2.5cqw); line-height:1.45; color:#e8eef4; margin-bottom:calc(1.2cqw * var(--fs-gap, 1));   max-width:30cqw; text-shadow:0 1px 6px rgba(0,0,0,.45); } .hero[data-theme=gradient] .h-sub { color:#f6fafd; } .h-sub .accent { background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent; font-weight:500; }@@ -476,6 +476,7 @@ body[data-mode=component] .mode-app { display:none; } #lintbox .lint-warn { color:#e8a13c; } #lintbox .lint-fail { color:#f85149; font-weight:600; } #lintbox .lint-ok { color:#3fb950; font-size:12px; }+#lintbox .lint-info { color:#8b949e; } #tourpop .tp-flag { background:rgba(248,81,73,.12); border:1px solid rgba(248,81,73,.6); color:#f85149; } #tourpop .tp-head { cursor:grab; user-select:none; } #tourpop.dragging .tp-head { cursor:grabbing; }@@ -516,6 +517,14 @@ body.zen main { max-width:none; } #tourpop .tp-row { display:flex; gap:8px; } .tour-above { position:relative !important; z-index:901 !important; } .hero.no-chrome .window .titlebar { display:none; }+/* bare (#34.1): the import IS the artwork; no frame, no bg, no shadow, alpha kept */+.hero.bare-chrome .window .titlebar { display:none; }+.hero.bare-chrome .window { background:transparent; border:none; box-shadow:none; border-radius:0; }+/* simulated-overlay fence (#32.3): overlays re-enabled OVER a full import are+   sims of elements the import may already carry; amber marks them as such */+.sim-fenced { outline:2px dashed #d29922; outline-offset:3px; }+/* the fence is studio chrome: it must never bake into the exported PNG */+body[data-render] .sim-fenced { outline:none !important; } .tour-glow { outline:3px solid var(--accent-bright) !important; outline-offset:3px; border-radius:4px;   box-shadow:0 0 24px rgba(0,230,220,.35) !important; } @@ -705,9 +714,12 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh     <label class="chk"><input type="checkbox" id="chk-title"> Title (big headline)</label>     <label class="chk"><input type="checkbox" id="chk-subtitle"> Value-prop subtitle</label>     <label class="chk"><input type="checkbox" id="chk-description"> Detail line</label>-    <div class="brandrow"><label>Title size</label><input type="range" id="fs-title" min="5" max="11" step="0.1" value="8"><span class="hint" id="fs-title-val">8</span></div>+    <div class="brandrow"><label>Title size</label><input type="range" id="fs-title" min="5" max="13" step="0.1" value="8"><span class="hint" id="fs-title-val">8</span></div>     <div class="brandrow"><label>Subtitle size</label><input type="range" id="fs-sub" min="1.8" max="3.5" step="0.1" value="2.5"><span class="hint" id="fs-sub-val">2.5</span></div>     <div class="brandrow"><label>Detail size</label><input type="range" id="fs-desc" min="1.0" max="2.0" step="0.05" value="1.35"><span class="hint" id="fs-desc-val">1.35</span></div>+    <div class="brandrow" data-tip="Title line-height: tighten multi-line titles without touching size"><label>Title leading</label><input type="range" id="fs-leading" min="0.92" max="1.3" step="0.01" value="1.02"><span class="hint" id="fs-leading-val">1.02</span></div>+    <div class="brandrow" data-tip="Scales the vertical gaps between title, subtitle and detail line"><label>Text gap</label><input type="range" id="fs-gap" min="0.5" max="2" step="0.05" value="1"><span class="hint" id="fs-gap-val">1</span></div>+    <div class="brandrow" data-tip="What bakes as the hero title. The page title is metadata and stays untouched; leave empty to use it (hero-studio#33)"><label>Headline</label><input type="text" id="headline-input" placeholder="(page title)" style="flex:1;min-width:0"></div>     <div class="hint">The defaults are tuned to read at dock size and set you up for success. You are the author: adjust if your name or copy needs it, thoughtfully.</div>      <h3>Title placement</h3>@@ -1013,7 +1025,9 @@ function render() {   hero.style.setProperty('--fs-title', (fs.title || 8) + 'cqw');   hero.style.setProperty('--fs-sub', (fs.sub || 2.5) + 'cqw');   hero.style.setProperty('--fs-desc', (fs.desc || 1.35) + 'cqw');-  for (const [id, val, dflt] of [['fs-title', fs.title, 8], ['fs-sub', fs.sub, 2.5], ['fs-desc', fs.desc, 1.35]]) {+  hero.style.setProperty('--fs-leading', String(fs.leading || 1.02));+  hero.style.setProperty('--fs-gap', String(fs.gap == null ? 1 : fs.gap));+  for (const [id, val, dflt] of [['fs-title', fs.title, 8], ['fs-sub', fs.sub, 2.5], ['fs-desc', fs.desc, 1.35], ['fs-leading', fs.leading, 1.02], ['fs-gap', fs.gap, 1]]) {     $(id).value = val || dflt;     $(id + '-val').textContent = (val || dflt);   }@@ -1089,8 +1103,14 @@ function render() {   // an import is present; the CSS mock keeps it. Explicit choice wins.   const chromeMode = vit.chrome || ((vit.src && !vit.full) ? 'none' : 'fake');   hero.classList.toggle('no-chrome', chromeMode === 'none');+  hero.classList.toggle('bare-chrome', chromeMode === 'bare');   $('vitrine-chrome').checked = chromeMode === 'fake';   hero.classList.toggle('fullimport', !!vit.src && !!vit.full);+  const simOverImport = !!(vit.src && vit.full);+  for (const sid of ['pill-tr', 'slug-br']) {+    const sEl = $(sid);+    if (sEl) sEl.classList.toggle('sim-fenced', simOverImport);+  }   $('vitrine-img').src = (!vit.full && vit.src) || '';   $('fullimport-img').src = (vit.full && vit.src) || '';   $('vitrine-full').checked = !!vit.full;@@ -1126,7 +1146,9 @@ function render() {   const showTitleInImage = state.show.title && state.titleMode !== 'overlay-only';   $('el-logo').style.display = (state.show.logo && !foreign) ? 'flex' : 'none';   $('el-title').style.display = showTitleInImage ? 'block' : 'none';-  $('el-title').textContent = p.title;+  $('el-title').textContent = state.headline || p.title;+  const hlIn = $('headline-input');+  if (hlIn && document.activeElement !== hlIn) hlIn.value = state.headline || '';   $('el-sub').style.display = state.show.subtitle ? 'block' : 'none';   $('el-sub').innerHTML = accentize(firstSentence(p.brief, 80));   $('el-desc').style.display = state.show.description ? 'block' : 'none';@@ -1284,7 +1306,24 @@ $('sel-comp').addEventListener('change', e => send({ comp: { id: e.target.value document.querySelectorAll('#grp-imgsrc .opt').forEach(b => b.addEventListener('click', () => send({ comp: { imgSrc: b.dataset.v } }))); document.querySelectorAll('#grp-clayout .opt').forEach(b => b.addEventListener('click', () => send({ comp: { layout: b.dataset.v } }))); document.querySelectorAll('#grp-mpn .opt').forEach(b => b.addEventListener('click', () => send({ comp: { mpn: b.dataset.v } })));-$('sel-page').addEventListener('change', e => { send({ page: e.target.value }); });+$('sel-page').addEventListener('change', e => {+  const v = e.target.value;+  send({ page: v });+  // #30: dropdown rows are pose-time snapshots; pull the live page so a+  // just-renamed title or brief cannot be ratified stale.+  fetch('refresh-page?page=' + encodeURIComponent(v), { method: 'POST' })+    .then(r => r.json())+    .then(j => {+      if (!j || !j.ok) return;+      return fetch('api/pages').then(r => r.json()).then(rows => {+        pages = rows;+        const sel = $('sel-page');+        sel.innerHTML = pages.map(p => '<option value="' + p.owner + '/' + p.slug + '">' + p.title + ' (' + p.type + ')</option>').join('');+        sel.value = v;+        render();+      });+    }).catch(() => {});+}); document.querySelectorAll('#grp-layout .opt').forEach(b => b.addEventListener('click', () => send({ layout: b.dataset.v }))); document.querySelectorAll('#grp-theme .swatch').forEach(b => b.addEventListener('click', () => send({ theme: b.dataset.v }))); document.querySelectorAll('#grp-titlemode .opt').forEach(b => b.addEventListener('click', () => send({ titleMode: b.dataset.v })));@@ -1600,6 +1639,52 @@ function runLint() {     if (lines >= 4) items.push({ id: 'title-wrap', level: 'warn', el: 'el-title', msg: 'title wraps to ' + lines + ' lines; shorten it or lower the title size (left panel)' });   } +  // 5. text vs vitrine (#25): the title owns its column. The fix is ALWAYS to+  // move or resize the VITRINE (layout, size), never to shrink the title:+  // small type is invisible on the dock, and #25's six-hour session shrank the+  // title four times before anyone questioned the vitrine.+  for (const t of texts) {+    if (vitrine && overlapPx(t.box, vitrine) > 9) {+      items.push({ id: 'vitrine-' + t.id, level: 'warn', el: t.id, msg: 'the ' + t.name + ' collides with the vitrine: resize or move the VITRINE (layout picker, vitrine size), do not shrink the ' + t.name + ' to fit' });+    }+  }+  const fsNow = (state.fontSize || {});+  if (vis(tEl) && (fsNow.title || 8) < 6.5) {+    items.push({ id: 'title-timid', level: 'warn', el: 'el-title', msg: 'title is at ' + fsNow.title + 'cqw, under the 6.5cqw family floor; dock thumbnails render this illegibly. If it was shrunk to clear the vitrine, resize the vitrine instead' });+  }++  // 6. optical alignment (retrospective #35 fix 4): glyph side bearing shifts+  // the INK edge off the box edge; boxes can align while the pixels do not.+  if (vis(tEl) && texts.length >= 2) {+    try {+      const cv2 = (runLint._cv || (runLint._cv = document.createElement('canvas').getContext('2d')));+      const inkLeft = (el) => {+        const ch = (el.textContent || '').trim().charAt(0);+        if (!ch) return null;+        const cs = getComputedStyle(el);+        cv2.font = cs.fontWeight + ' ' + cs.fontSize + ' ' + cs.fontFamily;+        const mm = cv2.measureText(ch);+        return el.getBoundingClientRect().left - (mm.actualBoundingBoxLeft || 0);+      };+      const a = inkLeft(tEl);+      const b = inkLeft($(texts[1].id));+      if (a != null && b != null) {+        const exportPx = Math.round(Math.abs(a - b) * 2000 / H.width);+        if (exportPx > 5) {+          items.push({ id: 'ink-align', level: 'warn', el: 'el-title', msg: 'optical alignment: the title\'s ink edge sits ' + exportPx + 'px (export scale) ' + (a > b ? 'right' : 'left') + ' of the ' + texts[1].name + '\'s because of glyph side bearing; reword the first word or accept it deliberately' });+        }+      }+    } catch {}+  }++  // 7. published headroom (#28): how much calm space is left under the lowest+  // text before the marquee band, in EXPORT pixels; informational.+  if (texts.length && state.mode === 'app') {+    const lowest = Math.max.apply(null, texts.map(t => t.box.bottom));+    const px = Math.round((bandTop - lowest) * 2000 / H.width);+    items.push({ id: 'headroom', level: 'info', msg: 'published headroom: ' + px + 'px between the lowest image-zone text and the marquee band at export scale (2000x1250)' });+  }+   renderLintBox(items);   // publish to the server (deduped) so the driving AI sees measured findings   const key = JSON.stringify(items.map(i => i.id + i.level));@@ -1869,7 +1954,13 @@ $('btn-reroll').addEventListener('click', () => { fetch('reroll', { method: 'POS  /* image-zone type sizes: instant local feedback, debounced send */ let fsTimer = null;-for (const [id, key] of [['fs-title', 'title'], ['fs-sub', 'sub'], ['fs-desc', 'desc']]) {+let hlTimer = null;+$('headline-input').addEventListener('input', e => {+  clearTimeout(hlTimer);+  const v = e.target.value;+  hlTimer = setTimeout(() => send({ headline: v }), 350);+});+for (const [id, key] of [['fs-title', 'title'], ['fs-sub', 'sub'], ['fs-desc', 'desc'], ['fs-leading', 'leading'], ['fs-gap', 'gap']]) {   $(id).addEventListener('input', e => {     const v = Number(e.target.value);     $(id + '-val').textContent = v;
render-pup.mjs+10−6
@@ -9,17 +9,21 @@ // Exit 0 with the cropped PNG at out.png; nonzero with a plain message. import { execFile } from 'child_process'; import fs from 'fs';+import { makeBridge } from './bridge.mjs';  const [,, url, out] = process.argv; if (!url || !out) { console.error('usage: render-pup.mjs <url> <out.png>'); process.exit(1); }  const SESSION = 'hero-render';-// ADOM_TARGET addresses a specific desktop when several share the relay (#24)-const AD_TARGET = process.env.ADOM_TARGET || '';-const sh = (args, timeout = 20000) => new Promise((res) => {-  execFile('adom-desktop', AD_TARGET ? [...args, '--target', AD_TARGET] : args, { timeout }, (err, stdout, stderr) =>-    res({ ok: !err, out: String(stdout || '') + String(stderr || '') }));-});+// ADOM_TARGET addresses a specific desktop when several share the relay (#24);+// the adapter resolves the binary + verb prefix (post-rename boxes, #31).+const bridge = await makeBridge(process.env.ADOM_TARGET || '');+const sh = async (args, timeout = 20000) => {+  // args[0] is a legacy browser_* verb name; map by suffix through the adapter+  const suffix = args[0].replace(/^browser_/, '');+  const r = await bridge.call(suffix, args[1] ? JSON.parse(args[1]) : {}, timeout);+  return { ok: r.ok, out: r.out + r.errOut };+}; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const die = (msg) => { console.error('pup renderer: ' + msg); process.exit(1); }; 
server.js+114−4
@@ -24,19 +24,20 @@ let state = {   theme: 'midnight',        // midnight | pcb | onion | pill | gradient   show: { logo: true, title: true, subtitle: true, description: false },   titleMode: 'image-only', // overlay-only | image-only | both; image-only per John: the overlay is dynamic data, not a second title-  overlay: { pill: true, scores: false, author: true, slug: true }, // scores are live-replaced on the marquee; sim defaults them off+  overlay: { pill: true, scores: false, author: true, slug: false }, // slug defaults OFF (#29: the baked slug doubled with the marquee's live slug and was the top rejection cause); scores are live-replaced on the marquee; sim defaults them off   brand: { accent: '', bg: '' },  // non-adom owner palette; empty = studio default   verify: false,  // the pose gate turns this on: verification chrome + checklist   confirm: { title: false, subtitle: false, vitrine: false, type: false },   flags: { title: false, subtitle: false, vitrine: false, type: false },  // needs-fixing verdicts (#14): hold Generate, visible to the driving AI   lint: [],  // measured layout findings from the UI's linter (#18): [{id, level: warn|fail, msg}]-  vitrine: { src: '', full: false, cover: false, chrome: '' },  // imported screenshot (data URL); full = whole-hero re-stamp; cover = crop-to-fill; chrome '' = auto (fake bar for the mock, none for imports), 'fake'|'none' = explicit+  vitrine: { src: '', full: false, cover: false, chrome: '' },  // chrome: ''=auto | 'fake' | 'none' (no titlebar) | 'bare' (no frame/bg/shadow at all, alpha preserved) // imported screenshot (data URL); full = whole-hero re-stamp; cover = crop-to-fill; chrome '' = auto (fake bar for the mock, none for imports), 'fake'|'none' = explicit   marqueePreview: false,  // composite today's homepage-marquee overlay over the hero (preview only)   tour: false,            // verification walkthrough: spotlight title -> subtitle -> vitrine -> type   slotSize: null,         // measured vitrine slot in export px, posted back by the UI ({w,h})   bgImage: { src: '', cover: false },  // imported background art for theme 'image' (hero-studio#24)   circuitSeed: Date.now() % 2147483647,  // procedural circuitry theme seed; /reroll re-rolls-  fontSize: { title: 8, sub: 2.5, desc: 1.35 },  // cqw; author-adjustable, defaults tuned for dock legibility+  fontSize: { title: 8, sub: 2.5, desc: 1.35, leading: 1.02, gap: 1 },  // cqw sizes + title line-height + text-block gap (em); author-adjustable, defaults tuned for dock legibility+  headline: '',    // what BAKES as the hero title; '' = use the page title (hero-studio#33: title != headline)   approved: null,  // { page, file, ts } set on Generate: the persistent gate-closed record   gradAngle: 155,   guides: true,@@ -59,7 +60,7 @@ const POSE_TRACKED = [   'mode', 'theme', 'layout', 'gradAngle', 'titleMode',   'show.logo', 'show.title', 'show.subtitle', 'show.description',   'overlay.pill', 'overlay.scores', 'overlay.author', 'overlay.slug',-  'brand.accent', 'brand.bg', 'fontSize.title', 'fontSize.sub', 'fontSize.desc',+  'brand.accent', 'brand.bg', 'fontSize.title', 'fontSize.sub', 'fontSize.desc', 'fontSize.leading', 'fontSize.gap', 'headline',   'vitrine.chrome', 'vitrine.full', 'vitrine.cover', ]; const getPath = (o, p) => p.split('.').reduce((a, k) => (a == null ? a : a[k]), o);@@ -79,6 +80,20 @@ function notePoseDiff(before) { // The valid enum sets (hero-studio#24 asks 1+2): validated on every /state // write and exposed on GET /state as `enums`, so a driving AI never has to // read index.html to learn the tokens.+// Named presets (#34.2): the family design systems lived in N private build+// scripts; a preset applies the family tokens in one call. bridge-family+// carries the constants pup measured and kicad/fusion/nb copied (title 12cqw,+// bare right-side vitrine, overlays off, calm band).+const PRESETS = {+  'bridge-family': {+    layout: 'showcase', theme: 'midnight', titleMode: 'image-only',+    fontSize: { title: 12, sub: 2.2, desc: 1.35 },+    overlay: { pill: true, scores: false, author: false, slug: false },+    vitrine: { chrome: 'bare' },+    guides: false,+  },+};+ const ENUMS = {   layout: ['bleed-bottom', 'bleed-right', 'bleed-top', 'bleed-corner-br', 'bleed-corner-tr', 'contained', 'mirror', 'showcase'],   theme: ['midnight', 'pcb', 'onion', 'pill', 'grid', 'gradient', 'aurora', 'mesh', 'circuitry', 'image'],@@ -146,6 +161,11 @@ const server = http.createServer((req, res) => {           return res.end(JSON.stringify({ ok: false, error: 'invalid ' + k + ' \'' + body[k] + '\'', valid }));         }       }+      const turningFullOn = body.vitrine && body.vitrine.full && !(state.vitrine && state.vitrine.full);+      if (turningFullOn && !body.overlay) {+        body.overlay = { pill: false, scores: false, author: false, slug: false };+        logLine('info', 'full import: composed overlays auto-disabled (#32); re-enable explicitly to simulate them (they render amber-fenced)');+      }       const before = snapshotPose();       merge(state, body);       notePoseDiff(before);@@ -281,6 +301,77 @@ const server = http.createServer((req, res) => {       finish(200, { ok: true, bytes: buf.length, theme: 'image' });     });   }+  if (p === '/preset' && req.method === 'POST') {+    return readBody(req, (err, body) => {+      const name = body && body.name;+      if (!name || !PRESETS[name]) {+        res.writeHead(400, { 'Content-Type': 'application/json' });+        return res.end(JSON.stringify({ ok: false, error: 'unknown preset', valid: Object.keys(PRESETS) }));+      }+      const before = snapshotPose();+      merge(state, PRESETS[name]);+      notePoseDiff(before);+      logLine('info', 'preset applied: ' + name);+      broadcast('state', state);+      res.writeHead(200, { 'Content-Type': 'application/json' });+      res.end(JSON.stringify({ ok: true, applied: name, patch: PRESETS[name] }));+    });+  }+  if (p === '/shutdown' && req.method === 'POST') {+    // instance lifecycle (#26): a polite stop verb so zombies don't accumulate+    logLine('info', 'shutdown requested');+    res.writeHead(200, { 'Content-Type': 'application/json' });+    res.end(JSON.stringify({ ok: true, port: PORT }));+    setTimeout(() => process.exit(0), 150);+    return;+  }+  if (p === '/refresh-page' && req.method === 'POST') {+    // Live wiki truth for the posed page (#33/#30): re-fetch title/brief/type+    // from the registry so the gate never shows a stale pose-time snapshot.+    const ref = url.searchParams.get('page') || state.page;+    const REG = process.env.ADOM_WIKI_REGISTRY || 'https://wiki.adom.inc';+    const headers = {};+    try { headers.Authorization = 'Bearer ' + fs.readFileSync('/var/run/adom/api-key', 'utf8').trim(); } catch {}+    return (async () => {+      const finish = (code, payload) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(payload)); };+      try {+        const r = await fetch(REG + '/api/pages/' + ref, { headers, redirect: 'follow' });+        if (!r.ok) return finish(200, { ok: false, error: 'page fetch ' + r.status });+        const pj = await r.json();+        const pg = pj.page || pj;+        const [owner, slug] = ref.split('/');+        const i = PAGES.findIndex((x) => x.owner === owner && x.slug === slug);+        const live = { owner, slug, title: pg.title, brief: pg.brief, type: pg.type === 'hardware' ? 'component' : pg.type, kind: pg.kind };+        const entry = Object.assign({ author: '', score: 0, stars: 0, installs30d: 0, tags: [] }, i >= 0 ? PAGES[i] : {}, Object.fromEntries(Object.entries(live).filter(([, v]) => v != null)));+        if (i >= 0) PAGES[i] = entry; else PAGES.unshift(entry);+        logLine('info', 'live refresh: ' + ref + ' title="' + pg.title + '"');+        broadcast('state', state);+        finish(200, { ok: true, title: pg.title, brief: pg.brief });+      } catch (e) { finish(200, { ok: false, error: e.message }); }+    })();+  }+  if (p === '/compose.png') {+    // Headless compose (#36.2): the current composed hero WITHOUT provenance,+    // so an agent can verify staging (slug bands, double pills, alignment)+    // without driving a browser. Costs one headless render (~3s).+    if (generating) { res.writeHead(503); return res.end('generate in progress'); }+    const tmp = path.join(__dirname, 'generated', 'compose-preview.png');+    fs.mkdirSync(path.dirname(tmp), { recursive: true });+    const child = spawn(process.execPath, [path.join(__dirname, 'render-hero.mjs'), 'http://localhost:' + PORT + '/?render=1', tmp], { env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });+    let eb = '';+    child.stderr.on('data', (c) => { eb += c; });+    const t = setTimeout(() => child.kill(), 60000);+    child.on('close', (code) => {+      clearTimeout(t);+      if (code !== 0 || !fs.existsSync(tmp)) {+        res.writeHead(500, { 'Content-Type': 'application/json' });+        return res.end(JSON.stringify({ ok: false, error: eb.trim().split('\n').pop() || 'compose render failed' }));+      }+      res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'no-cache' });+      res.end(fs.readFileSync(tmp));+    });+    return;+  }   if (p === '/reroll' && req.method === 'POST') {     state.circuitSeed = (state.circuitSeed * 48271 + Date.now()) % 2147483647;     broadcast('state', state);@@ -403,6 +494,25 @@ const server = http.createServer((req, res) => {       res.writeHead(200, { 'Content-Type': 'application/json' });       res.end(JSON.stringify(payload));     };+    // Passthrough (#36.1): a full-import with every overlay off composes+    // NOTHING in the browser, so a headless re-render can only recompress and+    // color-shift the author's pixels. Decode the import straight to disk and+    // let finalize do the standard 2000px normalize + provenance stamp.+    const ov = state.overlay || {};+    const fullImport = state.vitrine && state.vitrine.full && state.vitrine.src;+    if (fullImport && !ov.pill && !ov.scores && !ov.author && !ov.slug) {+      const b64 = String(state.vitrine.src).match(/^data:image\/png;base64,(.+)$/);+      if (b64) {+        try {+          fs.writeFileSync(outFile, Buffer.from(b64[1], 'base64'));+          logLine('info', 'full-import passthrough: composing nothing, stamping the imported pixels directly');+          finalize('passthrough');+          return;+        } catch (e) {+          logLine('warn', 'passthrough write failed (' + e.message + '); falling back to the render ladder');+        }+      }+    }     // Renderer ladder: local headless Chrome first (full fidelity), then the     // host browser via pup for containers that cannot run Chrome (#7).     const attempts = [];