app
Hero Studio
Public Made by Adomby adom
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. Every hero wears its P
master
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448
<!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>