master
Colby Knox Publish 0.18.0 54b4db3 2mo ago
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>