← Commit history
SKILL.md+33
@@ -9,6 +9,39 @@ A local web app (plain Node, no dependencies) that renders a full 16:10 hero liv pixel-faithful wiki contexts, and generates the provenance-stamped 2000x1250 PNG the registry's hero gate requires. +## The wizard is the surface (0.18)++The studio opens as a bottom-dock WIZARD: seven steps (Package, Showcase+image, Placement, Background, Headline and text, Finishing, Review and+generate), one decision at a time, the hero always live above, a dashed ring+marking what the current step edits. The old full control panel is one click+away (header "Classic") and keeps the component-mode extras. Component pages+ride the same wizard for now. The title ALWAYS bakes (it IS the message) and+its ink top-left is pixel-anchored at any font size (fixed leading + cap+trim; the leading control is gone, the wire field remains inert).++New wire fields for the words: `headline` (baked title override),+`subtitle` (the line under it; '' = the page brief), `accentWords` (word+indices that get the gradient; null = the legacy auto accent). The gate+count rides `gatesDone` in GET /state: at 4+ completed gates the Review+checklist renders pre-ticked (seasoned reviewer; expand to check one by+one). GET /state also carries `resolved` {title, headline, subtitle,+*Source}: the exact strings the surface is showing and where each came from+(live-wiki | fixture | override), so a driving AI never verifies via pixels.++## Automated mode: same-session repeats only++The FIRST Generate of a page each session is always clicked by the human in+the studio. After that, the driving AI may RE-generate that same page in+that same session:++    POST /generate {"aiThread": "...", "attest": {"human": "colby", "quote": "ship it"}}++The attestation is REQUIRED (who approved, their actual words), is baked+into the PNG provenance and the Approved record, and never counts toward+the seasoned gate tally. Everything else is refused with the exact retry.+Hard-fail lint findings lock every Generate path, human or attested.+ ## The ai-thread name is REQUIRED (hero-studio#39)  Every server start and every state-changing verb needs the global flag
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.17.0",+  "version": "0.18.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": [
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.17.0",+  "version": "0.18.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": [
public/hero.cssadded+641
@@ -0,0 +1,641 @@+/* hero.css: every rule that draws the hero, the in-context previews, and the+   ?render=1 export view. This file is the pixel firewall for the wizard+   redesign: the export must stay pixel-identical, so nothing here changes+   without a deliberate, parity-tested decision. Base tokens and fonts live+   at the top because everything else depends on them; hero.css loads after+   studio.css so original cascade order is preserved for the hero rules.+   Both stylesheets live at the public root, not a css/ subfolder: their+   url(...) references (fonts, adom-logo.svg, comp images) resolve relative+   to the stylesheet, and only the root keeps those identical to the old+   inline styles under the reverse proxy. */+++@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }++@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }++@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }++@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }++@font-face { font-family: 'JetBrains Mono'; src: url('fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }+++:root {+  --bg:#0d1117; --surface:#161b22; --elevated:#1c2128; --overlay:#21262d; --border:#30363d;+  --text:#e6edf3; --text-2:#8b949e; --text-3:#484f58;+  --accent:#00b8b0; --accent-bright:#00e6dc; --purple:#8c6bf7; --blue:#58a6ff;+  --green:#3fb950; --red:#f85149; --yellow:#d29922;+  --grad:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7);+}++* { box-sizing:border-box; margin:0; padding:0; }++html,body { height:100%; }++body {+  background:var(--bg); color:var(--text);+  font-family:'Satoshi',sans-serif; font-size:14px;+  display:flex; flex-direction:column; overflow:hidden;+}+++.hero[data-theme=image] .bg { background-color:#0a0e14; background-image:var(--bgimg, none); background-repeat:repeat; background-size:var(--bgtile, 22cqw); }++.hero[data-theme=image].bg-cover .bg { background-repeat:no-repeat; background-size:cover; background-position:center; }++.hero {+  /* equal safe-area margins on all four sides (John, hero-studio#1 ask 7);+     corner badges keep their own uniform tighter inset */+  --inset:4.5cqw; --badge-inset:2.6cqw;+  aspect-ratio:16/10; position:relative; overflow:hidden;+  border-radius:12px; border:1px solid var(--border); container-type:inline-size;+  --ga:155deg;+}++#hero { width:100%; max-width:1000px; flex:0 0 auto; box-shadow:0 24px 70px rgba(0,0,0,.5); }+++/* backgrounds: continue to the very bottom, per the new rule */+.bg { position:absolute; inset:0; }++/* mirror mode flips the whole background composition (glows, tiles, corner motifs) */+.hero[data-layout=mirror] .bg { transform:scaleX(-1); }++.hero[data-theme=midnight] .bg {+  background:+    radial-gradient(1200px 800px at 88% -10%, rgba(0,184,177,.22), transparent 60%),+    radial-gradient(900px 700px at -5% 110%, rgba(140,107,247,.18), transparent 55%),+    linear-gradient(var(--ga),#0a0e14 0%,#0d1117 45%,#071318 100%);+}++.hero[data-theme=midnight] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:radial-gradient(rgba(255,255,255,.05) 1px, transparent 1.4px);+  background-size:2.2cqw 2.2cqw;+  -webkit-mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);+          mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);+}++.hero[data-theme=pcb] .bg {+  background:+    radial-gradient(1100px 760px at 85% -8%, rgba(0,230,220,.14), transparent 60%),+    linear-gradient(var(--ga),#031b19 0%,#04302c 55%,#02211e 100%);+}++.hero[data-theme=pcb] .bg::after {+  content:''; position:absolute; inset:0; opacity:.5;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><g stroke="%2300e6dc" stroke-opacity=".09" fill="none" stroke-width="1.4"><path d="M10 20 h56 l18 18 v50"/><path d="M84 150 v-36 l22 -22 h44"/><path d="M20 140 v-48 l14 -14"/><circle cx="10" cy="20" r="3.2"/><circle cx="84" cy="88" r="3.2"/><circle cx="150" cy="92" r="3.2"/><circle cx="20" cy="140" r="3.2"/><circle cx="34" cy="78" r="3.2"/></g></svg>');+  background-size:10cqw 10cqw;+}++.hero[data-theme=onion] .bg {+  background:+    radial-gradient(900px 700px at -5% 112%, rgba(140,107,247,.16), transparent 55%),+    linear-gradient(var(--ga),#0b1322 0%,#101a2c 55%,#0a111e 100%);+}++/* authentic Kickstand corner dome (brand deck onion motif), teal-to-purple stroke */+.hero[data-theme=onion] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="%238c6bf7"/><stop offset=".55" stop-color="%2339b8ff"/><stop offset="1" stop-color="%2300e6dc"/></linearGradient></defs><g fill="none" stroke="url(%23g)" stroke-width="4" opacity=".5"><path d="M356 400 V234 Q356 60 200 60 Q44 60 44 234 V400"/><path d="M312 400 V250 Q312 110 200 110 Q88 110 88 250 V400"/><path d="M268 400 V266 Q268 160 200 160 Q132 160 132 266 V400"/><path d="M224 400 V282 Q224 210 200 210 Q176 210 176 282 V400"/></g></svg>');+  background-repeat:no-repeat;+  background-position:right -7cqw bottom -9cqw;+  background-size:50cqw auto;+}++.hero[data-theme=pill] .bg {+  background:+    radial-gradient(1000px 720px at 90% -8%, rgba(0,184,177,.12), transparent 60%),+    linear-gradient(var(--ga),#082a29 0%,#0c3a3c 55%,#062220 100%);+}++/* authentic pattern-pill: scattered capsule + circle outlines, mixed brand strokes */+.hero[data-theme=pill] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="260" height="200"><g fill="none" stroke-width="2"><rect x="16" y="22" width="26" height="62" rx="13" stroke="%2364ABFF" stroke-opacity=".3"/><circle cx="112" cy="38" r="12" stroke="%2300e6dc" stroke-opacity=".28"/><rect x="196" y="14" width="28" height="72" rx="14" stroke="%2339b8ff" stroke-opacity=".3"/><rect x="84" y="128" width="62" height="26" rx="13" stroke="%2300b8b1" stroke-opacity=".28"/><circle cx="226" cy="150" r="10" stroke="%238c6bf7" stroke-opacity=".26"/><rect x="20" y="150" width="26" height="40" rx="13" stroke="%2300e6dc" stroke-opacity=".2"/></g></svg>');+  background-size:16cqw auto;+}++.hero[data-theme=grid] .bg {+  background:+    radial-gradient(1000px 700px at 85% -10%, rgba(0,230,220,.10), transparent 60%),+    linear-gradient(var(--ga),#07302e 0%,#0c3a3c 55%,#052220 100%);+}++/* authentic pattern-grid: fine outlined-square field */+.hero[data-theme=grid] .bg::after {+  content:''; position:absolute; inset:0; opacity:.55;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><rect x="4" y="4" width="18" height="18" fill="none" stroke="%23ffffff" stroke-opacity=".13" stroke-width="1.3"/></svg>');+  background-size:2.4cqw 2.4cqw;+}++.hero[data-theme=gradient] .bg {+  background:+    linear-gradient(155deg, rgba(5,10,18,.42), rgba(5,10,18,.14) 55%, rgba(5,10,18,.5)),+    linear-gradient(var(--ga),#00c8bf 0%,#2f9dfa 55%,#8c6bf7 100%);+}+++/* ---------- image-zone text column ---------- */+.textzone {+  position:absolute; left:var(--inset); top:var(--inset); width:34cqw; z-index:3;+  padding-right:1.5cqw;+}++.hero[data-layout=mirror] .textzone { left:auto; right:var(--inset); }++.wordmark { display:flex; align-items:flex-start; height:1.72cqw; margin-bottom:3.2cqw; }++/* the official Adom wordmark (adom-logo.svg), recolored white for dark heroes via mask */+.wordmark .wmark {+  display:block; width:7.8cqw; height:1.72cqw; background:#e6edf3;+  -webkit-mask:url('adom-logo.svg') no-repeat left top / contain;+          mask:url('adom-logo.svg') no-repeat left top / contain;+}++.h-title {+  font-family:'Familjen Grotesk'; font-weight:700; font-size:var(--fs-title, 8cqw); line-height:1.04;+  letter-spacing:-.14cqw; color:#e6edf3; margin-bottom:calc(1.8cqw * var(--fs-gap, 1));+  /* the ink never moves: leading is fixed (no control), and cap trimming+     removes the size-dependent gap above the capitals, so the first cap+     sits exactly at the anchor at any font size */+  text-box-trim: trim-start;+  text-box-edge: cap alphabetic;+}++.h-title { max-width:30cqw; }++.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; }++.h-sub b { color:#e6edf3; font-weight:500; }++.h-desc { font-size:var(--fs-desc, 1.35cqw); line-height:1.5; color:#8b949e; max-width:30cqw; }+++/* ---------- the app-window mock (the "screenshot") ---------- */+.window {+  position:absolute; z-index:2; border-radius:.9cqw .9cqw 0 0; overflow:hidden;+  background:#10151c; border:1px solid rgba(255,255,255,.09);+  box-shadow:0 1.7cqw 5cqw rgba(0,0,0,.62), 0 0 8cqw rgba(0,184,177,.07);+  display:flex; flex-direction:column;+}++.hero[data-layout=bleed-bottom] .window { right:4.5cqw; top:6cqw; width:56cqw; height:64cqw; }++.hero[data-layout=bleed-right] .window { right:-8cqw; top:6cqw; width:56cqw; height:44cqw; border-radius:.9cqw 0 0 .9cqw; }++.hero[data-layout=bleed-top] .window { right:4.5cqw; top:-6cqw; width:52cqw; height:42cqw; border-radius:0 0 .9cqw .9cqw; }++.hero[data-layout=bleed-corner-br] .window { right:-7cqw; top:14cqw; width:56cqw; height:52cqw; border-radius:.9cqw 0 0 0; }++.hero[data-layout=bleed-corner-tr] .window { right:-7cqw; top:-6cqw; width:56cqw; height:44cqw; border-radius:0 0 0 .9cqw; }++.hero[data-layout=contained] .window { right:4.5cqw; top:6cqw; width:50cqw; height:33cqw; border-radius:.9cqw; }++.hero[data-layout=mirror] .window { left:4.5cqw; top:6cqw; width:56cqw; height:64cqw; }++.hero[data-layout=showcase] .window { right:-4cqw; top:4cqw; width:66cqw; height:58cqw; border-radius:.9cqw 0 0 .9cqw; }++.hero[data-layout=showcase]::after {+  content:''; position:absolute; inset:0; z-index:2; pointer-events:none;+  background:linear-gradient(100deg, rgba(5,9,14,.88) 0%, rgba(5,9,14,.55) 30%, rgba(5,9,14,0) 55%);+}++.hero[data-layout=showcase] .textzone { z-index:3; width:30cqw; }++.titlebar {+  flex:0 0 2.1cqw; background:#181f28; border-bottom:1px solid rgba(255,255,255,.06);+  display:flex; align-items:center; padding:0 .9cqw; gap:.6cqw;+}++.titlebar .t { font-family:'Satoshi'; font-size:.82cqw; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }++.winbtns { margin-left:auto; display:flex; gap:.9cqw; align-items:center; }++.winbtns svg { width:.72cqw; height:.72cqw; stroke:#8b949e; stroke-width:2; fill:none; }++.appbody { flex:1; display:flex; min-height:0; position:relative; }++.vitrine-img { display:none; position:absolute; inset:0; width:100%; height:100%;+  object-fit:contain; object-position:top center; background:#0d1117; z-index:2; }++.hero.vitrine-cover .vitrine-img { object-fit:cover; }++.hero.has-vitrine .vitrine-img { display:block; }++.fullimport-img { display:none; position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:1; }++.hero.fullimport .fullimport-img { display:block; }++.hero.fullimport .textzone, .hero.fullimport .window { display:none; }++.sidebar { flex:0 0 11cqw; background:#131920; border-right:1px solid rgba(255,255,255,.05); padding:.8cqw .7cqw; }++.srow { height:1.5cqw; border-radius:.35cqw; background:rgba(255,255,255,.05); margin-bottom:.55cqw; }++.srow.on { background:rgba(0,184,176,.18); border:1px solid rgba(0,184,176,.35); }++.content { flex:1; padding:1cqw 1.1cqw; display:flex; flex-direction:column; gap:.8cqw; }++.crow { display:flex; gap:.6cqw; align-items:center; }++.chipk { height:1.3cqw; border-radius:999px; background:rgba(255,255,255,.07); }++.bars { flex:1; display:flex; align-items:flex-end; gap:.5cqw; min-height:0; }++.bar { flex:1; border-radius:.3cqw .3cqw 0 0; background:linear-gradient(180deg, rgba(0,230,220,.55), rgba(0,184,176,.12)); }++.tbl { display:flex; flex-direction:column; gap:.45cqw; }++.trow { display:flex; gap:.5cqw; }++.tcell { height:1.05cqw; border-radius:.25cqw; background:rgba(255,255,255,.06); }+++/* ---------- the lower-third textual overlay ---------- */+#mqoverlay { display:none; position:absolute; left:0; right:0; bottom:0; z-index:5;+  padding:3cqw 3cqw 2.2cqw;+  background:linear-gradient(180deg, rgba(5,9,14,0) 0%, rgba(5,9,14,.55) 35%, rgba(5,9,14,.9) 100%); }++.hero.show-mq #mqoverlay { display:block; }++#mqoverlay .mqo-badges { display:flex; gap:1cqw; align-items:center; margin-bottom:.9cqw; }++#mqoverlay .mqo-title { font-family:'Familjen Grotesk'; font-weight:700; font-size:3.2cqw; color:#fff; margin:0 0 .5cqw; }++#mqoverlay .mqo-brief { font-size:1.5cqw; color:#c9d1d9; margin:0 0 .7cqw; max-width:60cqw; }++#mqoverlay .mqo-owner { font-family:'JetBrains Mono',monospace; font-size:1.1cqw; color:#00b8b1; }++.pill-tr { position:absolute; top:var(--badge-inset); right:var(--badge-inset); z-index:6; }++.slug-br { position:absolute; right:var(--badge-inset); bottom:var(--badge-inset); z-index:6; }++.lower {+  position:absolute; left:0; right:0; bottom:0; height:33.4%; z-index:4;+  background:linear-gradient(180deg, rgba(10,14,20,0) 0%, rgba(10,14,20,.62) 22%, rgba(10,14,20,.94) 62%, rgba(10,14,20,.98) 100%);+  display:flex; align-items:flex-end;+}++.lowerin {+  width:100%; padding:0 5.75cqw 3.1cqw; display:flex; align-items:flex-end; gap:2cqw;+}++.lmain { min-width:0; flex:1; }++.badges { display:flex; align-items:center; gap:.9cqw; margin-bottom:1cqw; }++.pill {+  font-family:'Familjen Grotesk'; font-weight:700; font-size:.85cqw; letter-spacing:.14em;+  padding:.3cqw .85cqw; border-radius:999px; border:1px solid; text-transform:uppercase;+}++.pill.app { color:#00e6dc; border-color:rgba(0,230,220,.45); background:rgba(0,184,176,.10); }++.pill.skill { color:#b39dfb; border-color:rgba(140,107,247,.5); background:rgba(140,107,247,.12); }++.pill.bootstrap { color:#79b8ff; border-color:rgba(88,166,255,.5); background:rgba(88,166,255,.12); }++.pill.component { color:#e3b341; border-color:rgba(210,153,34,.5); background:rgba(210,153,34,.12); }++.pill.service { color:#ff9d76; border-color:rgba(248,110,73,.5); background:rgba(248,110,73,.12); }++/* pill group (#19): multi-type pages render one clustered badge box */+.pill-tr { display:flex; gap:.5cqw; }++/* pill over vitrine (#17): the scrim makes it read as a deliberate badge, not+   debris on the app's own corner UI. Placement stays top-right per contract. */+.pill-tr.scrim .pill { background:rgba(8,12,18,.72); backdrop-filter:blur(5px); box-shadow:0 .2cqw 1cqw rgba(0,0,0,.5); }++/* dynamic-vs-baked (#16): the overlay breathes, tags itself, and hold-hides */+.hero.show-mq #mqoverlay { animation:mqbreathe 3.4s ease-in-out infinite; cursor:pointer; }++@keyframes mqbreathe { 0%,100% { opacity:1; } 50% { opacity:.5; } }++.hero.mq-hold #mqoverlay { display:none !important; }++#mqoverlay .mqo-prompt { display:flex; gap:.8cqw; align-items:center; margin-top:.6cqw; font-family:'JetBrains Mono',monospace; font-size:1.05cqw; color:#c9d1d9; }++#mqoverlay .mqo-prompt button { font-size:.95cqw; padding:.2cqw .8cqw; border-radius:.5cqw; border:1px solid rgba(232,161,60,.6); background:rgba(232,161,60,.12); color:#e8a13c; cursor:pointer; }++.hero.show-mq #mqoverlay { outline:2px dashed rgba(232,161,60,.85); outline-offset:-2px; background:rgba(232,161,60,.045); }++.obadge {+  display:flex; align-items:center; gap:.42cqw; font-family:'JetBrains Mono',monospace;+  font-size:.95cqw; color:#aeb8c4;+}++.obadge svg { width:1cqw; height:1cqw; stroke:#8b949e; stroke-width:1.8; fill:none; }++.obadge b { color:#e6edf3; font-weight:500; }++.l-title { display:block; font-family:'Familjen Grotesk'; font-weight:700; font-size:2.3cqw; line-height:1.1; color:#e6edf3; text-decoration:none; margin-bottom:.55cqw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }++.l-title:hover { color:#00e6dc; }++.l-desc { font-size:1.2cqw; color:#c3ccd6; line-height:1.4; max-width:58cqw; margin-bottom:.35cqw; }++.l-author { font-size:1.05cqw; color:#8b949e; }++.l-author a { color:#aeb8c4; text-decoration:none; }++.l-author a:hover { color:#00e6dc; }++.l-right { flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:.4cqw; padding-bottom:.15cqw; }++.l-mpn { font-family:'JetBrains Mono',monospace; font-size:.95cqw; color:#aeb8c4; display:none; }++.l-slug { font-family:'JetBrains Mono',monospace; font-size:.95cqw; color:#00b8b1; text-decoration:none; }++.l-slug:hover { color:#00e6dc; }++/* long refs shrink a step instead of eating the overlay (the studio-side answer to wiki#59) */+.l-mpn.fit, .l-slug.fit { font-size:.78cqw; }+++/* ---------- component mode ---------- */+.hero[data-mode=component] .textzone, .hero[data-mode=component] .window { display:none; }++.hero[data-mode=app] .comp-side, .hero[data-mode=app] .ctextzone { display:none; }++body[data-mode=app] .mode-component { display:none; }++body[data-mode=component] .mode-app { display:none; }++.comp-side { position:absolute; top:0; bottom:0; right:0; width:56%; z-index:1; }++.comp-img {+  position:absolute; inset:0; background-size:cover; background-position:center;+  -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 26%);+          mask-image:linear-gradient(90deg, transparent 0, #000 26%);+}++.hero[data-imgsrc=photo] .comp-img { background-image:url('comp/photo.png'); }++.hero[data-imgsrc=raytrace] .comp-img { background-image:url('comp/raytrace.png'); }++.hero[data-imgsrc=phong] .comp-img { background-image:url('comp/phong.png'); background-size:contain; background-repeat:no-repeat; background-position:center 40%; }++.hero[data-clayout=photo-left] .comp-side { left:0; right:auto; }++.hero[data-clayout=photo-left] .comp-img {+  -webkit-mask-image:linear-gradient(270deg, transparent 0, #000 26%);+          mask-image:linear-gradient(270deg, transparent 0, #000 26%);+}++.hero[data-clayout=photo-left] .ctextzone { left:auto; right:var(--inset); }++.hero[data-clayout=photo-full] .comp-side { left:0; width:100%; }++.hero[data-clayout=photo-full] .comp-img { -webkit-mask-image:none; mask-image:none; }++.hero[data-clayout=photo-full] .comp-img::after {+  content:''; position:absolute; inset:0;+  background:linear-gradient(90deg, rgba(10,14,20,.88), rgba(10,14,20,.3) 55%, rgba(10,14,20,.12));+}++.ctextzone { position:absolute; left:var(--inset); top:var(--inset); width:37cqw; z-index:3; }++.c-chips { display:flex; gap:.8cqw; margin-bottom:2cqw; flex-wrap:wrap; }++.c-family {+  display:inline-block; font-family:'JetBrains Mono',monospace; font-size:.9cqw; letter-spacing:.12em;+  color:#00e6dc; background:rgba(5,9,14,.55); border:1px solid rgba(0,230,220,.3);+  border-radius:999px; padding:.35cqw 1cqw; text-transform:uppercase;+}++.c-mpnchip { color:#aeb8c4; border-color:rgba(255,255,255,.16); display:none; }++.c-name { font-family:'Familjen Grotesk'; font-weight:700; font-size:5.5cqw; line-height:1.02; letter-spacing:-.08cqw; color:#e6edf3; margin-bottom:.9cqw; text-shadow:0 2px 12px rgba(0,0,0,.5); }++.c-mpn { font-family:'JetBrains Mono',monospace; font-size:1.05cqw; color:#8b949e; margin-bottom:1cqw; display:none; }++.c-spec { font-size:2cqw; color:#aeb8c4; line-height:1.45; max-width:30cqw; text-shadow:0 1px 8px rgba(0,0,0,.5); }++.hero[data-mpn=eyebrow] .c-mpnchip { display:inline-block; }++.hero[data-mpn=under-title] .c-mpn { display:block; }++.hero[data-mpn=overlay] .l-mpn { display:block; }+++/* guides */+.guide { position:absolute; left:0; right:0; top:66.6%; z-index:6; border-top:1.5px dashed rgba(0,230,220,.55); pointer-events:none; display:none; }++.guide span {+  position:absolute; right:1cqw; top:.4cqw; font-family:'JetBrains Mono',monospace; font-size:.8cqw;+  color:#00e6dc; background:rgba(10,14,20,.7); padding:.2cqw .6cqw; border-radius:.35cqw;+}++.hero.guides .guide { display:block; }+++/* ---------- in-context previews (chrome copied 1:1 from git-wiki style.css) ---------- */+#contexts { display:flex; flex-wrap:wrap; gap:16px; width:100%; max-width:1000px; align-items:flex-start; }++.ctx { min-width:0; }++.ctxlabel { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-2); margin-bottom:6px; }++.ctxframe { background:#0d1117; border:1px solid var(--border); border-radius:10px; padding:14px; }++.clone-slot { position:absolute; inset:0; }++.clone-slot .hero { width:100%; border-radius:0; border:none; }++.heroslot .hero { width:100%; border-radius:8px; }++.hero.preview .guide { display:none !important; }++.hero.preview a { pointer-events:none; }+++/* wiki type badges (style.css .badge) */+#contexts .badge { display:inline-block; padding:2px 10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.5px; border-radius:12px; line-height:1.6; text-align:center; }++#contexts .badge-app { background:rgba(56,139,253,.15); color:#58a6ff; border:1px solid rgba(56,139,253,.3); }++#contexts .badge-skill { background:rgba(188,140,255,.15); color:#bc8cff; border:1px solid rgba(188,140,255,.3); }++#contexts .badge-component { background:rgba(210,153,34,.15); color:#d29922; border:1px solid rgba(210,153,34,.3); }++#contexts .badge-bootstrap { background:rgba(0,184,176,.15); color:#00b8b0; border:1px solid rgba(0,184,176,.3); }+++/* homepage marquee (style.css .popular-marquee / .mq-* / .ts-slide-*) */+.mqwrap { height:430px; overflow:hidden; position:relative; }++.mqwrap::before, .mqwrap::after { content:""; position:absolute; left:0; right:0; height:64px; z-index:3; pointer-events:none; }++.mqwrap::before { top:0; background:linear-gradient(to bottom,#0d1117,transparent); }++.mqwrap::after { bottom:0; background:linear-gradient(to top,#0d1117,transparent); }++.mqcol { transform:perspective(900px) rotateY(15deg); }++.mq-track { display:flex; flex-direction:column; gap:20px; will-change:transform; animation:mq-roll-up 130s linear infinite; }++.mqwrap:hover .mq-track { animation-play-state:paused; }++@keyframes mq-roll-up { from { transform:translateY(0); } to { transform:translateY(-50%); } }++.ts-slide { position:relative; aspect-ratio:16/10; display:block; overflow:hidden; background:#161b22; color:#fff; border-radius:14px; flex:none; text-decoration:none; }++.ts-hero-fallback { width:100%; height:100%; display:flex; align-items:center; justify-content:center; padding:32px; text-align:center; background:linear-gradient(135deg,#0f766e,#1d4ed8 60%,#6d28d9); font-size:26px; font-weight:700; color:#fff; font-family:'Familjen Grotesk'; }++.ts-slide-shade { position:absolute; inset:0; background:linear-gradient(to top, rgba(5,9,14,.95) 0%, rgba(5,9,14,.82) 28%, rgba(5,9,14,.5) 55%, rgba(5,9,14,0) 82%); }++.ts-slide-info { position:absolute; left:0; right:0; bottom:0; padding:20px 26px; }++.ts-slide-badges { position:absolute; top:12px; right:14px; display:flex; align-items:center; gap:10px; }++#contexts .ts-slide-badges .badge { background:rgba(5,9,14,.72); box-shadow:0 1px 4px rgba(0,0,0,.4); }++.ts-slide-score { display:inline-flex; align-items:center; gap:4px; font-size:12px; font-weight:600; color:#fff; background:rgba(5,9,14,.72); padding:2px 9px; border-radius:10px; box-shadow:0 1px 4px rgba(0,0,0,.4); }++.ts-slide-stars { display:inline-flex; align-items:center; gap:3px; font-size:12px; color:rgba(255,255,255,.85); background:rgba(5,9,14,.72); padding:2px 9px; border-radius:10px; box-shadow:0 1px 4px rgba(0,0,0,.4); }++.ts-slide-title { font-size:23px; font-weight:700; margin:6px 0 3px; color:#fff; font-family:'Familjen Grotesk'; text-shadow:0 1px 2px rgba(0,0,0,.85), 0 2px 14px rgba(0,0,0,.55); }++.ts-slide-brief { font-size:15px; line-height:1.5; color:rgba(255,255,255,.88); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; margin:0 0 4px; text-shadow:0 1px 2px rgba(0,0,0,.85), 0 2px 10px rgba(0,0,0,.6); }++.ts-slide-owner { font-size:12px; color:rgba(255,255,255,.72); text-shadow:0 1px 2px rgba(0,0,0,.85); }+++/* index page card grid (style.css .page-grid / .page-card-*) */+.page-grid { display:grid; grid-template-columns:repeat(2, 1fr); gap:16px; }++.page-card { position:relative; display:flex; flex-direction:column; text-decoration:none; color:#e6edf3; padding:0; overflow:hidden; background:#161b22; border:1px solid #30363d; border-radius:6px; transition:border-color .2s, box-shadow .2s; }++.page-card:hover { border-color:rgba(0,184,176,.3); box-shadow:0 4px 12px rgba(0,0,0,.2); }++.page-card-hero { aspect-ratio:16/10; background:#1c2128; display:flex; align-items:center; justify-content:center; overflow:hidden; border-bottom:1px solid #30363d; position:relative; }++.page-card-hero-placeholder { display:flex; align-items:center; justify-content:center; width:100%; height:100%; }++.page-card-body { padding:16px; display:flex; flex-direction:column; gap:6px; flex:1; }++.page-card-top { position:absolute; top:8px; right:10px; z-index:2; display:flex; align-items:center; gap:8px; }++.page-card-title { font-size:15px; font-weight:600; line-height:1.3; margin:0; }++.page-card-brief { color:#8b949e; font-size:13px; margin:0; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }++.page-card-meta { font-size:12px; color:#484f58; display:flex; align-items:center; gap:4px; margin-top:auto; }++.page-card-stars { display:inline-flex; align-items:center; gap:3px; }++.page-card-tags { display:flex; gap:4px; flex-wrap:wrap; margin-top:4px; }++.chip-sm { display:inline-block; padding:1px 8px; font-size:11px; background:rgba(0,184,176,.15); color:#00b8b0; border-radius:12px; border:1px solid rgba(0,184,176,.3); line-height:1.5; }++.page-card-install { margin-top:8px; padding:6px 8px; background:#0d1117; border:1px solid #30363d; border-radius:6px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#8b949e; display:flex; align-items:center; gap:8px; }++.page-card-install code { background:transparent; color:#8b949e; flex:1; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }++.dot-sep { color:#484f58; }+++/* adom-desktop right bar (proposed v2 overlay style) */+.deskmock { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:10px; display:flex; flex-direction:column; gap:10px; }++.deskhead { font-family:'Familjen Grotesk'; font-weight:500; font-size:11px; letter-spacing:.07em; text-transform:uppercase; color:var(--text-2); }+++/* Other owners never get the Adom mark, and there is NO placeholder: the+   logo slot renders nothing in preview and export alike. Non-adom owners+   bring their own palette via the Owner brand controls; the placement grid+   is the consistency we keep, not the colors. */+.wordmark.noadom { visibility:hidden; } /* the slot is RESERVED: the title anchor holds with or without a logo */++/* full-import wordmark stamp (#37): the ONE standard logo (same size and+   position as every composed hero: 7.8cqw at the --inset corner) baked over+   an import, so builders stop drawing their own at improvised sizes */+.fi-logo {+  position:absolute; left:var(--inset); top:var(--inset); z-index:4; display:none;+  width:7.8cqw; height:1.72cqw; background:#e6edf3;+  -webkit-mask:url('adom-logo.svg') no-repeat left center / contain;+          mask:url('adom-logo.svg') no-repeat left center / contain;+}++.hero.fullimport.stamp-logo .fi-logo { display:block; }+++/* owner brand: non-adom accent + background */+#hero.branded { background: var(--brand-bg, #0d1117) !important; }++#hero.branded .bar { background: linear-gradient(180deg,+  color-mix(in srgb, var(--brand-accent, #00e6dc) 55%, transparent),+  color-mix(in srgb, var(--brand-accent, #00b8b0) 12%, transparent)); }++#hero.branded .h-sub .accent { background: linear-gradient(90deg,+  var(--brand-accent, #00e6dc),+  color-mix(in srgb, var(--brand-accent, #00e6dc) 45%, #fff));+  -webkit-background-clip:text; background-clip:text; }++/* new background families so heroes stop looking identical */+.hero[data-theme=aurora] {+  background:+    radial-gradient(90cqw 60cqw at 12% -10%, rgba(0,184,177,.28), transparent 60%),+    radial-gradient(70cqw 55cqw at 95% 20%, rgba(88,101,242,.22), transparent 65%),+    radial-gradient(80cqw 70cqw at 55% 115%, rgba(140,107,247,.20), transparent 60%),+    #0a0f16;+}++.hero[data-theme=mesh] {+  background:+    conic-gradient(from var(--ga) at 28% 30%, rgba(0,184,177,.16), transparent 32%),+    conic-gradient(from calc(var(--ga) + 120deg) at 75% 65%, rgba(88,166,255,.13), transparent 36%),+    conic-gradient(from calc(var(--ga) + 240deg) at 45% 90%, rgba(140,107,247,.12), transparent 30%),+    #0b1016;+}++.hero[data-theme=circuitry] { background:#080d13; }++#circuitcanvas { display:none; position:absolute; inset:0; width:100%; height:100%; z-index:0; }++.hero[data-theme=circuitry] #circuitcanvas { display:block; }++.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; }+++/* render mode: ?render=1 strips all chrome for the server-side PNG shot */+body[data-render] header, body[data-render] #panel, body[data-render] #contexts,+body[data-render] #toasts, body[data-render] .guide, body[data-render] #verifybar,+body[data-render] #approvedbar,+body[data-render] #tourpop { display:none !important; }++/* Per Colby (2026-07-23): the image carries the title, the type pill (top+   right), and the full slug (bottom right); the pill and slug BAKE into the+   PNG. The page shows the image bare. Only the homepage marquee overlays+   dynamic info (stats, author), and it brings its own, so the lower band+   sim never exports. */+body[data-render] .hero .lower { display:none !important; }++body[data-render] #mqoverlay { display:none !important; }++/* The dashed YOUR LOGO box is a posing placeholder, not content. No logo+   posed = nothing exported, not a placeholder. */+body[data-render] .wordmark.noadom { display:none !important; }++body[data-render] main, body[data-render] #stage { display:block; padding:0; overflow:hidden; }++body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; height:1000px; border-radius:0; border:none; box-shadow:none; }
public/index.html+24−1843
@@ -15,578 +15,8 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Hero Studio</title> <link rel="icon" href='data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23e6edf3" stroke-width="1.8"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="14.5" x2="21" y2="14.5"/><line x1="6" y1="17.5" x2="13" y2="17.5"/></svg>'>-<style>-@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }-@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }-@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }-@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }-@font-face { font-family: 'JetBrains Mono'; src: url('fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }--:root {-  --bg:#0d1117; --surface:#161b22; --elevated:#1c2128; --overlay:#21262d; --border:#30363d;-  --text:#e6edf3; --text-2:#8b949e; --text-3:#484f58;-  --accent:#00b8b0; --accent-bright:#00e6dc; --purple:#8c6bf7; --blue:#58a6ff;-  --green:#3fb950; --red:#f85149; --yellow:#d29922;-  --grad:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7);-}-* { box-sizing:border-box; margin:0; padding:0; }-html,body { height:100%; }-body {-  background:var(--bg); color:var(--text);-  font-family:'Satoshi',sans-serif; font-size:14px;-  display:flex; flex-direction:column; overflow:hidden;-}--/* ---------- 44px header, three zones ---------- */-header {-  height:44px; flex:0 0 44px; display:flex; align-items:center; gap:12px;-  padding:0 12px; background:var(--surface); border-bottom:1px solid var(--border);-}-.brand { display:flex; align-items:center; gap:8px; white-space:nowrap; }-.brand svg { width:16px; height:16px; stroke:var(--text); }-.brand .name { font-family:'Familjen Grotesk'; font-weight:500; font-size:14px; color:var(--accent); }-.brand .sub { color:var(--text-2); font-size:13px; }-.vsep { width:1px; height:22px; background:var(--border); flex:0 0 1px; }-.subject { display:flex; align-items:baseline; gap:10px; min-width:0; }-.subject b { font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }-.subject .meta { font-family:'JetBrains Mono',monospace; font-size:11.5px; color:var(--text-2); white-space:nowrap; }-.actions { margin-left:auto; display:flex; gap:6px; }-.iconbtn {-  width:30px; height:30px; border:1px solid var(--border); border-radius:8px; background:transparent;-  display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all .15s ease;-}-.iconbtn svg { width:16px; height:16px; stroke:var(--text); fill:none; stroke-width:1.8; }-.iconbtn:hover { background:var(--elevated); border-color:var(--text-3); }-.iconbtn.on { border-color:var(--accent); background:rgba(0,184,176,.08); }-@media (max-width:720px){ .subject .meta { display:none; } }--/* ---------- body ---------- */-main { flex:1; display:flex; min-height:0; }-#panel {-  flex:0 0 264px; overflow-y:auto; background:var(--surface); border-right:1px solid var(--border);-  padding:14px 14px 40px;-}-#panel h3 {-  font-family:'Familjen Grotesk'; font-weight:500; font-size:11px; letter-spacing:.08em;-  text-transform:uppercase; color:var(--text-2); margin:18px 0 8px;-}-#panel h3:first-child { margin-top:2px; }-select {-  width:100%; background:var(--elevated); color:var(--text); border:1px solid var(--border);-  border-radius:8px; padding:7px 8px; font-family:'Satoshi'; font-size:13px; cursor:pointer;-}-.optgrid { display:flex; flex-wrap:wrap; gap:6px; }-.opt {-  border:1px solid var(--border); background:var(--elevated); color:var(--text-2); border-radius:8px;-  padding:6px 9px; font-size:12.5px; cursor:pointer; transition:all .15s ease; font-family:'Satoshi';-}-.opt:hover { color:var(--text); border-color:var(--text-3); }-.opt.on { color:var(--text); border-color:var(--accent); background:rgba(0,184,176,.10); }-.swatches { display:flex; gap:6px; }-.swatch {-  width:38px; height:26px; border-radius:6px; border:1px solid var(--border); cursor:pointer;-  transition:all .15s ease; position:relative;-}-.swatch.on { outline:2px solid var(--accent); outline-offset:1px; }-.sw-midnight { background:linear-gradient(155deg,#0a0e14,#0d1117 45%,#071318); }-.sw-image { background:repeating-conic-gradient(#1c2530 0 25%, #0d1117 0 50%) 0 0/8px 8px; }-.hero[data-theme=image] .bg { background-color:#0a0e14; background-image:var(--bgimg, none); background-repeat:repeat; background-size:var(--bgtile, 22cqw); }-.hero[data-theme=image].bg-cover .bg { background-repeat:no-repeat; background-size:cover; background-position:center; }-.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); }-input[type=range] { width:100%; accent-color:var(--accent); cursor:pointer; }-label.chk { display:flex; align-items:center; gap:8px; padding:5px 0; cursor:pointer; color:var(--text); font-size:13px; }-label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:pointer; }-.hint { font-size:11.5px; color:var(--text-2); line-height:1.45; margin-top:6px; }-.btn {-  width:100%; margin-top:8px; padding:8px 10px; border-radius:8px; border:1px solid var(--border);-  background:var(--elevated); color:var(--text); font-family:'Satoshi'; font-size:13px; cursor:pointer;-  transition:all .15s ease;-}-.btn:hover { border-color:var(--accent); background:rgba(0,184,176,.08); }--/* ---------- stage + hero canvas ---------- */-#stage { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; gap:18px; padding:22px; min-width:0; overflow:auto; }-.hero {-  /* equal safe-area margins on all four sides (John, hero-studio#1 ask 7);-     corner badges keep their own uniform tighter inset */-  --inset:4.5cqw; --badge-inset:2.6cqw;-  aspect-ratio:16/10; position:relative; overflow:hidden;-  border-radius:12px; border:1px solid var(--border); container-type:inline-size;-  --ga:155deg;-}-#hero { width:100%; max-width:1000px; flex:0 0 auto; box-shadow:0 24px 70px rgba(0,0,0,.5); }--/* backgrounds: continue to the very bottom, per the new rule */-.bg { position:absolute; inset:0; }-/* mirror mode flips the whole background composition (glows, tiles, corner motifs) */-.hero[data-layout=mirror] .bg { transform:scaleX(-1); }-.hero[data-theme=midnight] .bg {-  background:-    radial-gradient(1200px 800px at 88% -10%, rgba(0,184,177,.22), transparent 60%),-    radial-gradient(900px 700px at -5% 110%, rgba(140,107,247,.18), transparent 55%),-    linear-gradient(var(--ga),#0a0e14 0%,#0d1117 45%,#071318 100%);-}-.hero[data-theme=midnight] .bg::after {-  content:''; position:absolute; inset:0;-  background-image:radial-gradient(rgba(255,255,255,.05) 1px, transparent 1.4px);-  background-size:2.2cqw 2.2cqw;-  -webkit-mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);-          mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);-}-.hero[data-theme=pcb] .bg {-  background:-    radial-gradient(1100px 760px at 85% -8%, rgba(0,230,220,.14), transparent 60%),-    linear-gradient(var(--ga),#031b19 0%,#04302c 55%,#02211e 100%);-}-.hero[data-theme=pcb] .bg::after {-  content:''; position:absolute; inset:0; opacity:.5;-  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><g stroke="%2300e6dc" stroke-opacity=".09" fill="none" stroke-width="1.4"><path d="M10 20 h56 l18 18 v50"/><path d="M84 150 v-36 l22 -22 h44"/><path d="M20 140 v-48 l14 -14"/><circle cx="10" cy="20" r="3.2"/><circle cx="84" cy="88" r="3.2"/><circle cx="150" cy="92" r="3.2"/><circle cx="20" cy="140" r="3.2"/><circle cx="34" cy="78" r="3.2"/></g></svg>');-  background-size:10cqw 10cqw;-}-.hero[data-theme=onion] .bg {-  background:-    radial-gradient(900px 700px at -5% 112%, rgba(140,107,247,.16), transparent 55%),-    linear-gradient(var(--ga),#0b1322 0%,#101a2c 55%,#0a111e 100%);-}-/* authentic Kickstand corner dome (brand deck onion motif), teal-to-purple stroke */-.hero[data-theme=onion] .bg::after {-  content:''; position:absolute; inset:0;-  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="%238c6bf7"/><stop offset=".55" stop-color="%2339b8ff"/><stop offset="1" stop-color="%2300e6dc"/></linearGradient></defs><g fill="none" stroke="url(%23g)" stroke-width="4" opacity=".5"><path d="M356 400 V234 Q356 60 200 60 Q44 60 44 234 V400"/><path d="M312 400 V250 Q312 110 200 110 Q88 110 88 250 V400"/><path d="M268 400 V266 Q268 160 200 160 Q132 160 132 266 V400"/><path d="M224 400 V282 Q224 210 200 210 Q176 210 176 282 V400"/></g></svg>');-  background-repeat:no-repeat;-  background-position:right -7cqw bottom -9cqw;-  background-size:50cqw auto;-}-.hero[data-theme=pill] .bg {-  background:-    radial-gradient(1000px 720px at 90% -8%, rgba(0,184,177,.12), transparent 60%),-    linear-gradient(var(--ga),#082a29 0%,#0c3a3c 55%,#062220 100%);-}-/* authentic pattern-pill: scattered capsule + circle outlines, mixed brand strokes */-.hero[data-theme=pill] .bg::after {-  content:''; position:absolute; inset:0;-  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="260" height="200"><g fill="none" stroke-width="2"><rect x="16" y="22" width="26" height="62" rx="13" stroke="%2364ABFF" stroke-opacity=".3"/><circle cx="112" cy="38" r="12" stroke="%2300e6dc" stroke-opacity=".28"/><rect x="196" y="14" width="28" height="72" rx="14" stroke="%2339b8ff" stroke-opacity=".3"/><rect x="84" y="128" width="62" height="26" rx="13" stroke="%2300b8b1" stroke-opacity=".28"/><circle cx="226" cy="150" r="10" stroke="%238c6bf7" stroke-opacity=".26"/><rect x="20" y="150" width="26" height="40" rx="13" stroke="%2300e6dc" stroke-opacity=".2"/></g></svg>');-  background-size:16cqw auto;-}-.hero[data-theme=grid] .bg {-  background:-    radial-gradient(1000px 700px at 85% -10%, rgba(0,230,220,.10), transparent 60%),-    linear-gradient(var(--ga),#07302e 0%,#0c3a3c 55%,#052220 100%);-}-/* authentic pattern-grid: fine outlined-square field */-.hero[data-theme=grid] .bg::after {-  content:''; position:absolute; inset:0; opacity:.55;-  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><rect x="4" y="4" width="18" height="18" fill="none" stroke="%23ffffff" stroke-opacity=".13" stroke-width="1.3"/></svg>');-  background-size:2.4cqw 2.4cqw;-}-.hero[data-theme=gradient] .bg {-  background:-    linear-gradient(155deg, rgba(5,10,18,.42), rgba(5,10,18,.14) 55%, rgba(5,10,18,.5)),-    linear-gradient(var(--ga),#00c8bf 0%,#2f9dfa 55%,#8c6bf7 100%);-}--/* ---------- image-zone text column ---------- */-.textzone {-  position:absolute; left:var(--inset); top:var(--inset); width:34cqw; z-index:3;-  padding-right:1.5cqw;-}-.hero[data-layout=mirror] .textzone { left:auto; right:var(--inset); }-.wordmark { display:flex; align-items:center; margin-bottom:3.2cqw; }-/* the official Adom wordmark (adom-logo.svg), recolored white for dark heroes via mask */-.wordmark .wmark {-  display:block; width:7.8cqw; height:1.72cqw; background:#e6edf3;-  -webkit-mask:url('adom-logo.svg') no-repeat left center / contain;-          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: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: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; }-.h-sub b { color:#e6edf3; font-weight:500; }-.h-desc { font-size:var(--fs-desc, 1.35cqw); line-height:1.5; color:#8b949e; max-width:30cqw; }--/* ---------- the app-window mock (the "screenshot") ---------- */-.window {-  position:absolute; z-index:2; border-radius:.9cqw .9cqw 0 0; overflow:hidden;-  background:#10151c; border:1px solid rgba(255,255,255,.09);-  box-shadow:0 1.7cqw 5cqw rgba(0,0,0,.62), 0 0 8cqw rgba(0,184,177,.07);-  display:flex; flex-direction:column;-}-.hero[data-layout=bleed-bottom] .window { right:4.5cqw; top:6cqw; width:56cqw; height:64cqw; }-.hero[data-layout=bleed-right] .window { right:-8cqw; top:6cqw; width:56cqw; height:44cqw; border-radius:.9cqw 0 0 .9cqw; }-.hero[data-layout=bleed-top] .window { right:4.5cqw; top:-6cqw; width:52cqw; height:42cqw; border-radius:0 0 .9cqw .9cqw; }-.hero[data-layout=bleed-corner-br] .window { right:-7cqw; top:14cqw; width:56cqw; height:52cqw; border-radius:.9cqw 0 0 0; }-.hero[data-layout=bleed-corner-tr] .window { right:-7cqw; top:-6cqw; width:56cqw; height:44cqw; border-radius:0 0 0 .9cqw; }-.hero[data-layout=contained] .window { right:4.5cqw; top:6cqw; width:50cqw; height:33cqw; border-radius:.9cqw; }-.hero[data-layout=mirror] .window { left:4.5cqw; top:6cqw; width:56cqw; height:64cqw; }-.hero[data-layout=showcase] .window { right:-4cqw; top:4cqw; width:66cqw; height:58cqw; border-radius:.9cqw 0 0 .9cqw; }-.hero[data-layout=showcase]::after {-  content:''; position:absolute; inset:0; z-index:2; pointer-events:none;-  background:linear-gradient(100deg, rgba(5,9,14,.88) 0%, rgba(5,9,14,.55) 30%, rgba(5,9,14,0) 55%);-}-.hero[data-layout=showcase] .textzone { z-index:3; width:30cqw; }-.titlebar {-  flex:0 0 2.1cqw; background:#181f28; border-bottom:1px solid rgba(255,255,255,.06);-  display:flex; align-items:center; padding:0 .9cqw; gap:.6cqw;-}-.titlebar .t { font-family:'Satoshi'; font-size:.82cqw; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }-.winbtns { margin-left:auto; display:flex; gap:.9cqw; align-items:center; }-.winbtns svg { width:.72cqw; height:.72cqw; stroke:#8b949e; stroke-width:2; fill:none; }-.appbody { flex:1; display:flex; min-height:0; position:relative; }-.vitrine-img { display:none; position:absolute; inset:0; width:100%; height:100%;-  object-fit:contain; object-position:top center; background:#0d1117; z-index:2; }-.hero.vitrine-cover .vitrine-img { object-fit:cover; }-.hero.has-vitrine .vitrine-img { display:block; }-.fullimport-img { display:none; position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:1; }-.hero.fullimport .fullimport-img { display:block; }-.hero.fullimport .textzone, .hero.fullimport .window { display:none; }-.sidebar { flex:0 0 11cqw; background:#131920; border-right:1px solid rgba(255,255,255,.05); padding:.8cqw .7cqw; }-.srow { height:1.5cqw; border-radius:.35cqw; background:rgba(255,255,255,.05); margin-bottom:.55cqw; }-.srow.on { background:rgba(0,184,176,.18); border:1px solid rgba(0,184,176,.35); }-.content { flex:1; padding:1cqw 1.1cqw; display:flex; flex-direction:column; gap:.8cqw; }-.crow { display:flex; gap:.6cqw; align-items:center; }-.chipk { height:1.3cqw; border-radius:999px; background:rgba(255,255,255,.07); }-.bars { flex:1; display:flex; align-items:flex-end; gap:.5cqw; min-height:0; }-.bar { flex:1; border-radius:.3cqw .3cqw 0 0; background:linear-gradient(180deg, rgba(0,230,220,.55), rgba(0,184,176,.12)); }-.tbl { display:flex; flex-direction:column; gap:.45cqw; }-.trow { display:flex; gap:.5cqw; }-.tcell { height:1.05cqw; border-radius:.25cqw; background:rgba(255,255,255,.06); }--/* ---------- the lower-third textual overlay ---------- */-#mqoverlay { display:none; position:absolute; left:0; right:0; bottom:0; z-index:5;-  padding:3cqw 3cqw 2.2cqw;-  background:linear-gradient(180deg, rgba(5,9,14,0) 0%, rgba(5,9,14,.55) 35%, rgba(5,9,14,.9) 100%); }-.hero.show-mq #mqoverlay { display:block; }-#mqoverlay .mqo-badges { display:flex; gap:1cqw; align-items:center; margin-bottom:.9cqw; }-#mqoverlay .mqo-title { font-family:'Familjen Grotesk'; font-weight:700; font-size:3.2cqw; color:#fff; margin:0 0 .5cqw; }-#mqoverlay .mqo-brief { font-size:1.5cqw; color:#c9d1d9; margin:0 0 .7cqw; max-width:60cqw; }-#mqoverlay .mqo-owner { font-family:'JetBrains Mono',monospace; font-size:1.1cqw; color:#00b8b1; }-.pill-tr { position:absolute; top:var(--badge-inset); right:var(--badge-inset); z-index:6; }-.slug-br { position:absolute; right:var(--badge-inset); bottom:var(--badge-inset); z-index:6; }-.lower {-  position:absolute; left:0; right:0; bottom:0; height:33.4%; z-index:4;-  background:linear-gradient(180deg, rgba(10,14,20,0) 0%, rgba(10,14,20,.62) 22%, rgba(10,14,20,.94) 62%, rgba(10,14,20,.98) 100%);-  display:flex; align-items:flex-end;-}-.lowerin {-  width:100%; padding:0 5.75cqw 3.1cqw; display:flex; align-items:flex-end; gap:2cqw;-}-.lmain { min-width:0; flex:1; }-.badges { display:flex; align-items:center; gap:.9cqw; margin-bottom:1cqw; }-.pill {-  font-family:'Familjen Grotesk'; font-weight:700; font-size:.85cqw; letter-spacing:.14em;-  padding:.3cqw .85cqw; border-radius:999px; border:1px solid; text-transform:uppercase;-}-.pill.app { color:#00e6dc; border-color:rgba(0,230,220,.45); background:rgba(0,184,176,.10); }-.pill.skill { color:#b39dfb; border-color:rgba(140,107,247,.5); background:rgba(140,107,247,.12); }-.pill.bootstrap { color:#79b8ff; border-color:rgba(88,166,255,.5); background:rgba(88,166,255,.12); }-.pill.component { color:#e3b341; border-color:rgba(210,153,34,.5); background:rgba(210,153,34,.12); }-.pill.service { color:#ff9d76; border-color:rgba(248,110,73,.5); background:rgba(248,110,73,.12); }-/* pill group (#19): multi-type pages render one clustered badge box */-.pill-tr { display:flex; gap:.5cqw; }-/* pill over vitrine (#17): the scrim makes it read as a deliberate badge, not-   debris on the app's own corner UI. Placement stays top-right per contract. */-.pill-tr.scrim .pill { background:rgba(8,12,18,.72); backdrop-filter:blur(5px); box-shadow:0 .2cqw 1cqw rgba(0,0,0,.5); }-/* dynamic-vs-baked (#16): the overlay breathes, tags itself, and hold-hides */-.hero.show-mq #mqoverlay { animation:mqbreathe 3.4s ease-in-out infinite; cursor:pointer; }-@keyframes mqbreathe { 0%,100% { opacity:1; } 50% { opacity:.5; } }-.hero.mq-hold #mqoverlay { display:none !important; }-#mqoverlay .mqo-prompt { display:flex; gap:.8cqw; align-items:center; margin-top:.6cqw; font-family:'JetBrains Mono',monospace; font-size:1.05cqw; color:#c9d1d9; }-#mqoverlay .mqo-prompt button { font-size:.95cqw; padding:.2cqw .8cqw; border-radius:.5cqw; border:1px solid rgba(232,161,60,.6); background:rgba(232,161,60,.12); color:#e8a13c; cursor:pointer; }-.hero.show-mq #mqoverlay { outline:2px dashed rgba(232,161,60,.85); outline-offset:-2px; background:rgba(232,161,60,.045); }-/* the stagebar (#21/#23): stage chrome lives ABOVE the stage, never on it */-#stagebar { display:none; align-items:center; gap:10px; margin:10px 0 8px; }-#stagebar.on { display:flex; }-#stagebar .seg { display:flex; border:1px solid #30363d; border-radius:8px; overflow:hidden; }-#stagebar .seg button { background:#0d1117; color:var(--text); border:0; padding:6px 12px; font-size:12.5px; cursor:pointer; }-#stagebar .seg button.on { background:rgba(0,184,176,.18); color:var(--accent-bright); }-#stagebar .legend { font-size:12px; color:#8b949e; line-height:1.35; }-#stagebar .legend b { color:#e8a13c; font-weight:600; }-.obadge {-  display:flex; align-items:center; gap:.42cqw; font-family:'JetBrains Mono',monospace;-  font-size:.95cqw; color:#aeb8c4;-}-.obadge svg { width:1cqw; height:1cqw; stroke:#8b949e; stroke-width:1.8; fill:none; }-.obadge b { color:#e6edf3; font-weight:500; }-.l-title { display:block; font-family:'Familjen Grotesk'; font-weight:700; font-size:2.3cqw; line-height:1.1; color:#e6edf3; text-decoration:none; margin-bottom:.55cqw; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }-.l-title:hover { color:#00e6dc; }-.l-desc { font-size:1.2cqw; color:#c3ccd6; line-height:1.4; max-width:58cqw; margin-bottom:.35cqw; }-.l-author { font-size:1.05cqw; color:#8b949e; }-.l-author a { color:#aeb8c4; text-decoration:none; }-.l-author a:hover { color:#00e6dc; }-.l-right { flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:.4cqw; padding-bottom:.15cqw; }-.l-mpn { font-family:'JetBrains Mono',monospace; font-size:.95cqw; color:#aeb8c4; display:none; }-.l-slug { font-family:'JetBrains Mono',monospace; font-size:.95cqw; color:#00b8b1; text-decoration:none; }-.l-slug:hover { color:#00e6dc; }-/* long refs shrink a step instead of eating the overlay (the studio-side answer to wiki#59) */-.l-mpn.fit, .l-slug.fit { font-size:.78cqw; }--/* ---------- component mode ---------- */-.hero[data-mode=component] .textzone, .hero[data-mode=component] .window { display:none; }-.hero[data-mode=app] .comp-side, .hero[data-mode=app] .ctextzone { display:none; }-body[data-mode=app] .mode-component { display:none; }-body[data-mode=component] .mode-app { display:none; }-.comp-side { position:absolute; top:0; bottom:0; right:0; width:56%; z-index:1; }-.comp-img {-  position:absolute; inset:0; background-size:cover; background-position:center;-  -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 26%);-          mask-image:linear-gradient(90deg, transparent 0, #000 26%);-}-.hero[data-imgsrc=photo] .comp-img { background-image:url('comp/photo.png'); }-.hero[data-imgsrc=raytrace] .comp-img { background-image:url('comp/raytrace.png'); }-.hero[data-imgsrc=phong] .comp-img { background-image:url('comp/phong.png'); background-size:contain; background-repeat:no-repeat; background-position:center 40%; }-.hero[data-clayout=photo-left] .comp-side { left:0; right:auto; }-.hero[data-clayout=photo-left] .comp-img {-  -webkit-mask-image:linear-gradient(270deg, transparent 0, #000 26%);-          mask-image:linear-gradient(270deg, transparent 0, #000 26%);-}-.hero[data-clayout=photo-left] .ctextzone { left:auto; right:var(--inset); }-.hero[data-clayout=photo-full] .comp-side { left:0; width:100%; }-.hero[data-clayout=photo-full] .comp-img { -webkit-mask-image:none; mask-image:none; }-.hero[data-clayout=photo-full] .comp-img::after {-  content:''; position:absolute; inset:0;-  background:linear-gradient(90deg, rgba(10,14,20,.88), rgba(10,14,20,.3) 55%, rgba(10,14,20,.12));-}-.ctextzone { position:absolute; left:var(--inset); top:var(--inset); width:37cqw; z-index:3; }-.c-chips { display:flex; gap:.8cqw; margin-bottom:2cqw; flex-wrap:wrap; }-.c-family {-  display:inline-block; font-family:'JetBrains Mono',monospace; font-size:.9cqw; letter-spacing:.12em;-  color:#00e6dc; background:rgba(5,9,14,.55); border:1px solid rgba(0,230,220,.3);-  border-radius:999px; padding:.35cqw 1cqw; text-transform:uppercase;-}-.c-mpnchip { color:#aeb8c4; border-color:rgba(255,255,255,.16); display:none; }-.c-name { font-family:'Familjen Grotesk'; font-weight:700; font-size:5.5cqw; line-height:1.02; letter-spacing:-.08cqw; color:#e6edf3; margin-bottom:.9cqw; text-shadow:0 2px 12px rgba(0,0,0,.5); }-.c-mpn { font-family:'JetBrains Mono',monospace; font-size:1.05cqw; color:#8b949e; margin-bottom:1cqw; display:none; }-.c-spec { font-size:2cqw; color:#aeb8c4; line-height:1.45; max-width:30cqw; text-shadow:0 1px 8px rgba(0,0,0,.5); }-.hero[data-mpn=eyebrow] .c-mpnchip { display:inline-block; }-.hero[data-mpn=under-title] .c-mpn { display:block; }-.hero[data-mpn=overlay] .l-mpn { display:block; }--/* guides */-.guide { position:absolute; left:0; right:0; top:66.6%; z-index:6; border-top:1.5px dashed rgba(0,230,220,.55); pointer-events:none; display:none; }-.guide span {-  position:absolute; right:1cqw; top:.4cqw; font-family:'JetBrains Mono',monospace; font-size:.8cqw;-  color:#00e6dc; background:rgba(10,14,20,.7); padding:.2cqw .6cqw; border-radius:.35cqw;-}-.hero.guides .guide { display:block; }--/* ---------- in-context previews (chrome copied 1:1 from git-wiki style.css) ---------- */-#contexts { display:flex; flex-wrap:wrap; gap:16px; width:100%; max-width:1000px; align-items:flex-start; }-.ctx { min-width:0; }-.ctxlabel { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-2); margin-bottom:6px; }-.ctxframe { background:#0d1117; border:1px solid var(--border); border-radius:10px; padding:14px; }-.clone-slot { position:absolute; inset:0; }-.clone-slot .hero { width:100%; border-radius:0; border:none; }-.heroslot .hero { width:100%; border-radius:8px; }-.hero.preview .guide { display:none !important; }-.hero.preview a { pointer-events:none; }--/* wiki type badges (style.css .badge) */-#contexts .badge { display:inline-block; padding:2px 10px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.5px; border-radius:12px; line-height:1.6; text-align:center; }-#contexts .badge-app { background:rgba(56,139,253,.15); color:#58a6ff; border:1px solid rgba(56,139,253,.3); }-#contexts .badge-skill { background:rgba(188,140,255,.15); color:#bc8cff; border:1px solid rgba(188,140,255,.3); }-#contexts .badge-component { background:rgba(210,153,34,.15); color:#d29922; border:1px solid rgba(210,153,34,.3); }-#contexts .badge-bootstrap { background:rgba(0,184,176,.15); color:#00b8b0; border:1px solid rgba(0,184,176,.3); }--/* homepage marquee (style.css .popular-marquee / .mq-* / .ts-slide-*) */-.mqwrap { height:430px; overflow:hidden; position:relative; }-.mqwrap::before, .mqwrap::after { content:""; position:absolute; left:0; right:0; height:64px; z-index:3; pointer-events:none; }-.mqwrap::before { top:0; background:linear-gradient(to bottom,#0d1117,transparent); }-.mqwrap::after { bottom:0; background:linear-gradient(to top,#0d1117,transparent); }-.mqcol { transform:perspective(900px) rotateY(15deg); }-.mq-track { display:flex; flex-direction:column; gap:20px; will-change:transform; animation:mq-roll-up 130s linear infinite; }-.mqwrap:hover .mq-track { animation-play-state:paused; }-@keyframes mq-roll-up { from { transform:translateY(0); } to { transform:translateY(-50%); } }-.ts-slide { position:relative; aspect-ratio:16/10; display:block; overflow:hidden; background:#161b22; color:#fff; border-radius:14px; flex:none; text-decoration:none; }-.ts-hero-fallback { width:100%; height:100%; display:flex; align-items:center; justify-content:center; padding:32px; text-align:center; background:linear-gradient(135deg,#0f766e,#1d4ed8 60%,#6d28d9); font-size:26px; font-weight:700; color:#fff; font-family:'Familjen Grotesk'; }-.ts-slide-shade { position:absolute; inset:0; background:linear-gradient(to top, rgba(5,9,14,.95) 0%, rgba(5,9,14,.82) 28%, rgba(5,9,14,.5) 55%, rgba(5,9,14,0) 82%); }-.ts-slide-info { position:absolute; left:0; right:0; bottom:0; padding:20px 26px; }-.ts-slide-badges { position:absolute; top:12px; right:14px; display:flex; align-items:center; gap:10px; }-#contexts .ts-slide-badges .badge { background:rgba(5,9,14,.72); box-shadow:0 1px 4px rgba(0,0,0,.4); }-.ts-slide-score { display:inline-flex; align-items:center; gap:4px; font-size:12px; font-weight:600; color:#fff; background:rgba(5,9,14,.72); padding:2px 9px; border-radius:10px; box-shadow:0 1px 4px rgba(0,0,0,.4); }-.ts-slide-stars { display:inline-flex; align-items:center; gap:3px; font-size:12px; color:rgba(255,255,255,.85); background:rgba(5,9,14,.72); padding:2px 9px; border-radius:10px; box-shadow:0 1px 4px rgba(0,0,0,.4); }-.ts-slide-title { font-size:23px; font-weight:700; margin:6px 0 3px; color:#fff; font-family:'Familjen Grotesk'; text-shadow:0 1px 2px rgba(0,0,0,.85), 0 2px 14px rgba(0,0,0,.55); }-.ts-slide-brief { font-size:15px; line-height:1.5; color:rgba(255,255,255,.88); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; margin:0 0 4px; text-shadow:0 1px 2px rgba(0,0,0,.85), 0 2px 10px rgba(0,0,0,.6); }-.ts-slide-owner { font-size:12px; color:rgba(255,255,255,.72); text-shadow:0 1px 2px rgba(0,0,0,.85); }--/* index page card grid (style.css .page-grid / .page-card-*) */-.page-grid { display:grid; grid-template-columns:repeat(2, 1fr); gap:16px; }-.page-card { position:relative; display:flex; flex-direction:column; text-decoration:none; color:#e6edf3; padding:0; overflow:hidden; background:#161b22; border:1px solid #30363d; border-radius:6px; transition:border-color .2s, box-shadow .2s; }-.page-card:hover { border-color:rgba(0,184,176,.3); box-shadow:0 4px 12px rgba(0,0,0,.2); }-.page-card-hero { aspect-ratio:16/10; background:#1c2128; display:flex; align-items:center; justify-content:center; overflow:hidden; border-bottom:1px solid #30363d; position:relative; }-.page-card-hero-placeholder { display:flex; align-items:center; justify-content:center; width:100%; height:100%; }-.page-card-body { padding:16px; display:flex; flex-direction:column; gap:6px; flex:1; }-.page-card-top { position:absolute; top:8px; right:10px; z-index:2; display:flex; align-items:center; gap:8px; }-.page-card-title { font-size:15px; font-weight:600; line-height:1.3; margin:0; }-.page-card-brief { color:#8b949e; font-size:13px; margin:0; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }-.page-card-meta { font-size:12px; color:#484f58; display:flex; align-items:center; gap:4px; margin-top:auto; }-.page-card-stars { display:inline-flex; align-items:center; gap:3px; }-.page-card-tags { display:flex; gap:4px; flex-wrap:wrap; margin-top:4px; }-.chip-sm { display:inline-block; padding:1px 8px; font-size:11px; background:rgba(0,184,176,.15); color:#00b8b0; border-radius:12px; border:1px solid rgba(0,184,176,.3); line-height:1.5; }-.page-card-install { margin-top:8px; padding:6px 8px; background:#0d1117; border:1px solid #30363d; border-radius:6px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#8b949e; display:flex; align-items:center; gap:8px; }-.page-card-install code { background:transparent; color:#8b949e; flex:1; min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }-.dot-sep { color:#484f58; }--/* adom-desktop right bar (proposed v2 overlay style) */-.deskmock { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:10px; display:flex; flex-direction:column; gap:10px; }-.deskhead { font-family:'Familjen Grotesk'; font-weight:500; font-size:11px; letter-spacing:.07em; text-transform:uppercase; color:var(--text-2); }--/* Other owners never get the Adom mark, and there is NO placeholder: the-   logo slot renders nothing in preview and export alike. Non-adom owners-   bring their own palette via the Owner brand controls; the placement grid-   is the consistency we keep, not the colors. */-.wordmark.noadom { display:none; }-/* full-import wordmark stamp (#37): the ONE standard logo (same size and-   position as every composed hero: 7.8cqw at the --inset corner) baked over-   an import, so builders stop drawing their own at improvised sizes */-.fi-logo {-  position:absolute; left:var(--inset); top:var(--inset); z-index:4; display:none;-  width:7.8cqw; height:1.72cqw; background:#e6edf3;-  -webkit-mask:url('adom-logo.svg') no-repeat left center / contain;-          mask:url('adom-logo.svg') no-repeat left center / contain;-}-.hero.fullimport.stamp-logo .fi-logo { display:block; }--/* owner brand: non-adom accent + background */-#hero.branded { background: var(--brand-bg, #0d1117) !important; }-#hero.branded .bar { background: linear-gradient(180deg,-  color-mix(in srgb, var(--brand-accent, #00e6dc) 55%, transparent),-  color-mix(in srgb, var(--brand-accent, #00b8b0) 12%, transparent)); }-#hero.branded .h-sub .accent { background: linear-gradient(90deg,-  var(--brand-accent, #00e6dc),-  color-mix(in srgb, var(--brand-accent, #00e6dc) 45%, #fff));-  -webkit-background-clip:text; background-clip:text; }-.brandrow { display:flex; align-items:center; gap:8px; margin:6px 0; }-.brandrow label { font-size:12px; color:var(--text-2); width:84px; }-.brandrow input[type=color] { width:44px; height:26px; border:1px solid var(--border); border-radius:6px; background:var(--surface); padding:1px; cursor:pointer; }--#approvedbar { display:none; margin:0 0 14px; border:1px solid #2ea06b; background:rgba(46,160,107,.09);-  border-radius:10px; padding:14px 16px; }-#approvedbar .ab-head { font-family:'Familjen Grotesk'; font-weight:700; color:#4ade80; margin-bottom:6px; }-#approvedbar .ab-body { font-size:13px; color:var(--text); line-height:1.5; margin-bottom:10px; }-#genmodal .gen-approved { font-family:'Familjen Grotesk'; font-weight:700; color:#4ade80; margin-bottom:6px; }-#verifybar { display:none; margin:0 0 14px; border:1px solid var(--accent); background:rgba(0,184,176,.07); border-radius:10px; padding:14px 16px; }-#verifybar .vhead { font-size:13px; color:var(--text); margin-bottom:10px; line-height:1.5; }-#verifybar .vchk { display:block; margin:6px 0; }-#verifybar .vchk .warn { color:#e8a13c; }-#verifybar .vchk .flagged { color:#f85149; font-weight:600; margin-left:6px; }-#v-generate { margin-top:10px; font-size:14px; padding:10px 18px; }-#v-generate:disabled { opacity:.45; cursor:not-allowed; }-/* the layout linter (#18): the studio notices, the human decides */-#lintbox { margin-top:8px; }-#lintbox .lint-item { font-size:12.5px; margin:4px 0; cursor:pointer; line-height:1.4; }-#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; }-/* page-field editors (#20) */-.pf-in { width:100%; box-sizing:border-box; background:#0d1117; color:var(--text); border:1px solid #30363d; border-radius:6px; padding:6px 8px; font:inherit; font-size:12.5px; margin:2px 0 8px; }-.pf-in:focus { border-color:var(--accent); outline:none; }-/* zen focus (#12): the hero gets the whole surface */-body.zen #panel { display:none; }-body.zen main { max-width:none; }-/* new background families so heroes stop looking identical */-.hero[data-theme=aurora] {-  background:-    radial-gradient(90cqw 60cqw at 12% -10%, rgba(0,184,177,.28), transparent 60%),-    radial-gradient(70cqw 55cqw at 95% 20%, rgba(88,101,242,.22), transparent 65%),-    radial-gradient(80cqw 70cqw at 55% 115%, rgba(140,107,247,.20), transparent 60%),-    #0a0f16;-}-.hero[data-theme=mesh] {-  background:-    conic-gradient(from var(--ga) at 28% 30%, rgba(0,184,177,.16), transparent 32%),-    conic-gradient(from calc(var(--ga) + 120deg) at 75% 65%, rgba(88,166,255,.13), transparent 36%),-    conic-gradient(from calc(var(--ga) + 240deg) at 45% 90%, rgba(140,107,247,.12), transparent 30%),-    #0b1016;-}-.hero[data-theme=circuitry] { background:#080d13; }-#circuitcanvas { display:none; position:absolute; inset:0; width:100%; height:100%; z-index:0; }-.hero[data-theme=circuitry] #circuitcanvas { display:block; }-.sw-aurora { background:radial-gradient(8px 6px at 30% 20%, rgba(0,230,220,.8), transparent), radial-gradient(10px 8px 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 1px, transparent 1px 5px), #080d13; }--/* verification walkthrough popout */-#tourpop { display:none; position:fixed; z-index:900; max-width:300px;-  background:var(--elevated); border:1px solid var(--accent); border-radius:10px;-  padding:12px 14px; box-shadow:0 12px 40px rgba(0,0,0,.6); font-size:13px; }-#tourpop .tp-title { font-family:'Familjen Grotesk'; font-weight:700; margin-bottom:6px; color:var(--accent-bright); }-#tourpop .tp-body { color:var(--text); line-height:1.45; margin-bottom:10px; }-#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; }--/* render mode: ?render=1 strips all chrome for the server-side PNG shot */-body[data-render] header, body[data-render] #panel, body[data-render] #contexts,-body[data-render] #toasts, body[data-render] .guide, body[data-render] #verifybar,-body[data-render] #approvedbar,-body[data-render] #tourpop { display:none !important; }-/* Per Colby (2026-07-23): the image carries the title, the type pill (top-   right), and the full slug (bottom right); the pill and slug BAKE into the-   PNG. The page shows the image bare. Only the homepage marquee overlays-   dynamic info (stats, author), and it brings its own, so the lower band-   sim never exports. */-body[data-render] .hero .lower { display:none !important; }-body[data-render] #mqoverlay { display:none !important; }-/* The dashed YOUR LOGO box is a posing placeholder, not content. No logo-   posed = nothing exported, not a placeholder. */-body[data-render] .wordmark.noadom { display:none !important; }-body[data-render] main, body[data-render] #stage { display:block; padding:0; overflow:hidden; }-body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; height:1000px; border-radius:0; border:none; box-shadow:none; }--/* generated-hero preview modal */-#genmodal { position:fixed; inset:0; z-index:99997; background:rgba(5,9,14,.8); display:none; align-items:center; justify-content:center; padding:32px; }-#genmodal.show { display:flex; }-.genbox { max-width:min(1000px, 92vw); background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:14px; box-shadow:0 24px 80px rgba(0,0,0,.6); }-.genbox img { display:block; width:100%; border-radius:8px; border:1px solid var(--border); }-.genrow { display:flex; align-items:center; gap:10px; margin-top:10px; }-.genrow .hint { margin:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }-.genrow a { color:var(--accent); font-size:13px; text-decoration:none; }-.genrow a:hover { color:var(--accent-bright); }-.btn-primary { border-color:var(--accent); background:rgba(0,184,176,.12); font-weight:500; }-.btn-primary:hover { background:rgba(0,184,176,.2); }-.btn-primary:disabled { opacity:.55; cursor:wait; }--/* tooltip + toast */-#tip {-  position:fixed; z-index:99999; background:var(--overlay); border:1px solid var(--border);-  color:var(--text); font-size:12px; line-height:1.4; padding:6px 9px; border-radius:8px;-  max-width:240px; pointer-events:none; opacity:0; transform:translateY(3px);-  transition:opacity .18s ease, transform .18s ease; text-transform:none;-}-#tip.show { opacity:1; transform:none; }-#toasts { position:fixed; left:50%; bottom:18px; transform:translateX(-50%); z-index:99998; display:flex; flex-direction:column-reverse; gap:8px; }-.toast {-  display:flex; align-items:center; gap:8px; background:var(--overlay); border:1px solid var(--border);-  border-left:3px solid var(--accent); border-radius:8px; padding:9px 13px; font-size:13px;-  box-shadow:0 8px 30px rgba(0,0,0,.45); animation:tin .2s ease;-}-.toast.success { border-left-color:var(--green); }-.toast.error { border-left-color:var(--red); }-@keyframes tin { from { opacity:0; transform:translateY(6px);} }-</style>+<link rel="stylesheet" href="studio.css">+<link rel="stylesheet" href="hero.css"> </head> <body> @@ -605,6 +35,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh     <button class="iconbtn" id="btn-guides" data-tip="Toggle the lower-third guide line on the canvas">       <svg viewBox="0 0 24 24"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.6"/></svg>     </button>+    <button class="btn small" id="wiz-classic" data-tip="Flip between the step-by-step wizard and the classic full control panel">Classic</button>     <button class="iconbtn" id="btn-rand" data-tip="Randomize package, layout and theme">       <svg viewBox="0 0 24 24"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>     </button>@@ -728,7 +159,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh     <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" style="display:none"><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>@@ -871,6 +302,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh       </div>        <div class="guide"></div>+      <div id="weditring"></div>     </div>      <div id="contexts">@@ -918,1276 +350,25 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh   </div> </div> -<script>-if (new URLSearchParams(location.search).has('render')) document.body.dataset.render = '1';--/* console relay: the AI reads GET /console instead of DevTools */-(function () {-  const post = (level, args) => fetch('console', { method:'POST', headers:{'Content-Type':'application/json'},-    body: JSON.stringify({ level, text: args.map(a => { try { return typeof a === 'string' ? a : JSON.stringify(a); } catch { return String(a); } }).join(' ') }) }).catch(()=>{});-  ['log','warn','error'].forEach(k => { const o = console[k].bind(console); console[k] = (...a) => { o(...a); post(k, a); }; });-  window.onerror = (m, s, l) => post('error', [m + ' @' + s + ':' + l]);-  window.addEventListener('unhandledrejection', e => post('error', ['unhandledrejection: ' + e.reason]));-})();--let pages = [];-let comps = [];-let state = null;--const $ = (id) => document.getElementById(id);-// Every state-changing POST from this page is attributed to the server's-// owning ai-thread (#39): the page IS that instance's surface, and the human-// acting through it acts within that thread's session.-const withThread = (o) => Object.assign({ aiThread: (state && state.aiThread) || 'hero-studio ui' }, o || {});-const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(withThread(body)) });-const send = (patch) => post('state', patch).catch(() => {});--function canonType(t) { return t === 'hardware' ? 'component' : (t || 'app'); }-// the LAUNCH spelling for display + bake: the wiki's public name is hardware-function wireLabel(t) { return canonType(t) === 'component' ? 'hardware' : (t || 'app'); }-function pageByKey(key) { return pages.find(p => p.owner + '/' + p.slug === key) || pages[0]; }-function compById(id) { return comps.find(c => c.id === id) || comps[0]; }-/* the object the overlay, header, and context cards render: page or component */-function displayEntry() {-  if (state.mode === 'component' && comps.length) {-    const c = compById(state.comp.id);-    return { owner: c.owner, slug: c.slug, type: 'component', title: c.name, brief: c.spec,-      author: c.author, score: c.score, stars: c.stars, installs30d: null,-      tags: [c.mfr.toLowerCase()], updated_at: null, mpn: c.mpn };-  }-  return pageByKey(state.page);-}--/* long mono strings in the overlay: shrink a step past 40 chars; past 54,-   middle-truncate so the owner and the tail stay readable (full string in title=) */-function fitMono(s) {-  if (s.length <= 40) return { text: s, small: false };-  if (s.length <= 54) return { text: s, small: true };-  return { text: s.slice(0, 33) + '...' + s.slice(-18), small: true };-}--function firstSentence(s, max) {-  let t = (s || '').split(/(?<=\.)\s/)[0] || s || '';-  if (t.length > max) t = t.slice(0, max).replace(/\s+\S*$/, '') + '...';-  return t;-}--function accentize(text) {-  const words = text.split(' ');-  if (words.length < 3) return '<span class="accent">' + text + '</span>';-  return '<span class="accent">' + words.slice(0, 2).join(' ') + '</span> ' + words.slice(2).join(' ');-}--function render() {-  if (!state || !pages.length) return;-  const p = pageByKey(state.page);-  const d = displayEntry();-  const mode = state.mode || 'app';-  const hero = $('hero');-  document.body.dataset.mode = mode;-  hero.dataset.mode = mode;-  hero.dataset.theme = state.theme;-  hero.dataset.layout = state.layout;-  hero.dataset.clayout = state.comp.layout;-  hero.dataset.imgsrc = state.comp.imgSrc;-  hero.dataset.mpn = state.comp.mpn;-  hero.classList.toggle('guides', !!state.guides);-  // imported background art (theme 'image', hero-studio#24 ask 7)-  const bg = state.bgImage || {};-  hero.style.setProperty('--bgimg', bg.src ? 'url(' + bg.src + ')' : 'none');-  hero.classList.toggle('bg-cover', !!bg.cover);-  $('bg-cover').checked = !!bg.cover;-  // ?? not ||: an angle of 0 is a real choice, not an unset value-  hero.style.setProperty('--ga', (state.gradAngle ?? 155) + 'deg');-  $('rng-angle').value = state.gradAngle ?? 155;-  $('angle-val').textContent = state.gradAngle ?? 155;--  // header subject-  $('subj-title').textContent = d.title;-  $('subj-meta').textContent = mode === 'component'-    ? d.type + ' · score ' + d.score + ' · MPN ' + d.mpn-    : d.type + ' · score ' + d.score + ' · ' + d.stars + ' stars · ' + d.installs30d + ' installs/30d';-  $('btn-guides').classList.toggle('on', !!state.guides);--  // verification chrome (the pose gate sets state.verify); per-item confirms-  const confirm = state.confirm || {};-  const flags = state.flags || {};-  $('v-title').checked = !!confirm.title;-  $('v-sub').checked = !!confirm.subtitle;-  $('v-vitrine').checked = !!confirm.vitrine;-  $('v-type').checked = !!confirm.type;-  // "Needs fixing" verdicts (#14): flags hold Generate and are visible to the-  // driving AI via state, so it can adjust and re-present instead of the-  // human laundering a problem through an approve-only checklist.-  for (const [el, key] of [['v-title','title'],['v-sub','subtitle'],['v-vitrine','vitrine'],['v-type','type']]) {-    const label = $(el).parentElement;-    let mark = label.querySelector('.flagged');-    if (flags[key] && !mark) {-      mark = document.createElement('span');-      mark.className = 'flagged';-      mark.textContent = 'NEEDS FIXING (flagged; the AI sees this in state.flags)';-      label.appendChild(mark);-    } else if (!flags[key] && mark) mark.remove();-  }-  const types = (Array.isArray(d.types) && d.types.length ? d.types : [d.type]);-  const modeMatchesType = (canonType(d.type) === 'component') === (mode === 'component');-  $('v-type-label').textContent = 'Page type matches: page is "' + types.map(wireLabel).join(' + ') + '", studio mode is "' + mode + '"' + (modeMatchesType ? '' : ' (MISMATCH: switch mode at the top of the panel)');-  $('v-type-label').classList.toggle('warn', !modeMatchesType);-  const anyFlag = Object.values(flags).some(Boolean);-  const lintFail = (state.lint || []).some(l => l.level === 'fail');-  $('v-generate').disabled = !(confirm.title && confirm.subtitle && confirm.vitrine && confirm.type) || anyFlag || lintFail;--  // author-adjustable image-zone type (hero-studio#4); cqw so preview == render-  const fs = state.fontSize || {};-  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');-  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);-  }--  // the gate visibly CLOSES (hero-studio#5): approval is persistent state-  const ap = state.approved;-  const approvedHere = !!(ap && ap.page === state.page);-  $('approvedbar').style.display = approvedHere ? 'block' : 'none';-  if (approvedHere) {-    $('ab-body').textContent = ap.file + ', generated ' + new Date(ap.ts).toLocaleString() +-      '. The gate is satisfied; the PNG carries the provenance stamp. Apply it with: adom-wiki page hero ' + state.page + ' --image <repo path>';-  }-  $('verifybar').style.display = state.verify && !approvedHere ? 'block' : 'none';--  // stagebar (#21/#23): stage chrome above the stage, never on it-  $('stagebar').classList.toggle('on', !!state.verify || !!state.marqueePreview);-  $('sb-png').classList.toggle('on', !state.marqueePreview);-  $('sb-mq').classList.toggle('on', !!state.marqueePreview);--  // walkthrough + procedural background + measured layout lint + page fields-  showTourStep();-  scheduleLint();-  renderPageFields();-  if (state.theme === 'circuitry') requestAnimationFrame(drawCircuitry);-  $('btn-reroll').style.display = state.theme === 'circuitry' ? '' : 'none';--  // marquee overlay preview (ask 5): today's REAL homepage treatment,-  // composited over the hero so redundancy and balance are judgeable-  hero.classList.toggle('show-mq', !!state.marqueePreview && mode === 'app');-  if (state.marqueePreview) {-    $('mqo-badges').innerHTML = '<span class="badge badge-' + canonType(d.type) + '">' + wireLabel(d.type) + '</span>' +-      (d.score > 0 ? '<span class="ts-slide-score">' + TREND13 + ' ' + d.score + '</span>' : '') +-      (d.stars > 0 ? '<span class="ts-slide-stars">' + STAR12 + ' ' + d.stars + '</span>' : '');-    // TODAY-REALITY on purpose (Colby): the homepage still overlays its own-    // title, so show the doubling instead of hiding it; the tag names the-    // planned fix so nobody thinks it is an accident.-    $('mqo-title').textContent = d.title;-    $('mqo-brief').textContent = firstSentence(d.brief || p.brief || '', 120);-    $('mqo-owner').textContent = 'wiki.adom.inc/' + d.owner + '/' + d.slug;-    // sample prompt: DYNAMIC by design (#22): metadata the page renders with a-    // copy button; if it were pixels nobody could copy it-    const sp = (Array.isArray(d.sample_prompts) && d.sample_prompts[0] && d.sample_prompts[0].prompt) || state.samplePrompt || '';-    $('mqo-prompt').style.display = sp ? 'flex' : 'none';-    $('mqo-prompt-text').textContent = sp ? '"' + sp + '"' : '';-  }-  $('chk-mqpreview').checked = !!state.marqueePreview;--  // vitrine slot size (ask 1): measure the window body, scale to export px-  requestAnimationFrame(() => {-    const win = hero.querySelector('.appbody');-    const el = $('slot-size');-    if (!win || !el) return;-    const hb = hero.getBoundingClientRect();-    const wb = win.getBoundingClientRect();-    if (hb.width < 10 || wb.width < 10) { el.textContent = 'vitrine slot hidden in this layout'; return; }-    const k = 2000 / hb.width;-    const w = Math.round(wb.width * k);-    const h = Math.round(wb.height * k);-    el.dataset.size = w + 'x' + h;-    el.textContent = 'shoot the vitrine screenshot at ' + w + 'x' + h +-      ' (aspect ' + (wb.width / wb.height).toFixed(2) + ') for this placement; click to copy';-    const cur = state.slotSize || {};-    if (cur.w !== w || cur.h !== h) send({ slotSize: { w: w, h: h } });-  });--  // vitrine content: real screenshot in the window, or full-hero import-  const vit = state.vitrine || {};-  hero.classList.toggle('vitrine-cover', !!vit.cover);-  $('vitrine-cover').checked = !!vit.cover;-  hero.classList.toggle('has-vitrine', !!vit.src && !vit.full);-  // fake titlebar: real window screenshots carry their own chrome, so a-  // second bar above it reads as an obvious fake (#11). Auto = off whenever-  // 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;--  // Adom branding only on adom-owned pages; everyone else: no mark, own palette.-  // Every preview wordmark (hero AND the in-context mock cards) follows.-  const foreign = d.owner !== 'adom';-  document.querySelectorAll('.wordmark').forEach(w => w.classList.toggle('noadom', foreign));-  // #37: the standard-wordmark stamp for imports; adom-owned pages only (the-  // no-Adom-mark-for-foreign-owners policy applies to the stamp too)-  hero.classList.toggle('stamp-logo', !!(state.overlay || {}).logo && !foreign);-  $('stamp-logo-row').style.display = (simOverImport && !foreign) ? '' : 'none';-  $('chk-stamp-logo').checked = !!(state.overlay || {}).logo;-  $('fi-logo').classList.toggle('sim-fenced', simOverImport && !!(state.overlay || {}).logo);-  const brand = state.brand || {};-  hero.classList.toggle('branded', foreign && !!(brand.accent || brand.bg));-  if (foreign && brand.accent) hero.style.setProperty('--brand-accent', brand.accent);-  else hero.style.removeProperty('--brand-accent');-  if (foreign && brand.bg) hero.style.setProperty('--brand-bg', brand.bg);-  else hero.style.removeProperty('--brand-bg');-  const secBrand = document.getElementById('sec-brand');-  if (secBrand) {-    secBrand.style.display = (mode === 'app' && foreign) ? '' : 'none';-    if (brand.accent) $('brand-accent').value = brand.accent;-    if (brand.bg) $('brand-bg').value = brand.bg;-  }--  // component image zone-  if (mode === 'component' && comps.length) {-    const c = compById(state.comp.id);-    $('c-family').textContent = c.family;-    $('c-mpn-chip').textContent = c.mpn;-    $('c-name').textContent = c.name;-    $('c-mpn-under').textContent = c.mpn;-    $('c-spec').textContent = c.spec;-  }--  // image zone-  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 = 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';-  $('el-desc').textContent = firstSentence(p.brief.slice(80), 110);-  $('win-label').textContent = p.title + ' · Adom webview';--  // lower band: gone for app pages (the page shows the image bare); kept in-  // component mode for the MPN-in-overlay placement option-  hero.querySelector('.lower').style.display = mode === 'app' ? 'none' : '';-  const showTitleInOverlay = mode === 'component' || state.titleMode !== 'image-only';-  // Pill GROUP (#19): a page carrying several types (app + service) renders-  // them as one clustered badge box; the linter treats the cluster as one box.-  const ptr = $('pill-tr');-  ptr.style.display = state.overlay.pill ? '' : 'none';-  ptr.innerHTML = types.map(t => '<span class="pill ' + canonType(t) + '">' + wireLabel(t).toUpperCase() + '</span>').join('');-  $('l-trend').style.display = state.overlay.scores ? '' : 'none';-  $('l-trend').querySelector('b').textContent = d.score.toLocaleString('en-US');-  $('l-stars').style.display = state.overlay.scores ? '' : 'none';-  $('l-stars').querySelector('b').textContent = d.stars;-  $('l-installs').style.display = (state.overlay.scores && d.installs30d != null) ? '' : 'none';-  $('l-installs').querySelector('b').textContent = d.installs30d;-  const url = 'https://wiki.adom.inc/' + d.owner + '/' + d.slug;-  $('l-title').style.display = showTitleInOverlay ? 'block' : 'none';-  $('l-title').textContent = d.title;-  $('l-title').href = url;-  $('l-desc').textContent = firstSentence(d.brief || p.brief || '', 140);-  const subtitleInImage = mode === 'app' && state.show.subtitle;-  $('l-desc').style.display = (!subtitleInImage && (d.brief || p.brief)) ? 'block' : 'none';-  $('l-author').style.display = state.overlay.author ? '' : 'none';-  $('l-author').innerHTML = 'by <a href="' + url + '" target="_blank">' + d.author + '</a>';-  const mpnFit = fitMono(d.mpn || '');-  $('l-mpn').textContent = mpnFit.text;-  $('l-mpn').title = d.mpn || '';-  $('l-mpn').classList.toggle('fit', mpnFit.small);-  const ref = 'wiki.adom.inc/' + d.owner + '/' + d.slug;-  const refFit = fitMono(ref);-  $('l-slug').style.display = state.overlay.slug ? '' : 'none';-  $('l-slug').textContent = refFit.text;-  $('l-slug').title = ref;-  $('l-slug').classList.toggle('fit', refFit.small);-  $('l-slug').href = url;--  // controls reflect state-  document.querySelectorAll('#grp-mode .opt').forEach(b => b.classList.toggle('on', b.dataset.v === mode));-  if (comps.length) $('sel-comp').value = compById(state.comp.id).id;-  document.querySelectorAll('#grp-imgsrc .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.comp.imgSrc));-  document.querySelectorAll('#grp-clayout .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.comp.layout));-  document.querySelectorAll('#grp-mpn .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.comp.mpn));-  $('sel-page').value = state.page;-  document.querySelectorAll('#grp-layout .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.layout));-  document.querySelectorAll('#grp-theme .swatch').forEach(b => b.classList.toggle('on', b.dataset.v === state.theme));-  document.querySelectorAll('#grp-titlemode .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.titleMode));-  $('chk-logo').checked = state.show.logo;-  $('chk-title').checked = state.show.title;-  $('chk-subtitle').checked = state.show.subtitle;-  $('chk-description').checked = state.show.description;-  $('chk-pill').checked = state.overlay.pill;-  $('chk-scores').checked = state.overlay.scores;-  $('chk-author').checked = state.overlay.author;-  $('chk-slug').checked = state.overlay.slug;--  syncPreviews();-}--/* in-context previews: markup mirrors git-wiki lib/templates.js (popularMarquee + categoryListPage) */-const STAR12 = '<svg width="12" height="12" viewBox="0 0 24 24" fill="#d29922" stroke="#d29922" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';-const TREND13 = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>';-const IMGPH = '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#484f58" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';--function relTime(iso) {-  if (!iso) return '';-  const s = (Date.now() - new Date(iso).getTime()) / 1000;-  if (s < 3600) return Math.max(1, Math.floor(s / 60)) + 'm ago';-  if (s < 86400) return Math.floor(s / 3600) + 'h ago';-  return Math.floor(s / 86400) + 'd ago';-}--function marqueeTile(p, isLive) {-  const heroPart = isLive ? '<div class="clone-slot"></div>'-    : '<div class="ts-hero-fallback"><span>' + p.title + '</span></div>';-  // DECIDED (Colby, 2026-07-26, adom/wiki#80): hero-bearing tiles get badges-  // and owner only; the baked title is the single title. Heroless fallback-  // tiles keep the text overlay, which is their only title.-  const textPart = isLive-    ? ''-    : '<h3 class="ts-slide-title">' + p.title + '</h3>' +-      (p.brief ? '<p class="ts-slide-brief">' + p.brief + '</p>' : '');-  return '<a class="ts-slide mq-slide">' + heroPart +-    '<div class="ts-slide-shade"></div><div class="ts-slide-info">' +-    '<div class="ts-slide-badges"><span class="badge badge-' + p.type + '">' + p.type + '</span>' +-    (p.score > 0 ? '<span class="ts-slide-score">' + TREND13 + ' ' + p.score + '</span>' : '') +-    (p.stars > 0 ? '<span class="ts-slide-stars">' + STAR12 + ' ' + p.stars + '</span>' : '') +-    '</div>' + textPart +-    '<span class="ts-slide-owner">' + p.owner + '/' + p.slug + '</span></div></a>';-}--function indexCard(p, isLive) {-  const hero = isLive ? '<div class="clone-slot"></div>'-    : '<div class="page-card-hero-placeholder">' + IMGPH + '</div>';-  return '<a class="page-card">' +-    '<div class="page-card-hero">' + hero + '</div>' +-    '<div class="page-card-body">' +-    '<div class="page-card-top"><span class="badge badge-' + p.type + '">' + p.type + '</span></div>' +-    '<h3 class="page-card-title">' + p.title + '</h3>' +-    (p.brief ? '<p class="page-card-brief">' + p.brief + '</p>' : '') +-    '<div class="page-card-meta"><span>' + p.owner + '/' + p.slug + '</span>' +-    (p.updated_at ? '<span class="dot-sep">&middot;</span><span>' + relTime(p.updated_at) + '</span>' : '') +-    (p.stars > 0 ? '<span class="dot-sep">&middot;</span><span class="page-card-stars">' + STAR12 + ' ' + p.stars + '</span>' : '') +-    '</div>' +-    (p.tags && p.tags.length ? '<div class="page-card-tags">' + p.tags.map(t => '<span class="chip-sm">' + t + '</span>').join('') + '</div>' : '') +-    '<div class="page-card-install"><code>adom-wiki pkg install ' + p.owner + '/' + p.slug + '</code></div>' +-    '</div></a>';-}--function cloneHero(imageOnly) {-  const c = $('hero').cloneNode(true);-  c.removeAttribute('id');-  c.querySelectorAll('[id]').forEach(n => n.removeAttribute('id'));-  c.classList.add('preview');-  c.classList.remove('guides');-  if (imageOnly) { const l = c.querySelector('.lower'); if (l) l.remove(); }-  return c;-}--function syncPreviews() {-  const p = displayEntry();-  const nb = pages.filter(x => x.owner + '/' + x.slug !== p.owner + '/' + p.slug).slice(0, 3);-  // marquee: live tile among real neighbors, content doubled for the -50% loop-  const seq = marqueeTile(p, true) + nb.map(x => marqueeTile(x)).join('');-  $('mq-track').innerHTML = seq + seq;-  // index grid: live card first, then real neighbor cards (no-hero placeholder icon)-  $('wikigrid').innerHTML = indexCard(p, true) + nb.map(x => indexCard(x)).join('');-  // wiki surfaces get the IMAGE PART only (the wiki overlays its own text);-  // the desk bar gets the full v2 hero with its baked-in text overlay-  document.querySelectorAll('#contexts .clone-slot').forEach(s => s.appendChild(cloneHero(true)));-  document.querySelectorAll('#contexts .heroslot').forEach(s => { s.innerHTML = ''; s.appendChild(cloneHero(false)); });-}--let seenToastSeq = 0;-function handleToast(t) {-  if (t && t.seq > seenToastSeq) { seenToastSeq = t.seq; toast(t.text, t.kind); }-}--function toast(text, kind) {-  const el = document.createElement('div');-  el.className = 'toast ' + (kind || 'info');-  el.textContent = text;-  $('toasts').appendChild(el);-  setTimeout(() => el.remove(), 4000);-}--/* wire controls: server owns the truth, SSE echoes back */-document.querySelectorAll('#grp-mode .opt').forEach(b => b.addEventListener('click', () => send({ mode: b.dataset.v })));-$('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 => {-  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.-  post('refresh-page?page=' + encodeURIComponent(v))-    .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 })));-$('chk-logo').addEventListener('change', e => send({ show: { logo: e.target.checked } }));-$('chk-title').addEventListener('change', e => send({ show: { title: e.target.checked } }));-$('chk-subtitle').addEventListener('change', e => send({ show: { subtitle: e.target.checked } }));-$('chk-description').addEventListener('change', e => send({ show: { description: e.target.checked } }));-$('chk-pill').addEventListener('change', e => send({ overlay: { pill: e.target.checked } }));-$('chk-scores').addEventListener('change', e => send({ overlay: { scores: e.target.checked } }));-$('chk-author').addEventListener('change', e => send({ overlay: { author: e.target.checked } }));-$('chk-slug').addEventListener('change', e => send({ overlay: { slug: e.target.checked } }));-$('btn-guides').addEventListener('click', () => send({ guides: !state.guides }));-let angleTimer = null;-$('rng-angle').addEventListener('input', e => {-  $('angle-val').textContent = e.target.value;-  $('hero').style.setProperty('--ga', e.target.value + 'deg'); // instant local feedback-  clearTimeout(angleTimer);-  angleTimer = setTimeout(() => send({ gradAngle: +e.target.value }), 120);-});-$('btn-rand').addEventListener('click', () => {-  const themes = ['midnight','pcb','onion','pill','grid','gradient'];-  const angles = [65, 100, 155, 210, 245, 290, 335];-  const theme = themes[Math.floor(Math.random()*themes.length)];-  const gradAngle = angles[Math.floor(Math.random()*angles.length)];-  if (state.mode === 'component') {-    const clayouts = ['photo-right','photo-left','photo-full'];-    const srcs = ['photo','raytrace','phong'];-    const mpns = ['eyebrow','under-title','overlay'];-    const c = comps[Math.floor(Math.random() * comps.length)];-    send({ theme, gradAngle, comp: { id: c.id, layout: clayouts[Math.floor(Math.random()*clayouts.length)],-      imgSrc: srcs[Math.floor(Math.random()*srcs.length)], mpn: mpns[Math.floor(Math.random()*mpns.length)] } });-    toast('Randomized: ' + c.name, 'info');-  } else {-    const layouts = ['bleed-bottom','bleed-right','bleed-top','bleed-corner-br','bleed-corner-tr','contained','mirror'];-    const p = pages[Math.floor(Math.random() * pages.length)];-    send({ page: p.owner + '/' + p.slug, layout: layouts[Math.floor(Math.random()*layouts.length)], theme, gradAngle });-    toast('Randomized: ' + p.title, 'info');-  }-});-let seenGenSeq = 0;-function showGenerated(g) {-  $('gen-img').src = g.url + '?t=' + g.ts;-  $('gen-path').textContent = g.file;-  $('gen-open').href = g.url;-  $('genmodal').classList.add('show');-}-/* fires for local clicks AND for API-triggered generates (SSE or poll), deduped by seq */-function handleGen(g) {-  if (g && g.ok && g.seq > seenGenSeq) { seenGenSeq = g.seq; showGenerated(g); }-}-$('gen-close').addEventListener('click', e => { e.preventDefault(); $('genmodal').classList.remove('show'); });-$('genmodal').addEventListener('click', e => { if (e.target === $('genmodal')) $('genmodal').classList.remove('show'); });-function runGenerate() {-  const b = $('btn-generate');-  b.disabled = true; b.textContent = 'Generating...';-  toast('Rendering the hero on the server', 'info');-  post('generate').then(r => r.json()).then(g => {-    b.disabled = false; b.textContent = 'Generate hero PNG';-    if (g.ok) handleGen(g); // success toast arrives via the live channel-    else toast('Generate failed: ' + (g.error || 'unknown') + (g.hint ? '. ' + g.hint : ''), 'error');-  }).catch(() => { b.disabled = false; b.textContent = 'Generate hero PNG'; toast('Generate failed: network error', 'error'); });-}-$('btn-generate').addEventListener('click', runGenerate);-$('v-generate').addEventListener('click', runGenerate);--$('btn-reset').addEventListener('click', () => {-  send({ mode:'app', layout:'bleed-bottom', theme:'midnight', titleMode:'image-only', gradAngle:155,-    comp:{ id:'sm02b-srss-tb', imgSrc:'photo', layout:'photo-right', mpn:'under-title' },-    show:{ logo:true, title:true, subtitle:true, description:false },-    fontSize:{ title:8, sub:2.5, desc:1.35 },-    overlay:{ pill:true, scores:true, author:true, slug:true }, guides:true });-  toast('Reset to the proposed default: overlay title, image keeps value-prop', 'success');-});--/* tooltips: fixed div, 600ms delay, clamped to viewport on both axes */-(function () {-  const tip = $('tip');-  let timer = null;-  document.addEventListener('mouseover', e => {-    const t = e.target.closest('[data-tip]');-    if (!t) return;-    clearTimeout(timer);-    timer = setTimeout(() => {-      tip.textContent = t.getAttribute('data-tip');-      tip.classList.add('show');-      const r = t.getBoundingClientRect();-      let x = r.left + 14, y = r.bottom + 10;-      const w = tip.offsetWidth, h = tip.offsetHeight, M = 8;-      if (y + h > innerHeight - M) y = r.top - h - 10;-      x = Math.max(M, Math.min(x, innerWidth - w - M));-      y = Math.max(M, Math.min(y, innerHeight - h - M));-      tip.style.left = x + 'px'; tip.style.top = y + 'px';-    }, 600);-  });-  document.addEventListener('mouseout', e => {-    if (e.target.closest && e.target.closest('[data-tip]')) { clearTimeout(timer); tip.classList.remove('show'); }-  });-})();--/* verification checklist + vitrine controls */-[['v-title','title'],['v-sub','subtitle'],['v-vitrine','vitrine'],['v-type','type']].forEach(([el, key]) => {-  // ticking a box also clears any needs-fixing flag on that item (#14)-  $(el).addEventListener('change', e => send({ confirm: { [key]: e.target.checked }, flags: { [key]: false } }));-});-$('verifybar').addEventListener('click', e => {-  if (e.target.id !== 'v-generate' && !$('v-generate').disabled) return;-  if ($('v-generate').disabled && (e.target.id === 'v-generate' || e.target.closest('#v-generate'))) {-    const flags = Object.entries(state.flags || {}).filter(([, v]) => v).map(([k]) => k);-    const fails = (state.lint || []).filter(l => l.level === 'fail');-    let why = 'Confirm all four checks first; Generate unlocks when every item is ratified';-    if (flags.length) why = 'Generate is locked: ' + flags.join(', ') + ' flagged as needs-fixing. Fix and re-tick, or the AI adjusts and re-presents';-    else if (fails.length) why = 'Generate is locked by the layout linter: ' + fails.map(f => f.msg).join(' | ');-    handleToast({ text: why, kind: 'warn', seq: Date.now() });-  }-});-$('bg-file').addEventListener('change', e => {-  const f = e.target.files && e.target.files[0];-  if (!f) return;-  const r = new FileReader();-  r.onload = () => send({ bgImage: { src: r.result }, theme: 'image' });-  r.readAsDataURL(f);-});-$('bg-cover').addEventListener('change', e => send({ bgImage: { cover: e.target.checked } }));-$('vitrine-file').addEventListener('change', e => {-  const f = e.target.files && e.target.files[0];-  if (!f) return;-  const r = new FileReader();-  r.onload = () => send({ vitrine: { src: r.result } });-  r.readAsDataURL(f);-});-$('vitrine-full').addEventListener('change', e => send({ vitrine: { full: e.target.checked } }));-$('chk-stamp-logo').addEventListener('change', e => send({ overlay: { logo: e.target.checked } }));-$('vitrine-cover').addEventListener('change', e => send({ vitrine: { cover: e.target.checked } }));-$('vitrine-chrome').addEventListener('change', e => send({ vitrine: { chrome: e.target.checked ? 'fake' : 'none' } }));-$('chk-mqpreview').addEventListener('change', e => send({ marqueePreview: e.target.checked }));-$('slot-size').addEventListener('click', () => {-  const sz = $('slot-size').dataset.size;-  if (sz && navigator.clipboard) navigator.clipboard.writeText(sz)-    .then(() => handleToast({ text: 'Copied ' + sz, kind: 'success', seq: Date.now() })).catch(() => {});-});-$('vitrine-clear').addEventListener('click', () => send({ vitrine: { src: '', full: false } }));-$('vitrine-pull').addEventListener('click', () => {-  post('vitrine-from-page?page=' + encodeURIComponent(state.page))-    .then(r => r.json())-    .then(j => handleToast({ text: j.ok ? 'Vitrine loaded: ' + j.picked : 'No page screenshot: ' + j.error, kind: j.ok ? 'success' : 'warn', seq: Date.now() }))-    .catch(() => {});-});--/* verification walkthrough: spotlight each checklist item in place */-const TOUR_STEPS = [-  { key: 'title', el: 'el-title', title: 'This is your title',-    body: 'A hero gets across exactly ONE message, and this is it: the title, ideally approaching one word. Six messages equal zero messages. It bakes at this exact size; make sure it reads at dock size.' },-  { key: 'subtitle', el: 'el-sub', title: 'This is your subtitle',-    body: 'Deliberately small BECAUSE it is assumed nobody reads it. If the design only works when someone reads this line, the hero has failed. Check the wording anyway.' },-  { key: 'vitrine', el: 'vitrine-img', fallbackEl: 'circuitcanvas', winSel: '.window', title: 'This is your vitrine',-    body: 'Supporting evidence, NOT the message; the title is the message. It should show YOUR app, not a generic chart, and it should leave white space: negative space is what makes the one message land.' },-  { key: 'type', el: 'pill-tr', title: 'This is your type pill',-    body: 'app / skill / hardware / bootstrap (hardware is the launch name; component is its legacy spelling and survives as a kind). AIs pick this wrong often; if it says the wrong thing, yell at yours now, before it bakes.' },-];-let tourIdx = -1;-function tourTarget(step) {-  if (step.winSel) { const w = hero.querySelector(step.winSel); if (w && w.offsetParent) return w; }-  const el = $(step.el);-  if (el && el.offsetParent) return el;-  return step.fallbackEl ? $(step.fallbackEl) : null;-}-function clearTourGlow() {-  document.querySelectorAll('.tour-glow').forEach(n => n.classList.remove('tour-glow'));-}-function showTourStep() {-  clearTourGlow();-  const confirm = state.confirm || {};-  const flags = state.flags || {};-  // a flagged step is reviewed (verdict: needs fixing), so the tour moves on;-  // the flag itself keeps Generate locked until it is cleared-  const remaining = TOUR_STEPS.filter(st => !confirm[st.key] && !flags[st.key]);-  if (!state.tour || !state.verify || remaining.length === 0) {-    $('tourpop').style.display = 'none';-    if (state.tour && remaining.length === 0) send({ tour: false });-    return;-  }-  const step = remaining[0];-  tourIdx = TOUR_STEPS.indexOf(step);-  const target = tourTarget(step);-  if (!target) { $('tourpop').style.display = 'none'; return; }-  target.classList.add('tour-glow');-  const r = target.getBoundingClientRect();-  const pop = $('tourpop');-  $('tp-title').textContent = step.title;-  $('tp-body').textContent = step.body;-  pop.style.display = 'block';-  placeTourPop(pop, target, r);-}-/* The card must NEVER cover the element it asks the human to verify: a-   coach-mark over its own subject collects blind confirmations (#10).-   Try below / above / right / left; take the first spot that fits the-   viewport and does not intersect the target. If no spot exists (target-   nearly fills the window), lift the target above the card instead. */-function placeTourPop(pop, target, r) {-  const W = window.innerWidth, H = window.innerHeight, M = 12, GAP = 14;-  const pw = pop.offsetWidth, ph = pop.offsetHeight;-  const candidates = [-    { top: r.bottom + GAP, left: r.left },-    { top: r.top - ph - GAP, left: r.left },-    { top: r.top, left: r.right + GAP },-    { top: r.top, left: r.left - pw - GAP },-  ];-  const fits = c => c.top >= M && c.left >= M && c.top + ph <= H - M && c.left + pw <= W - M;-  const hides = c => !(c.left + pw <= r.left || c.left >= r.right || c.top + ph <= r.top || c.top >= r.bottom);-  let spot = candidates.find(c => fits(c) && !hides(c));-  if (!spot) {-    // clamp into the viewport on the roomier vertical side of the target-    const left = Math.max(M, Math.min(W - pw - M, r.left));-    spot = (H - r.bottom >= r.top)-      ? { top: Math.max(M, Math.min(H - ph - M, r.bottom + GAP)), left }-      : { top: Math.max(M, r.top - ph - GAP), left };-  }-  document.querySelectorAll('.tour-above').forEach(n => n.classList.remove('tour-above'));-  if (hides(spot)) target.classList.add('tour-above');-  // apply the human's drag displacement (#13): anchored spawn gives context,-  // the remembered offset keeps later steps out of the way they chose-  pop._anchor = { top: spot.top, left: spot.left };-  const top = Math.max(4, Math.min(window.innerHeight - 60, spot.top + tpDrag.dy));-  const left = Math.max(4, Math.min(window.innerWidth - 80, spot.left + tpDrag.dx));-  pop.style.top = top + 'px';-  pop.style.left = left + 'px';-}-window.addEventListener('resize', () => { if (state.tour && state.verify) showTourStep(); });--/* ---- layout linter (#18): the studio notices, the human decides -----   Most engineers are not graphic designers; the studio is the only party that-   reliably knows the geometry, so it encodes the judgment. Runs after every-   render (debounced), posts results to state.lint (visible to the driving AI),-   and hard failures (clipped text) lock Generate. Warnings advise. */-let lintTimer = null;-let lastLintKey = '';-function scheduleLint() { clearTimeout(lintTimer); lintTimer = setTimeout(runLint, 450); }-function runLint() {-  if (!state) return;-  const hero = $('hero');-  const H = hero.getBoundingClientRect();-  if (H.width < 60) return;-  const items = [];-  const rel = (r) => ({ left: r.left - H.left, top: r.top - H.top, right: r.right - H.left, bottom: r.bottom - H.top, width: r.width, height: r.height });-  const vis = (el) => el && el.offsetParent && el.getBoundingClientRect().width > 2;-  const boxOf = (el) => rel(el.getBoundingClientRect());-  const overlapPx = (a, b) => Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left)) * Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));--  const ptrEl = $('pill-tr');-  const pill = vis(ptrEl) ? boxOf(ptrEl) : null;-  const winEl = hero.querySelector('.window');-  const vitrine = (winEl && winEl.offsetParent && state.mode !== 'component') ? boxOf(winEl) : null;-  const texts = [['title', 'el-title'], ['subtitle', 'el-sub'], ['detail line', 'el-desc']]-    .filter(([, id]) => vis($(id)))-    .map(([name, id]) => ({ name, id, box: boxOf($(id)) }));--  // 1. pill vs vitrine / pill vs baked text (#17). Pill placement is-  // contract-fixed top right, so the in-contract fix is a backdrop scrim-  // (reads as a deliberate badge) plus this warning.-  ptrEl.classList.toggle('scrim', !!(pill && vitrine && overlapPx(pill, vitrine) > 9));-  if (pill && vitrine && overlapPx(pill, vitrine) > 9) {-    items.push({ id: 'pill-vitrine', level: 'warn', el: 'pill-tr', msg: 'type pill sits on the vitrine (scrim applied so it reads as a badge). If the app has its own corner UI there, pick a layout that leaves the top-right clear' });-  }-  for (const t of texts) {-    if (pill && overlapPx(pill, t.box) > 9) items.push({ id: 'pill-' + t.id, level: 'warn', el: 'pill-tr', msg: 'type pill overlaps the ' + t.name });-  }--  // 2. image-zone text intruding into the dynamic marquee band (#14): the-  // homepage overlays its own info over the lower band; keep it calm.-  const bandTop = H.height * 0.76;-  for (const t of texts) {-    if (t.box.bottom > bandTop + 2) {-      items.push({ id: 'band-' + t.id, level: 'warn', el: t.id, msg: t.name + ' extends ' + Math.round(t.box.bottom - bandTop) + 'px into the dynamic marquee band; shrink the title font or shorten the copy so the lower band stays calm' });-    }-  }--  // 3. clipped text = hard fail (the registry would ship a broken hero);-  // near-edge text = warning. The vitrine is exempt: bleeding off the edge is-  // a deliberate layout feature.-  const inset = H.width * 0.015;-  for (const t of texts) {-    if (t.box.left < -1 || t.box.top < -1 || t.box.right > H.width + 1 || t.box.bottom > H.height + 1) {-      items.push({ id: 'clip-' + t.id, level: 'fail', el: t.id, msg: t.name + ' is CLIPPED by the hero edge; Generate stays locked until it fits' });-    } else if (t.box.left < inset || t.box.top < inset || H.width - t.box.right < inset) {-      items.push({ id: 'inset-' + t.id, level: 'warn', el: t.id, msg: t.name + ' is closer than 1.5% to the hero edge' });-    }-  }--  // 4b. full-hero imports (#22): the composed checks cannot see imported-  // pixels; say so instead of reporting all clear, and run the band scan.-  const vit2 = state.vitrine || {};-  if (vit2.full && vit2.src) {-    scanImportBand(vit2.src);-    items.push({ id: 'import-unlinted', level: 'warn', el: 'fullimport-img', msg: 'full-hero import: composed-layout checks cannot inspect imported pixels; verify by eye before ratifying. Doctrine still applies (#32/#37): title set in Familjen Grotesk 700, body copy in Satoshi, optical left alignment on the ink edge, calm lower band; wordmark at the ONE standard size (156x35px at export scale, 90px inset top-left): if the import lacks it, use Stamp the standard Adom wordmark instead of drawing your own' });-    if (bandScan.status === 'done' && bandScan.texty) {-      items.push({ id: 'import-band-text', level: 'warn', el: 'fullimport-img', msg: 'the imported image appears to carry text or graphics in the lower dynamic band (heuristic edge scan): the homepage overlays its own info there, keep the band calm' });-    }-  }--  // 4c. prompt-looking text baked into pixels (#22): sample prompts belong in-  // page metadata (sample_prompts), rendered dynamically with a copy button.-  for (const t of texts) {-    const txt = ($(t.id).textContent || '');-    if (/["\u201c\u201d'][^"\u201c\u201d']{12,}["\u201c\u201d']/.test(txt)) {-      items.push({ id: 'prompt-' + t.id, level: 'warn', el: t.id, msg: t.name + ' contains quoted prompt-looking text; a baked prompt cannot be copied. Put it in sample_prompts (page metadata) and the page renders it dynamically with a copy button' });-    }-  }--  // 4. title wrap count: a 4-line title is almost never intended.-  const tEl = $('el-title');-  if (vis(tEl)) {-    const lh = parseFloat(getComputedStyle(tEl).lineHeight);-    const lines = lh ? Math.round(tEl.getBoundingClientRect().height / lh) : 1;-    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 {}-  }--  // 6b. vitrine coverage (#43): the title is the message, the vitrine is-  // supporting evidence. When the vitrine swallows the hero, the hierarchy-  // inverts and white space (what makes the one message land) is gone.-  if (vitrine) {-    const iw = Math.max(0, Math.min(vitrine.right, H.width) - Math.max(vitrine.left, 0));-    const ih = Math.max(0, Math.min(vitrine.bottom, H.height) - Math.max(vitrine.top, 0));-    const cov = (iw * ih) / (H.width * H.height);-    if (cov > 0.72) {-      items.push({ id: 'vitrine-coverage', level: 'warn', el: null, msg: 'the vitrine covers ' + Math.round(cov * 100) + '% of the hero. The title is the ONE message this image sends; the vitrine is supporting evidence. Pick a layout that gives the title its white space' });-    }-  }--  // 6c. cover-crop accounting (#41): a cover fit silently discards source-  // pixels; say how much, and check the crop lines for beheaded content.-  const vimg = $('vitrine-img');-  if (vit2.src && !vit2.full && vit2.cover && vimg && vimg.naturalWidth > 0) {-    const win2 = hero.querySelector('.appbody');-    if (win2 && win2.offsetParent) {-      const wb = win2.getBoundingClientRect();-      const arImg = vimg.naturalWidth / vimg.naturalHeight;-      const arSlot = wb.width / wb.height;-      const discard = 1 - Math.min(arImg / arSlot, arSlot / arImg);-      if (discard > 0.25) {-        const axis = arImg > arSlot ? 'left and right edges' : 'top and bottom edges';-        items.push({ id: 'cover-discard', level: 'warn', el: 'vitrine-img', msg: 'cover fit discards ' + Math.round(discard * 100) + '% of the source screenshot (' + axis + '). Check nothing that matters was cropped out; a screenshot captured AT the slot aspect (left panel size) loses nothing' });-      }-      scanCropEdges(vimg, arSlot);-      if (cropScan.status === 'done' && cropScan.cut) {-        items.push({ id: 'crop-edge-cut', level: 'warn', el: 'vitrine-img', msg: 'content appears cut mid-element at the ' + cropScan.where + ' crop line (edge-density heuristic): logos or headings beheaded by a crop read as broken. Recapture at the slot aspect or move the crop' });-      }-    }-  }--  // 6d. multi-message import (#43): one hero, one message.-  if (vit2.full && vit2.src && bandScan.status === 'done' && bandScan.busyCells >= 6) {-    items.push({ id: 'import-multi-message', level: 'warn', el: 'fullimport-img', msg: 'the imported image reads as ' + bandScan.busyCells + ' of 12 zones text/graphic-dense: that is several competing messages, and a hero sends ONE. The title should be the message; white space is what makes it land' });-  }--  // 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));-  if (key !== lastLintKey) {-    lastLintKey = key;-    send({ lint: items.map(({ id, level, msg }) => ({ id, level, msg })) });-  }-}-function renderLintBox(items) {-  const box = $('lintbox');-  if (!state.verify) { box.innerHTML = ''; return; }-  if (!items.length) { box.innerHTML = '<div class="lint-ok">layout linter: no findings (pill, margins, band, clipping, wrap all clear)</div>'; return; }-  box.innerHTML = items.map((i, n) =>-    '<div class="lint-item lint-' + i.level + '" data-el="' + (i.el || '') + '" data-n="' + n + '">' +-    (i.level === 'fail' ? 'BLOCKS GENERATE: ' : '') + i.msg + '</div>').join('');-}-$('lintbox').addEventListener('click', (e) => {-  const item = e.target.closest('.lint-item');-  if (!item || !item.dataset.el) return;-  const el = $(item.dataset.el);-  if (!el) return;-  clearTourGlow();-  el.classList.add('tour-glow');-  setTimeout(() => el.classList.remove('tour-glow'), 2200);-});--/* draggable tour dialogs (#13): pointer-capture drag on the header, clamped;-   the offset persists across steps for this tour session */-let tpDrag = { dx: 0, dy: 0 };-(() => {-  const pop = $('tourpop');-  const head = pop.querySelector('.tp-head');-  let grab = null;-  head.addEventListener('pointerdown', (e) => {-    grab = { x: e.clientX - pop.offsetLeft, y: e.clientY - pop.offsetTop };-    pop.classList.add('dragging');-    head.setPointerCapture(e.pointerId);-  });-  head.addEventListener('pointermove', (e) => {-    if (!grab) return;-    const left = Math.max(4, Math.min(window.innerWidth - 80, e.clientX - grab.x));-    const top = Math.max(4, Math.min(window.innerHeight - 60, e.clientY - grab.y));-    pop.style.left = left + 'px';-    pop.style.top = top + 'px';-    if (pop._anchor) tpDrag = { dx: left - pop._anchor.left, dy: top - pop._anchor.top };-  });-  const drop = () => { grab = null; pop.classList.remove('dragging'); };-  head.addEventListener('pointerup', drop);-  head.addEventListener('pointercancel', drop);-})();-$('tp-ok').addEventListener('click', () => {-  const step = TOUR_STEPS[tourIdx];-  if (step) send({ confirm: { [step.key]: true }, flags: { [step.key]: false } });-});-$('tp-flag').addEventListener('click', () => {-  const step = TOUR_STEPS[tourIdx];-  if (!step) return;-  send({ flags: { [step.key]: true }, confirm: { [step.key]: false } });-  handleToast({ text: step.key + ' flagged as needs-fixing: Generate stays locked and the driving AI can read the flag from state and adjust', kind: 'warn', seq: Date.now() });-});-$('tp-skip').addEventListener('click', () => { clearTourGlow(); $('tourpop').style.display = 'none'; send({ tour: false }); });--$('mqo-prompt-copy').addEventListener('click', async (e) => {-  e.stopPropagation();-  const txt = $('mqo-prompt-text').textContent.replace(/^"|"$/g, '');-  try { await navigator.clipboard.writeText(txt); } catch {}-  handleToast({ text: 'sample prompt copied', kind: 'success', seq: Date.now() });-});--/* full-hero import band scan (#22.1): composed-layout lint cannot see imported-   pixels, so the class of hero most likely to violate the band skipped every-   check. A cheap edge-density scan of the bottom 24% flags text-like structure-   in the band; it is a heuristic and says so. */-let bandScan = { key: '', status: 'idle', texty: false, busyCells: 0, cells: 12 };-function edgeDensity(g, sx, sy, sw, sh, w, h) {-  // draw a region to a small canvas and return horizontal edge density-  g.canvas.width = w; g.canvas.height = h;-  g.drawImage(g._img, sx, sy, sw, sh, 0, 0, w, h);-  const d = g.getImageData(0, 0, w, h).data;-  let edges = 0, n = 0;-  for (let y = 0; y < h; y++) {-    for (let x = 1; x < w; x++) {-      const i = (y * w + x) * 4, j = i - 4;-      const a = d[i] * .3 + d[i + 1] * .59 + d[i + 2] * .11;-      const b = d[j] * .3 + d[j + 1] * .59 + d[j + 2] * .11;-      if (Math.abs(a - b) > 60) edges++;-      n++;-    }-  }-  return n ? edges / n : 0;-}-function scanImportBand(src) {-  const key = src.length + ':' + src.slice(40, 72);-  if (bandScan.key === key) return;-  bandScan = { key, status: 'pending', texty: false, busyCells: 0, cells: 12 };-  const img = new Image();-  img.onload = () => {-    try {-      const g = document.createElement('canvas').getContext('2d');-      g._img = img;-      // 1. the calm-band check (bottom 24%), unchanged-      const w = Math.min(800, img.width);-      const bandH = Math.max(10, Math.round(img.height * 0.24 * (w / img.width)));-      bandScan.texty = edgeDensity(g, 0, img.height * 0.76, img.width, img.height * 0.24, w, bandH) > 0.0015;-      // 2. message-block grid (#43): a hero sends ONE message; count how many-      // zones of the upper 76% read as text/graphic-dense. 4x3 grid, cheap.-      let busy = 0;-      for (let gy = 0; gy < 3; gy++) {-        for (let gx = 0; gx < 4; gx++) {-          const dens = edgeDensity(g,-            img.width * gx / 4, img.height * 0.76 * gy / 3,-            img.width / 4, img.height * 0.76 / 3, 200, 100);-          if (dens > 0.003) busy++;-        }-      }-      bandScan.busyCells = busy;-      bandScan.status = 'done';-    } catch { bandScan.status = 'failed'; }-    scheduleLint();-  };-  img.onerror = () => { bandScan.status = 'failed'; scheduleLint(); };-  img.src = src;-}--/* crop-line scan (#41): sample thin strips just inside the visible window of-   a cover-cropped vitrine; text straddling the crop line = beheaded content */-let cropScan = { key: '', status: 'idle', cut: false, where: '' };-function scanCropEdges(vimg, arSlot) {-  const key = vimg.src.length + ':' + arSlot.toFixed(3);-  if (cropScan.key === key) return;-  cropScan = { key, status: 'pending', cut: false, where: '' };-  try {-    const g = document.createElement('canvas').getContext('2d');-    g._img = vimg;-    const iw = vimg.naturalWidth, ih = vimg.naturalHeight;-    const arImg = iw / ih;-    let cut = false, where = '';-    if (arImg > arSlot) {-      const visW = ih * arSlot;-      const cutL = (iw - visW) / 2;-      const strip = Math.max(8, iw * 0.02);-      if (edgeDensity(g, cutL, 0, strip, ih, 24, 300) > 0.004) { cut = true; where = 'left'; }-      if (edgeDensity(g, cutL + visW - strip, 0, strip, ih, 24, 300) > 0.004) { cut = true; where = where ? 'left and right' : 'right'; }-    } else if (arImg < arSlot) {-      const visH = iw / arSlot;-      const cutT = (ih - visH) / 2;-      const strip = Math.max(8, ih * 0.02);-      if (edgeDensity(g, 0, cutT, iw, strip, 300, 24) > 0.004) { cut = true; where = 'top'; }-      if (edgeDensity(g, 0, cutT + visH - strip, iw, strip, 300, 24) > 0.004) { cut = true; where = where ? 'top and bottom' : 'bottom'; }-    }-    cropScan.cut = cut; cropScan.where = where; cropScan.status = 'done';-  } catch { cropScan.status = 'failed'; }-}--/* auto-fit (#40): the largest title size that keeps <=3 lines, positive-   headroom, and no clipping, measured with the REAL layout: sweep the CSS-   var coarse-to-fine, read the boxes, restore. Runs on the fit SSE event-   (drive verb + preset re-fit) and on headline edits; never in render mode. */-function solveTitleFit(maxCqw) {-  const tEl = $('el-title');-  if (!tEl || !tEl.offsetParent || document.body.dataset.render !== undefined) return null;-  const H = hero.getBoundingClientRect();-  if (H.width < 60) return null;-  const bandTop = H.top + H.height * 0.76;-  const measure = (v) => {-    hero.style.setProperty('--fs-title', v + 'cqw');-    void tEl.offsetHeight;-    const b = tEl.getBoundingClientRect();-    const lh = parseFloat(getComputedStyle(tEl).lineHeight) || 1;-    return { b, lines: Math.max(1, Math.round(b.height / lh)) };-  };-  const inHero = (b) => b.bottom < bandTop && b.right <= H.right + 1 && b.left >= H.left - 1 && b.top >= H.top - 1;-  const fits = (v) => { const m = measure(v); return m.lines <= 3 && inHero(m.b); };-  const fitsLoose = (v) => inHero(measure(v).b);-  const hi = Math.min(maxCqw || 13, 13);-  const sweep = (pred) => {-    let best = null;-    for (let v = hi; v >= 5 && best == null; v = Math.round((v - 0.5) * 10) / 10) {-      if (pred(v)) best = v;-    }-    if (best != null && best < hi) {-      for (let v = Math.min(hi, best + 0.4); v > best; v = Math.round((v - 0.1) * 10) / 10) {-        if (pred(v)) { best = v; break; }-      }-    }-    return best;-  };-  let best = sweep(fits);-  let loose = false;-  if (best == null) {-    // Nothing satisfies <=3 lines at any legible size: the headline itself is-    // the problem. Relax to the HARD constraints only (inside the hero, above-    // the band) so the pose is at least never broken, and say so.-    loose = true;-    best = sweep(fitsLoose);-  }-  const cur = (state.fontSize || {}).title || 8;-  hero.style.setProperty('--fs-title', cur + 'cqw');-  if (best == null) return null;-  hero.style.setProperty('--fs-title', best + 'cqw');-  void tEl.offsetHeight;-  const lines = Math.max(1, Math.round(tEl.getBoundingClientRect().height / (parseFloat(getComputedStyle(tEl).lineHeight) || 1)));-  hero.style.setProperty('--fs-title', cur + 'cqw');-  return { size: best, loose, lines };-}-function runFit(maxCqw) {-  const r = solveTitleFit(maxCqw);-  if (r == null) {-    handleToast({ text: 'fit: no title size in 5-13cqw keeps this headline inside the hero for this layout; shorten the headline or change layout', kind: 'warn', seq: Date.now() });-    return;-  }-  const cur = (state.fontSize || {}).title || 8;-  if (Math.abs(r.size - cur) >= 0.05) send({ fontSize: { title: r.size } });-  if (r.loose) {-    handleToast({ text: 'fit: this headline cannot make 3 lines at any legible size; set ' + r.size + 'cqw (' + r.lines + ' lines, still clear of the band). SHORTER COPY READS BIGGER: the title is the one message, aim for a few words', kind: 'warn', seq: Date.now() });-  } else if (Math.abs(r.size - cur) >= 0.05) {-    handleToast({ text: 'fit: title ' + r.size + 'cqw (largest size with <=3 lines and positive headroom for this headline + layout)', kind: 'info', seq: Date.now() });-  }-}--/* the stagebar (#21/#23): explicit dynamic on/off, keyboard D, legend off-stage */-$('sb-png').addEventListener('click', () => send({ marqueePreview: false }));-$('sb-mq').addEventListener('click', () => send({ marqueePreview: true }));-document.addEventListener('keydown', (e) => {-  if (e.key.toLowerCase() === 'd' && !e.target.closest('input,textarea')) {-    send({ marqueePreview: !state.marqueePreview });-  }-});--/* hold-to-hide the dynamic overlay (#16): press the band to see the pure-   export, release to bring the overlay back */-(() => {-  const mq = $('mqoverlay');-  const hero = $('hero');-  mq.addEventListener('pointerdown', (e) => { e.preventDefault(); hero.classList.add('mq-hold'); });-  document.addEventListener('pointerup', () => hero.classList.remove('mq-hold'));-  document.addEventListener('pointercancel', () => hero.classList.remove('mq-hold'));-})();--/* ---- page fields + Apply + Copy Changelog Prompt (#20) -----   The human edits page-level fields at the gate; the change ledger on the-   server records everything (page fields AND pose deltas), Apply writes to-   the wiki or stashes for the first publish, and the prompt hands the whole-   session back to the AI that owns the package working copy. */-let pfEditing = false;-function renderPageFields() {-  if (pfEditing || !state) return;-  const d = pageByKey(state.page);-  if (!d) return;-  $('pf-title').value = d.title || '';-  $('pf-brief').value = d.brief || '';-  $('pf-tags').value = (d.tags || []).join(', ');-}-async function refreshChangesBadge() {-  try {-    const c = await (await fetch('changes')).json();-    const n = Object.keys(c.page || {}).length;-    $('pf-apply').textContent = 'Apply changes to wiki' + (n && !c.applied ? ' (' + n + ' unapplied)' : '');-    $('pf-apply').disabled = !n || !!c.applied;-    if (c.applied) $('pf-status').textContent = c.applied === 'wiki'-      ? 'Applied to the wiki. Still run Copy Changelog Prompt: the local package checkout needs syncing too.'-      : 'Stashed for the next publish (the page or edit route is not on the wiki yet); the changelog prompt carries the edits.';-  } catch {}-}-function pushPageField() {-  const [owner, slug] = (state.page || '/').split('/');-  const body = { owner, slug, title: $('pf-title').value.trim(), brief: $('pf-brief').value.trim(),-    tags: $('pf-tags').value.split(',').map(t => t.trim()).filter(Boolean) };-  post('api/pages', body)-    .then(() => fetch('api/pages')).then(r => r.json()).then(rows => { pages = rows; render(); refreshChangesBadge(); })-    .catch(() => {});-}-for (const id of ['pf-title', 'pf-brief', 'pf-tags']) {-  $(id).addEventListener('focus', () => { pfEditing = true; });-  $(id).addEventListener('blur', () => { pfEditing = false; pushPageField(); });-}-$('pf-apply').addEventListener('click', async () => {-  const r = await (await post('apply-page')).json().catch(() => ({}));-  if (!r.ok) { handleToast({ text: r.error || 'nothing to apply', kind: 'warn', seq: Date.now() }); return; }-  handleToast({-    text: r.applied === 'wiki' ? 'Page edits applied to the wiki (' + r.fields.join(', ') + ')'-      : 'Edits stashed for the next publish' + (r.http ? ' (wiki said HTTP ' + r.http + ')' : ''),-    kind: r.applied === 'wiki' ? 'success' : 'info', seq: Date.now(),-  });-  refreshChangesBadge();-});-$('pf-prompt').addEventListener('click', async () => {-  const c = await (await fetch('changes')).json().catch(() => null);-  if (!c) return;-  const lines = ['While reviewing the hero in Hero Studio I made these changes to ' + (c.ref || state.page) + ':'];-  const fmt = (v) => typeof v === 'string' ? '"' + v + '"' : JSON.stringify(v);-  for (const [f, ch] of Object.entries(c.page || {})) lines.push('- ' + f + ': ' + fmt(ch.from) + ' -> ' + fmt(ch.to));-  for (const [f, ch] of Object.entries(c.pose || {})) lines.push('- pose ' + f + ': ' + fmt(ch.from) + ' -> ' + fmt(ch.to));-  if (lines.length === 1) lines.push('- (no changes recorded this session)');-  lines.push('- Applied to wiki: ' + (c.applied === 'wiki' ? 'yes' : c.applied === 'stashed' ? 'no, stashed: please include in the next publish' : 'no: please include in the next publish'));-  lines.push('Please update the local package (page.json / package.json / README) to match.');-  const text = lines.join('\n');-  try {-    await navigator.clipboard.writeText(text);-    handleToast({ text: 'Changelog prompt copied: paste it into the AI session that owns the package', kind: 'success', seq: Date.now() });-  } catch {-    const ta = document.createElement('textarea');-    ta.value = text; document.body.appendChild(ta); ta.select();-    document.execCommand('copy'); ta.remove();-    handleToast({ text: 'Changelog prompt copied (fallback path)', kind: 'success', seq: Date.now() });-  }-});-setInterval(refreshChangesBadge, 3000);--/* zen focus (#12): the hero gets the whole surface */-$('btn-zen').addEventListener('click', () => {-  const on = document.body.classList.toggle('zen');-  $('btn-zen').classList.toggle('on', on);-  try { on ? document.documentElement.requestFullscreen() : document.exitFullscreen(); } catch {}-});-document.addEventListener('keydown', (e) => {-  if (e.key === 'Escape' && document.body.classList.contains('zen')) {-    document.body.classList.remove('zen');-    $('btn-zen').classList.remove('on');-  }-});--/* procedural circuitry background (seeded, gently different every roll) */-function mulberry32(a) {-  return function () {-    a |= 0; a = (a + 0x6D2B79F5) | 0;-    let t = Math.imul(a ^ (a >>> 15), 1 | a);-    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;-    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;-  };-}-let lastCircuitKey = '';-function drawCircuitry() {-  const cv = $('circuitcanvas');-  const rect = hero.getBoundingClientRect();-  if (rect.width < 10) return;-  const key = state.circuitSeed + 'x' + Math.round(rect.width);-  if (key === lastCircuitKey) return;-  lastCircuitKey = key;-  const W = (cv.width = Math.round(rect.width * 2));-  const H = (cv.height = Math.round(rect.height * 2));-  const g = cv.getContext('2d');-  const rnd = mulberry32(state.circuitSeed || 1);-  g.clearRect(0, 0, W, H);-  const grid = Math.round(W / 46);-  const traces = 34;-  for (let i = 0; i < traces; i++) {-    const hue = rnd();-    g.strokeStyle = hue < 0.72 ? 'rgba(0,184,177,' + (0.05 + rnd() * 0.10).toFixed(3) + ')'-      : hue < 0.9 ? 'rgba(88,166,255,' + (0.05 + rnd() * 0.08).toFixed(3) + ')'-      : 'rgba(140,107,247,' + (0.05 + rnd() * 0.08).toFixed(3) + ')';-    g.lineWidth = rnd() < 0.85 ? 2 : 4;-    g.lineCap = 'round';-    let x = Math.round(rnd() * (W / grid)) * grid;-    let y = Math.round(rnd() * (H / grid)) * grid;-    g.beginPath();-    g.arc(x, y, g.lineWidth * 1.6, 0, Math.PI * 2);-    g.stroke();-    g.beginPath();-    g.moveTo(x, y);-    let dir = Math.floor(rnd() * 4); // 0 r, 1 d, 2 l, 3 u-    const segs = 3 + Math.floor(rnd() * 5);-    for (let sVar = 0; sVar < segs; sVar++) {-      const len = grid * (1 + Math.floor(rnd() * 5));-      // 45-degree bend half the time, PCB style-      if (rnd() < 0.5) dir = (dir + (rnd() < 0.5 ? 1 : 3)) % 4;-      const diag = rnd() < 0.4;-      const dx = dir === 0 ? len : dir === 2 ? -len : diag ? len * 0.7 : 0;-      const dy = dir === 1 ? len : dir === 3 ? -len : diag ? len * 0.7 : 0;-      x += dx; y += dy;-      g.lineTo(x, y);-    }-    g.stroke();-    g.beginPath();-    g.arc(x, y, g.lineWidth * 1.6, 0, Math.PI * 2);-    g.stroke();-  }-}-$('btn-reroll').addEventListener('click', () => { post('reroll').catch(() => {}); });--/* image-zone type sizes: instant local feedback, debounced send */-let fsTimer = null;-let hlTimer = null;-$('headline-input').addEventListener('input', e => {-  clearTimeout(hlTimer);-  const v = e.target.value;-  hlTimer = setTimeout(() => { send({ headline: v }); setTimeout(() => runFit(null), 250); }, 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;-    hero.style.setProperty('--' + id.replace('fs-', 'fs-'), v + 'cqw');-    hero.style.setProperty('--fs-' + key.replace('sub', 'sub').replace('desc', 'desc'), v + 'cqw');-    clearTimeout(fsTimer);-    fsTimer = setTimeout(() => send({ fontSize: { [key]: v } }), 150);-  });-}-$('ab-again').addEventListener('click', runGenerate);--/* owner brand controls */-$('brand-accent').addEventListener('input', e => send({ brand: { accent: e.target.value } }));-$('brand-bg').addEventListener('input', e => send({ brand: { bg: e.target.value } }));-$('brand-clear').addEventListener('click', () => send({ brand: { accent: '', bg: '' } }));+<div id="wdock">+  <div id="wdockintro">You're designing the <b>hero image</b> for <span class="ref" id="wintro-ref"></span>, the picture at the top of its wiki page. Everything updates live above. Nothing is saved until you hit Generate.</div>+  <div id="wrailwrap"><div id="wsteprail"></div></div>+  <div id="wdockhead">+    <span class="wdtitle" id="wd-title"></span>+    <span class="wdcount" id="wd-count"></span>+    <span class="wdhint" id="wd-hint"></span>+    <button class="btn small" id="wbtn-finetune" style="display:none">Fine tune</button>+    <button class="btn btn-primary" id="wbtn-generate" style="display:none">Generate hero PNG</button>+    <button class="btn small btn-primary" id="wbtn-next">Next</button>+  </div>+  <div id="wcardsrow">+    <button class="warrow" id="warr-l">&#8249;</button>+    <div id="wcards"></div>+    <button class="warrow" id="warr-r">&#8250;</button>+  </div>+</div> -/* boot: load real pages, subscribe to the live channel */-Promise.all([-  fetch('api/pages').then(r => r.json()),-  fetch('api/components').then(r => r.json()),-  fetch('state').then(r => r.json()),-]).then(([pg, cp, st]) => {-  pages = pg;-  comps = cp;-  state = st;-  seenToastSeq = (st._toast && st._toast.seq) || 0; // don't replay a stale toast on load-  seenGenSeq = (st._gen && st._gen.seq) || 0;       // nor a stale generated modal-  const sel = $('sel-page');-  sel.innerHTML = pages.map(p =>-    '<option value="' + p.owner + '/' + p.slug + '">' + p.title + ' (' + p.type + ')</option>').join('');-  $('sel-comp').innerHTML = comps.map(c =>-    '<option value="' + c.id + '">' + c.name + (c.sample ? ' (sample)' : '') + '</option>').join('');-  render();-  console.log('hero-studio ready: ' + pages.length + ' live pages');-  if (location.hash) {-    const el = document.querySelector(location.hash);-    if (el) setTimeout(() => el.scrollIntoView({ block: 'start' }), 300);-  }-  const es = new EventSource('events');-  es.addEventListener('state', e => { state = JSON.parse(e.data); render(); });-  es.addEventListener('fit', e => { let m = null; try { m = (JSON.parse(e.data) || {}).max; } catch {} setTimeout(() => runFit(m), 80); });-  es.addEventListener('toast', e => { handleToast(JSON.parse(e.data)); });-  es.addEventListener('generated', e => { handleGen(JSON.parse(e.data)); });-  // poll fallback in case the proxy buffers the event stream-  setInterval(() => {-    fetch('state').then(r => r.json()).then(st => {-      handleToast(st._toast);-      handleGen(st._gen);-      if (JSON.stringify(st) !== JSON.stringify(state)) { state = st; render(); }-    }).catch(()=>{});-  }, 1000);-});-</script>+<script src="js/app.js"></script>+<script src="js/wizard.js"></script> </body> </html>
public/js/app.jsadded+1295
@@ -0,0 +1,1295 @@+/* app.js: the entire studio front-end, moved verbatim from index.html in the+   M3 split (wizard redesign). Decomposition into hero/lint/fit/wizard modules+   lands with M4, when the panel code entangled with render() is deleted. */++if (new URLSearchParams(location.search).has('render')) document.body.dataset.render = '1';++/* console relay: the AI reads GET /console instead of DevTools */+(function () {+  const post = (level, args) => fetch('console', { method:'POST', headers:{'Content-Type':'application/json'},+    body: JSON.stringify({ level, text: args.map(a => { try { return typeof a === 'string' ? a : JSON.stringify(a); } catch { return String(a); } }).join(' ') }) }).catch(()=>{});+  ['log','warn','error'].forEach(k => { const o = console[k].bind(console); console[k] = (...a) => { o(...a); post(k, a); }; });+  window.onerror = (m, s, l) => post('error', [m + ' @' + s + ':' + l]);+  window.addEventListener('unhandledrejection', e => post('error', ['unhandledrejection: ' + e.reason]));+})();++let pages = [];+let comps = [];+let state = null;++const $ = (id) => document.getElementById(id);+// Every state-changing POST from this page is attributed to the server's+// owning ai-thread (#39): the page IS that instance's surface, and the human+// acting through it acts within that thread's session.+const withThread = (o) => Object.assign({ aiThread: (state && state.aiThread) || 'hero-studio ui' }, o || {});+const post = (path, body) => fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(withThread(body)) });+const send = (patch) => post('state', patch).catch(() => {});++function canonType(t) { return t === 'hardware' ? 'component' : (t || 'app'); }+// the LAUNCH spelling for display + bake: the wiki's public name is hardware+function wireLabel(t) { return canonType(t) === 'component' ? 'hardware' : (t || 'app'); }+function pageByKey(key) { return pages.find(p => p.owner + '/' + p.slug === key) || pages[0]; }+function compById(id) { return comps.find(c => c.id === id) || comps[0]; }+/* the object the overlay, header, and context cards render: page or component */+function displayEntry() {+  if (state.mode === 'component' && comps.length) {+    const c = compById(state.comp.id);+    return { owner: c.owner, slug: c.slug, type: 'component', title: c.name, brief: c.spec,+      author: c.author, score: c.score, stars: c.stars, installs30d: null,+      tags: [c.mfr.toLowerCase()], updated_at: null, mpn: c.mpn };+  }+  return pageByKey(state.page);+}++/* long mono strings in the overlay: shrink a step past 40 chars; past 54,+   middle-truncate so the owner and the tail stay readable (full string in title=) */+function fitMono(s) {+  if (s.length <= 40) return { text: s, small: false };+  if (s.length <= 54) return { text: s, small: true };+  return { text: s.slice(0, 33) + '...' + s.slice(-18), small: true };+}++function firstSentence(s, max) {+  let t = (s || '').split(/(?<=\.)\s/)[0] || s || '';+  if (t.length > max) t = t.slice(0, max).replace(/\s+\S*$/, '') + '...';+  return t;+}++function accentize(text) {+  const words = text.split(' ');+  if (words.length < 3) return '<span class="accent">' + text + '</span>';+  return '<span class="accent">' + words.slice(0, 2).join(' ') + '</span> ' + words.slice(2).join(' ');+}++function render() {+  if (!state || !pages.length) return;+  if (window.wizardRender) { try { wizardRender(); } catch (e) { console.error('wizard render:', e); } }+  const p = pageByKey(state.page);+  const d = displayEntry();+  const mode = state.mode || 'app';+  const hero = $('hero');+  document.body.dataset.mode = mode;+  hero.dataset.mode = mode;+  hero.dataset.theme = state.theme;+  hero.dataset.layout = state.layout;+  hero.dataset.clayout = state.comp.layout;+  hero.dataset.imgsrc = state.comp.imgSrc;+  hero.dataset.mpn = state.comp.mpn;+  hero.classList.toggle('guides', !!state.guides);+  // imported background art (theme 'image', hero-studio#24 ask 7)+  const bg = state.bgImage || {};+  hero.style.setProperty('--bgimg', bg.src ? 'url(' + bg.src + ')' : 'none');+  hero.classList.toggle('bg-cover', !!bg.cover);+  $('bg-cover').checked = !!bg.cover;+  // ?? not ||: an angle of 0 is a real choice, not an unset value+  hero.style.setProperty('--ga', (state.gradAngle ?? 155) + 'deg');+  $('rng-angle').value = state.gradAngle ?? 155;+  $('angle-val').textContent = state.gradAngle ?? 155;++  // header subject+  $('subj-title').textContent = d.title;+  $('subj-meta').textContent = mode === 'component'+    ? d.type + ' · score ' + d.score + ' · MPN ' + d.mpn+    : d.type + ' · score ' + d.score + ' · ' + d.stars + ' stars · ' + d.installs30d + ' installs/30d';+  $('btn-guides').classList.toggle('on', !!state.guides);++  // verification chrome (the pose gate sets state.verify); per-item confirms+  const confirm = state.confirm || {};+  const flags = state.flags || {};+  $('v-title').checked = !!confirm.title;+  $('v-sub').checked = !!confirm.subtitle;+  $('v-vitrine').checked = !!confirm.vitrine;+  $('v-type').checked = !!confirm.type;+  // "Needs fixing" verdicts (#14): flags hold Generate and are visible to the+  // driving AI via state, so it can adjust and re-present instead of the+  // human laundering a problem through an approve-only checklist.+  for (const [el, key] of [['v-title','title'],['v-sub','subtitle'],['v-vitrine','vitrine'],['v-type','type']]) {+    const label = $(el).parentElement;+    let mark = label.querySelector('.flagged');+    if (flags[key] && !mark) {+      mark = document.createElement('span');+      mark.className = 'flagged';+      mark.textContent = 'NEEDS FIXING (flagged; the AI sees this in state.flags)';+      label.appendChild(mark);+    } else if (!flags[key] && mark) mark.remove();+  }+  const types = (Array.isArray(d.types) && d.types.length ? d.types : [d.type]);+  const modeMatchesType = (canonType(d.type) === 'component') === (mode === 'component');+  $('v-type-label').textContent = 'Page type matches: page is "' + types.map(wireLabel).join(' + ') + '", studio mode is "' + mode + '"' + (modeMatchesType ? '' : ' (MISMATCH: switch mode at the top of the panel)');+  $('v-type-label').classList.toggle('warn', !modeMatchesType);+  const anyFlag = Object.values(flags).some(Boolean);+  const lintFail = (state.lint || []).some(l => l.level === 'fail');+  $('v-generate').disabled = !(confirm.title && confirm.subtitle && confirm.vitrine && confirm.type) || anyFlag || lintFail;++  // author-adjustable image-zone type (hero-studio#4); cqw so preview == render+  const fs = state.fontSize || {};+  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');+  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);+  }++  // the gate visibly CLOSES (hero-studio#5): approval is persistent state+  const ap = state.approved;+  const approvedHere = !!(ap && ap.page === state.page);+  $('approvedbar').style.display = approvedHere ? 'block' : 'none';+  if (approvedHere) {+    $('ab-body').textContent = ap.file + ', generated ' + new Date(ap.ts).toLocaleString() ++      '. The gate is satisfied; the PNG carries the provenance stamp. Apply it with: adom-wiki page hero ' + state.page + ' --image <repo path>';+  }+  $('verifybar').style.display = state.verify && !approvedHere ? 'block' : 'none';++  // stagebar (#21/#23): stage chrome above the stage, never on it+  $('stagebar').classList.toggle('on', !!state.verify || !!state.marqueePreview);+  $('sb-png').classList.toggle('on', !state.marqueePreview);+  $('sb-mq').classList.toggle('on', !!state.marqueePreview);++  // walkthrough + procedural background + measured layout lint + page fields+  showTourStep();+  scheduleLint();+  renderPageFields();+  if (state.theme === 'circuitry') requestAnimationFrame(drawCircuitry);+  $('btn-reroll').style.display = state.theme === 'circuitry' ? '' : 'none';++  // marquee overlay preview (ask 5): today's REAL homepage treatment,+  // composited over the hero so redundancy and balance are judgeable+  hero.classList.toggle('show-mq', !!state.marqueePreview && mode === 'app');+  if (state.marqueePreview) {+    $('mqo-badges').innerHTML = '<span class="badge badge-' + canonType(d.type) + '">' + wireLabel(d.type) + '</span>' ++      (d.score > 0 ? '<span class="ts-slide-score">' + TREND13 + ' ' + d.score + '</span>' : '') ++      (d.stars > 0 ? '<span class="ts-slide-stars">' + STAR12 + ' ' + d.stars + '</span>' : '');+    // TODAY-REALITY on purpose (Colby): the homepage still overlays its own+    // title, so show the doubling instead of hiding it; the tag names the+    // planned fix so nobody thinks it is an accident.+    $('mqo-title').textContent = d.title;+    $('mqo-brief').textContent = firstSentence(d.brief || p.brief || '', 120);+    $('mqo-owner').textContent = 'wiki.adom.inc/' + d.owner + '/' + d.slug;+    // sample prompt: DYNAMIC by design (#22): metadata the page renders with a+    // copy button; if it were pixels nobody could copy it+    const sp = (Array.isArray(d.sample_prompts) && d.sample_prompts[0] && d.sample_prompts[0].prompt) || state.samplePrompt || '';+    $('mqo-prompt').style.display = sp ? 'flex' : 'none';+    $('mqo-prompt-text').textContent = sp ? '"' + sp + '"' : '';+  }+  $('chk-mqpreview').checked = !!state.marqueePreview;++  // vitrine slot size (ask 1): measure the window body, scale to export px+  requestAnimationFrame(() => {+    const win = hero.querySelector('.appbody');+    const el = $('slot-size');+    if (!win || !el) return;+    const hb = hero.getBoundingClientRect();+    const wb = win.getBoundingClientRect();+    if (hb.width < 10 || wb.width < 10) { el.textContent = 'vitrine slot hidden in this layout'; return; }+    const k = 2000 / hb.width;+    const w = Math.round(wb.width * k);+    const h = Math.round(wb.height * k);+    el.dataset.size = w + 'x' + h;+    el.textContent = 'shoot the vitrine screenshot at ' + w + 'x' + h ++      ' (aspect ' + (wb.width / wb.height).toFixed(2) + ') for this placement; click to copy';+    const cur = state.slotSize || {};+    if (cur.w !== w || cur.h !== h) send({ slotSize: { w: w, h: h } });+  });++  // vitrine content: real screenshot in the window, or full-hero import+  const vit = state.vitrine || {};+  hero.classList.toggle('vitrine-cover', !!vit.cover);+  $('vitrine-cover').checked = !!vit.cover;+  hero.classList.toggle('has-vitrine', !!vit.src && !vit.full);+  // fake titlebar: real window screenshots carry their own chrome, so a+  // second bar above it reads as an obvious fake (#11). Auto = off whenever+  // 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;++  // Adom branding only on adom-owned pages; everyone else: no mark, own palette.+  // Every preview wordmark (hero AND the in-context mock cards) follows.+  const foreign = d.owner !== 'adom';+  document.querySelectorAll('.wordmark').forEach(w => w.classList.toggle('noadom', foreign));+  // #37: the standard-wordmark stamp for imports; adom-owned pages only (the+  // no-Adom-mark-for-foreign-owners policy applies to the stamp too)+  hero.classList.toggle('stamp-logo', !!(state.overlay || {}).logo && !foreign);+  $('stamp-logo-row').style.display = (simOverImport && !foreign) ? '' : 'none';+  $('chk-stamp-logo').checked = !!(state.overlay || {}).logo;+  $('fi-logo').classList.toggle('sim-fenced', simOverImport && !!(state.overlay || {}).logo);+  const brand = state.brand || {};+  hero.classList.toggle('branded', foreign && !!(brand.accent || brand.bg));+  if (foreign && brand.accent) hero.style.setProperty('--brand-accent', brand.accent);+  else hero.style.removeProperty('--brand-accent');+  if (foreign && brand.bg) hero.style.setProperty('--brand-bg', brand.bg);+  else hero.style.removeProperty('--brand-bg');+  const secBrand = document.getElementById('sec-brand');+  if (secBrand) {+    secBrand.style.display = (mode === 'app' && foreign) ? '' : 'none';+    if (brand.accent) $('brand-accent').value = brand.accent;+    if (brand.bg) $('brand-bg').value = brand.bg;+  }++  // component image zone+  if (mode === 'component' && comps.length) {+    const c = compById(state.comp.id);+    $('c-family').textContent = c.family;+    $('c-mpn-chip').textContent = c.mpn;+    $('c-name').textContent = c.name;+    $('c-mpn-under').textContent = c.mpn;+    $('c-spec').textContent = c.spec;+  }++  // image zone+  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 = 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';+  const subText = state.subtitle || firstSentence(p.brief, 80);+  if (Array.isArray(state.accentWords)) {+    $('el-sub').innerHTML = '';+    const on = new Set(state.accentWords);+    let run = null;+    subText.split(/\s+/).filter(Boolean).forEach((w, i) => {+      const sel = on.has(i);+      if (run && run.dataset.acc === (sel ? '1' : '0')) { run.textContent += ' ' + w; return; }+      if (run) $('el-sub').appendChild(document.createTextNode(' '));+      run = document.createElement('span');+      run.dataset.acc = sel ? '1' : '0';+      if (sel) run.className = 'accent';+      run.textContent = w;+      $('el-sub').appendChild(run);+    });+  } else {+    $('el-sub').innerHTML = accentize(subText);+  }+  $('el-desc').style.display = state.show.description ? 'block' : 'none';+  $('el-desc').textContent = firstSentence(p.brief.slice(80), 110);+  $('win-label').textContent = p.title + ' · Adom webview';++  // lower band: gone for app pages (the page shows the image bare); kept in+  // component mode for the MPN-in-overlay placement option+  hero.querySelector('.lower').style.display = mode === 'app' ? 'none' : '';+  const showTitleInOverlay = mode === 'component' || state.titleMode !== 'image-only';+  // Pill GROUP (#19): a page carrying several types (app + service) renders+  // them as one clustered badge box; the linter treats the cluster as one box.+  const ptr = $('pill-tr');+  ptr.style.display = state.overlay.pill ? '' : 'none';+  ptr.innerHTML = types.map(t => '<span class="pill ' + canonType(t) + '">' + wireLabel(t).toUpperCase() + '</span>').join('');+  $('l-trend').style.display = state.overlay.scores ? '' : 'none';+  $('l-trend').querySelector('b').textContent = d.score.toLocaleString('en-US');+  $('l-stars').style.display = state.overlay.scores ? '' : 'none';+  $('l-stars').querySelector('b').textContent = d.stars;+  $('l-installs').style.display = (state.overlay.scores && d.installs30d != null) ? '' : 'none';+  $('l-installs').querySelector('b').textContent = d.installs30d;+  const url = 'https://wiki.adom.inc/' + d.owner + '/' + d.slug;+  $('l-title').style.display = showTitleInOverlay ? 'block' : 'none';+  $('l-title').textContent = d.title;+  $('l-title').href = url;+  $('l-desc').textContent = firstSentence(d.brief || p.brief || '', 140);+  const subtitleInImage = mode === 'app' && state.show.subtitle;+  $('l-desc').style.display = (!subtitleInImage && (d.brief || p.brief)) ? 'block' : 'none';+  $('l-author').style.display = state.overlay.author ? '' : 'none';+  $('l-author').innerHTML = 'by <a href="' + url + '" target="_blank">' + d.author + '</a>';+  const mpnFit = fitMono(d.mpn || '');+  $('l-mpn').textContent = mpnFit.text;+  $('l-mpn').title = d.mpn || '';+  $('l-mpn').classList.toggle('fit', mpnFit.small);+  const ref = 'wiki.adom.inc/' + d.owner + '/' + d.slug;+  const refFit = fitMono(ref);+  $('l-slug').style.display = state.overlay.slug ? '' : 'none';+  $('l-slug').textContent = refFit.text;+  $('l-slug').title = ref;+  $('l-slug').classList.toggle('fit', refFit.small);+  $('l-slug').href = url;++  // controls reflect state+  document.querySelectorAll('#grp-mode .opt').forEach(b => b.classList.toggle('on', b.dataset.v === mode));+  if (comps.length) $('sel-comp').value = compById(state.comp.id).id;+  document.querySelectorAll('#grp-imgsrc .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.comp.imgSrc));+  document.querySelectorAll('#grp-clayout .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.comp.layout));+  document.querySelectorAll('#grp-mpn .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.comp.mpn));+  $('sel-page').value = state.page;+  document.querySelectorAll('#grp-layout .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.layout));+  document.querySelectorAll('#grp-theme .swatch').forEach(b => b.classList.toggle('on', b.dataset.v === state.theme));+  document.querySelectorAll('#grp-titlemode .opt').forEach(b => b.classList.toggle('on', b.dataset.v === state.titleMode));+  $('chk-logo').checked = state.show.logo;+  $('chk-title').checked = state.show.title;+  $('chk-subtitle').checked = state.show.subtitle;+  $('chk-description').checked = state.show.description;+  $('chk-pill').checked = state.overlay.pill;+  $('chk-scores').checked = state.overlay.scores;+  $('chk-author').checked = state.overlay.author;+  $('chk-slug').checked = state.overlay.slug;++  syncPreviews();+}++/* in-context previews: markup mirrors git-wiki lib/templates.js (popularMarquee + categoryListPage) */+const STAR12 = '<svg width="12" height="12" viewBox="0 0 24 24" fill="#d29922" stroke="#d29922" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';+const TREND13 = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>';+const IMGPH = '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#484f58" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';++function relTime(iso) {+  if (!iso) return '';+  const s = (Date.now() - new Date(iso).getTime()) / 1000;+  if (s < 3600) return Math.max(1, Math.floor(s / 60)) + 'm ago';+  if (s < 86400) return Math.floor(s / 3600) + 'h ago';+  return Math.floor(s / 86400) + 'd ago';+}++function marqueeTile(p, isLive) {+  const heroPart = isLive ? '<div class="clone-slot"></div>'+    : '<div class="ts-hero-fallback"><span>' + p.title + '</span></div>';+  // DECIDED (Colby, 2026-07-26, adom/wiki#80): hero-bearing tiles get badges+  // and owner only; the baked title is the single title. Heroless fallback+  // tiles keep the text overlay, which is their only title.+  const textPart = isLive+    ? ''+    : '<h3 class="ts-slide-title">' + p.title + '</h3>' ++      (p.brief ? '<p class="ts-slide-brief">' + p.brief + '</p>' : '');+  return '<a class="ts-slide mq-slide">' + heroPart ++    '<div class="ts-slide-shade"></div><div class="ts-slide-info">' ++    '<div class="ts-slide-badges"><span class="badge badge-' + p.type + '">' + p.type + '</span>' ++    (p.score > 0 ? '<span class="ts-slide-score">' + TREND13 + ' ' + p.score + '</span>' : '') ++    (p.stars > 0 ? '<span class="ts-slide-stars">' + STAR12 + ' ' + p.stars + '</span>' : '') ++    '</div>' + textPart ++    '<span class="ts-slide-owner">' + p.owner + '/' + p.slug + '</span></div></a>';+}++function indexCard(p, isLive) {+  const hero = isLive ? '<div class="clone-slot"></div>'+    : '<div class="page-card-hero-placeholder">' + IMGPH + '</div>';+  return '<a class="page-card">' ++    '<div class="page-card-hero">' + hero + '</div>' ++    '<div class="page-card-body">' ++    '<div class="page-card-top"><span class="badge badge-' + p.type + '">' + p.type + '</span></div>' ++    '<h3 class="page-card-title">' + p.title + '</h3>' ++    (p.brief ? '<p class="page-card-brief">' + p.brief + '</p>' : '') ++    '<div class="page-card-meta"><span>' + p.owner + '/' + p.slug + '</span>' ++    (p.updated_at ? '<span class="dot-sep">&middot;</span><span>' + relTime(p.updated_at) + '</span>' : '') ++    (p.stars > 0 ? '<span class="dot-sep">&middot;</span><span class="page-card-stars">' + STAR12 + ' ' + p.stars + '</span>' : '') ++    '</div>' ++    (p.tags && p.tags.length ? '<div class="page-card-tags">' + p.tags.map(t => '<span class="chip-sm">' + t + '</span>').join('') + '</div>' : '') ++    '<div class="page-card-install"><code>adom-wiki pkg install ' + p.owner + '/' + p.slug + '</code></div>' ++    '</div></a>';+}++function cloneHero(imageOnly) {+  const c = $('hero').cloneNode(true);+  c.removeAttribute('id');+  c.querySelectorAll('[id]').forEach(n => n.removeAttribute('id'));+  c.classList.add('preview');+  c.classList.remove('guides');+  if (imageOnly) { const l = c.querySelector('.lower'); if (l) l.remove(); }+  return c;+}++function syncPreviews() {+  const p = displayEntry();+  const nb = pages.filter(x => x.owner + '/' + x.slug !== p.owner + '/' + p.slug).slice(0, 3);+  // marquee: live tile among real neighbors, content doubled for the -50% loop+  const seq = marqueeTile(p, true) + nb.map(x => marqueeTile(x)).join('');+  $('mq-track').innerHTML = seq + seq;+  // index grid: live card first, then real neighbor cards (no-hero placeholder icon)+  $('wikigrid').innerHTML = indexCard(p, true) + nb.map(x => indexCard(x)).join('');+  // wiki surfaces get the IMAGE PART only (the wiki overlays its own text);+  // the desk bar gets the full v2 hero with its baked-in text overlay+  document.querySelectorAll('#contexts .clone-slot').forEach(s => s.appendChild(cloneHero(true)));+  document.querySelectorAll('#contexts .heroslot').forEach(s => { s.innerHTML = ''; s.appendChild(cloneHero(false)); });+}++let seenToastSeq = 0;+function handleToast(t) {+  if (t && t.seq > seenToastSeq) { seenToastSeq = t.seq; toast(t.text, t.kind); }+}++function toast(text, kind) {+  const el = document.createElement('div');+  el.className = 'toast ' + (kind || 'info');+  el.textContent = text;+  $('toasts').appendChild(el);+  setTimeout(() => el.remove(), 4000);+}++/* wire controls: server owns the truth, SSE echoes back */+document.querySelectorAll('#grp-mode .opt').forEach(b => b.addEventListener('click', () => send({ mode: b.dataset.v })));+$('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 => {+  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.+  post('refresh-page?page=' + encodeURIComponent(v))+    .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 })));+$('chk-logo').addEventListener('change', e => send({ show: { logo: e.target.checked } }));+$('chk-title').addEventListener('change', e => send({ show: { title: e.target.checked } }));+$('chk-subtitle').addEventListener('change', e => send({ show: { subtitle: e.target.checked } }));+$('chk-description').addEventListener('change', e => send({ show: { description: e.target.checked } }));+$('chk-pill').addEventListener('change', e => send({ overlay: { pill: e.target.checked } }));+$('chk-scores').addEventListener('change', e => send({ overlay: { scores: e.target.checked } }));+$('chk-author').addEventListener('change', e => send({ overlay: { author: e.target.checked } }));+$('chk-slug').addEventListener('change', e => send({ overlay: { slug: e.target.checked } }));+$('btn-guides').addEventListener('click', () => send({ guides: !state.guides }));+let angleTimer = null;+$('rng-angle').addEventListener('input', e => {+  $('angle-val').textContent = e.target.value;+  $('hero').style.setProperty('--ga', e.target.value + 'deg'); // instant local feedback+  clearTimeout(angleTimer);+  angleTimer = setTimeout(() => send({ gradAngle: +e.target.value }), 120);+});+$('btn-rand').addEventListener('click', () => {+  const themes = ['midnight','pcb','onion','pill','grid','gradient'];+  const angles = [65, 100, 155, 210, 245, 290, 335];+  const theme = themes[Math.floor(Math.random()*themes.length)];+  const gradAngle = angles[Math.floor(Math.random()*angles.length)];+  if (state.mode === 'component') {+    const clayouts = ['photo-right','photo-left','photo-full'];+    const srcs = ['photo','raytrace','phong'];+    const mpns = ['eyebrow','under-title','overlay'];+    const c = comps[Math.floor(Math.random() * comps.length)];+    send({ theme, gradAngle, comp: { id: c.id, layout: clayouts[Math.floor(Math.random()*clayouts.length)],+      imgSrc: srcs[Math.floor(Math.random()*srcs.length)], mpn: mpns[Math.floor(Math.random()*mpns.length)] } });+    toast('Randomized: ' + c.name, 'info');+  } else {+    const layouts = ['bleed-bottom','bleed-right','bleed-top','bleed-corner-br','bleed-corner-tr','contained','mirror'];+    const p = pages[Math.floor(Math.random() * pages.length)];+    send({ page: p.owner + '/' + p.slug, layout: layouts[Math.floor(Math.random()*layouts.length)], theme, gradAngle });+    toast('Randomized: ' + p.title, 'info');+  }+});+let seenGenSeq = 0;+function showGenerated(g) {+  $('gen-img').src = g.url + '?t=' + g.ts;+  $('gen-path').textContent = g.file;+  $('gen-open').href = g.url;+  $('genmodal').classList.add('show');+}+/* fires for local clicks AND for API-triggered generates (SSE or poll), deduped by seq */+function handleGen(g) {+  if (g && g.ok && window.wizardOnGenerated) { try { window.wizardOnGenerated(); } catch {} }+  if (g && g.ok && g.seq > seenGenSeq) { seenGenSeq = g.seq; showGenerated(g); }+}+$('gen-close').addEventListener('click', e => { e.preventDefault(); $('genmodal').classList.remove('show'); });+$('genmodal').addEventListener('click', e => { if (e.target === $('genmodal')) $('genmodal').classList.remove('show'); });+function runGenerate() {+  const b = $('btn-generate');+  b.disabled = true; b.textContent = 'Generating...';+  toast('Rendering the hero on the server', 'info');+  post('generate', { fromGate: true }).then(r => r.json()).then(g => {+    b.disabled = false; b.textContent = 'Generate hero PNG';+    if (g.ok) handleGen(g); // success toast arrives via the live channel+    else toast('Generate failed: ' + (g.error || 'unknown') + (g.hint ? '. ' + g.hint : ''), 'error');+  }).catch(() => { b.disabled = false; b.textContent = 'Generate hero PNG'; toast('Generate failed: network error', 'error'); });+}+$('btn-generate').addEventListener('click', runGenerate);+$('v-generate').addEventListener('click', runGenerate);++$('btn-reset').addEventListener('click', () => {+  post('preset', { name: 'studio-default' });+  toast('Back to the studio defaults (server truth, not a client copy)', 'success');+});++/* tooltips: fixed div, 600ms delay, clamped to viewport on both axes */+(function () {+  const tip = $('tip');+  let timer = null;+  document.addEventListener('mouseover', e => {+    const t = e.target.closest('[data-tip]');+    if (!t) return;+    clearTimeout(timer);+    timer = setTimeout(() => {+      tip.textContent = t.getAttribute('data-tip');+      tip.classList.add('show');+      const r = t.getBoundingClientRect();+      let x = r.left + 14, y = r.bottom + 10;+      const w = tip.offsetWidth, h = tip.offsetHeight, M = 8;+      if (y + h > innerHeight - M) y = r.top - h - 10;+      x = Math.max(M, Math.min(x, innerWidth - w - M));+      y = Math.max(M, Math.min(y, innerHeight - h - M));+      tip.style.left = x + 'px'; tip.style.top = y + 'px';+    }, 600);+  });+  document.addEventListener('mouseout', e => {+    if (e.target.closest && e.target.closest('[data-tip]')) { clearTimeout(timer); tip.classList.remove('show'); }+  });+})();++/* verification checklist + vitrine controls */+[['v-title','title'],['v-sub','subtitle'],['v-vitrine','vitrine'],['v-type','type']].forEach(([el, key]) => {+  // ticking a box also clears any needs-fixing flag on that item (#14)+  $(el).addEventListener('change', e => send({ confirm: { [key]: e.target.checked }, flags: { [key]: false } }));+});+$('verifybar').addEventListener('click', e => {+  if (e.target.id !== 'v-generate' && !$('v-generate').disabled) return;+  if ($('v-generate').disabled && (e.target.id === 'v-generate' || e.target.closest('#v-generate'))) {+    const flags = Object.entries(state.flags || {}).filter(([, v]) => v).map(([k]) => k);+    const fails = (state.lint || []).filter(l => l.level === 'fail');+    let why = 'Confirm all four checks first; Generate unlocks when every item is ratified';+    if (flags.length) why = 'Generate is locked: ' + flags.join(', ') + ' flagged as needs-fixing. Fix and re-tick, or the AI adjusts and re-presents';+    else if (fails.length) why = 'Generate is locked by the layout linter: ' + fails.map(f => f.msg).join(' | ');+    handleToast({ text: why, kind: 'warn', seq: Date.now() });+  }+});+$('bg-file').addEventListener('change', e => {+  const f = e.target.files && e.target.files[0];+  if (!f) return;+  const r = new FileReader();+  r.onload = () => send({ bgImage: { src: r.result }, theme: 'image' });+  r.readAsDataURL(f);+});+$('bg-cover').addEventListener('change', e => send({ bgImage: { cover: e.target.checked } }));+$('vitrine-file').addEventListener('change', e => {+  const f = e.target.files && e.target.files[0];+  if (!f) return;+  const r = new FileReader();+  r.onload = () => send({ vitrine: { src: r.result } });+  r.readAsDataURL(f);+});+$('vitrine-full').addEventListener('change', e => send({ vitrine: { full: e.target.checked } }));+$('chk-stamp-logo').addEventListener('change', e => send({ overlay: { logo: e.target.checked } }));+$('vitrine-cover').addEventListener('change', e => send({ vitrine: { cover: e.target.checked } }));+$('vitrine-chrome').addEventListener('change', e => send({ vitrine: { chrome: e.target.checked ? 'fake' : 'none' } }));+$('chk-mqpreview').addEventListener('change', e => send({ marqueePreview: e.target.checked }));+$('slot-size').addEventListener('click', () => {+  const sz = $('slot-size').dataset.size;+  if (sz && navigator.clipboard) navigator.clipboard.writeText(sz)+    .then(() => handleToast({ text: 'Copied ' + sz, kind: 'success', seq: Date.now() })).catch(() => {});+});+$('vitrine-clear').addEventListener('click', () => send({ vitrine: { src: '', full: false } }));+$('vitrine-pull').addEventListener('click', () => {+  post('vitrine-from-page?page=' + encodeURIComponent(state.page))+    .then(r => r.json())+    .then(j => handleToast({ text: j.ok ? 'Vitrine loaded: ' + j.picked : 'No page screenshot: ' + j.error, kind: j.ok ? 'success' : 'warn', seq: Date.now() }))+    .catch(() => {});+});++/* verification walkthrough: spotlight each checklist item in place */+const TOUR_STEPS = [+  { key: 'title', el: 'el-title', title: 'This is your title',+    body: 'A hero gets across exactly ONE message, and this is it: the title, ideally approaching one word. Six messages equal zero messages. It bakes at this exact size; make sure it reads at dock size.' },+  { key: 'subtitle', el: 'el-sub', title: 'This is your subtitle',+    body: 'Deliberately small BECAUSE it is assumed nobody reads it. If the design only works when someone reads this line, the hero has failed. Check the wording anyway.' },+  { key: 'vitrine', el: 'vitrine-img', fallbackEl: 'circuitcanvas', winSel: '.window', title: 'This is your vitrine',+    body: 'Supporting evidence, NOT the message; the title is the message. It should show YOUR app, not a generic chart, and it should leave white space: negative space is what makes the one message land.' },+  { key: 'type', el: 'pill-tr', title: 'This is your type pill',+    body: 'app / skill / hardware / bootstrap (hardware is the launch name; component is its legacy spelling and survives as a kind). AIs pick this wrong often; if it says the wrong thing, yell at yours now, before it bakes.' },+];+let tourIdx = -1;+function tourTarget(step) {+  if (step.winSel) { const w = hero.querySelector(step.winSel); if (w && w.offsetParent) return w; }+  const el = $(step.el);+  if (el && el.offsetParent) return el;+  return step.fallbackEl ? $(step.fallbackEl) : null;+}+function clearTourGlow() {+  document.querySelectorAll('.tour-glow').forEach(n => n.classList.remove('tour-glow'));+}+function showTourStep() {+  clearTourGlow();+  const confirm = state.confirm || {};+  const flags = state.flags || {};+  // a flagged step is reviewed (verdict: needs fixing), so the tour moves on;+  // the flag itself keeps Generate locked until it is cleared+  const remaining = TOUR_STEPS.filter(st => !confirm[st.key] && !flags[st.key]);+  if (!state.tour || !state.verify || remaining.length === 0) {+    $('tourpop').style.display = 'none';+    if (state.tour && remaining.length === 0) send({ tour: false });+    return;+  }+  const step = remaining[0];+  tourIdx = TOUR_STEPS.indexOf(step);+  const target = tourTarget(step);+  if (!target) { $('tourpop').style.display = 'none'; return; }+  target.classList.add('tour-glow');+  const r = target.getBoundingClientRect();+  const pop = $('tourpop');+  $('tp-title').textContent = step.title;+  $('tp-body').textContent = step.body;+  pop.style.display = 'block';+  placeTourPop(pop, target, r);+}+/* The card must NEVER cover the element it asks the human to verify: a+   coach-mark over its own subject collects blind confirmations (#10).+   Try below / above / right / left; take the first spot that fits the+   viewport and does not intersect the target. If no spot exists (target+   nearly fills the window), lift the target above the card instead. */+function placeTourPop(pop, target, r) {+  const W = window.innerWidth, H = window.innerHeight, M = 12, GAP = 14;+  const pw = pop.offsetWidth, ph = pop.offsetHeight;+  const candidates = [+    { top: r.bottom + GAP, left: r.left },+    { top: r.top - ph - GAP, left: r.left },+    { top: r.top, left: r.right + GAP },+    { top: r.top, left: r.left - pw - GAP },+  ];+  const fits = c => c.top >= M && c.left >= M && c.top + ph <= H - M && c.left + pw <= W - M;+  const hides = c => !(c.left + pw <= r.left || c.left >= r.right || c.top + ph <= r.top || c.top >= r.bottom);+  let spot = candidates.find(c => fits(c) && !hides(c));+  if (!spot) {+    // clamp into the viewport on the roomier vertical side of the target+    const left = Math.max(M, Math.min(W - pw - M, r.left));+    spot = (H - r.bottom >= r.top)+      ? { top: Math.max(M, Math.min(H - ph - M, r.bottom + GAP)), left }+      : { top: Math.max(M, r.top - ph - GAP), left };+  }+  document.querySelectorAll('.tour-above').forEach(n => n.classList.remove('tour-above'));+  if (hides(spot)) target.classList.add('tour-above');+  // apply the human's drag displacement (#13): anchored spawn gives context,+  // the remembered offset keeps later steps out of the way they chose+  pop._anchor = { top: spot.top, left: spot.left };+  const top = Math.max(4, Math.min(window.innerHeight - 60, spot.top + tpDrag.dy));+  const left = Math.max(4, Math.min(window.innerWidth - 80, spot.left + tpDrag.dx));+  pop.style.top = top + 'px';+  pop.style.left = left + 'px';+}+window.addEventListener('resize', () => { if (state.tour && state.verify) showTourStep(); });++/* ---- layout linter (#18): the studio notices, the human decides ----+   Most engineers are not graphic designers; the studio is the only party that+   reliably knows the geometry, so it encodes the judgment. Runs after every+   render (debounced), posts results to state.lint (visible to the driving AI),+   and hard failures (clipped text) lock Generate. Warnings advise. */+let lintTimer = null;+let lastLintKey = '';+function scheduleLint() { clearTimeout(lintTimer); lintTimer = setTimeout(runLint, 450); }+function runLint() {+  if (!state) return;+  const hero = $('hero');+  const H = hero.getBoundingClientRect();+  if (H.width < 60) return;+  const items = [];+  const rel = (r) => ({ left: r.left - H.left, top: r.top - H.top, right: r.right - H.left, bottom: r.bottom - H.top, width: r.width, height: r.height });+  const vis = (el) => el && el.offsetParent && el.getBoundingClientRect().width > 2;+  const boxOf = (el) => rel(el.getBoundingClientRect());+  const overlapPx = (a, b) => Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left)) * Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));++  const ptrEl = $('pill-tr');+  const pill = vis(ptrEl) ? boxOf(ptrEl) : null;+  const winEl = hero.querySelector('.window');+  const vitrine = (winEl && winEl.offsetParent && state.mode !== 'component') ? boxOf(winEl) : null;+  const texts = [['title', 'el-title'], ['subtitle', 'el-sub'], ['detail line', 'el-desc']]+    .filter(([, id]) => vis($(id)))+    .map(([name, id]) => ({ name, id, box: boxOf($(id)) }));++  // 1. pill vs vitrine / pill vs baked text (#17). Pill placement is+  // contract-fixed top right, so the in-contract fix is a backdrop scrim+  // (reads as a deliberate badge) plus this warning.+  ptrEl.classList.toggle('scrim', !!(pill && vitrine && overlapPx(pill, vitrine) > 9));+  if (pill && vitrine && overlapPx(pill, vitrine) > 9) {+    items.push({ id: 'pill-vitrine', level: 'warn', el: 'pill-tr', msg: 'type pill sits on the vitrine (scrim applied so it reads as a badge). If the app has its own corner UI there, pick a layout that leaves the top-right clear' });+  }+  for (const t of texts) {+    if (pill && overlapPx(pill, t.box) > 9) items.push({ id: 'pill-' + t.id, level: 'warn', el: 'pill-tr', msg: 'type pill overlaps the ' + t.name });+  }++  // 2. image-zone text intruding into the dynamic marquee band (#14): the+  // homepage overlays its own info over the lower band; keep it calm.+  const bandTop = H.height * 0.76;+  for (const t of texts) {+    if (t.box.bottom > bandTop + 2) {+      items.push({ id: 'band-' + t.id, level: 'warn', el: t.id, msg: t.name + ' extends ' + Math.round(t.box.bottom - bandTop) + 'px into the dynamic marquee band; shrink the title font or shorten the copy so the lower band stays calm' });+    }+  }++  // 3. clipped text = hard fail (the registry would ship a broken hero);+  // near-edge text = warning. The vitrine is exempt: bleeding off the edge is+  // a deliberate layout feature.+  const inset = H.width * 0.015;+  for (const t of texts) {+    if (t.box.left < -1 || t.box.top < -1 || t.box.right > H.width + 1 || t.box.bottom > H.height + 1) {+      items.push({ id: 'clip-' + t.id, level: 'fail', el: t.id, msg: t.name + ' is CLIPPED by the hero edge; Generate stays locked until it fits' });+    } else if (t.box.left < inset || t.box.top < inset || H.width - t.box.right < inset) {+      items.push({ id: 'inset-' + t.id, level: 'warn', el: t.id, msg: t.name + ' is closer than 1.5% to the hero edge' });+    }+  }++  // 4b. full-hero imports (#22): the composed checks cannot see imported+  // pixels; say so instead of reporting all clear, and run the band scan.+  const vit2 = state.vitrine || {};+  if (vit2.full && vit2.src) {+    scanImportBand(vit2.src);+    items.push({ id: 'import-unlinted', level: 'warn', el: 'fullimport-img', msg: 'full-hero import: composed-layout checks cannot inspect imported pixels; verify by eye before ratifying. Doctrine still applies (#32/#37): title set in Familjen Grotesk 700, body copy in Satoshi, optical left alignment on the ink edge, calm lower band; wordmark at the ONE standard size (156x35px at export scale, 90px inset top-left): if the import lacks it, use Stamp the standard Adom wordmark instead of drawing your own' });+    if (bandScan.status === 'done' && bandScan.texty) {+      items.push({ id: 'import-band-text', level: 'warn', el: 'fullimport-img', msg: 'the imported image appears to carry text or graphics in the lower dynamic band (heuristic edge scan): the homepage overlays its own info there, keep the band calm' });+    }+  }++  // 4c. prompt-looking text baked into pixels (#22): sample prompts belong in+  // page metadata (sample_prompts), rendered dynamically with a copy button.+  for (const t of texts) {+    const txt = ($(t.id).textContent || '');+    if (/["\u201c\u201d'][^"\u201c\u201d']{12,}["\u201c\u201d']/.test(txt)) {+      items.push({ id: 'prompt-' + t.id, level: 'warn', el: t.id, msg: t.name + ' contains quoted prompt-looking text; a baked prompt cannot be copied. Put it in sample_prompts (page metadata) and the page renders it dynamically with a copy button' });+    }+  }++  // 4. title wrap count: a 4-line title is almost never intended.+  const tEl = $('el-title');+  if (vis(tEl)) {+    const lh = parseFloat(getComputedStyle(tEl).lineHeight);+    const lines = lh ? Math.round(tEl.getBoundingClientRect().height / lh) : 1;+    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 {}+  }++  // 6b. vitrine coverage (#43): the title is the message, the vitrine is+  // supporting evidence. When the vitrine swallows the hero, the hierarchy+  // inverts and white space (what makes the one message land) is gone.+  if (vitrine) {+    const iw = Math.max(0, Math.min(vitrine.right, H.width) - Math.max(vitrine.left, 0));+    const ih = Math.max(0, Math.min(vitrine.bottom, H.height) - Math.max(vitrine.top, 0));+    const cov = (iw * ih) / (H.width * H.height);+    if (cov > 0.72) {+      items.push({ id: 'vitrine-coverage', level: 'warn', el: null, msg: 'the vitrine covers ' + Math.round(cov * 100) + '% of the hero. The title is the ONE message this image sends; the vitrine is supporting evidence. Pick a layout that gives the title its white space' });+    }+  }++  // 6c. cover-crop accounting (#41): a cover fit silently discards source+  // pixels; say how much, and check the crop lines for beheaded content.+  const vimg = $('vitrine-img');+  if (vit2.src && !vit2.full && vit2.cover && vimg && vimg.naturalWidth > 0) {+    const win2 = hero.querySelector('.appbody');+    if (win2 && win2.offsetParent) {+      const wb = win2.getBoundingClientRect();+      const arImg = vimg.naturalWidth / vimg.naturalHeight;+      const arSlot = wb.width / wb.height;+      const discard = 1 - Math.min(arImg / arSlot, arSlot / arImg);+      if (discard > 0.25) {+        const axis = arImg > arSlot ? 'left and right edges' : 'top and bottom edges';+        items.push({ id: 'cover-discard', level: 'warn', el: 'vitrine-img', msg: 'cover fit discards ' + Math.round(discard * 100) + '% of the source screenshot (' + axis + '). Check nothing that matters was cropped out; a screenshot captured AT the slot aspect (left panel size) loses nothing' });+      }+      scanCropEdges(vimg, arSlot);+      if (cropScan.status === 'done' && cropScan.cut) {+        items.push({ id: 'crop-edge-cut', level: 'warn', el: 'vitrine-img', msg: 'content appears cut mid-element at the ' + cropScan.where + ' crop line (edge-density heuristic): logos or headings beheaded by a crop read as broken. Recapture at the slot aspect or move the crop' });+      }+    }+  }++  // 6d. multi-message import (#43): one hero, one message.+  if (vit2.full && vit2.src && bandScan.status === 'done' && bandScan.busyCells >= 6) {+    items.push({ id: 'import-multi-message', level: 'warn', el: 'fullimport-img', msg: 'the imported image reads as ' + bandScan.busyCells + ' of 12 zones text/graphic-dense: that is several competing messages, and a hero sends ONE. The title should be the message; white space is what makes it land' });+  }++  // 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));+  if (key !== lastLintKey) {+    lastLintKey = key;+    send({ lint: items.map(({ id, level, msg }) => ({ id, level, msg })) });+  }+}+function renderLintBox(items) {+  const box = $('lintbox');+  if (!state.verify) { box.innerHTML = ''; return; }+  if (!items.length) { box.innerHTML = '<div class="lint-ok">layout linter: no findings (pill, margins, band, clipping, wrap all clear)</div>'; return; }+  box.innerHTML = items.map((i, n) =>+    '<div class="lint-item lint-' + i.level + '" data-el="' + (i.el || '') + '" data-n="' + n + '">' ++    (i.level === 'fail' ? 'BLOCKS GENERATE: ' : '') + i.msg + '</div>').join('');+}+$('lintbox').addEventListener('click', (e) => {+  const item = e.target.closest('.lint-item');+  if (!item || !item.dataset.el) return;+  const el = $(item.dataset.el);+  if (!el) return;+  clearTourGlow();+  el.classList.add('tour-glow');+  setTimeout(() => el.classList.remove('tour-glow'), 2200);+});++/* draggable tour dialogs (#13): pointer-capture drag on the header, clamped;+   the offset persists across steps for this tour session */+let tpDrag = { dx: 0, dy: 0 };+(() => {+  const pop = $('tourpop');+  const head = pop.querySelector('.tp-head');+  let grab = null;+  head.addEventListener('pointerdown', (e) => {+    grab = { x: e.clientX - pop.offsetLeft, y: e.clientY - pop.offsetTop };+    pop.classList.add('dragging');+    head.setPointerCapture(e.pointerId);+  });+  head.addEventListener('pointermove', (e) => {+    if (!grab) return;+    const left = Math.max(4, Math.min(window.innerWidth - 80, e.clientX - grab.x));+    const top = Math.max(4, Math.min(window.innerHeight - 60, e.clientY - grab.y));+    pop.style.left = left + 'px';+    pop.style.top = top + 'px';+    if (pop._anchor) tpDrag = { dx: left - pop._anchor.left, dy: top - pop._anchor.top };+  });+  const drop = () => { grab = null; pop.classList.remove('dragging'); };+  head.addEventListener('pointerup', drop);+  head.addEventListener('pointercancel', drop);+})();+$('tp-ok').addEventListener('click', () => {+  const step = TOUR_STEPS[tourIdx];+  if (step) send({ confirm: { [step.key]: true }, flags: { [step.key]: false } });+});+$('tp-flag').addEventListener('click', () => {+  const step = TOUR_STEPS[tourIdx];+  if (!step) return;+  send({ flags: { [step.key]: true }, confirm: { [step.key]: false } });+  handleToast({ text: step.key + ' flagged as needs-fixing: Generate stays locked and the driving AI can read the flag from state and adjust', kind: 'warn', seq: Date.now() });+});+$('tp-skip').addEventListener('click', () => { clearTourGlow(); $('tourpop').style.display = 'none'; send({ tour: false }); });++$('mqo-prompt-copy').addEventListener('click', async (e) => {+  e.stopPropagation();+  const txt = $('mqo-prompt-text').textContent.replace(/^"|"$/g, '');+  try { await navigator.clipboard.writeText(txt); } catch {}+  handleToast({ text: 'sample prompt copied', kind: 'success', seq: Date.now() });+});++/* full-hero import band scan (#22.1): composed-layout lint cannot see imported+   pixels, so the class of hero most likely to violate the band skipped every+   check. A cheap edge-density scan of the bottom 24% flags text-like structure+   in the band; it is a heuristic and says so. */+let bandScan = { key: '', status: 'idle', texty: false, busyCells: 0, cells: 12 };+function edgeDensity(g, sx, sy, sw, sh, w, h) {+  // draw a region to a small canvas and return horizontal edge density+  g.canvas.width = w; g.canvas.height = h;+  g.drawImage(g._img, sx, sy, sw, sh, 0, 0, w, h);+  const d = g.getImageData(0, 0, w, h).data;+  let edges = 0, n = 0;+  for (let y = 0; y < h; y++) {+    for (let x = 1; x < w; x++) {+      const i = (y * w + x) * 4, j = i - 4;+      const a = d[i] * .3 + d[i + 1] * .59 + d[i + 2] * .11;+      const b = d[j] * .3 + d[j + 1] * .59 + d[j + 2] * .11;+      if (Math.abs(a - b) > 60) edges++;+      n++;+    }+  }+  return n ? edges / n : 0;+}+function scanImportBand(src) {+  const key = src.length + ':' + src.slice(40, 72);+  if (bandScan.key === key) return;+  bandScan = { key, status: 'pending', texty: false, busyCells: 0, cells: 12 };+  const img = new Image();+  img.onload = () => {+    try {+      const g = document.createElement('canvas').getContext('2d');+      g._img = img;+      // 1. the calm-band check (bottom 24%), unchanged+      const w = Math.min(800, img.width);+      const bandH = Math.max(10, Math.round(img.height * 0.24 * (w / img.width)));+      bandScan.texty = edgeDensity(g, 0, img.height * 0.76, img.width, img.height * 0.24, w, bandH) > 0.0015;+      // 2. message-block grid (#43): a hero sends ONE message; count how many+      // zones of the upper 76% read as text/graphic-dense. 4x3 grid, cheap.+      let busy = 0;+      for (let gy = 0; gy < 3; gy++) {+        for (let gx = 0; gx < 4; gx++) {+          const dens = edgeDensity(g,+            img.width * gx / 4, img.height * 0.76 * gy / 3,+            img.width / 4, img.height * 0.76 / 3, 200, 100);+          if (dens > 0.003) busy++;+        }+      }+      bandScan.busyCells = busy;+      bandScan.status = 'done';+    } catch { bandScan.status = 'failed'; }+    scheduleLint();+  };+  img.onerror = () => { bandScan.status = 'failed'; scheduleLint(); };+  img.src = src;+}++/* crop-line scan (#41): sample thin strips just inside the visible window of+   a cover-cropped vitrine; text straddling the crop line = beheaded content */+let cropScan = { key: '', status: 'idle', cut: false, where: '' };+function scanCropEdges(vimg, arSlot) {+  const key = vimg.src.length + ':' + arSlot.toFixed(3);+  if (cropScan.key === key) return;+  cropScan = { key, status: 'pending', cut: false, where: '' };+  try {+    const g = document.createElement('canvas').getContext('2d');+    g._img = vimg;+    const iw = vimg.naturalWidth, ih = vimg.naturalHeight;+    const arImg = iw / ih;+    let cut = false, where = '';+    if (arImg > arSlot) {+      const visW = ih * arSlot;+      const cutL = (iw - visW) / 2;+      const strip = Math.max(8, iw * 0.02);+      if (edgeDensity(g, cutL, 0, strip, ih, 24, 300) > 0.004) { cut = true; where = 'left'; }+      if (edgeDensity(g, cutL + visW - strip, 0, strip, ih, 24, 300) > 0.004) { cut = true; where = where ? 'left and right' : 'right'; }+    } else if (arImg < arSlot) {+      const visH = iw / arSlot;+      const cutT = (ih - visH) / 2;+      const strip = Math.max(8, ih * 0.02);+      if (edgeDensity(g, 0, cutT, iw, strip, 300, 24) > 0.004) { cut = true; where = 'top'; }+      if (edgeDensity(g, 0, cutT + visH - strip, iw, strip, 300, 24) > 0.004) { cut = true; where = where ? 'top and bottom' : 'bottom'; }+    }+    cropScan.cut = cut; cropScan.where = where; cropScan.status = 'done';+  } catch { cropScan.status = 'failed'; }+}++/* auto-fit (#40): the largest title size that keeps <=3 lines, positive+   headroom, and no clipping, measured with the REAL layout: sweep the CSS+   var coarse-to-fine, read the boxes, restore. Runs on the fit SSE event+   (drive verb + preset re-fit) and on headline edits; never in render mode. */+function solveTitleFit(maxCqw) {+  const tEl = $('el-title');+  if (!tEl || !tEl.offsetParent || document.body.dataset.render !== undefined) return null;+  const H = hero.getBoundingClientRect();+  if (H.width < 60) return null;+  const bandTop = H.top + H.height * 0.76;+  const measure = (v) => {+    hero.style.setProperty('--fs-title', v + 'cqw');+    void tEl.offsetHeight;+    const b = tEl.getBoundingClientRect();+    const lh = parseFloat(getComputedStyle(tEl).lineHeight) || 1;+    return { b, lines: Math.max(1, Math.round(b.height / lh)) };+  };+  const inHero = (b) => b.bottom < bandTop && b.right <= H.right + 1 && b.left >= H.left - 1 && b.top >= H.top - 1;+  const fits = (v) => { const m = measure(v); return m.lines <= 3 && inHero(m.b); };+  const fitsLoose = (v) => inHero(measure(v).b);+  const hi = Math.min(maxCqw || 13, 13);+  const sweep = (pred) => {+    let best = null;+    for (let v = hi; v >= 5 && best == null; v = Math.round((v - 0.5) * 10) / 10) {+      if (pred(v)) best = v;+    }+    if (best != null && best < hi) {+      for (let v = Math.min(hi, best + 0.4); v > best; v = Math.round((v - 0.1) * 10) / 10) {+        if (pred(v)) { best = v; break; }+      }+    }+    return best;+  };+  let best = sweep(fits);+  let loose = false;+  if (best == null) {+    // Nothing satisfies <=3 lines at any legible size: the headline itself is+    // the problem. Relax to the HARD constraints only (inside the hero, above+    // the band) so the pose is at least never broken, and say so.+    loose = true;+    best = sweep(fitsLoose);+  }+  const cur = (state.fontSize || {}).title || 8;+  hero.style.setProperty('--fs-title', cur + 'cqw');+  if (best == null) return null;+  hero.style.setProperty('--fs-title', best + 'cqw');+  void tEl.offsetHeight;+  const lines = Math.max(1, Math.round(tEl.getBoundingClientRect().height / (parseFloat(getComputedStyle(tEl).lineHeight) || 1)));+  hero.style.setProperty('--fs-title', cur + 'cqw');+  return { size: best, loose, lines };+}+function runFit(maxCqw) {+  const r = solveTitleFit(maxCqw);+  if (r == null) {+    handleToast({ text: 'fit: no title size in 5-13cqw keeps this headline inside the hero for this layout; shorten the headline or change layout', kind: 'warn', seq: Date.now() });+    return;+  }+  const cur = (state.fontSize || {}).title || 8;+  if (Math.abs(r.size - cur) >= 0.05) send({ fontSize: { title: r.size } });+  if (r.loose) {+    handleToast({ text: 'fit: this headline cannot make 3 lines at any legible size; set ' + r.size + 'cqw (' + r.lines + ' lines, still clear of the band). SHORTER COPY READS BIGGER: the title is the one message, aim for a few words', kind: 'warn', seq: Date.now() });+  } else if (Math.abs(r.size - cur) >= 0.05) {+    handleToast({ text: 'fit: title ' + r.size + 'cqw (largest size with <=3 lines and positive headroom for this headline + layout)', kind: 'info', seq: Date.now() });+  }+}++/* the stagebar (#21/#23): explicit dynamic on/off, keyboard D, legend off-stage */+$('sb-png').addEventListener('click', () => send({ marqueePreview: false }));+$('sb-mq').addEventListener('click', () => send({ marqueePreview: true }));+document.addEventListener('keydown', (e) => {+  if (e.key.toLowerCase() === 'd' && !e.target.closest('input,textarea')) {+    send({ marqueePreview: !state.marqueePreview });+  }+});++/* hold-to-hide the dynamic overlay (#16): press the band to see the pure+   export, release to bring the overlay back */+(() => {+  const mq = $('mqoverlay');+  const hero = $('hero');+  mq.addEventListener('pointerdown', (e) => { e.preventDefault(); hero.classList.add('mq-hold'); });+  document.addEventListener('pointerup', () => hero.classList.remove('mq-hold'));+  document.addEventListener('pointercancel', () => hero.classList.remove('mq-hold'));+})();++/* ---- page fields + Apply + Copy Changelog Prompt (#20) ----+   The human edits page-level fields at the gate; the change ledger on the+   server records everything (page fields AND pose deltas), Apply writes to+   the wiki or stashes for the first publish, and the prompt hands the whole+   session back to the AI that owns the package working copy. */+let pfEditing = false;+function renderPageFields() {+  if (pfEditing || !state) return;+  const d = pageByKey(state.page);+  if (!d) return;+  $('pf-title').value = d.title || '';+  $('pf-brief').value = d.brief || '';+  $('pf-tags').value = (d.tags || []).join(', ');+}+async function refreshChangesBadge() {+  try {+    const c = await (await fetch('changes')).json();+    const n = Object.keys(c.page || {}).length;+    $('pf-apply').textContent = 'Apply changes to wiki' + (n && !c.applied ? ' (' + n + ' unapplied)' : '');+    $('pf-apply').disabled = !n || !!c.applied;+    if (c.applied) $('pf-status').textContent = c.applied === 'wiki'+      ? 'Applied to the wiki. Still run Copy Changelog Prompt: the local package checkout needs syncing too.'+      : 'Stashed for the next publish (the page or edit route is not on the wiki yet); the changelog prompt carries the edits.';+  } catch {}+}+function pushPageField() {+  const [owner, slug] = (state.page || '/').split('/');+  const body = { owner, slug, title: $('pf-title').value.trim(), brief: $('pf-brief').value.trim(),+    tags: $('pf-tags').value.split(',').map(t => t.trim()).filter(Boolean) };+  post('api/pages', body)+    .then(() => fetch('api/pages')).then(r => r.json()).then(rows => { pages = rows; render(); refreshChangesBadge(); })+    .catch(() => {});+}+for (const id of ['pf-title', 'pf-brief', 'pf-tags']) {+  $(id).addEventListener('focus', () => { pfEditing = true; });+  $(id).addEventListener('blur', () => { pfEditing = false; pushPageField(); });+}+$('pf-apply').addEventListener('click', async () => {+  const r = await (await post('apply-page')).json().catch(() => ({}));+  if (!r.ok) { handleToast({ text: r.error || 'nothing to apply', kind: 'warn', seq: Date.now() }); return; }+  handleToast({+    text: r.applied === 'wiki' ? 'Page edits applied to the wiki (' + r.fields.join(', ') + ')'+      : 'Edits stashed for the next publish' + (r.http ? ' (wiki said HTTP ' + r.http + ')' : ''),+    kind: r.applied === 'wiki' ? 'success' : 'info', seq: Date.now(),+  });+  refreshChangesBadge();+});+$('pf-prompt').addEventListener('click', async () => {+  const c = await (await fetch('changes')).json().catch(() => null);+  if (!c) return;+  const lines = ['While reviewing the hero in Hero Studio I made these changes to ' + (c.ref || state.page) + ':'];+  const fmt = (v) => typeof v === 'string' ? '"' + v + '"' : JSON.stringify(v);+  for (const [f, ch] of Object.entries(c.page || {})) lines.push('- ' + f + ': ' + fmt(ch.from) + ' -> ' + fmt(ch.to));+  for (const [f, ch] of Object.entries(c.pose || {})) lines.push('- pose ' + f + ': ' + fmt(ch.from) + ' -> ' + fmt(ch.to));+  if (lines.length === 1) lines.push('- (no changes recorded this session)');+  lines.push('- Applied to wiki: ' + (c.applied === 'wiki' ? 'yes' : c.applied === 'stashed' ? 'no, stashed: please include in the next publish' : 'no: please include in the next publish'));+  lines.push('Please update the local package (page.json / package.json / README) to match.');+  const text = lines.join('\n');+  try {+    await navigator.clipboard.writeText(text);+    handleToast({ text: 'Changelog prompt copied: paste it into the AI session that owns the package', kind: 'success', seq: Date.now() });+  } catch {+    const ta = document.createElement('textarea');+    ta.value = text; document.body.appendChild(ta); ta.select();+    document.execCommand('copy'); ta.remove();+    handleToast({ text: 'Changelog prompt copied (fallback path)', kind: 'success', seq: Date.now() });+  }+});+setInterval(refreshChangesBadge, 3000);++/* zen focus (#12): the hero gets the whole surface */+$('btn-zen').addEventListener('click', () => {+  const on = document.body.classList.toggle('zen');+  $('btn-zen').classList.toggle('on', on);+  try { on ? document.documentElement.requestFullscreen() : document.exitFullscreen(); } catch {}+});+document.addEventListener('keydown', (e) => {+  if (e.key === 'Escape' && document.body.classList.contains('zen')) {+    document.body.classList.remove('zen');+    $('btn-zen').classList.remove('on');+  }+});++/* procedural circuitry background (seeded, gently different every roll) */+function mulberry32(a) {+  return function () {+    a |= 0; a = (a + 0x6D2B79F5) | 0;+    let t = Math.imul(a ^ (a >>> 15), 1 | a);+    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;+    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;+  };+}+let lastCircuitKey = '';+function drawCircuitry() {+  const cv = $('circuitcanvas');+  const rect = hero.getBoundingClientRect();+  if (rect.width < 10) return;+  const key = state.circuitSeed + 'x' + Math.round(rect.width);+  if (key === lastCircuitKey) return;+  lastCircuitKey = key;+  const W = (cv.width = Math.round(rect.width * 2));+  const H = (cv.height = Math.round(rect.height * 2));+  const g = cv.getContext('2d');+  const rnd = mulberry32(state.circuitSeed || 1);+  g.clearRect(0, 0, W, H);+  const grid = Math.round(W / 46);+  const traces = 34;+  for (let i = 0; i < traces; i++) {+    const hue = rnd();+    g.strokeStyle = hue < 0.72 ? 'rgba(0,184,177,' + (0.05 + rnd() * 0.10).toFixed(3) + ')'+      : hue < 0.9 ? 'rgba(88,166,255,' + (0.05 + rnd() * 0.08).toFixed(3) + ')'+      : 'rgba(140,107,247,' + (0.05 + rnd() * 0.08).toFixed(3) + ')';+    g.lineWidth = rnd() < 0.85 ? 2 : 4;+    g.lineCap = 'round';+    let x = Math.round(rnd() * (W / grid)) * grid;+    let y = Math.round(rnd() * (H / grid)) * grid;+    g.beginPath();+    g.arc(x, y, g.lineWidth * 1.6, 0, Math.PI * 2);+    g.stroke();+    g.beginPath();+    g.moveTo(x, y);+    let dir = Math.floor(rnd() * 4); // 0 r, 1 d, 2 l, 3 u+    const segs = 3 + Math.floor(rnd() * 5);+    for (let sVar = 0; sVar < segs; sVar++) {+      const len = grid * (1 + Math.floor(rnd() * 5));+      // 45-degree bend half the time, PCB style+      if (rnd() < 0.5) dir = (dir + (rnd() < 0.5 ? 1 : 3)) % 4;+      const diag = rnd() < 0.4;+      const dx = dir === 0 ? len : dir === 2 ? -len : diag ? len * 0.7 : 0;+      const dy = dir === 1 ? len : dir === 3 ? -len : diag ? len * 0.7 : 0;+      x += dx; y += dy;+      g.lineTo(x, y);+    }+    g.stroke();+    g.beginPath();+    g.arc(x, y, g.lineWidth * 1.6, 0, Math.PI * 2);+    g.stroke();+  }+}+$('btn-reroll').addEventListener('click', () => { post('reroll').catch(() => {}); });++/* image-zone type sizes: instant local feedback, debounced send */+let fsTimer = null;+let hlTimer = null;+$('headline-input').addEventListener('input', e => {+  clearTimeout(hlTimer);+  const v = e.target.value;+  hlTimer = setTimeout(() => { send({ headline: v }); setTimeout(() => runFit(null), 250); }, 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;+    hero.style.setProperty('--' + id.replace('fs-', 'fs-'), v + 'cqw');+    hero.style.setProperty('--fs-' + key.replace('sub', 'sub').replace('desc', 'desc'), v + 'cqw');+    clearTimeout(fsTimer);+    fsTimer = setTimeout(() => send({ fontSize: { [key]: v } }), 150);+  });+}+$('ab-again').addEventListener('click', runGenerate);++/* owner brand controls */+$('brand-accent').addEventListener('input', e => send({ brand: { accent: e.target.value } }));+$('brand-bg').addEventListener('input', e => send({ brand: { bg: e.target.value } }));+$('brand-clear').addEventListener('click', () => send({ brand: { accent: '', bg: '' } }));++/* boot: load real pages, subscribe to the live channel */+Promise.all([+  fetch('api/pages').then(r => r.json()),+  fetch('api/components').then(r => r.json()),+  fetch('state?src=ref').then(r => r.json()),+]).then(([pg, cp, st]) => {+  pages = pg;+  comps = cp;+  state = st;+  seenToastSeq = (st._toast && st._toast.seq) || 0; // don't replay a stale toast on load+  seenGenSeq = (st._gen && st._gen.seq) || 0;       // nor a stale generated modal+  const sel = $('sel-page');+  sel.innerHTML = pages.map(p =>+    '<option value="' + p.owner + '/' + p.slug + '">' + p.title + ' (' + p.type + ')</option>').join('');+  $('sel-comp').innerHTML = comps.map(c =>+    '<option value="' + c.id + '">' + c.name + (c.sample ? ' (sample)' : '') + '</option>').join('');+  render();+  console.log('hero-studio ready: ' + pages.length + ' live pages');+  if (location.hash) {+    const el = document.querySelector(location.hash);+    if (el) setTimeout(() => el.scrollIntoView({ block: 'start' }), 300);+  }+  const es = new EventSource('events');+  es.addEventListener('state', e => { state = JSON.parse(e.data); render(); });+  es.addEventListener('fit', e => { let m = null; try { m = (JSON.parse(e.data) || {}).max; } catch {} setTimeout(() => runFit(m), 80); });+  es.addEventListener('toast', e => { handleToast(JSON.parse(e.data)); });+  es.addEventListener('generated', e => { handleGen(JSON.parse(e.data)); });+  // poll fallback in case the proxy buffers the event stream; the ETag+  // makes the idle second cost a 304 with no body instead of the full state+  let stateEtag = '';+  setInterval(() => {+    fetch('state?src=ref', { headers: stateEtag ? { 'If-None-Match': stateEtag } : {} }).then(r => {+      if (r.status === 304) return null;+      stateEtag = r.headers.get('ETag') || '';+      return r.json();+    }).then(st => {+      if (!st) return;+      handleToast(st._toast);+      handleGen(st._gen);+      if (JSON.stringify(st) !== JSON.stringify(state)) { state = st; render(); }+    }).catch(()=>{});+  }, 1000);+});
public/js/wizard.jsadded+503
@@ -0,0 +1,503 @@+/* Hero Studio wizard (M4): the bottom-dock, one-decision-at-a-time chrome+   Colby picked over the panel. Runs as the DEFAULT surface; the classic+   panel stays one toggle away (header "Classic"). This file binds the+   mockup's proven interaction to the LIVE state: every choice is a send()+   patch, every render follows the same SSE loop the panel uses.+   Loads after app.js (classic scripts share top-level bindings). */++/* ---------- step engine ---------- */+let wizStep = 'package';+let wizExpanded = false;   // seasoned checklist expanded back to one-by-one+let wizUploads = [];       // this session's uploaded images (client-side gallery)+let wizFineTune = false;++function wp() { return pageByKey(state.page); }+function wSubText() { return state.subtitle || firstSentence(wp().brief, 80); }+function wSubWords() { return wSubText().split(/\s+/).filter(Boolean); }++const WIZ_LAYOUTS = [+  { v: 'bleed-bottom',     label: 'Off the bottom' },+  { v: 'bleed-right',      label: 'Off the right' },+  { v: 'bleed-top',        label: 'Off the top' },+  { v: 'bleed-corner-br',  label: 'Bottom-right corner' },+  { v: 'bleed-corner-tr',  label: 'Top-right corner' },+  { v: 'contained',        label: 'Floating inside' },+  { v: 'mirror',           label: 'Mirrored (text right)' },+  { 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' },+];++const WIZ_STEPS = [+  {+    id: 'package', title: 'Package',+    hint: 'Which page this hero is for.',+    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() }); },+  },+  {+    id: 'showcase', title: 'Showcase image',+    hint: 'The screenshot shown in your hero. Use your real app, not a mock.',+    summary: () => (state.vitrine && state.vitrine.src ? (state.vitrine.full ? 'Full-hero import' : 'Screenshot') : 'Drawn mock'),+    options: () => [+      { v: 'page',   label: 'From the wiki page', sub: 'The screenshot already on the page' },+      { v: 'upload', label: 'Upload images', sub: wizUploads.length ? 'Add more; keep several and switch' : 'Pick PNGs from this machine' },+      ...wizUploads.map((u, i) => ({ v: 'up:' + i, label: u.name.length > 18 ? u.name.slice(0, 17) + '…' : u.name, sub: 'Uploaded image ' + (i + 1) })),+      { v: 'mock',   label: 'Drawn mock', sub: 'The generic drawn window' },+    ],+    get: () => {+      if (!(state.vitrine && state.vitrine.src)) return 'mock';+      const i = wizUploads.findIndex(u => u.data === state.vitrine.src);+      return i >= 0 ? 'up:' + i : 'page';+    },+    set: (v) => {+      if (v === 'mock') { send({ 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 } }); }+    },+  },+  {+    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 })),+    get: () => state.layout,+    set: (v) => send({ 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 })),+    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 });+    },+  },+  {+    id: 'headline', title: 'Headline and text',+    hint: 'The words in your hero. Fewer words read bigger.',+    summary: () => state.headline || wp().title,+    custom: (host) => {+      const wrap = document.createElement('div');+      wrap.className = 'wiz-headline';+      const mk = (labText) => {+        const l = document.createElement('div');+        l.className = 'wiz-lab'; l.textContent = labText;+        return l;+      };+      const hin = document.createElement('input');+      hin.type = 'text'; hin.className = 'hin';+      hin.placeholder = wp().title + ' (the page title)';+      hin.value = state.headline || '';+      let ht = null;+      hin.addEventListener('input', () => {+        clearTimeout(ht);+        ht = setTimeout(() => { send({ 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';+      fitb.addEventListener('click', () => runFit(null));+      const sin = document.createElement('input');+      sin.type = 'text'; sin.className = 'hin';+      sin.placeholder = firstSentence(wp().brief, 80);+      sin.value = state.subtitle || '';+      let st = null;+      sin.addEventListener('input', () => {+        clearTimeout(st);+        st = setTimeout(() => {+          const patch = { subtitle: sin.value };+          if (Array.isArray(state.accentWords)) {+            const n = (sin.value || firstSentence(wp().brief, 80)).split(/\s+/).filter(Boolean).length;+            patch.accentWords = state.accentWords.filter(i => i < n);+          }+          send(patch);+        }, 350);+      });+      const pick = document.createElement('div');+      pick.className = 'wordpick';+      const renderPick = () => {+        pick.textContent = '';+        const on = new Set(Array.isArray(state.accentWords) ? state.accentWords : []);+        wSubWords().forEach((w, i) => {+          const t = document.createElement('span');+          t.className = 'wtok' + (on.has(i) ? ' on' : '');+          t.textContent = w;+          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) });+          });+          pick.appendChild(t);+        });+      };+      renderPick();+      wrap.append(mk('Headline'), hin, fitb, mk('Text under it'), sin, mk('Tap words to color them'), pick);+      host.appendChild(wrap);+    },+  },+  {+    id: 'finishing', title: 'Finishing',+    hint: 'The small marks in the corners.',+    summary: () => ['pill', 'logo', 'slug', 'author'].filter(k => state.overlay[k]).join(' + ') || 'clean',+    custom: (host) => {+      const wrap = document.createElement('div');+      wrap.className = 'wiz-finish';+      const items = [+        ['pill',   'Type pill (top right)'],+        ['author', 'Author line'],+        ['slug',   'Wiki address (bottom right)'],+        ['logo',   'Adom wordmark stamp (full imports)'],+      ];+      items.forEach(([k, lab]) => {+        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] } }));+        wrap.appendChild(c);+      });+      host.appendChild(wrap);+    },+  },+  {+    id: 'review', title: 'Review and generate',+    hint: 'See it everywhere it will show up, then make it real.',+    review: true,+    summary: () => (wizAllConfirmed() ? 'ready' : 'pending'),+    custom: (host) => wizRenderReview(host),+  },+];++const wizStepIndex = (id) => WIZ_STEPS.findIndex(s => s.id === id);+const wizActive = () => WIZ_STEPS[wizStepIndex(wizStep)] || WIZ_STEPS[0];+function wizAllConfirmed() {+  const c = state.confirm || {};+  return c.title && c.subtitle && c.vitrine && c.type+    && !Object.values(state.flags || {}).some(Boolean)+    && !(state.lint || []).some(l => l.level === 'fail');+}+function wizGoTo(id) {+  wizStep = id;+  wizExpanded = false;+  wizardRender();+  requestAnimationFrame(positionWizRing);+}++/* ---------- rail ---------- */+function wizRenderRail() {+  const rail = $('wsteprail');+  rail.textContent = '';+  WIZ_STEPS.forEach((s, i) => {+    const chip = document.createElement('div');+    const done = s.review ? wizAllConfirmed() : i < wizStepIndex(wizStep);+    chip.className = 'schip' + (s.id === wizStep ? ' active' : '') + (done ? ' done' : '');+    const n = document.createElement('span');+    n.className = 'n';+    n.innerHTML = done && s.id !== wizStep ? '<svg viewBox="0 0 24 24"><polyline points="5 13 10 18 19 7"/></svg>' : String(i + 1);+    const t = document.createElement('span');+    t.className = 't'; t.textContent = s.title;+    chip.append(n, t);+    const sum = s.summary();+    if (sum && s.id !== wizStep) {+      const v = document.createElement('span');+      v.className = 'v'; v.textContent = sum;+      chip.appendChild(v);+    }+    chip.addEventListener('click', () => wizGoTo(s.id));+    rail.appendChild(chip);+  });+  wizFitRail();+  const act = rail.querySelector('.schip.active');+  if (act) act.scrollIntoView({ inline: 'center', block: 'nearest' });+}+function wizFitRail() {+  const rail = $('wsteprail');+  const wrap = $('wrailwrap');+  rail.classList.remove('compact', 'mini');+  if (rail.scrollWidth > rail.clientWidth + 1) rail.classList.add('compact');+  if (rail.scrollWidth > rail.clientWidth + 1) rail.classList.add('mini');+  const upd = () => {+    wrap.classList.toggle('can-left', rail.scrollLeft > 2);+    wrap.classList.toggle('can-right', rail.scrollLeft + rail.clientWidth < rail.scrollWidth - 2);+  };+  upd();+  rail.onscroll = upd;+}+window.addEventListener('resize', () => { wizFitRail(); positionWizRing(); });+if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => wizFitRail());++/* ---------- edit ring: the dashed box around what this step edits ---------- */+const WIZ_EDIT_TARGETS = {+  showcase:   () => (state.vitrine && state.vitrine.full ? $('hero') : hero.querySelector('.window')),+  placement:  () => hero.querySelector('.window'),+  background: () => $('hero'),+  headline:   () => hero.querySelector('.textzone'),+  finishing:  () => $('pill-tr'),+};+function positionWizRing() {+  const ring = $('weditring');+  if (!ring || !document.body.classList.contains('wizard')) return;+  const getT = WIZ_EDIT_TARGETS[wizStep];+  const t = getT && getT();+  if (!t || (!t.offsetParent && t !== $('hero'))) { ring.classList.remove('on'); return; }+  const hb = $('hero').getBoundingClientRect();+  const tb = t.getBoundingClientRect();+  const pad = t === $('hero') ? -8 : 8;+  ring.style.left = (tb.left - hb.left - pad) + 'px';+  ring.style.top = (tb.top - hb.top - pad) + 'px';+  ring.style.width = (tb.width + pad * 2) + 'px';+  ring.style.height = (tb.height + pad * 2) + 'px';+  ring.classList.add('on');+}++/* ---------- review ---------- */+const WIZ_CHECKS = [+  { key: 'title',    q: 'Title reads right' },+  { key: 'subtitle', q: 'The line under the title reads right' },+  { key: 'vitrine',  q: 'The screenshot shows the right thing' },+  { key: 'type',     q: () => 'Page type matches (this page says "' + wireLabel(wp().type) + '")' },+];+function wizRenderReview(host) {+  const wrap = document.createElement('div');+  wrap.className = 'wiz-review';++  const views = document.createElement('div');+  views.className = 'rv-col';+  const vlab = document.createElement('div');+  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 b = document.createElement('button');+    b.textContent = o.label;+    if (!!state.marqueePreview === o.v) b.classList.add('on');+    b.addEventListener('click', () => send({ marqueePreview: o.v }));+    seg.appendChild(b);+  });+  const legend = document.createElement('div');+  legend.className = 'wiz-legend';+  legend.textContent = 'Amber-outlined = the homepage adds it, it is not in your PNG. Press D to flip.';+  views.append(vlab, seg, legend);++  const checks = document.createElement('div');+  checks.className = 'rv-col';+  const seasoned = (state.gatesDone || 0) >= 4;+  if (seasoned && !wizExpanded) {+    const row = document.createElement('div');+    row.className = 'rv-check ok';+    const q = document.createElement('span');+    q.className = 'q';+    q.textContent = 'You’ve done this ' + state.gatesDone + ' times, so the checks are pre-ticked. You saw every step on the way here.';+    const ex = document.createElement('button');+    ex.className = 'btn small';+    ex.textContent = 'Check one by one';+    ex.addEventListener('click', () => {+      wizExpanded = true;+      send({ 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)) {+      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({+      confirm: { title: true, subtitle: true, vitrine: true, type: true },+      flags: { title: false, subtitle: false, vitrine: false, type: false },+    }));+    checks.appendChild(allb);+    WIZ_CHECKS.forEach(c => {+      const row = document.createElement('div');+      row.className = 'rv-check' + (state.confirm[c.key] ? ' ok' : '') + (state.flags[c.key] ? ' flag' : '');+      const q = document.createElement('span');+      q.className = 'q'; q.textContent = typeof c.q === 'function' ? c.q() : c.q;+      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 } }));+      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 } }));+      row.append(q, ok, fl);+      checks.appendChild(row);+    });+  }++  const act = document.createElement('div');+  act.className = 'rv-col';+  const lintBox = document.createElement('div');+  const fails = (state.lint || []).filter(l => l.level === 'fail').length;+  const warns = (state.lint || []).filter(l => l.level === 'warn').length;+  lintBox.innerHTML = '<div class="wiz-lab">Layout lint</div>' + (+    fails ? '<div class="lint-item lint-fail">' + fails + ' hard problem(s): Generate stays locked. Details in the classic lint box.</div>'+      : warns ? '<div class="lint-item lint-warn">' + warns + ' warning(s). The studio measures; you decide.</div>'+        : '<div class="lint-item lint-ok">No layout findings.</div>');+  act.append(lintBox);++  wrap.append(views, checks, act);+  host.appendChild(wrap);+}++/* ---------- confetti: the finale earns it ---------- */+function wizConfetti(fromEl) {+  const c = document.createElement('canvas');+  c.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999';+  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,+    col: colors[i % colors.length],+  }));+  let t = 0;+  (function tick() {+    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;+      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.restore();+    }+    if (t < 95) requestAnimationFrame(tick);+    else c.remove();+  })();+}+window.wizardOnGenerated = () => { const b = $('wbtn-generate'); if (b) wizConfetti(b); };++/* ---------- 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;+  c.addEventListener('click', onPick);+  return c;+}+function wizardRender() {+  if (!document.body.classList.contains('wizard')) return;+  if (!state || !pages.length || !$('wdock')) return;+  const s = wizActive();+  wizRenderRail();+  $('wintro-ref').textContent = state.page;+  $('wd-title').textContent = (wizStepIndex(wizStep) + 1) + '. ' + s.title;+  $('wd-hint').textContent = s.hint;+  const next = WIZ_STEPS[wizStepIndex(wizStep) + 1];+  $('wbtn-next').textContent = next ? 'Next: ' + next.title : 'Done';+  $('wbtn-next').style.display = next && !s.review ? '' : 'none';+  const gbtn = $('wbtn-generate');+  gbtn.style.display = s.review ? '' : 'none';+  if (s.review) {+    gbtn.disabled = !wizAllConfirmed();+    gbtn.title = gbtn.disabled ? 'Confirm the four checks first' : '';+  }++  const cards = $('wcards');+  cards.textContent = '';+  const showArrows = !!s.options;+  $('warr-l').style.visibility = showArrows ? 'visible' : 'hidden';+  $('warr-r').style.visibility = showArrows ? 'visible' : 'hidden';+  if (s.options) {+    const opts = s.options();+    const cur = s.get();+    const idx = opts.findIndex(o => o.v === cur);+    $('wd-count').textContent = (Math.max(0, idx) + 1) + ' of ' + opts.length;+    opts.forEach(o => cards.appendChild(wizCard(o, o.v === cur, () => { s.set(o.v); })));+    const on = cards.querySelector('.card.on');+    if (on) on.scrollIntoView({ inline: 'center', block: 'nearest' });+  } else {+    $('wd-count').textContent = '';+    if (s.custom) s.custom(cards);+  }+  requestAnimationFrame(positionWizRing);+}++/* arrows cycle option steps; left/right keys too */+function wizCycle(dir) {+  const s = wizActive();+  if (!s.options) return;+  const opts = s.options();+  const i = opts.findIndex(o => o.v === s.get());+  const n = opts[(i + dir + opts.length) % opts.length];+  s.set(n.v);+}+$('warr-l').addEventListener('click', () => wizCycle(-1));+$('warr-r').addEventListener('click', () => wizCycle(1));+$('wbtn-next').addEventListener('click', () => {+  const next = WIZ_STEPS[wizStepIndex(wizStep) + 1];+  if (next) wizGoTo(next.id);+});+$('wbtn-generate').addEventListener('click', () => {+  if ($('wbtn-generate').disabled) return;+  runGenerate();+});+document.addEventListener('keydown', (e) => {+  if (!document.body.classList.contains('wizard')) return;+  if (e.target.closest('input,textarea')) return;+  if (e.key === 'ArrowLeft') wizCycle(-1);+  if (e.key === 'ArrowRight') wizCycle(1);+});++/* multi-upload rides the existing file input; keep a session gallery */+$('vitrine-file').setAttribute('multiple', '');+$('vitrine-file').addEventListener('change', () => {+  if (!document.body.classList.contains('wizard')) return;+  const files = [...$('vitrine-file').files];+  files.forEach((f) => {+    const r = new FileReader();+    r.onload = () => {+      wizUploads.push({ name: f.name, data: r.result });+      if (wizUploads.length && f === files[files.length - 1]) {+        send({ vitrine: { src: r.result } });+      }+    };+    r.readAsDataURL(f);+  });+});++/* classic toggle: the old panel is one click away, never gone */+$('wiz-classic').addEventListener('click', () => {+  const toClassic = document.body.classList.contains('wizard');+  document.body.classList.toggle('wizard', !toClassic);+  document.body.classList.toggle('classic', toClassic);+  $('wiz-classic').textContent = toClassic ? 'Wizard' : 'Classic';+  if (!toClassic) wizardRender();+});++/* boot: wizard is the default surface; a pose (verify) starts at Showcase+   since Package is locked. Component pages ride the same wizard for now+   (Colby, 2026-08-13); their extra image options stay in Classic. */+if (document.body.dataset.render === undefined) document.body.classList.add('wizard');+setTimeout(() => {+  if (state && state.verify) wizStep = 'showcase';+  wizardRender();+}, 400);
public/mock-sizes.htmladded+44
@@ -0,0 +1,44 @@+<!doctype html>+<html lang="en">+<head>+<meta charset="utf-8">+<title>Wizard mockup size harness</title>+<style>+body { background:#0d1117; color:#e6edf3; font-family:sans-serif; margin:0; padding:12px; display:flex; flex-wrap:wrap; gap:14px; }+.slot { border:1px solid #30363d; border-radius:8px; overflow:hidden; background:#000; position:relative; }+.slot .tag { position:absolute; left:6px; top:6px; z-index:2; font-size:11px; font-family:monospace; color:#e8a13c;+  background:rgba(5,9,14,.85); border:1px solid rgba(232,161,60,.5); border-radius:5px; padding:2px 7px; }+iframe { border:0; transform-origin:top left; display:block; }+</style>+</head>+<body>+<script>+'use strict';+const TARGET_W = 590;+const SIZES = [+  { w:1440, h:900, q:'?step=placement' },+  { w:1152, h:720, q:'?step=background&theme=circuitry&ft=1' },+  { w:1024, h:640, q:'?step=review&confirm=1' },+  { w:900,  h:600, q:'?step=headline&ft=1' },+  { w:760,  h:560, q:'?step=package' },+  { w:640,  h:520, q:'?step=showcase' },+];+for (const s of SIZES) {+  const scale = TARGET_W / s.w;+  const slot = document.createElement('div');+  slot.className = 'slot';+  slot.style.width = (s.w * scale) + 'px';+  slot.style.height = (s.h * scale) + 'px';+  const tag = document.createElement('span');+  tag.className = 'tag';+  tag.textContent = s.w + ' x ' + s.h + '  ' + s.q;+  const f = document.createElement('iframe');+  f.src = 'mock.html' + s.q;+  f.width = s.w; f.height = s.h;+  f.style.transform = 'scale(' + scale + ')';+  slot.append(tag, f);+  document.body.appendChild(slot);+}+</script>+</body>+</html>
public/mock.htmladded+1448
@@ -0,0 +1,1448 @@+<!doctype html>+<html lang="en">+<head>+<meta charset="utf-8">+<meta name="viewport" content="width=device-width, initial-scale=1">+<title>Hero Studio wizard mockup</title>+<style>+@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }+@font-face { font-family: 'Familjen Grotesk'; src: url('fonts/familjen-grotesk-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }+@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }+@font-face { font-family: 'Satoshi'; src: url('fonts/satoshi-500.woff2') format('woff2'); font-weight: 500; font-display: swap; }+@font-face { font-family: 'JetBrains Mono'; src: url('fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 400; font-display: swap; }++:root {+  --bg:#0d1117; --surface:#161b22; --elevated:#1c2128; --overlay:#21262d; --border:#30363d;+  --text:#e6edf3; --text-2:#8b949e; --text-3:#484f58;+  --accent:#00b8b0; --accent-bright:#00e6dc; --purple:#8c6bf7; --blue:#58a6ff;+  --green:#3fb950; --red:#f85149; --yellow:#d29922;+  --grad:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7);+}+* { box-sizing:border-box; margin:0; padding:0; }+html,body { height:100%; }+body {+  background:var(--bg); color:var(--text);+  font-family:'Satoshi',sans-serif; font-size:14px;+  display:flex; flex-direction:column; overflow:hidden;+}++/* ---------- 44px header ---------- */+header {+  height:44px; flex:0 0 44px; display:flex; align-items:center; gap:12px;+  padding:0 12px; background:var(--surface); border-bottom:1px solid var(--border);+}+.brand { display:flex; align-items:center; gap:8px; white-space:nowrap; }+.brand svg { width:16px; height:16px; stroke:var(--text); }+.brand .name { font-family:'Familjen Grotesk'; font-weight:500; font-size:14px; color:var(--accent); }+.brand .sub { color:var(--text-2); font-size:13px; }+.mocktag {+  font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.1em; color:var(--yellow);+  border:1px solid rgba(210,153,34,.5); background:rgba(210,153,34,.1); border-radius:6px; padding:2px 7px;+}+.vsep { width:1px; height:22px; background:var(--border); flex:0 0 1px; }+.subject { display:flex; align-items:baseline; gap:10px; min-width:0; }+.subject b { font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }+.subject .meta { font-family:'JetBrains Mono',monospace; font-size:11.5px; color:var(--text-2); white-space:nowrap; }+.actions { margin-left:auto; display:flex; gap:6px; }+.iconbtn {+  width:30px; height:30px; border:1px solid var(--border); border-radius:8px; background:transparent;+  display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all .15s ease;+}+.iconbtn svg { width:16px; height:16px; stroke:var(--text); fill:none; stroke-width:1.8; }+.iconbtn:hover { background:var(--elevated); border-color:var(--text-3); }+.iconbtn.on { border-color:var(--accent); background:rgba(0,184,176,.08); }+@media (max-width:720px){ .subject .meta { display:none; } }++/* ---------- stage ---------- */+main { flex:1; display:flex; flex-direction:column; min-height:0; }+#stage { flex:1; display:flex; align-items:center; justify-content:center; padding:16px 22px; min-height:0; position:relative; }+.hero {+  --inset:4.5cqw; --badge-inset:2.6cqw;+  aspect-ratio:16/10; position:relative; overflow:hidden;+  border-radius:12px; border:1px solid var(--border); container-type:inline-size;+  --ga:155deg;+}+#hero { flex:0 0 auto; box-shadow:0 24px 70px rgba(0,0,0,.5); }++.bg { position:absolute; inset:0; }+.hero[data-layout=mirror] .bg { transform:scaleX(-1); }+.hero[data-theme=midnight] .bg {+  background:+    radial-gradient(1200px 800px at 88% -10%, rgba(0,184,177,.22), transparent 60%),+    radial-gradient(900px 700px at -5% 110%, rgba(140,107,247,.18), transparent 55%),+    linear-gradient(var(--ga),#0a0e14 0%,#0d1117 45%,#071318 100%);+}+.hero[data-theme=midnight] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:radial-gradient(rgba(255,255,255,.05) 1px, transparent 1.4px);+  background-size:2.2cqw 2.2cqw;+  -webkit-mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);+          mask-image:radial-gradient(700px 560px at 30% 30%, #000, transparent 75%);+}+.hero[data-theme=pcb] .bg {+  background:+    radial-gradient(1100px 760px at 85% -8%, rgba(0,230,220,.14), transparent 60%),+    linear-gradient(var(--ga),#031b19 0%,#04302c 55%,#02211e 100%);+}+.hero[data-theme=pcb] .bg::after {+  content:''; position:absolute; inset:0; opacity:.5;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><g stroke="%2300e6dc" stroke-opacity=".09" fill="none" stroke-width="1.4"><path d="M10 20 h56 l18 18 v50"/><path d="M84 150 v-36 l22 -22 h44"/><path d="M20 140 v-48 l14 -14"/><circle cx="10" cy="20" r="3.2"/><circle cx="84" cy="88" r="3.2"/><circle cx="150" cy="92" r="3.2"/><circle cx="20" cy="140" r="3.2"/><circle cx="34" cy="78" r="3.2"/></g></svg>');+  background-size:10cqw 10cqw;+}+.hero[data-theme=onion] .bg {+  background:+    radial-gradient(900px 700px at -5% 112%, rgba(140,107,247,.16), transparent 55%),+    linear-gradient(var(--ga),#0b1322 0%,#101a2c 55%,#0a111e 100%);+}+.hero[data-theme=onion] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"><defs><linearGradient id="g" x1="0" y1="1" x2="1" y2="0"><stop offset="0" stop-color="%238c6bf7"/><stop offset=".55" stop-color="%2339b8ff"/><stop offset="1" stop-color="%2300e6dc"/></linearGradient></defs><g fill="none" stroke="url(%23g)" stroke-width="4" opacity=".5"><path d="M356 400 V234 Q356 60 200 60 Q44 60 44 234 V400"/><path d="M312 400 V250 Q312 110 200 110 Q88 110 88 250 V400"/><path d="M268 400 V266 Q268 160 200 160 Q132 160 132 266 V400"/><path d="M224 400 V282 Q224 210 200 210 Q176 210 176 282 V400"/></g></svg>');+  background-repeat:no-repeat;+  background-position:right -7cqw bottom -9cqw;+  background-size:50cqw auto;+}+.hero[data-theme=pill] .bg {+  background:+    radial-gradient(1000px 720px at 90% -8%, rgba(0,184,177,.12), transparent 60%),+    linear-gradient(var(--ga),#082a29 0%,#0c3a3c 55%,#062220 100%);+}+.hero[data-theme=pill] .bg::after {+  content:''; position:absolute; inset:0;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="260" height="200"><g fill="none" stroke-width="2"><rect x="16" y="22" width="26" height="62" rx="13" stroke="%2364ABFF" stroke-opacity=".3"/><circle cx="112" cy="38" r="12" stroke="%2300e6dc" stroke-opacity=".28"/><rect x="196" y="14" width="28" height="72" rx="14" stroke="%2339b8ff" stroke-opacity=".3"/><rect x="84" y="128" width="62" height="26" rx="13" stroke="%2300b8b1" stroke-opacity=".28"/><circle cx="226" cy="150" r="10" stroke="%238c6bf7" stroke-opacity=".26"/><rect x="20" y="150" width="26" height="40" rx="13" stroke="%2300e6dc" stroke-opacity=".2"/></g></svg>');+  background-size:16cqw auto;+}+.hero[data-theme=grid] .bg {+  background:+    radial-gradient(1000px 700px at 85% -10%, rgba(0,230,220,.10), transparent 60%),+    linear-gradient(var(--ga),#07302e 0%,#0c3a3c 55%,#052220 100%);+}+.hero[data-theme=grid] .bg::after {+  content:''; position:absolute; inset:0; opacity:.55;+  background-image:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28"><rect x="4" y="4" width="18" height="18" fill="none" stroke="%23ffffff" stroke-opacity=".13" stroke-width="1.3"/></svg>');+  background-size:2.4cqw 2.4cqw;+}+.hero[data-theme=gradient] .bg {+  background:+    linear-gradient(155deg, rgba(5,10,18,.42), rgba(5,10,18,.14) 55%, rgba(5,10,18,.5)),+    linear-gradient(var(--ga),#00c8bf 0%,#2f9dfa 55%,#8c6bf7 100%);+}+.hero[data-theme=aurora] .bg {+  background:+    radial-gradient(90cqw 60cqw at 12% -10%, rgba(0,184,177,.28), transparent 60%),+    radial-gradient(70cqw 55cqw at 95% 20%, rgba(88,101,242,.22), transparent 65%),+    radial-gradient(80cqw 70cqw at 55% 115%, rgba(140,107,247,.20), transparent 60%),+    #0a0f16;+}+.hero[data-theme=mesh] .bg {+  background:+    conic-gradient(from var(--ga) at 28% 30%, rgba(0,184,177,.16), transparent 32%),+    conic-gradient(from calc(var(--ga) + 120deg) at 75% 65%, rgba(88,166,255,.13), transparent 36%),+    conic-gradient(from calc(var(--ga) + 240deg) at 45% 90%, rgba(140,107,247,.12), transparent 30%),+    #0b1016;+}+.hero[data-theme=circuitry] .bg { background:#080d13; }+#circuitcanvas { display:none; position:absolute; inset:0; width:100%; height:100%; z-index:0; }+.hero[data-theme=circuitry] #circuitcanvas { display:block; }+.hero[data-theme=image] .bg { background-color:#0a0e14; background-image:var(--bgimg, none); background-repeat:repeat; background-size:var(--bgtile, 22cqw); }+.hero[data-theme=image].bg-cover .bg { background-repeat:no-repeat; background-size:cover; background-position:center; }++/* text column */+.textzone { position:absolute; left:var(--inset); top:var(--inset); width:34cqw; z-index:3; padding-right:1.5cqw; }+.hero[data-layout=mirror] .textzone { left:auto; right:var(--inset); }+/* Fixed anchors: the logo's top-left sits at the inset corner and the+   headline's top-left sits exactly one logo slot below it, on every hero,+   at every font size. The slot is reserved even when no logo renders. */+.wordmark { display:flex; align-items:flex-start; height:1.72cqw; margin-bottom:3.2cqw; }+.wordmark .wmark {+  display:block; width:7.8cqw; height:1.72cqw; background:#e6edf3;+  -webkit-mask:url('adom-logo.svg') no-repeat left top / contain;+          mask:url('adom-logo.svg') no-repeat left top / contain;+}+.wordmark.noadom { visibility:hidden; }+.h-title {+  font-family:'Familjen Grotesk'; font-weight:700; font-size:var(--fs-title, 8cqw); line-height:1.04;+  letter-spacing:-.14cqw; color:#e6edf3; margin-bottom:calc(1.8cqw * var(--fs-gap, 1)); max-width:30cqw;+  /* Colby 2026-08-12: the title's INK top-left never moves, at any font size.+     line-height is FIXED (no leading control), and cap trimming removes the+     size-dependent gap between the box top and the capital letters, so the+     first cap always sits exactly at the anchor. */+  text-box-trim: trim-start;+  text-box-edge: cap alphabetic;+}+.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; }+.h-desc { font-size:var(--fs-desc, 1.35cqw); line-height:1.5; color:#8b949e; max-width:30cqw; }+.hero.hide-logo .wordmark { visibility:hidden; }+.hero.hide-title .h-title, .hero.hide-sub .h-sub, .hero.hide-desc .h-desc { display:none; }++/* app window */+.window {+  position:absolute; z-index:2; border-radius:.9cqw .9cqw 0 0; overflow:hidden;+  background:#10151c; border:1px solid rgba(255,255,255,.09);+  box-shadow:0 1.7cqw 5cqw rgba(0,0,0,.62), 0 0 8cqw rgba(0,184,177,.07);+  display:flex; flex-direction:column;+}+.hero[data-layout=bleed-bottom] .window { right:4.5cqw; top:6cqw; width:56cqw; height:64cqw; }+.hero[data-layout=bleed-right] .window { right:-8cqw; top:6cqw; width:56cqw; height:44cqw; border-radius:.9cqw 0 0 .9cqw; }+.hero[data-layout=bleed-top] .window { right:4.5cqw; top:-6cqw; width:52cqw; height:42cqw; border-radius:0 0 .9cqw .9cqw; }+.hero[data-layout=bleed-corner-br] .window { right:-7cqw; top:14cqw; width:56cqw; height:52cqw; border-radius:.9cqw 0 0 0; }+.hero[data-layout=bleed-corner-tr] .window { right:-7cqw; top:-6cqw; width:56cqw; height:44cqw; border-radius:0 0 0 .9cqw; }+.hero[data-layout=contained] .window { right:4.5cqw; top:6cqw; width:50cqw; height:33cqw; border-radius:.9cqw; }+.hero[data-layout=mirror] .window { left:4.5cqw; top:6cqw; width:56cqw; height:64cqw; }+.hero[data-layout=showcase] .window { right:-4cqw; top:4cqw; width:66cqw; height:58cqw; border-radius:.9cqw 0 0 .9cqw; }+.hero[data-layout=showcase]::after {+  content:''; position:absolute; inset:0; z-index:2; pointer-events:none;+  background:linear-gradient(100deg, rgba(5,9,14,.88) 0%, rgba(5,9,14,.55) 30%, rgba(5,9,14,0) 55%);+}+.hero[data-layout=showcase] .textzone { z-index:3; width:30cqw; }+.titlebar {+  flex:0 0 2.1cqw; background:#181f28; border-bottom:1px solid rgba(255,255,255,.06);+  display:flex; align-items:center; padding:0 .9cqw; gap:.6cqw;+}+.titlebar .t { font-family:'Satoshi'; font-size:.82cqw; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }+.winbtns { margin-left:auto; display:flex; gap:.9cqw; align-items:center; }+.winbtns svg { width:.72cqw; height:.72cqw; stroke:#8b949e; stroke-width:2; fill:none; }+.appbody { flex:1; display:flex; min-height:0; position:relative; }+.vitrine-img { display:none; position:absolute; inset:0; width:100%; height:100%;+  object-fit:contain; object-position:top center; background:#0d1117; z-index:2; }+.hero.vitrine-cover .vitrine-img { object-fit:cover; }+.hero.has-vitrine .vitrine-img { display:block; }+.hero.no-chrome .window .titlebar { display:none; }+.hero.bare-chrome .window .titlebar { display:none; }+.hero.bare-chrome .window { background:transparent; border:none; box-shadow:none; border-radius:0; }+.sidebar { flex:0 0 11cqw; background:#131920; border-right:1px solid rgba(255,255,255,.05); padding:.8cqw .7cqw; }+.srow { height:1.5cqw; border-radius:.35cqw; background:rgba(255,255,255,.05); margin-bottom:.55cqw; }+.srow.on { background:rgba(0,184,176,.18); border:1px solid rgba(0,184,176,.35); }+.content { flex:1; padding:1cqw 1.1cqw; display:flex; flex-direction:column; gap:.8cqw; }+.crow { display:flex; gap:.6cqw; align-items:center; }+.chipk { height:1.3cqw; border-radius:999px; background:rgba(255,255,255,.07); }+.bars { flex:1; display:flex; align-items:flex-end; gap:.5cqw; min-height:0; }+.bar { flex:1; border-radius:.3cqw .3cqw 0 0; background:linear-gradient(180deg, rgba(0,230,220,.55), rgba(0,184,176,.12)); }+.tbl { display:flex; flex-direction:column; gap:.45cqw; }+.trow { display:flex; gap:.5cqw; }+.tcell { height:1.05cqw; border-radius:.25cqw; background:rgba(255,255,255,.06); }++/* owner brand */+#hero.branded { background: var(--brand-bg, #0d1117) !important; }+#hero.branded .bar { background: linear-gradient(180deg,+  color-mix(in srgb, var(--brand-accent, #00e6dc) 55%, transparent),+  color-mix(in srgb, var(--brand-accent, #00b8b0) 12%, transparent)); }+#hero.branded .h-sub .accent { background: linear-gradient(90deg,+  var(--brand-accent, #00e6dc),+  color-mix(in srgb, var(--brand-accent, #00e6dc) 45%, #fff));+  -webkit-background-clip:text; background-clip:text; }++/* badges + homepage preview overlay */+.pill-tr { position:absolute; top:var(--badge-inset); right:var(--badge-inset); z-index:6; display:flex; gap:.5cqw; }+.slug-br { position:absolute; right:var(--badge-inset); bottom:var(--badge-inset); z-index:6; }+.pill {+  font-family:'Familjen Grotesk'; font-weight:700; font-size:.85cqw; letter-spacing:.14em;+  padding:.3cqw .85cqw; border-radius:999px; border:1px solid; text-transform:uppercase;+}+.pill.app { color:#00e6dc; border-color:rgba(0,230,220,.45); background:rgba(0,184,176,.10); }+.pill.skill { color:#b39dfb; border-color:rgba(140,107,247,.5); background:rgba(140,107,247,.12); }+.pill.component { color:#e3b341; border-color:rgba(210,153,34,.5); background:rgba(210,153,34,.12); }+.pill-tr.scrim .pill { background:rgba(8,12,18,.72); backdrop-filter:blur(5px); box-shadow:0 .2cqw 1cqw rgba(0,0,0,.5); }+.l-slug { font-family:'JetBrains Mono',monospace; font-size:.95cqw; color:#00b8b1; text-decoration:none; }+#mqoverlay { display:none; position:absolute; left:0; right:0; bottom:0; z-index:5;+  padding:3cqw 3cqw 2.2cqw;+  background:linear-gradient(180deg, rgba(5,9,14,0) 0%, rgba(5,9,14,.55) 35%, rgba(5,9,14,.9) 100%); }+.hero.show-mq #mqoverlay { display:block; animation:mqbreathe 3.4s ease-in-out infinite;+  outline:2px dashed rgba(232,161,60,.85); outline-offset:-2px; background-color:rgba(232,161,60,.045); }+@keyframes mqbreathe { 0%,100% { opacity:1; } 50% { opacity:.5; } }+#mqoverlay .mqo-badges { display:flex; gap:1cqw; align-items:center; margin-bottom:.9cqw; }+#mqoverlay .mqo-stat { font-family:'JetBrains Mono',monospace; font-size:1cqw; color:#fff;+  background:rgba(5,9,14,.72); padding:.2cqw .9cqw; border-radius:1cqw; }+#mqoverlay .mqo-title { font-family:'Familjen Grotesk'; font-weight:700; font-size:3.2cqw; color:#fff; margin:0 0 .5cqw; }+#mqoverlay .mqo-brief { font-size:1.5cqw; color:#c9d1d9; margin:0 0 .7cqw; max-width:60cqw; }+#mqoverlay .mqo-owner { font-family:'JetBrains Mono',monospace; font-size:1.1cqw; color:#00b8b1; }+.guide { position:absolute; left:0; right:0; top:66.6%; z-index:6; border-top:1.5px dashed rgba(0,230,220,.55); pointer-events:none; display:none; }+.guide span {+  position:absolute; right:1cqw; top:.4cqw; font-family:'JetBrains Mono',monospace; font-size:.8cqw;+  color:#00e6dc; background:rgba(10,14,20,.7); padding:.2cqw .6cqw; border-radius:.35cqw;+}+.hero.guides .guide { display:block; }+.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; }++/* ---------- review context views ---------- */+#ctxview { display:none; width:100%; max-width:980px; max-height:100%; overflow:auto; }+#ctxview.show { display:block; }+#stage.ctxmode #hero { display:none; }+.ctxframe { background:#0d1117; border:1px solid var(--border); border-radius:10px; padding:14px; }+.page-grid { display:grid; grid-template-columns:repeat(2, 1fr); gap:16px; }+.page-card { position:relative; display:flex; flex-direction:column; color:#e6edf3; overflow:hidden; background:#161b22; border:1px solid #30363d; border-radius:6px; }+.page-card-hero { aspect-ratio:16/10; background:#1c2128; display:flex; align-items:center; justify-content:center; overflow:hidden; border-bottom:1px solid #30363d; position:relative; }+.page-card-body { padding:16px; display:flex; flex-direction:column; gap:6px; flex:1; }+.page-card-title { font-size:15px; font-weight:600; line-height:1.3; margin:0; }+.page-card-brief { color:#8b949e; font-size:13px; margin:0; }+.page-card-meta { font-size:12px; color:#484f58; display:flex; align-items:center; gap:4px; margin-top:auto; }+.ts-hero-fallback { width:100%; height:100%; display:flex; align-items:center; justify-content:center; padding:32px; text-align:center; background:linear-gradient(135deg,#0f766e,#1d4ed8 60%,#6d28d9); font-size:22px; font-weight:700; color:#fff; font-family:'Familjen Grotesk'; }+.deskmock { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:10px; display:flex; flex-direction:column; gap:10px; max-width:420px; margin:0 auto; }+.deskhead { font-family:'Familjen Grotesk'; font-weight:500; font-size:11px; letter-spacing:.07em; text-transform:uppercase; color:var(--text-2); }+.clonewrap .hero { width:100%; border-radius:8px; box-shadow:none; }+.clonewrap .hero .guide { display:none !important; }+.page-card-hero .clonewrap { position:absolute; inset:0; }+.page-card-hero .clonewrap .hero { border-radius:0; border:none; }++/* ---------- the wizard dock ---------- */+#dock { flex:0 0 auto; background:var(--surface); border-top:1px solid var(--border); padding:10px 18px 14px; display:flex; flex-direction:column; gap:10px;+  height:min(48vh, 360px); overflow-y:auto; scrollbar-width:thin; }+#railwrap { position:relative; min-width:0; }+#railwrap::before, #railwrap::after { content:''; position:absolute; top:0; bottom:2px; width:36px; pointer-events:none; opacity:0; transition:opacity .15s ease; z-index:2; }+#railwrap::before { left:0; background:linear-gradient(90deg, var(--surface) 20%, transparent); }+#railwrap::after { right:0; background:linear-gradient(270deg, var(--surface) 20%, transparent); }+#railwrap.can-left::before { opacity:1; }+#railwrap.can-right::after { opacity:1; }+#steprail { display:flex; gap:6px; overflow-x:auto; padding-bottom:2px; scrollbar-width:none; }+/* fit by MEASUREMENT, not breakpoints (fitRail): drop summaries first, then+   collapse inactive chips to their number. 7 number-only chips fit anywhere,+   so the rail is normally never in scroll at all. */+#steprail.compact .schip .v { display:none; }+#steprail.mini .schip:not(.active) .t { display:none; }+#steprail.mini .schip:not(.active) { padding:4px 8px 4px 5px; }+/* center when there is room, scroll (never clip) when there is not:+   auto margins on the ends collapse to 0 inside an overflowing scroller */+#steprail > :first-child, #cards > :first-child { margin-left:auto; }+#steprail > :last-child, #cards > :last-child { margin-right:auto; }+#steprail::-webkit-scrollbar, #cards::-webkit-scrollbar { display:none; }+.schip { display:flex; align-items:center; gap:8px; border:1px solid var(--border); background:var(--elevated); border-radius:999px;+  padding:4px 12px 4px 5px; cursor:pointer; white-space:nowrap; transition:all .15s ease; font-family:'Satoshi'; }+.schip:hover { border-color:var(--text-3); }+.schip .n { width:20px; height:20px; border-radius:50%; display:flex; align-items:center; justify-content:center;+  font-size:11px; font-family:'JetBrains Mono',monospace; background:var(--overlay); color:var(--text-2); border:1px solid var(--border); flex:0 0 20px; }+.schip .n svg { width:11px; height:11px; stroke:var(--green); stroke-width:2.5; fill:none; }+.schip .t { font-size:12.5px; color:var(--text-2); }+.schip .v { font-size:11px; color:var(--text-3); max-width:110px; overflow:hidden; text-overflow:ellipsis; }+.schip.active { border-color:var(--accent); background:rgba(0,184,176,.10); }+.schip.active .t { color:var(--text); font-weight:500; }+.schip.active .n { background:var(--accent); color:#04211f; border-color:transparent; }+.schip.done .n { background:rgba(63,185,80,.12); border-color:rgba(63,185,80,.45); }+.schip.locked .t::after { content:' (locked)'; color:var(--text-3); font-size:10.5px; }++#btn-generate { font-size:15px; font-weight:600; padding:10px 26px; border-radius:10px; }+#btn-generate:not(:disabled) { box-shadow:0 0 18px rgba(0,184,176,.35); }+#editring { position:absolute; border:2px dashed var(--accent); border-radius:10px; pointer-events:none;+  z-index:9; opacity:0; transition:opacity .25s ease, top .2s ease, left .2s ease, width .2s ease, height .2s ease;+  box-shadow:0 0 22px rgba(0,184,176,.18); }+#editring.on { opacity:.85; }+.wordpick { display:flex; flex-wrap:wrap; gap:4px; max-width:440px; }+.wtok { padding:2px 8px; border:1px solid var(--border); border-radius:6px; font-size:12px; cursor:pointer;+  color:var(--text-2); background:var(--elevated); transition:all .12s ease; user-select:none; }+.wtok:hover { border-color:var(--text-3); }+.wtok.on { border-color:var(--accent); background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent; font-weight:600; }+#dockintro { font-size:12px; color:var(--text-2); padding:0 2px 2px; }+#dockintro b { color:var(--text); font-weight:600; }+#dockintro .ref { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--accent); }+#dockhead { display:flex; align-items:baseline; gap:8px 12px; min-height:22px; flex-wrap:wrap; }+#dockhead .dtitle { font-family:'Familjen Grotesk'; font-weight:700; font-size:15px; color:var(--text); }+#dockhead .dcount { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-2); }+#dockhead .dhint { font-size:12px; color:var(--text-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0; }+#dockhead .dright { margin-left:auto; display:flex; gap:8px; flex:0 0 auto; }++#dockmain { display:flex; align-items:stretch; gap:12px; min-height:96px; }+.arrow { flex:0 0 46px; border:1px solid var(--border); border-radius:10px; background:var(--elevated); cursor:pointer;+  display:flex; align-items:center; justify-content:center; transition:all .15s ease; }+.arrow svg { width:20px; height:20px; stroke:var(--text); stroke-width:2; fill:none; }+.arrow:hover { border-color:var(--accent); background:rgba(0,184,176,.08); }+.arrow[disabled] { opacity:.3; cursor:default; }+.arrow[disabled]:hover { border-color:var(--border); background:var(--elevated); }+#cards { flex:1; display:flex; gap:10px; align-items:stretch; overflow-x:auto; padding:2px; min-width:0; scrollbar-width:none; }+.card { flex:0 0 auto; width:118px; border:1px solid var(--border); border-radius:10px; background:var(--elevated); cursor:pointer;+  padding:8px 8px 7px; display:flex; flex-direction:column; align-items:center; gap:6px; transition:all .15s ease; }+.card:hover { border-color:var(--text-3); }+.card.on { border-color:var(--accent); background:rgba(0,184,176,.10); box-shadow:0 0 0 1px var(--accent); }+.card .thumb { width:100%; aspect-ratio:16/10; border-radius:6px; overflow:hidden; border:1px solid var(--border); position:relative; }+.card .thumb svg { display:block; width:100%; height:100%; }+.card .lbl { font-size:12px; color:var(--text); text-align:center; line-height:1.25; }+.card .sub { font-size:10.5px; color:var(--text-2); text-align:center; line-height:1.3; }+.card.on .lbl { color:var(--accent-bright); }+.card.wide { width:170px; }+.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; }++.btn {+  padding:8px 14px; border-radius:8px; border:1px solid var(--border);+  background:var(--elevated); color:var(--text); font-family:'Satoshi'; font-size:13px; cursor:pointer;+  transition:all .15s ease;+}+.btn:hover { border-color:var(--accent); background:rgba(0,184,176,.08); }+.btn-primary { border-color:var(--accent); background:rgba(0,184,176,.12); font-weight:500; }+.btn-primary:hover { background:rgba(0,184,176,.2); }+.btn:disabled { opacity:.45; cursor:not-allowed; }+.btn.small { padding:5px 10px; font-size:12px; }++#finetune { display:none; border-top:1px dashed var(--border); padding-top:10px; }+#finetune.open { display:flex; gap:26px; flex-wrap:wrap; align-items:flex-start; }+.ft-group { display:flex; flex-direction:column; gap:6px; min-width:170px; }+.ft-group .ft-label { font-family:'Familjen Grotesk'; font-weight:500; font-size:10.5px; letter-spacing:.08em; text-transform:uppercase; color:var(--text-2); }+label.chk { display:flex; align-items:center; gap:8px; padding:2px 0; cursor:pointer; color:var(--text); font-size:12.5px; }+label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:pointer; }+.seg { display:inline-flex; flex-wrap:wrap; border:1px solid var(--border); border-radius:8px; overflow:hidden; max-width:100%; }+.seg button { background:var(--bg); color:var(--text); border:0; padding:6px 12px; font-size:12.5px; cursor:pointer; font-family:'Satoshi'; white-space:nowrap; }+.seg button.on { background:rgba(0,184,176,.18); color:var(--accent-bright); }+.sl { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text-2); }+.sl label { width:74px; flex:0 0 74px; }+.sl input[type=range] { width:120px; accent-color:var(--accent); cursor:pointer; }+.sl .val { font-family:'JetBrains Mono',monospace; font-size:11px; width:34px; }+.brandrow { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--text-2); }+.brandrow input[type=color] { width:38px; height:24px; border:1px solid var(--border); border-radius:6px; background:var(--surface); padding:1px; cursor:pointer; }+input[type=text].hin { background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:8px;+  padding:8px 10px; font:inherit; font-size:13px; width:min(380px, 40vw); }+input[type=text].hin:focus { border-color:var(--accent); outline:none; }++/* review step */+.review { flex:1; display:flex; gap:14px 22px; align-items:flex-start; justify-content:center; min-width:0; flex-wrap:wrap; }+.rv-col { display:flex; flex-direction:column; gap:8px; min-width:0; }+.rv-check { display:flex; align-items:center; gap:10px; border:1px solid var(--border); border-radius:8px; padding:6px 10px; background:var(--elevated); }+.rv-check .q { font-size:12.5px; flex:1; min-width:0; }+.rv-check.ok { border-color:rgba(63,185,80,.5); }+.rv-check.ok .q { color:var(--green); }+.rv-check.flag { border-color:rgba(248,81,73,.55); }+.rv-check.flag .q { color:var(--red); }+.rv-check .btn.small.okon { border-color:var(--green); color:var(--green); }+.rv-check .btn.small.flagon { border-color:var(--red); color:var(--red); }+.lint-item { font-size:12px; margin:2px 0; line-height:1.35; }+.lint-warn { color:#e8a13c; }+.lint-ok { color:#3fb950; }+#rv-generate { font-size:14px; padding:10px 18px; }++/* toasts */+#toasts { position:fixed; left:50%; bottom:18px; transform:translateX(-50%); z-index:99998; display:flex; flex-direction:column-reverse; gap:8px; }+.toast {+  display:flex; align-items:center; gap:8px; background:var(--overlay); border:1px solid var(--border);+  border-left:3px solid var(--accent); border-radius:8px; padding:9px 13px; font-size:13px;+  box-shadow:0 8px 30px rgba(0,0,0,.45); animation:tin .2s ease;+}+.toast.success { border-left-color:var(--green); }+@keyframes tin { from { opacity:0; transform:translateY(6px);} }++/* ---------- responsive: every width and height must degrade, never clip ---------- */+@media (max-width:900px) {+  #dockhead .dhint { display:none; }+  .card { width:104px; }+  .card.wide { width:150px; }+  .arrow { flex:0 0 38px; }+  #dock { padding:8px 10px 10px; }+  #stage { padding:12px; }+  input[type=text].hin { width:min(300px, 56vw); }+}+@media (max-width:640px) {+  .subject .meta { display:none; }+  .card { width:92px; }+  #dockmain { gap:8px; }+}+@media (max-height:760px) {+  #dockmain { min-height:78px; }+  #stage { padding:10px; }+  #dock { gap:7px; padding-bottom:10px; }+}+</style>+</head>+<body>++<header>+  <div class="brand">+    <svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="14.5" x2="21" y2="14.5"/><line x1="6" y1="17.5" x2="13" y2="17.5"/></svg>+    <span class="name">Hero Studio</span>+    <span class="sub">hero image studio</span>+    <span class="mocktag">UI MOCKUP</span>+  </div>+  <div class="vsep"></div>+  <div class="subject"><b id="subj-title">...</b><span class="meta" id="subj-meta"></span></div>+  <div class="actions">+    <button class="iconbtn" id="btn-guides" title="Toggle the lower-third guide line">+      <svg viewBox="0 0 24 24"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.6"/></svg>+    </button>+    <button class="iconbtn" id="btn-rand" title="Randomize placement and background">+      <svg viewBox="0 0 24 24"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>+    </button>+  </div>+</header>++<main>+  <div id="stage">+    <div id="hero" class="hero" data-theme="midnight" data-layout="bleed-bottom">+      <div class="bg"></div>+      <canvas id="circuitcanvas"></canvas>+      <div class="textzone">+        <div class="wordmark" id="el-logo"><span class="wmark"></span></div>+        <div class="h-title" id="el-title"></div>+        <div class="h-sub" id="el-sub"></div>+        <div class="h-desc" id="el-desc"></div>+      </div>+      <div class="window" id="el-window">+        <div class="titlebar">+          <span class="t" id="win-label"></span>+          <span class="winbtns">+            <svg viewBox="0 0 10 10"><line x1="1" y1="5" x2="9" y2="5"/></svg>+            <svg viewBox="0 0 10 10"><rect x="1.5" y="1.5" width="7" height="7"/></svg>+            <svg viewBox="0 0 10 10"><line x1="1.5" y1="1.5" x2="8.5" y2="8.5"/><line x1="8.5" y1="1.5" x2="1.5" y2="8.5"/></svg>+          </span>+        </div>+        <div class="appbody">+          <img class="vitrine-img" id="shot-img" alt="">+          <div class="sidebar">+            <div class="srow on"></div><div class="srow"></div><div class="srow"></div>+            <div class="srow"></div><div class="srow"></div><div class="srow"></div><div class="srow"></div>+          </div>+          <div class="content">+            <div class="crow"><span class="chipk" style="width:22%"></span><span class="chipk" style="width:12%"></span><span class="chipk" style="width:16%"></span></div>+            <div class="bars">+              <div class="bar" style="height:42%"></div><div class="bar" style="height:66%"></div>+              <div class="bar" style="height:35%"></div><div class="bar" style="height:82%"></div>+              <div class="bar" style="height:58%"></div><div class="bar" style="height:91%"></div>+              <div class="bar" style="height:49%"></div><div class="bar" style="height:73%"></div>+            </div>+            <div class="tbl">+              <div class="trow"><span class="tcell" style="flex:2"></span><span class="tcell" style="flex:1"></span><span class="tcell" style="flex:1"></span></div>+              <div class="trow"><span class="tcell" style="flex:2"></span><span class="tcell" style="flex:1"></span><span class="tcell" style="flex:1"></span></div>+              <div class="trow"><span class="tcell" style="flex:2"></span><span class="tcell" style="flex:1"></span><span class="tcell" style="flex:1"></span></div>+            </div>+          </div>+        </div>+      </div>+      <div id="mqoverlay">+        <div class="mqo-badges" id="mqo-badges"></div>+        <h3 class="mqo-title" id="mqo-title"></h3>+        <p class="mqo-brief" id="mqo-brief"></p>+        <span class="mqo-owner" id="mqo-owner"></span>+      </div>+      <div class="pill-tr scrim" id="pill-tr"><span class="pill app" id="l-pill">APP</span></div>+      <div class="slug-br" id="slug-br"><a class="l-slug" id="l-slug" href="#" onclick="return false"></a></div>+      <div class="guide"><span>lower third</span></div>+      <div id="editring"></div>+    </div>+    <div id="ctxview"></div>+  </div>++  <div id="dock">+    <div id="dockintro">You're designing the <b>hero image</b> for <span class="ref" id="intro-ref"></span>, the picture at the top of its wiki page. Everything updates live above. Nothing is saved until you hit Generate.</div>+    <div id="railwrap"><div id="steprail"></div></div>+    <div id="dockhead">+      <span class="dtitle" id="d-title"></span>+      <span class="dcount" id="d-count"></span>+      <span class="dhint" id="d-hint"></span>+      <div class="dright">+        <button class="btn small" id="btn-finetune">Fine tune</button>+        <button class="btn btn-primary" id="btn-generate" style="display:none">Generate hero PNG</button>+        <button class="btn small btn-primary" id="btn-next">Next</button>+      </div>+    </div>+    <div id="dockmain">+      <button class="arrow" id="arr-l" title="Previous option (left arrow key)"><svg viewBox="0 0 24 24"><polyline points="14 5 7 12 14 19"/></svg></button>+      <div id="cards"></div>+      <button class="arrow" id="arr-r" title="Next option (right arrow key)"><svg viewBox="0 0 24 24"><polyline points="10 5 17 12 10 19"/></svg></button>+    </div>+    <div id="finetune"></div>+  </div>+</main>++<div id="toasts"></div>+<input type="file" id="file-shot" accept="image/*" multiple style="display:none">+<input type="file" id="file-bg" accept="image/*" style="display:none">++<script>+'use strict';+const $ = (id) => document.getElementById(id);++/* ---------- mock data ---------- */+const PAGES = [+  { key:'adom/adom-tts',      title:'Adom TTS',      type:'app',   owner:'adom', brief:'Text to speech for every container, one CLI verb away.', score:97, stars:31, installs:214 },+  { key:'adom/parts-search',  title:'Parts Search',  type:'app',   owner:'adom', brief:'One search across Mouser, DigiKey and JLCPCB.',          score:93, stars:24, installs:180 },+  { key:'adom/hero-studio',   title:'Hero Studio',   type:'app',   owner:'adom', brief:'Design hero images with a human approval gate.',         score:88, stars:19, installs:122 },+  { key:'john/wire-bender',   title:'Wire Bender',   type:'skill', owner:'john', brief:'Parametric wire forming layouts for the workcell.',      score:74, stars:12, installs:64 },+];+const pageOf = () => PAGES.find(p => p.key === mock.page) || PAGES[0];++const mock = {+  page:'adom/adom-tts', layout:'bleed-bottom', theme:'midnight',+  headline:'', titleMode:'image-only',+  source:'mock', cover:false, frame:'auto', full:false,+  show:{ logo:true, title:true, subtitle:true, description:false },+  fs:{ title:8, sub:2.5, desc:1.35, gap:1 },+  uploads:[],             // [{name, data}] every image the user brought in+  upload:-1,              // active index into uploads+  gatesDone:0,            // completed-gate count (seasoned mode, #45; ?gates=N)+  subtitle:'',            // '' = the page brief's first sentence+  accentWords:[0, 1, 2],  // word indices that get the gradient treatment  // leading removed on purpose: fixed anchors beat adjustable leading (Colby 2026-08-12)+  pill:true, slug:false, gradAngle:155, guides:false, mq:false,+  brand:{ accent:'', bg:'' },+  step:'package', view:'png', finetuneOpen:false,+  confirm:{ title:false, subtitle:false, showcase:false, type:false },+  flags:{ title:false, subtitle:false, showcase:false, type:false },+  seed:7,+};++/* ---------- toasts ---------- */+function toast(msg, cls) {+  const t = document.createElement('div');+  t.className = 'toast' + (cls ? ' ' + cls : '');+  t.textContent = msg;+  $('toasts').appendChild(t);+  setTimeout(() => t.remove(), 4000);+}++/* ---------- placement thumbnails ---------- */+function layoutThumb(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)"/>' : '';+  return `<svg viewBox="0 0 100 62" preserveAspectRatio="none">+    <defs><linearGradient id="scr" 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>`;+}++/* ---------- steps ---------- */+const LAYOUTS = [+  { v:'bleed-bottom',    label:'Off the bottom' },+  { v:'bleed-right',     label:'Off the right' },+  { v:'bleed-top',       label:'Off the top' },+  { v:'bleed-corner-br', label:'Bottom-right corner' },+  { v:'bleed-corner-tr', label:'Top-right corner' },+  { v:'contained',       label:'Floating inside' },+  { v:'mirror',          label:'Flipped, text right' },+  { v:'showcase',        label:'Backdrop' },+];+const THEMES = [+  { 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 STEPS = [+  {+    id:'package', title:'Package',+    hint:'Which page this hero is for.',+    summary:() => pageOf().title,+    options:() => PAGES.map(p => ({ v:p.key, label:p.title, sub:p.key, kind:'page' })),+    get:() => mock.page,+    set:(v) => { mock.page = v; },+  },+  {+    id:'showcase', title:'Showcase image',+    hint:'The screenshot shown in your hero. Use your real app, not a mock.',+    summary:() => ({ page:'From the page', upload:'Uploaded', mock:'Drawn mock' })[mock.source],+    options:() => [+      { v:'page',   label:'From the wiki page', sub:'The screenshot already on the page' },+      { v:'upload', label:'Upload images', sub:mock.uploads.length ? 'Add more; keep several and switch' : 'Pick PNGs from this machine' },+      ...mock.uploads.map((u, i) => ({ v:'up:' + i, label:u.name.length > 18 ? u.name.slice(0, 17) + '\u2026' : u.name, sub:'Uploaded image ' + (i + 1) })),+      { v:'mock',   label:'Drawn mock',         sub:'The generic drawn window' },+    ],+    get:() => (mock.source === 'upload' && mock.upload >= 0 ? 'up:' + mock.upload : mock.source),+    set:(v) => pickSource(v),+    fineTune:(box) => {+      ftChk(box, 'Crop to fill instead of letterboxing', mock.cover, (on) => { mock.cover = on; apply(); });+      ftSeg(box, 'Window frame', [+        { v:'auto',  label:'Auto' },+        { v:'fake',  label:'Drawn title bar' },+        { v:'none',  label:'No title bar' },+        { v:'bare',  label:'Bare image' },+      ], mock.frame, (v) => { mock.frame = v; apply(); });+      ftChk(box, 'Use this image as the entire hero', mock.full, (on) => {+        if (on && mock.source === 'mock') { toast('Upload or pull an image first; the drawn mock can\'t be the whole hero.'); renderDock(); return; }+        mock.full = on; apply();+        toast(on ? 'In the real app this disables all overlays and re-stamps provenance.' : 'Back to the composed hero.');+      });+    },+  },+  {+    id:'placement', title:'Placement',+    hint:'Where the screenshot sits. Arrow keys work too.',+    summary:() => LAYOUTS.find(l => l.v === mock.layout).label,+    options:() => LAYOUTS.map(l => ({ v:l.v, label:l.label, thumb:layoutThumb(l.v) })),+    get:() => mock.layout,+    set:(v) => { mock.layout = v; },+  },+  {+    id:'background', title:'Background',+    hint:'The scene behind everything.',+    summary:() => THEMES.find(t => t.v === mock.theme).label,+    options:() => THEMES.map(t => ({ v:t.v, label:t.label, sw:t.sw })),+    get:() => mock.theme,+    set:(v) => {+      mock.theme = v;+      if (v === 'image' && !heroBg) { $('file-bg').click(); }+    },+    fineTune:(box) => {+      ftSlider(box, 'Angle', 0, 360, 5, mock.gradAngle, (v) => { mock.gradAngle = v; apply(); });+      if (mock.theme === 'circuitry') {+        ftBtn(box, 'Reroll the traces', () => { mock.seed = (mock.seed * 16807) % 2147483647; apply(); });+      }+      if (mock.theme === 'image') {+        ftBtn(box, 'Import background art', () => $('file-bg').click());+      }+      if (pageOf().owner !== 'adom') {+        const g = ftGroup(box, 'Owner brand (' + pageOf().owner + ' brings their own colors)');+        ftColor(g, 'Accent', mock.brand.accent || '#00e6dc', (v) => { mock.brand.accent = v; apply(); });+        ftColor(g, 'Background', mock.brand.bg || '#0d1117', (v) => { mock.brand.bg = v; apply(); });+        ftBtn(g, 'Use the studio palette', () => { mock.brand = { accent:'', bg:'' }; apply(); renderDock(); });+      }+    },+  },+  {+    id:'headline', title:'Headline and text',+    hint:'The words in your hero. Fewer words read bigger.',+    summary:() => (mock.headline || pageOf().title),+    custom:(host) => {+      const wrap = document.createElement('div');+      wrap.style.cssText = 'flex:1;display:flex;gap:22px;align-items:center;justify-content:center;flex-wrap:wrap';+      const left = document.createElement('div');+      left.style.cssText = 'display:flex;flex-direction:column;gap:8px';+      const lab = document.createElement('div');+      lab.style.cssText = 'font-size:11px;color:var(--text-2);letter-spacing:.06em;text-transform:uppercase;font-family:Familjen Grotesk';+      lab.textContent = 'Headline';+      const inp = document.createElement('input');+      inp.type = 'text'; inp.className = 'hin'; inp.placeholder = pageOf().title + ' (the page title)'; inp.value = mock.headline;+      inp.addEventListener('input', () => { mock.headline = inp.value; apply(); });+      const fitb = document.createElement('button');+      fitb.className = 'btn small'; fitb.textContent = 'Make it as big as fits';+      fitb.addEventListener('click', () => { fitTitle(); });+      const slab = document.createElement('div');+      slab.style.cssText = lab.style.cssText; slab.style.marginTop = '6px';+      slab.textContent = 'Text under it';+      const sinp = document.createElement('input');+      sinp.type = 'text'; sinp.className = 'hin';+      sinp.placeholder = pageOf().brief.split('.')[0] + '.';+      sinp.value = mock.subtitle;+      const glab = document.createElement('div');+      glab.style.cssText = lab.style.cssText; glab.style.marginTop = '6px';+      glab.textContent = 'Tap words to color them';+      const pick = document.createElement('div');+      pick.className = 'wordpick';+      const renderPick = () => {+        pick.textContent = '';+        const on = new Set(mock.accentWords);+        subtitleWords().forEach((w, i) => {+          const t = document.createElement('span');+          t.className = 'wtok' + (on.has(i) ? ' on' : '');+          t.textContent = w;+          t.addEventListener('click', () => {+            const set = new Set(mock.accentWords);+            set.has(i) ? set.delete(i) : set.add(i);+            mock.accentWords = [...set].sort((a, b) => a - b);+            renderPick(); apply();+          });+          pick.appendChild(t);+        });+      };+      sinp.addEventListener('input', () => {+        mock.subtitle = sinp.value;+        const n = subtitleWords().length;+        mock.accentWords = mock.accentWords.filter((i) => i < n);+        renderPick(); apply();+      });+      renderPick();+      left.append(lab, inp, fitb, slab, sinp, glab, pick);+      wrap.appendChild(left);+      host.appendChild(wrap);+    },+    fineTune:(box) => {+      const g = ftGroup(box, 'Show');+      ftChk(g, 'Adom wordmark',      mock.show.logo,        (on) => { mock.show.logo = on; apply(); });+      ftChk(g, 'Title',              mock.show.title,       (on) => { mock.show.title = on; apply(); });+      ftChk(g, 'Value-prop subtitle', mock.show.subtitle,   (on) => { mock.show.subtitle = on; apply(); });+      ftChk(g, 'Detail line',        mock.show.description, (on) => { mock.show.description = on; apply(); });+      const s = ftGroup(box, 'Sizes');+      ftSlider(s, 'Title',    5,   13,  0.1,  mock.fs.title,   (v) => { mock.fs.title = v; apply(); });+      ftSlider(s, 'Subtitle', 1.8, 3.5, 0.1,  mock.fs.sub,     (v) => { mock.fs.sub = v; apply(); });+      ftSlider(s, 'Detail',   1.0, 2.0, 0.05, mock.fs.desc,    (v) => { mock.fs.desc = v; apply(); });+      ftSlider(s, 'Gap',      0.5, 2,   0.05, mock.fs.gap,     (v) => { mock.fs.gap = v; apply(); });+    },+  },+  {+    id:'finishing', title:'Finishing',+    hint:'The small marks in the corners.',+    summary:() => [mock.pill ? 'pill' : null, mock.slug ? 'slug' : null].filter(Boolean).join(' + ') || 'none',+    toggles:() => [+      { label:'Type pill, top right',    sub:'APP, SKILL or COMPONENT', on:mock.pill, flip:() => { mock.pill = !mock.pill; } },+      { label:'Wiki slug, bottom right', sub:'owner/slug in mono',      on:mock.slug, flip:() => { mock.slug = !mock.slug; } },+    ],+  },+  {+    id:'review', title:'Review and generate',+    hint:'See it everywhere it will show up, then make it real.',+    summary:() => allConfirmed() ? 'ready' : 'pending',+    review:true,+  },+];++const stepIndex = (id) => STEPS.findIndex(s => s.id === id);+const activeStep = () => STEPS[stepIndex(mock.step)];+const allConfirmed = () => Object.values(mock.confirm).every(Boolean) && !Object.values(mock.flags).some(Boolean);++/* ---------- fine tune builders ---------- */+function ftGroup(box, label) {+  const g = document.createElement('div');+  g.className = 'ft-group';+  const l = document.createElement('div');+  l.className = 'ft-label'; l.textContent = label;+  g.appendChild(l);+  box.appendChild(g);+  return g;+}+function ftChk(box, label, on, fn) {+  const l = document.createElement('label');+  l.className = 'chk';+  const c = document.createElement('input');+  c.type = 'checkbox'; c.checked = on;+  c.addEventListener('change', () => fn(c.checked));+  l.append(c, document.createTextNode(label));+  box.appendChild(l);+}+function ftSeg(box, label, options, value, fn) {+  const g = ftGroup(box, label);+  const seg = document.createElement('div');+  seg.className = 'seg';+  options.forEach(o => {+    const b = document.createElement('button');+    b.textContent = o.label;+    if (o.v === value) b.classList.add('on');+    b.addEventListener('click', () => { fn(o.v); renderDock(); });+    seg.appendChild(b);+  });+  g.appendChild(seg);+}+function ftSlider(box, label, min, max, step, value, fn) {+  const row = document.createElement('div');+  row.className = 'sl';+  const l = document.createElement('label'); l.textContent = label;+  const r = document.createElement('input');+  r.type = 'range'; r.min = min; r.max = max; r.step = step; r.value = value;+  const v = document.createElement('span'); v.className = 'val'; v.textContent = value;+  r.addEventListener('input', () => { v.textContent = r.value; fn(parseFloat(r.value)); });+  row.append(l, r, v);+  box.appendChild(row);+}+function ftBtn(box, label, fn) {+  const b = document.createElement('button');+  b.className = 'btn small'; b.textContent = label;+  b.addEventListener('click', fn);+  box.appendChild(b);+}+function ftColor(box, label, value, fn) {+  const row = document.createElement('div');+  row.className = 'brandrow';+  const l = document.createElement('span'); l.textContent = label;+  const c = document.createElement('input');+  c.type = 'color'; c.value = value;+  c.addEventListener('input', () => fn(c.value));+  row.append(l, c);+  box.appendChild(row);+}++/* ---------- dock rendering ---------- */+function card(opt, on, pick) {+  const c = document.createElement('div');+  c.className = 'card' + (on ? ' on' : '');+  if (opt.thumb) {+    const t = document.createElement('div');+    t.className = 'thumb'; t.innerHTML = opt.thumb;+    c.appendChild(t);+  } else if (opt.sw) {+    const t = document.createElement('div');+    t.className = 'swx ' + opt.sw;+    c.appendChild(t);+  }+  const l = document.createElement('div');+  l.className = 'lbl'; l.textContent = opt.label;+  c.appendChild(l);+  if (opt.sub) {+    const s = document.createElement('div');+    s.className = 'sub'; s.textContent = opt.sub;+    c.appendChild(s);+  }+  c.addEventListener('click', pick);+  return c;+}++function renderRail() {+  const rail = $('steprail');+  rail.textContent = '';+  STEPS.forEach((s, i) => {+    const chip = document.createElement('div');+    const done = s.review ? allConfirmed() : i < stepIndex(mock.step);+    chip.className = 'schip' + (s.id === mock.step ? ' active' : '') + (done ? ' done' : '');+    const n = document.createElement('span');+    n.className = 'n';+    n.innerHTML = done && s.id !== mock.step ? '<svg viewBox="0 0 24 24"><polyline points="5 13 10 18 19 7"/></svg>' : String(i + 1);+    const t = document.createElement('span');+    t.className = 't'; t.textContent = s.title;+    chip.append(n, t);+    const sum = s.summary();+    if (sum && s.id !== mock.step) {+      const v = document.createElement('span');+      v.className = 'v'; v.textContent = sum;+      chip.appendChild(v);+    }+    chip.addEventListener('click', () => goTo(s.id));+    rail.appendChild(chip);+  });+  fitRail();+  const act = rail.querySelector('.schip.active');+  if (act) act.scrollIntoView({ inline:'center', block:'nearest' });+}++/* The rail must never CLIP (hero-studio wizard feedback 2026-08-12): degrade+   until it fits, and when it still scrolls, SAY so (edge fades + wheel). */+function fitRail() {+  const rail = $('steprail');+  const wrap = document.getElementById('railwrap');+  rail.classList.remove('compact', 'mini');+  if (rail.scrollWidth > rail.clientWidth + 1) rail.classList.add('compact');+  if (rail.scrollWidth > rail.clientWidth + 1) rail.classList.add('mini');+  const upd = () => {+    wrap.classList.toggle('can-left', rail.scrollLeft > 2);+    wrap.classList.toggle('can-right', rail.scrollLeft + rail.clientWidth < rail.scrollWidth - 2);+  };+  upd();+  rail.onscroll = upd;+}+window.addEventListener('resize', fitRail);+// web fonts change chip widths after the first measure; re-fit at ready so an+// early over-collapse (or overflow) never sticks+if (document.fonts && document.fonts.ready) document.fonts.ready.then(fitRail);+document.getElementById('steprail').addEventListener('wheel', (e) => {+  const rail = e.currentTarget;+  if (!e.deltaX && e.deltaY && rail.scrollWidth > rail.clientWidth) {+    rail.scrollLeft += e.deltaY;+    e.preventDefault();+  }+}, { passive: false });++// what each step edits, for the on-stage cue ring (Colby 2026-08-13)+const EDIT_TARGETS = {+  showcase:  () => mock.full ? $('hero') : hero.querySelector('.window'),+  placement: () => hero.querySelector('.window'),+  background:() => $('hero'),+  headline:  () => hero.querySelector('.textzone'),+  finishing: () => $('pill-tr'),+};+function positionEditRing() {+  const ring = $('editring');+  const getT = EDIT_TARGETS[mock.step];+  const t = getT && getT();+  if (!t || !t.offsetParent && t !== $('hero')) { ring.classList.remove('on'); return; }+  const hb = $('hero').getBoundingClientRect();+  const tb = t.getBoundingClientRect();+  const pad = t === $('hero') ? -8 : 8;+  ring.style.left = (tb.left - hb.left - pad) + 'px';+  ring.style.top = (tb.top - hb.top - pad) + 'px';+  ring.style.width = (tb.width + pad * 2) + 'px';+  ring.style.height = (tb.height + pad * 2) + 'px';+  ring.classList.add('on');+}+window.addEventListener('resize', () => positionEditRing());++function renderDock() {+  const s = activeStep();+  renderRail();+  requestAnimationFrame(positionEditRing);+  $('d-title').textContent = (stepIndex(mock.step) + 1) + '. ' + s.title;+  $('d-hint').textContent = s.hint;+  const next = STEPS[stepIndex(mock.step) + 1];+  $('btn-next').textContent = next ? 'Next: ' + next.title : 'Done';+  $('btn-next').style.display = next && !s.review ? '' : 'none';+  const gbtn = $('btn-generate');+  gbtn.style.display = s.review ? '' : 'none';+  if (s.review) {+    gbtn.disabled = !allConfirmed();+    gbtn.title = gbtn.disabled ? 'Confirm the four checks first' : '';+  }+  $('btn-finetune').style.display = s.fineTune ? '' : 'none';++  const cards = $('cards');+  cards.textContent = '';+  const showArrows = !!s.options;+  $('arr-l').style.visibility = showArrows ? 'visible' : 'hidden';+  $('arr-r').style.visibility = showArrows ? 'visible' : 'hidden';++  if (s.options) {+    const opts = s.options();+    const cur = s.get();+    const idx = opts.findIndex(o => o.v === cur);+    $('d-count').textContent = (idx + 1) + ' of ' + opts.length;+    opts.forEach(o => cards.appendChild(card(o, o.v === cur, () => { s.set(o.v); apply(); renderDock(); })));+    const on = cards.querySelector('.card.on');+    if (on) on.scrollIntoView({ inline:'center', block:'nearest' });+  } else if (s.toggles) {+    $('d-count').textContent = '';+    s.toggles().forEach(t => {+      const c = card({ v:t.label, label:t.label, sub:t.sub }, t.on, () => { t.flip(); apply(); renderDock(); });+      c.classList.add('wide');+      cards.appendChild(c);+    });+  } else if (s.custom) {+    $('d-count').textContent = '';+    s.custom(cards);+  } else if (s.review) {+    $('d-count').textContent = '';+    renderReview(cards);+  }++  const ft = $('finetune');+  ft.textContent = '';+  ft.classList.toggle('open', mock.finetuneOpen && !!s.fineTune);+  $('btn-finetune').classList.toggle('btn-primary', mock.finetuneOpen && !!s.fineTune);+  if (mock.finetuneOpen && s.fineTune) s.fineTune(ft);+}++/* ---------- review step ---------- */+const CHECKS = [+  { key:'title',    q:'Title reads right' },+  { key:'subtitle', q:'The line under the title reads right' },+  { key:'showcase', q:'The screenshot shows the right thing' },+  { key:'type',     q:'Page type matches (this page says ' + '"app")' },+];+function renderReview(host) {+  const wrap = document.createElement('div');+  wrap.className = 'review';++  const views = document.createElement('div');+  views.className = 'rv-col';+  const vlab = document.createElement('div');+  vlab.className = 'ft-label'; vlab.textContent = 'See it as';+  const seg = document.createElement('div');+  seg.className = 'seg';+  [ { v:'png',  label:'Exactly my PNG' },+    { v:'mq',   label:'Homepage preview' },+    { v:'grid', label:'Wiki grid' },+    { v:'desk', label:'Desktop bar' },+  ].forEach(o => {+    const b = document.createElement('button');+    b.textContent = o.label;+    if (mock.view === o.v) b.classList.add('on');+    b.addEventListener('click', () => { setView(o.v); renderDock(); });+    seg.appendChild(b);+  });+  const legend = document.createElement('div');+  legend.className = 'lint-item lint-warn';+  legend.textContent = 'Amber-outlined = the homepage adds it, it is not in your PNG. Press D to flip.';+  views.append(vlab, seg, legend);++  const checks = document.createElement('div');+  checks.className = 'rv-col';+  if (mock.gatesDone >= 4 && !mock.rvExpanded) {+    // Seasoned reviewer (#45): the steps were walked with eyes on the live+    // preview; the four boxes are ceremony by now. Pre-confirm, one Generate,+    // full checklist one click away. Hard-fail lints would still lock.+    Object.keys(mock.confirm).forEach((k) => { if (!mock.flags[k]) mock.confirm[k] = true; });+    const row = document.createElement('div');+    row.className = 'rv-check ok';+    const q = document.createElement('span');+    q.className = 'q';+    q.textContent = 'You\'ve done this ' + mock.gatesDone + ' times, so the checks are pre-ticked. You saw every step on the way here.';+    const ex = document.createElement('button');+    ex.className = 'btn small';+    ex.textContent = 'Check one by one';+    ex.addEventListener('click', () => { mock.rvExpanded = true; Object.keys(mock.confirm).forEach((k) => { mock.confirm[k] = false; }); renderDock(); });+    row.append(q, ex);+    checks.appendChild(row);+  } else {+    const allb = document.createElement('button');+    allb.className = 'btn small';+    allb.textContent = 'All of it looks good';+    allb.addEventListener('click', () => {+      Object.keys(mock.confirm).forEach((k) => { mock.confirm[k] = true; mock.flags[k] = false; });+      renderDock();+    });+    checks.appendChild(allb);+    CHECKS.forEach(c => {+    const row = document.createElement('div');+    row.className = 'rv-check' + (mock.confirm[c.key] ? ' ok' : '') + (mock.flags[c.key] ? ' flag' : '');+    const q = document.createElement('span');+    q.className = 'q'; q.textContent = c.q;+    const ok = document.createElement('button');+    ok.className = 'btn small' + (mock.confirm[c.key] ? ' okon' : '');+    ok.textContent = 'Looks good';+    ok.addEventListener('click', () => { mock.confirm[c.key] = true; mock.flags[c.key] = false; renderDock(); });+    const fl = document.createElement('button');+    fl.className = 'btn small' + (mock.flags[c.key] ? ' flagon' : '');+    fl.textContent = 'Needs fixing';+    fl.addEventListener('click', () => { mock.flags[c.key] = true; mock.confirm[c.key] = false; renderDock(); toast('In the real app the driving AI sees this flag and adjusts.'); });+    row.append(q, ok, fl);+    checks.appendChild(row);+  });+  }++  const act = document.createElement('div');+  act.className = 'rv-col';+  const lint = document.createElement('div');+  lint.innerHTML = '<div class="ft-label">Layout lint</div>'+    + '<div class="lint-item lint-ok">No layout findings. The studio measures; you decide.</div>';+  act.append(lint);++  wrap.append(views, checks, act);+  host.appendChild(wrap);+}++function fireConfetti(fromEl) {+  const c = document.createElement('canvas');+  c.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:9999';+  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,+    col: colors[i % colors.length],+  }));+  let t = 0;+  (function tick() {+    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;+      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.restore();+    }+    if (t < 95) requestAnimationFrame(tick);+    else c.remove();+  })();+}+$('btn-generate').addEventListener('click', () => {+  const btn = $('btn-generate');+  if (btn.disabled) return;+  fireConfetti(btn);+  toast('In the real app this renders the 2000x1250 PNG with the provenance stamp.', 'success');+});++/* ---------- state -> hero ---------- */+let shotData = '';+let heroBg = '';+function pickSource(v) {+  if (v.startsWith('up:')) {+    mock.upload = Number(v.slice(3));+    mock.source = 'upload';+    shotData = mock.uploads[mock.upload].data;+    return;+  }+  mock.source = v;+  if (v === 'upload') { $('file-shot').click(); return; }+  if (v === 'page') {+    shotData = '../generated/latest.png';+    toast('In the real app this pulls the screenshot from the wiki page files.');+  }+  if (v === 'mock') { shotData = ''; mock.full = false; }+}+$('file-shot').addEventListener('change', () => {+  const files = [...$('file-shot').files];+  if (!files.length) return;+  let left = files.length;+  files.forEach((f) => {+    const r = new FileReader();+    r.onload = () => {+      mock.uploads.push({ name: f.name, data: r.result });+      if (--left === 0) {+        mock.upload = mock.uploads.length - 1;+        mock.source = 'upload';+        shotData = mock.uploads[mock.upload].data;+        apply(); renderDock();+      }+    };+    r.readAsDataURL(f);+  });+});+$('file-bg').addEventListener('change', () => {+  const f = $('file-bg').files[0];+  if (!f) return;+  const r = new FileReader();+  r.onload = () => { heroBg = r.result; mock.theme = 'image'; apply(); renderDock(); };+  r.readAsDataURL(f);+});++function subtitleText() {+  const p = pageOf();+  return (mock.subtitle || (p.brief.split('.')[0] + '.')).trim();+}+function subtitleWords() {+  return subtitleText().split(/\s+/).filter(Boolean);+}++function apply() {+  const hero = $('hero');+  const p = pageOf();+  const ir = document.getElementById('intro-ref');+  if (ir) ir.textContent = p.key;++  hero.dataset.theme = mock.theme;+  hero.dataset.layout = mock.layout;+  hero.style.setProperty('--ga', mock.gradAngle + 'deg');+  hero.style.setProperty('--fs-title', mock.fs.title + 'cqw');+  hero.style.setProperty('--fs-sub', mock.fs.sub + 'cqw');+  hero.style.setProperty('--fs-desc', mock.fs.desc + 'cqw');+  hero.style.setProperty('--fs-gap', mock.fs.gap);+  hero.style.setProperty('--bgimg', heroBg ? 'url(' + heroBg + ')' : 'none');++  $('el-title').textContent = mock.headline || p.title;+  $('el-sub').innerHTML = '';+  const words = subtitleWords();+  const on = new Set(mock.accentWords);+  let run = null;+  words.forEach((w, i) => {+    const sel = on.has(i);+    if (sel && run && run.dataset.acc === '1') { run.textContent += ' ' + w; return; }+    if (!sel && run && run.dataset.acc === '0') { run.textContent += ' ' + w; return; }+    if (run) $('el-sub').appendChild(document.createTextNode(' '));+    run = document.createElement('span');+    run.dataset.acc = sel ? '1' : '0';+    if (sel) run.className = 'accent';+    run.textContent = w;+    $('el-sub').appendChild(run);+  });+  $('el-desc').textContent = 'Part of the Adom platform.';+  $('win-label').textContent = p.key.split('/')[1];+  $('l-slug').textContent = 'wiki.adom.inc/' + p.key;+  $('l-pill').textContent = p.type.toUpperCase();+  $('l-pill').className = 'pill ' + p.type;+  $('subj-title').textContent = p.title;+  $('subj-meta').textContent = p.key + ' | ' + p.type + ' | score ' + p.score + ' | ' + p.stars + ' stars';++  $('el-logo').classList.toggle('noadom', p.owner !== 'adom');+  hero.classList.toggle('hide-logo', !mock.show.logo);+  hero.classList.toggle('hide-title', !mock.show.title || mock.titleMode === 'overlay-only');+  hero.classList.toggle('hide-sub', !mock.show.subtitle);+  hero.classList.toggle('hide-desc', !mock.show.description);++  hero.classList.toggle('has-vitrine', !!shotData && mock.source !== 'mock');+  $('shot-img').src = shotData || '';+  hero.classList.toggle('vitrine-cover', mock.cover);+  const hasImport = !!shotData && mock.source !== 'mock';+  const frame = mock.frame === 'auto' ? (hasImport ? 'none' : 'fake') : mock.frame;+  hero.classList.toggle('no-chrome', frame === 'none');+  hero.classList.toggle('bare-chrome', frame === 'bare');++  $('pill-tr').style.display = mock.pill ? '' : 'none';+  $('slug-br').style.display = mock.slug ? '' : 'none';+  hero.classList.toggle('guides', mock.guides);+  hero.classList.toggle('show-mq', mock.mq || mock.view === 'mq');++  hero.classList.toggle('branded', !!(mock.brand.accent || mock.brand.bg));+  hero.style.setProperty('--brand-accent', mock.brand.accent || '#00e6dc');+  hero.style.setProperty('--brand-bg', mock.brand.bg || '#0d1117');++  $('mqo-badges').innerHTML = '<span class="mqo-stat">score ' + p.score + '</span><span class="mqo-stat">' + p.stars + ' stars</span>';+  $('mqo-title').textContent = p.title;+  $('mqo-brief').textContent = p.brief;+  $('mqo-owner').textContent = p.key;++  if (mock.view !== 'png' && mock.view !== 'mq') buildCtx();+  fitHero();+  if (mock.theme === 'circuitry') drawCircuitry();+}++/* ---------- review context views ---------- */+function setView(v) {+  mock.view = v;+  const ctx = v === 'grid' || v === 'desk';+  $('stage').classList.toggle('ctxmode', ctx);+  $('ctxview').classList.toggle('show', ctx);+  apply();+}+function cloneHero() {+  const c = $('hero').cloneNode(true);+  c.removeAttribute('id');+  c.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));+  c.classList.remove('show-mq', 'guides');+  c.style.width = '100%';+  const w = document.createElement('div');+  w.className = 'clonewrap';+  w.appendChild(c);+  return w;+}+function buildCtx() {+  const host = $('ctxview');+  host.textContent = '';+  const p = pageOf();+  if (mock.view === 'grid') {+    const frame = document.createElement('div');+    frame.className = 'ctxframe';+    const grid = document.createElement('div');+    grid.className = 'page-grid';+    const mine = document.createElement('div');+    mine.className = 'page-card';+    const mh = document.createElement('div');+    mh.className = 'page-card-hero';+    mh.appendChild(cloneHero());+    const mb = document.createElement('div');+    mb.className = 'page-card-body';+    mb.innerHTML = '<h3 class="page-card-title">' + p.title + '</h3><p class="page-card-brief">' + p.brief + '</p><div class="page-card-meta">' + p.installs + ' installs</div>';+    mine.append(mh, mb);+    grid.appendChild(mine);+    [['Parts Search','#0f766e'], ['KiCad Tour','#7c2d8f'], ['Shotlog','#1d4ed8']].forEach(([t, c]) => {+      const pc = document.createElement('div');+      pc.className = 'page-card';+      pc.innerHTML = '<div class="page-card-hero"><div class="ts-hero-fallback" style="background:linear-gradient(135deg,' + c + ',#0d1117)">' + t + '</div></div>'+        + '<div class="page-card-body"><h3 class="page-card-title">' + t + '</h3><p class="page-card-brief">Another page on the index grid.</p></div>';+      grid.appendChild(pc);+    });+    frame.appendChild(grid);+    host.appendChild(frame);+  }+  if (mock.view === 'desk') {+    const d = document.createElement('div');+    d.className = 'deskmock';+    d.innerHTML = '<div class="deskhead">Trending on the wiki</div>';+    d.appendChild(cloneHero());+    host.appendChild(d);+  }+}++/* ---------- circuitry ---------- */+function drawCircuitry() {+  const cv = $('circuitcanvas');+  const hero = $('hero');+  cv.width = hero.clientWidth * 2;+  cv.height = hero.clientHeight * 2;+  const g = cv.getContext('2d');+  g.clearRect(0, 0, cv.width, cv.height);+  let s = mock.seed;+  const rnd = () => { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; };+  g.lineWidth = 2;+  for (let i = 0; i < 26; i++) {+    let x = rnd() * cv.width, y = rnd() * cv.height;+    g.strokeStyle = 'rgba(0,184,177,' + (0.05 + rnd() * 0.12) + ')';+    g.beginPath(); g.moveTo(x, y);+    for (let k = 0; k < 4; k++) {+      const len = 40 + rnd() * 220;+      const dir = Math.floor(rnd() * 4);+      if (dir === 0) x += len; else if (dir === 1) x -= len; else if (dir === 2) y += len; else y -= len;+      g.lineTo(x, y);+    }+    g.stroke();+    g.fillStyle = 'rgba(0,230,220,.25)';+    g.beginPath(); g.arc(x, y, 4, 0, 7); g.fill();+  }+}++/* ---------- sizing ---------- */+function fitHero() {+  const stage = $('stage');+  const hero = $('hero');+  const availW = stage.clientWidth - 44;+  const availH = stage.clientHeight - 32;+  hero.style.width = Math.min(1100, availW, availH * 1.6) + 'px';+}+window.addEventListener('resize', () => { fitHero(); if (mock.theme === 'circuitry') drawCircuitry(); });+new ResizeObserver(() => { fitHero(); if (mock.theme === 'circuitry') drawCircuitry(); }).observe($('dock'));++/* ---------- navigation ---------- */+function goTo(id) {+  mock.step = id;+  mock.finetuneOpen = false;+  if (id !== 'review' && (mock.view === 'grid' || mock.view === 'desk')) setView('png');+  renderDock();+  apply();+}+function cycle(dir) {+  const s = activeStep();+  if (!s.options) return;+  const opts = s.options();+  const idx = opts.findIndex(o => o.v === s.get());+  const nx = (idx + dir + opts.length) % opts.length;+  s.set(opts[nx].v);+  apply();+  renderDock();+}+$('arr-l').addEventListener('click', () => cycle(-1));+$('arr-r').addEventListener('click', () => cycle(1));+$('btn-next').addEventListener('click', () => {+  const i = stepIndex(mock.step);+  if (i < STEPS.length - 1) goTo(STEPS[i + 1].id);+});+$('btn-finetune').addEventListener('click', () => { mock.finetuneOpen = !mock.finetuneOpen; renderDock(); });+$('btn-guides').addEventListener('click', () => { mock.guides = !mock.guides; $('btn-guides').classList.toggle('on', mock.guides); apply(); });+$('btn-rand').addEventListener('click', () => {+  mock.layout = LAYOUTS[Math.floor(Math.random() * LAYOUTS.length)].v;+  const th = THEMES.filter(t => t.v !== 'image');+  mock.theme = th[Math.floor(Math.random() * th.length)].v;+  apply(); renderDock();+});++document.addEventListener('keydown', (e) => {+  if (e.target.closest('input,textarea,select')) return;+  if (e.key === 'ArrowLeft') { cycle(-1); e.preventDefault(); }+  else if (e.key === 'ArrowRight') { cycle(1); e.preventDefault(); }+  else if (e.key === 'Enter' && !e.shiftKey) { $('btn-next').click(); }+  else if (e.key === 'Enter' && e.shiftKey) { const i = stepIndex(mock.step); if (i > 0) goTo(STEPS[i - 1].id); }+  else if (e.key >= '1' && e.key <= String(STEPS.length)) { goTo(STEPS[Number(e.key) - 1].id); }+  else if (e.key === 'd' || e.key === 'D') { mock.mq = !mock.mq; apply(); }+});++/* ---------- headline fit ---------- */+function fitTitle() {+  const el = $('el-title');+  const lh = () => parseFloat(getComputedStyle(el).lineHeight);+  let size = 13;+  mock.fs.title = size; apply();+  while (size > 5 && el.scrollHeight > lh() * 3.2) {+    size -= 0.2;+    mock.fs.title = Math.round(size * 10) / 10;+    apply();+  }+  toast('Fitted: ' + mock.fs.title + ' is the biggest the title can be here.');+  renderDock();+}++/* ---------- boot ---------- */+$('shot-img').addEventListener('error', () => {+  if (mock.source === 'page') { shotData = ''; mock.source = 'mock'; apply(); renderDock(); toast('No page screenshot in this mockup; back to the drawn mock.'); }+});+const q = new URLSearchParams(location.search);+if (q.get('page')) mock.page = q.get('page');+if (q.get('gates')) mock.gatesDone = Number(q.get('gates')) || 0;+mock.titleMode = 'image-only';  // composed heroes always bake the title (#43/#80); the field survives on the wire only+if (q.get('layout')) mock.layout = q.get('layout');+if (q.get('theme')) mock.theme = q.get('theme');+if (q.get('step')) mock.step = q.get('step');+if (q.get('ft')) mock.finetuneOpen = true;+if (q.get('mq')) mock.mq = true;+if (q.get('confirm')) { Object.keys(mock.confirm).forEach(k => mock.confirm[k] = true); }+if (q.get('view')) setView(q.get('view')); else apply();+renderDock();+</script>+</body>+</html>
public/studio.cssadded+359
@@ -0,0 +1,359 @@+/* studio.css: the app chrome around the hero (header, panel, gate bars,+   modal, toasts, tooltips). Redesign work happens here and only here. */++++/* ---------- 44px header, three zones ---------- */+header {+  height:44px; flex:0 0 44px; display:flex; align-items:center; gap:12px;+  padding:0 12px; background:var(--surface); border-bottom:1px solid var(--border);+}++.brand { display:flex; align-items:center; gap:8px; white-space:nowrap; }++.brand svg { width:16px; height:16px; stroke:var(--text); }++.brand .name { font-family:'Familjen Grotesk'; font-weight:500; font-size:14px; color:var(--accent); }++.brand .sub { color:var(--text-2); font-size:13px; }++.vsep { width:1px; height:22px; background:var(--border); flex:0 0 1px; }++.subject { display:flex; align-items:baseline; gap:10px; min-width:0; }++.subject b { font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }++.subject .meta { font-family:'JetBrains Mono',monospace; font-size:11.5px; color:var(--text-2); white-space:nowrap; }++.actions { margin-left:auto; display:flex; gap:6px; }++.iconbtn {+  width:30px; height:30px; border:1px solid var(--border); border-radius:8px; background:transparent;+  display:flex; align-items:center; justify-content:center; cursor:pointer; transition:all .15s ease;+}++.iconbtn svg { width:16px; height:16px; stroke:var(--text); fill:none; stroke-width:1.8; }++.iconbtn:hover { background:var(--elevated); border-color:var(--text-3); }++.iconbtn.on { border-color:var(--accent); background:rgba(0,184,176,.08); }++@media (max-width:720px){ .subject .meta { display:none; } }+++/* ---------- body ---------- */+main { flex:1; display:flex; min-height:0; }++#panel {+  flex:0 0 264px; overflow-y:auto; background:var(--surface); border-right:1px solid var(--border);+  padding:14px 14px 40px;+}++#panel h3 {+  font-family:'Familjen Grotesk'; font-weight:500; font-size:11px; letter-spacing:.08em;+  text-transform:uppercase; color:var(--text-2); margin:18px 0 8px;+}++#panel h3:first-child { margin-top:2px; }++select {+  width:100%; background:var(--elevated); color:var(--text); border:1px solid var(--border);+  border-radius:8px; padding:7px 8px; font-family:'Satoshi'; font-size:13px; cursor:pointer;+}++.optgrid { display:flex; flex-wrap:wrap; gap:6px; }++.opt {+  border:1px solid var(--border); background:var(--elevated); color:var(--text-2); border-radius:8px;+  padding:6px 9px; font-size:12.5px; cursor:pointer; transition:all .15s ease; font-family:'Satoshi';+}++.opt:hover { color:var(--text); border-color:var(--text-3); }++.opt.on { color:var(--text); border-color:var(--accent); background:rgba(0,184,176,.10); }++.swatches { display:flex; gap:6px; }++.swatch {+  width:38px; height:26px; border-radius:6px; border:1px solid var(--border); cursor:pointer;+  transition:all .15s ease; position:relative;+}++.swatch.on { outline:2px solid var(--accent); outline-offset:1px; }++.sw-midnight { background:linear-gradient(155deg,#0a0e14,#0d1117 45%,#071318); }++.sw-image { background:repeating-conic-gradient(#1c2530 0 25%, #0d1117 0 50%) 0 0/8px 8px; }++.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); }++input[type=range] { width:100%; accent-color:var(--accent); cursor:pointer; }++label.chk { display:flex; align-items:center; gap:8px; padding:5px 0; cursor:pointer; color:var(--text); font-size:13px; }++label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:pointer; }++.hint { font-size:11.5px; color:var(--text-2); line-height:1.45; margin-top:6px; }++.btn {+  width:100%; margin-top:8px; padding:8px 10px; border-radius:8px; border:1px solid var(--border);+  background:var(--elevated); color:var(--text); font-family:'Satoshi'; font-size:13px; cursor:pointer;+  transition:all .15s ease;+}++.btn:hover { border-color:var(--accent); background:rgba(0,184,176,.08); }+++/* ---------- stage + hero canvas ---------- */+#stage { flex:1; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; gap:18px; padding:22px; min-width:0; overflow:auto; }++/* the stagebar (#21/#23): stage chrome lives ABOVE the stage, never on it */+#stagebar { display:none; align-items:center; gap:10px; margin:10px 0 8px; }++#stagebar.on { display:flex; }++#stagebar .seg { display:flex; border:1px solid #30363d; border-radius:8px; overflow:hidden; }++#stagebar .seg button { background:#0d1117; color:var(--text); border:0; padding:6px 12px; font-size:12.5px; cursor:pointer; }++#stagebar .seg button.on { background:rgba(0,184,176,.18); color:var(--accent-bright); }++#stagebar .legend { font-size:12px; color:#8b949e; line-height:1.35; }++#stagebar .legend b { color:#e8a13c; font-weight:600; }++.brandrow { display:flex; align-items:center; gap:8px; margin:6px 0; }++.brandrow label { font-size:12px; color:var(--text-2); width:84px; }++.brandrow input[type=color] { width:44px; height:26px; border:1px solid var(--border); border-radius:6px; background:var(--surface); padding:1px; cursor:pointer; }+++#approvedbar { display:none; margin:0 0 14px; border:1px solid #2ea06b; background:rgba(46,160,107,.09);+  border-radius:10px; padding:14px 16px; }++#approvedbar .ab-head { font-family:'Familjen Grotesk'; font-weight:700; color:#4ade80; margin-bottom:6px; }++#approvedbar .ab-body { font-size:13px; color:var(--text); line-height:1.5; margin-bottom:10px; }++#genmodal .gen-approved { font-family:'Familjen Grotesk'; font-weight:700; color:#4ade80; margin-bottom:6px; }++#verifybar { display:none; margin:0 0 14px; border:1px solid var(--accent); background:rgba(0,184,176,.07); border-radius:10px; padding:14px 16px; }++#verifybar .vhead { font-size:13px; color:var(--text); margin-bottom:10px; line-height:1.5; }++#verifybar .vchk { display:block; margin:6px 0; }++#verifybar .vchk .warn { color:#e8a13c; }++#verifybar .vchk .flagged { color:#f85149; font-weight:600; margin-left:6px; }++#v-generate { margin-top:10px; font-size:14px; padding:10px 18px; }++#v-generate:disabled { opacity:.45; cursor:not-allowed; }++/* the layout linter (#18): the studio notices, the human decides */+#lintbox { margin-top:8px; }++#lintbox .lint-item { font-size:12.5px; margin:4px 0; cursor:pointer; line-height:1.4; }++#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; }++/* page-field editors (#20) */+.pf-in { width:100%; box-sizing:border-box; background:#0d1117; color:var(--text); border:1px solid #30363d; border-radius:6px; padding:6px 8px; font:inherit; font-size:12.5px; margin:2px 0 8px; }++.pf-in:focus { border-color:var(--accent); outline:none; }++/* zen focus (#12): the hero gets the whole surface */+body.zen #panel { display:none; }++body.zen main { max-width:none; }++.sw-aurora { background:radial-gradient(8px 6px at 30% 20%, rgba(0,230,220,.8), transparent), radial-gradient(10px 8px 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 1px, transparent 1px 5px), #080d13; }+++/* verification walkthrough popout */+#tourpop { display:none; position:fixed; z-index:900; max-width:300px;+  background:var(--elevated); border:1px solid var(--accent); border-radius:10px;+  padding:12px 14px; box-shadow:0 12px 40px rgba(0,0,0,.6); font-size:13px; }++#tourpop .tp-title { font-family:'Familjen Grotesk'; font-weight:700; margin-bottom:6px; color:var(--accent-bright); }++#tourpop .tp-body { color:var(--text); line-height:1.45; margin-bottom:10px; }++#tourpop .tp-row { display:flex; gap:8px; }++.tour-above { position:relative !important; z-index:901 !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; }+++/* generated-hero preview modal */+#genmodal { position:fixed; inset:0; z-index:99997; background:rgba(5,9,14,.8); display:none; align-items:center; justify-content:center; padding:32px; }++#genmodal.show { display:flex; }++.genbox { max-width:min(1000px, 92vw); background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:14px; box-shadow:0 24px 80px rgba(0,0,0,.6); }++.genbox img { display:block; width:100%; border-radius:8px; border:1px solid var(--border); }++.genrow { display:flex; align-items:center; gap:10px; margin-top:10px; }++.genrow .hint { margin:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }++.genrow a { color:var(--accent); font-size:13px; text-decoration:none; }++.genrow a:hover { color:var(--accent-bright); }++.btn-primary { border-color:var(--accent); background:rgba(0,184,176,.12); font-weight:500; }++.btn-primary:hover { background:rgba(0,184,176,.2); }++.btn-primary:disabled { opacity:.55; cursor:wait; }+++/* tooltip + toast */+#tip {+  position:fixed; z-index:99999; background:var(--overlay); border:1px solid var(--border);+  color:var(--text); font-size:12px; line-height:1.4; padding:6px 9px; border-radius:8px;+  max-width:240px; pointer-events:none; opacity:0; transform:translateY(3px);+  transition:opacity .18s ease, transform .18s ease; text-transform:none;+}++#tip.show { opacity:1; transform:none; }++#toasts { position:fixed; left:50%; bottom:18px; transform:translateX(-50%); z-index:99998; display:flex; flex-direction:column-reverse; gap:8px; }++.toast {+  display:flex; align-items:center; gap:8px; background:var(--overlay); border:1px solid var(--border);+  border-left:3px solid var(--accent); border-radius:8px; padding:9px 13px; font-size:13px;+  box-shadow:0 8px 30px rgba(0,0,0,.45); animation:tin .2s ease;+}++.toast.success { border-left-color:var(--green); }++.toast.error { border-left-color:var(--red); }++@keyframes tin { from { opacity:0; transform:translateY(6px);} }++/* ---------- the wizard dock (M4): default chrome; classic is one toggle away ---------- */+body.wizard #panel { display:none; }+body.wizard #tourpop { display:none !important; }+body.wizard #verifybar { display:none !important; }+body.classic #wdock, body.classic #weditring { display:none !important; }+#wdock { flex:0 0 auto; background:var(--surface); border-top:1px solid var(--border); padding:10px 18px 14px;+  display:flex; flex-direction:column; gap:10px; height:min(44vh, 330px); overflow-y:auto; scrollbar-width:thin; }+#wdockintro { font-size:12px; color:var(--text-2); padding:0 2px 2px; }+#wdockintro b { color:var(--text); font-weight:600; }+#wdockintro .ref { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--accent); }+#wrailwrap { position:relative; min-width:0; }+#wrailwrap::before, #wrailwrap::after { content:''; position:absolute; top:0; bottom:2px; width:36px; pointer-events:none; opacity:0; transition:opacity .15s ease; z-index:2; }+#wrailwrap::before { left:0; background:linear-gradient(90deg, var(--surface) 20%, transparent); }+#wrailwrap::after { right:0; background:linear-gradient(270deg, var(--surface) 20%, transparent); }+#wrailwrap.can-left::before { opacity:1; }+#wrailwrap.can-right::after { opacity:1; }+#wsteprail { display:flex; gap:6px; overflow-x:auto; padding-bottom:2px; scrollbar-width:none; }+#wsteprail > :first-child { margin-left:auto; }+#wsteprail > :last-child { margin-right:auto; }+#wsteprail::-webkit-scrollbar { display:none; }+#wsteprail.compact .schip .v { display:none; }+#wsteprail.mini .schip:not(.active) .t { display:none; }+#wsteprail.mini .schip:not(.active) { padding:4px 8px 4px 5px; }+.schip { display:flex; align-items:center; gap:8px; border:1px solid var(--border); background:var(--elevated); border-radius:999px;+  padding:4px 12px 4px 5px; cursor:pointer; white-space:nowrap; transition:all .15s ease; font-family:'Satoshi'; }+.schip:hover { border-color:var(--text-3); }+.schip .n { width:20px; height:20px; border-radius:50%; display:flex; align-items:center; justify-content:center;+  font-size:11px; font-family:'JetBrains Mono',monospace; background:var(--overlay); color:var(--text-2); border:1px solid var(--border); flex:0 0 20px; }+.schip .n svg { width:11px; height:11px; stroke:var(--green); stroke-width:2.5; fill:none; }+.schip .t { font-size:12.5px; color:var(--text-2); }+.schip .v { font-size:11px; color:var(--text-3); max-width:110px; overflow:hidden; text-overflow:ellipsis; }+.schip.active { border-color:var(--accent); background:rgba(0,184,176,.10); }+.schip.active .t { color:var(--text); font-weight:500; }+.schip.active .n { background:var(--accent); color:#04211f; border-color:transparent; }+.schip.done .n { background:rgba(63,185,80,.12); border-color:rgba(63,185,80,.45); }+#wdockhead { display:flex; align-items:center; gap:8px 12px; min-height:26px; flex-wrap:wrap; }+#wdockhead .wdtitle { font-family:'Familjen Grotesk'; font-weight:700; font-size:15px; color:var(--text); }+#wdockhead .wdcount { font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--text-2); }+#wdockhead .wdhint { font-size:12px; color:var(--text-2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0; }+#wbtn-generate { font-size:15px; font-weight:600; padding:10px 26px; border-radius:10px; }+#wbtn-generate:not(:disabled) { box-shadow:0 0 18px rgba(0,184,176,.35); }+#wcardsrow { display:flex; gap:10px; align-items:stretch; min-height:0; flex:1; }+#wcards { display:flex; gap:10px; overflow-x:auto; flex:1; scrollbar-width:none; align-items:stretch; padding:2px; }+#wcards::-webkit-scrollbar { display:none; }+#wcards > :first-child { margin-left:auto; }+#wcards > :last-child { margin-right:auto; }+.warrow { flex:0 0 44px; border:1px solid var(--border); background:var(--elevated); border-radius:10px; color:var(--text-2);+  font-size:20px; cursor:pointer; }+.warrow:hover { border-color:var(--text-3); color:var(--text); }+#wcards .card { flex:0 0 auto; width:128px; border:1px solid var(--border); background:var(--elevated); border-radius:10px;+  padding:10px; cursor:pointer; display:flex; flex-direction:column; gap:4px; justify-content:center; text-align:center; transition:all .15s ease; }+#wcards .card:hover { border-color:var(--text-3); }+#wcards .card.on { border-color:var(--accent); box-shadow:0 0 0 1px var(--accent); }+#wcards .card.wide { width:170px; }+#wcards .card .card-label { font-size:13px; color:var(--text); }+#wcards .card.on .card-label { color:var(--accent); }+#wcards .card .card-sub { font-size:11px; color:var(--text-3); line-height:1.35; }+#weditring { position:absolute; border:2px dashed var(--accent); border-radius:10px; pointer-events:none;+  z-index:9; opacity:0; transition:opacity .25s ease, top .2s ease, left .2s ease, width .2s ease, height .2s ease;+  box-shadow:0 0 22px rgba(0,184,176,.18); }+#weditring.on { opacity:.85; }+.wiz-lab { font-size:11px; color:var(--text-2); letter-spacing:.06em; text-transform:uppercase; font-family:'Familjen Grotesk'; }+.wiz-headline { flex:1; display:flex; flex-direction:column; gap:7px; max-width:560px; margin:0 auto; }+.wiz-headline .hin { background:var(--overlay); border:1px solid var(--border); border-radius:8px; color:var(--text);+  padding:8px 12px; font-size:14px; font-family:'Satoshi'; }+.wordpick { display:flex; flex-wrap:wrap; gap:4px; max-width:560px; }+.wtok { padding:2px 8px; border:1px solid var(--border); border-radius:6px; font-size:12px; cursor:pointer;+  color:var(--text-2); background:var(--elevated); transition:all .12s ease; user-select:none; }+.wtok:hover { border-color:var(--text-3); }+.wtok.on { border-color:var(--accent); background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent; font-weight:600; }+.wiz-finish { flex:1; display:flex; gap:10px; align-items:center; justify-content:center; flex-wrap:wrap; }+.wiz-review { flex:1; display:flex; gap:14px 22px; align-items:flex-start; justify-content:center; min-width:0; flex-wrap:wrap; }+.wiz-review .rv-col { display:flex; flex-direction:column; gap:8px; min-width:230px; }+.rv-check { display:flex; align-items:center; gap:10px; border:1px solid var(--border); border-radius:9px; padding:7px 10px; }+.rv-check .q { flex:1; font-size:12.5px; color:var(--text-2); }+.rv-check.ok { border-color:rgba(63,185,80,.5); }+.rv-check.ok .q { color:var(--green); }+.rv-check.flag { border-color:rgba(248,81,73,.5); }+.wseg { display:flex; border:1px solid var(--border); border-radius:9px; overflow:hidden; width:fit-content; }+.wseg button { background:var(--elevated); border:none; color:var(--text-2); font-size:12px; padding:7px 12px; cursor:pointer; }+.wseg button.on { background:rgba(0,184,176,.14); color:var(--accent); }+.wiz-legend { font-size:11.5px; color:#e8a13c; max-width:280px; }+/* wizard layout: header / stage / dock as one column; the dock owns the+   bottom edge and the stage flexes (its own overflow already scrolls) */+body.wizard { display:flex; flex-direction:column; height:100vh; overflow:hidden; }+body.wizard main { flex:1 1 auto; min-height:0; }+/* dockhead buttons: right-aligned cluster, never stretched */+#wdockhead .wdhint { order:0; }+#wbtn-finetune, #wbtn-generate, #wbtn-next { flex:0 0 auto; width:auto; margin-left:0; }+#wbtn-generate { margin-left:auto; }+#wbtn-next { margin-left:auto; }+#wbtn-generate + #wbtn-next { margin-left:8px; }+.rv-check .q { min-width:150px; }+.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; }
server.js+170−17
@@ -3,6 +3,7 @@ const http = require('http'); const fs = require('fs'); const path = require('path'); const { spawn, execFile } = require('child_process');+const crypto = require('crypto');  // Port: --port flag > HERO_STUDIO_PORT env > 8846 ('auto' = bind an OS-picked // free port). Multiple instances on separate ports = concurrent studios@@ -57,6 +58,8 @@ let state = {   circuitSeed: Date.now() % 2147483647,  // procedural circuitry theme seed; /reroll re-rolls   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)+  subtitle: '',    // the text under the title; '' = the page brief's first sentence (wizard round 3)+  accentWords: null, // subtitle word indices that get the gradient; null = legacy auto-accent (pixel parity)   approved: null,  // { page, file, ts } set on Generate: the persistent gate-closed record   gradAngle: 155,   guides: true,@@ -80,7 +83,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', 'overlay.logo',-  'brand.accent', 'brand.bg', 'fontSize.title', 'fontSize.sub', 'fontSize.desc', 'fontSize.leading', 'fontSize.gap', 'headline',+  'brand.accent', 'brand.bg', 'fontSize.title', 'fontSize.sub', 'fontSize.desc', 'fontSize.leading', 'fontSize.gap', 'headline', 'subtitle', 'accentWords',   'vitrine.chrome', 'vitrine.full', 'vitrine.cover', ]; const getPath = (o, p) => p.split('.').reduce((a, k) => (a == null ? a : a[k]), o);@@ -105,6 +108,19 @@ function notePoseDiff(before) { // carries the constants pup measured and kicad/fusion/nb copied (title 12cqw, // bare right-side vitrine, overlays off, calm band). const PRESETS = {+  // The true boot defaults, server-owned so the UI's reset can never drift+  // from them again (the old client reset restored overlay defaults that no+  // longer existed).+  'studio-default': {+    mode: 'app',+    layout: 'bleed-bottom', theme: 'midnight', titleMode: 'image-only',+    show: { logo: true, title: true, subtitle: true, description: false },+    overlay: { pill: true, scores: false, author: true, slug: false, logo: false },+    brand: { accent: '', bg: '' },+    fontSize: { title: 8, sub: 2.5, desc: 1.35, leading: 1.02, gap: 1 },+    headline: '', gradAngle: 155,+    vitrine: { chrome: '', cover: false },+  },   'bridge-family': {     layout: 'showcase', theme: 'midnight', titleMode: 'image-only',     fontSize: { title: 12, sub: 2.2, desc: 1.35 },@@ -130,8 +146,41 @@ let genSeq = Date.now(); let lastGen = null; let generating = false; +// Transport rev: every observable change bumps it. GET /state carries it as+// an ETag so pollers 304 instead of re-downloading multi-MB bodies; the+// serialized payload is cached per rev and variant.+let stateRev = 1;+const stateBodyCache = {};++// Lean variant: staged imports are multi-MB base64 data URLs inside state.+// The browser opts in with ?src=ref and gets a same-origin path instead; the+// bytes come from /vitrine.png and /bg.png. Plain GET /state stays+// full-fidelity because pose.mjs reads vitrine.src directly.+const srcHashCache = new Map();+function srcHash(src) {+  let h = srcHashCache.get(src);+  if (!h) {+    h = crypto.createHash('sha1').update(src).digest('hex').slice(0, 12);+    srcHashCache.set(src, h);+    if (srcHashCache.size > 4) srcHashCache.delete(srcHashCache.keys().next().value);+  }+  return h;+}+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) });+  }+  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) });+  }+  return out;+}+ function broadcast(event, data) {-  const msg = 'event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n';+  stateRev += 1;+  const payload = event === 'state' ? leanState() : data;+  const msg = 'event: ' + event + '\ndata: ' + JSON.stringify(payload) + '\n\n';   for (const res of clients) res.write(msg);   if (event === 'state') saveSnapshot(); }@@ -235,6 +284,57 @@ const MIME = { // Read-only endpoints stay open (observation and debugging are never gated); // every state-changing POST must carry the ai-thread name. const THREAD_EXEMPT = new Set(['/console', '/events']);++// #44: the strings the surface is actually rendering, plus their source, so+// a driving AI can verify what the human sees without reading pixels.+function firstSentenceSrv(str, max) {+  let t = (str || '').split(/(?<=\.)\s/)[0] || str || '';+  if (t.length > max) t = t.slice(0, max).replace(/\s+\S*$/, '') + '...';+  return t;+}+function resolvedText() {+  const [owner, slug] = String(state.page || '').split('/');+  const row = PAGES.find((x) => x.owner === owner && x.slug === slug) || {};+  const rowSrc = row.live ? 'live-wiki' : 'fixture';+  return {+    title: row.title || '',+    headline: state.headline || row.title || '',+    subtitle: state.subtitle || firstSentenceSrv(row.brief, 80),+    titleSource: rowSrc,+    headlineSource: state.headline ? 'override' : rowSrc,+    subtitleSource: state.subtitle ? 'override' : rowSrc,+  };+}++// Seasoned-review gate count (#45, Colby: same-session automation only; the+// count feeds the pre-ticked checklist). Container-local v1; per-account+// needs wiki identity (backend ask filed separately).+const GATES_FILE = path.join(INSTANCES_DIR, 'gates.json');+function readGates() {+  try { return JSON.parse(fs.readFileSync(GATES_FILE, 'utf8')).count || 0; } catch { return 0; }+}+function bumpGates() {+  try { fs.writeFileSync(GATES_FILE, JSON.stringify({ count: readGates() + 1, updated: new Date().toISOString() })); } catch {}+}++// Live wiki re-fetch for a page row, shared by /refresh-page, the dropdown,+// and now raw POST /state page changes (#44: the fixture-row hole).+async function refreshPageLive(ref) {+  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 {}+  const r = await fetch(REG + '/api/pages/' + ref, { headers, redirect: 'follow' });+  if (!r.ok) throw new 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, live: true };+  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 + '"');+  return pg;+} const server = http.createServer((req, res) => {   const url = new URL(req.url, 'http://x');   const p = url.pathname;@@ -263,8 +363,32 @@ const server = http.createServer((req, res) => { function route(req, res, url, p) {    if (p === '/state' && req.method === 'GET') {-    res.writeHead(200, { 'Content-Type': 'application/json' });-    return res.end(JSON.stringify(Object.assign({}, state, { _toast: lastToast, _gen: lastGen, enums: ENUMS, version: PKG_VERSION, aiThread: AI_THREAD })));+    const lean = url.searchParams.get('src') === 'ref';+    const variant = lean ? 'lean' : 'full';+    const etag = '"' + stateRev + '-' + variant + '"';+    if (req.headers['if-none-match'] === etag) {+      res.writeHead(304, { ETag: etag });+      return res.end();+    }+    const cached = stateBodyCache[variant];+    let body;+    if (cached && cached.rev === stateRev) body = cached.body;+    else {+      body = JSON.stringify(Object.assign({}, lean ? leanState() : state, { _toast: lastToast, _gen: lastGen, enums: ENUMS, version: PKG_VERSION, aiThread: AI_THREAD, gatesDone: readGates(), resolved: resolvedText() }));+      stateBodyCache[variant] = { rev: stateRev, body };+    }+    res.writeHead(200, { 'Content-Type': 'application/json', ETag: etag, 'Cache-Control': 'no-cache' });+    return res.end(body);+  }+  if ((p === '/vitrine.png' || p === '/bg.png') && req.method === 'GET') {+    const src = p === '/vitrine.png' ? (state.vitrine && state.vitrine.src) : (state.bgImage && state.bgImage.src);+    if (!src || !src.startsWith('data:')) { res.writeHead(404, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ ok: false, error: 'no image staged' })); }+    const m = src.match(/^data:([^;,]+)?(;base64)?,/);+    const mime = (m && m[1]) || 'image/png';+    const raw = src.slice(src.indexOf(',') + 1);+    const buf = (m && m[2]) ? Buffer.from(raw, 'base64') : Buffer.from(decodeURIComponent(raw));+    res.writeHead(200, { 'Content-Type': mime, 'Content-Length': buf.length, 'Cache-Control': 'public, max-age=31536000, immutable' });+    return res.end(buf);   }   if (p === '/version') {     res.writeHead(200, { 'Content-Type': 'application/json' });@@ -279,6 +403,7 @@ function route(req, res, url, p) {           return res.end(JSON.stringify({ ok: false, error: 'invalid ' + k + ' \'' + body[k] + '\'', valid }));         }       }+      const pageChanged = typeof body.page === 'string' && body.page !== state.page;       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, logo: false };@@ -286,6 +411,11 @@ function route(req, res, url, p) {       }       const before = snapshotPose();       merge(state, body);+      if (pageChanged) {+        refreshPageLive(state.page)+          .then(() => { stateRev += 1; broadcast('state', state); })+          .catch((e) => logLine('warn', 'live refresh failed for ' + state.page + ': ' + e.message + ' (fixture row shown until it succeeds)'));+      }       notePoseDiff(before);       logLine('info', 'state <- ' + JSON.stringify(body).slice(0, 500));       broadcast('state', state);@@ -355,7 +485,7 @@ function route(req, res, url, p) {       'X-Accel-Buffering': 'no',     });     res.write(':' + ' '.repeat(2048) + '\n\n'); // flush proxy buffers-    res.write('event: state\ndata: ' + JSON.stringify(state) + '\n\n');+    res.write('event: state\ndata: ' + JSON.stringify(leanState()) + '\n\n');     clients.add(res);     const hb = setInterval(() => res.write(': hb\n\n'), 15000);     req.on('close', () => { clearInterval(hb); clients.delete(res); });@@ -458,16 +588,7 @@ function route(req, res, url, p) {     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 + '"');+        const pg = await refreshPageLive(ref);         broadcast('state', state);         finish(200, { ok: true, title: pg.title, brief: pg.brief });       } catch (e) { finish(200, { ok: false, error: e.message }); }@@ -611,6 +732,35 @@ function route(req, res, url, p) {     // body {"renderer":"local"|"pup"} pins one ladder leg for this request     // (the test hook for the pup path); HERO_RENDERER env pins it server-wide.     const pinned = (!bodyErr && body && body.renderer) || process.env.HERO_RENDERER;+    const attest = (!bodyErr && body && body.attest) || null;+    const fromGate = !bodyErr && body && body.fromGate === true;+    const refuse = (error, hint) => {+      res.writeHead(200, { 'Content-Type': 'application/json' });+      res.end(JSON.stringify({ ok: false, errorCode: 'HUMAN_GATE', error, hint }));+    };+    // Hard-fail lint findings lock EVERY Generate path, gate or attested.+    if ((state.lint || []).some((l) => l.level === 'fail')) {+      return refuse('hard-fail layout findings lock Generate',+        'fix the failing items in state.lint first; they lock the human gate and automated mode alike');+    }+    if (!fromGate) {+      // Automated mode (#45, decided by Colby 2026-08-13): SAME-SESSION+      // REPEATS ONLY. The first Generate of a page is always human-clicked;+      // after that, the driving AI may re-generate THAT page with a recorded+      // attestation of the human's actual approval words.+      if (!attest) {+        return refuse('Generate is human-gated: the human clicks it in the studio',+          'a human must generate this page first (hero-studio pose opens the gate). To RE-generate a page the human already generated this session, POST /generate with attest: {human: "<who approved>", quote: "<their actual words>"}');+      }+      if (!(state.approved && state.approved.page === state.page)) {+        return refuse('automated Generate is only allowed for a page a human already generated this session',+          'pose ' + state.page + ' and let the human click Generate once; after that, attested re-generates of the same page are allowed');+      }+      if (!attest.human || !String(attest.quote || '').trim()) {+        return refuse('the attestation must name the human and quote their actual approval words',+        'attest: {human: "colby", quote: "ship it"} -- the quote is recorded in the PNG provenance and shown in the Approved banner');+      }+    }     generating = true;     const slug = state.mode === 'component'       ? ((COMPONENTS.find(c => c.id === state.comp.id) || COMPONENTS[0]).slug)@@ -705,7 +855,7 @@ function route(req, res, url, p) {         return finish({ ok: false, error: 'could not read the rendered PNG: ' + e.message, hint: "run 'hero-studio doctor'" });       }       // downscale to the hero standard 2000x1250 and stamp provenance metadata-      const meta = JSON.stringify({ generator: 'hero-studio', version: PKG_VERSION, ts, renderer: renderedVia, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle, brand: state.brand, fontSize: state.fontSize, verify: state.verify, confirm: state.confirm, vitrine_used: !!(state.vitrine && state.vitrine.src), vitrine_full: !!(state.vitrine && state.vitrine.full), circuitSeed: state.theme === 'circuitry' ? state.circuitSeed : undefined }, changes: (Object.keys(changes.page).length || Object.keys(changes.pose).length) ? { page: changes.page, pose: changes.pose, applied: changes.applied } : undefined });+      const meta = JSON.stringify({ generator: 'hero-studio', version: PKG_VERSION, ts, renderer: renderedVia, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle, brand: state.brand, fontSize: state.fontSize, verify: state.verify, confirm: state.confirm, vitrine_used: !!(state.vitrine && state.vitrine.src), vitrine_full: !!(state.vitrine && state.vitrine.full), circuitSeed: state.theme === 'circuitry' ? state.circuitSeed : undefined }, attestation: attest ? { human: attest.human, quote: String(attest.quote).slice(0, 300), aiThread: req.aiThread || '', ts } : undefined, changes: (Object.keys(changes.page).length || Object.keys(changes.pose).length) ? { page: changes.page, pose: changes.pose, applied: changes.applied } : undefined });       const py = 'import sys\n' +         'from PIL import Image\n' +         'from PIL.PngImagePlugin import PngInfo\n' +@@ -727,7 +877,10 @@ function route(req, res, url, p) {         genSeq += 1;         lastGen = { seq: genSeq, ok: true, file, url: 'generated/' + file, latest: 'generated/latest.png', ts, slug };         // hero-studio#5: approval must be a persistent state, not a toast.-        state.approved = { page: state.page, file, ts };+        state.approved = attest+          ? { page: state.page, file, ts, attested: { human: attest.human, quote: String(attest.quote).slice(0, 300) } }+          : { page: state.page, file, ts };+        if (fromGate) bumpGates();         broadcast('state', state);         logLine('info', 'generated ' + file);         broadcast('generated', lastGen);