12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Adom Video Post</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<style>
@font-face {
  font-family: 'Familjen Grotesk';
  src: url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-700-normal.woff2') format('woff2');
  font-weight: 700; font-display: swap;
}
@font-face {
  font-family: 'Familjen Grotesk';
  src: url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-600-normal.woff2') format('woff2');
  font-weight: 600; font-display: swap;
}
@font-face {
  font-family: 'Familjen Grotesk';
  src: url('https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-500-normal.woff2') format('woff2');
  font-weight: 500; font-display: swap;
}
@font-face {
  font-family: 'Satoshi';
  src: url('https://adom.inc/fonts/satoshi/satoshi-400-normal.woff2') format('woff2');
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: 'Satoshi';
  src: url('https://adom.inc/fonts/satoshi/satoshi-500-normal.woff2') format('woff2');
  font-weight: 500; font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5/files/jetbrains-mono-latin-400-normal.woff2') format('woff2');
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5/files/jetbrains-mono-latin-500-normal.woff2') format('woff2');
  font-weight: 500; font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('https://cdn.jsdelivr.net/npm/@fontsource/jetbrains-mono@5/files/jetbrains-mono-latin-600-normal.woff2') format('woff2');
  font-weight: 600; font-display: swap;
}
  :root {
    --bg: #0d1117;
    --surface: #161b22;
    --elevated: #1c2128;
    --border: #30363d;
    --border-muted: #21262d;
    --text: #e6edf3;
    --text2: #7d8590;
    --text3: #484f58;
    --accent: #00b8b0;
    --accent-bright: #00e6dc;
    --accent-glow: rgba(0,184,176,0.25);
    --purple: #8C6BF7;
    --purple-light: #a890ff;
    --green: #4aa857;
    --red: #f85149;
    --yellow: #d4aa3e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: 'Familjen Grotesk', 'Satoshi', -apple-system, "Segoe UI", sans-serif;
    font-size: 14px;
    line-height: 1.5;
  }

  /* Canonical Adom app-header — see gallia/skills/adom-app-header/SKILL.md.
     Single row, 44 px tall + phase tabs as a separate row beneath. */
  header {
    padding: 0;
    border-bottom: 1px solid var(--border-muted);
    background: var(--bg);
    position: relative;
  }
  header .header-row {
    height: 44px;
    padding: 0 16px;
    display: flex;
    align-items: center;
    gap: 12px;
    font-family: 'Familjen Grotesk', sans-serif;
  }
  header .brand {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--accent);
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.01em;
    cursor: default;
    flex-shrink: 0;
    outline: none;
  }
  header .brand:hover .app-name { text-decoration: underline; text-decoration-color: var(--accent); text-underline-offset: 3px; }
  header .brand svg.logo { width: 16px; height: 16px; flex-shrink: 0; }
  header .brand .app-name { color: var(--accent); }
  header .brand .app-subtitle { color: var(--text2); font-weight: 500; }
  header .brand .sep { color: var(--text3); font-weight: 400; }
  header .vsep {
    width: 1px;
    height: 18px;
    background: var(--border);
    flex-shrink: 0;
  }
  header .subject {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    flex: 1 1 auto;
  }
  header .subject-name {
    color: var(--text);
    font-family: 'Familjen Grotesk', sans-serif;
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    min-width: 0;
  }
  header .subject-meta {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--text2);
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    white-space: nowrap;
    flex-shrink: 0;
  }
  header .subject-meta .sep { color: var(--text3); }
  @media (max-width: 720px) {
    header .subject-meta { display: none; }
  }
  header .hover-popover {
    position: absolute;
    top: calc(100% + 6px);
    left: 16px;
    background: var(--elevated);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.5);
    padding: 12px 14px;
    min-width: 300px;
    max-width: 420px;
    font-family: 'Satoshi', sans-serif;
    font-size: 12px;
    color: var(--text);
    z-index: 99999;
    opacity: 0;
    transform: translateY(-2px);
    pointer-events: none;
    transition: opacity 120ms ease, transform 120ms ease;
  }
  header .hover-popover.show { opacity: 1; transform: none; pointer-events: auto; }
  header .hover-popover .popover-title { font-family: 'Familjen Grotesk', sans-serif; font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 8px; }
  header .hover-popover .popover-row { display: flex; gap: 12px; padding: 3px 0; align-items: baseline; }
  header .hover-popover .popover-row .k { color: var(--text3); font-family: 'JetBrains Mono', monospace; font-size: 10px; text-transform: lowercase; width: 64px; flex-shrink: 0; }
  header .hover-popover .popover-row .v { color: var(--text); font-family: 'Satoshi', sans-serif; font-size: 12px; flex: 1; word-break: break-word; }
  header .hover-popover .popover-actions { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-muted); display: flex; gap: 12px; }
  header .hover-popover .popover-link { color: var(--accent); text-decoration: none; font-family: 'Familjen Grotesk', sans-serif; font-size: 12px; font-weight: 600; }
  header .hover-popover .popover-link:hover { text-decoration: underline; }

  /* Phase selector: three tabs below the title */
  .phase-tabs {
    display: flex;
    gap: 0;
    margin-top: 14px;
    border-bottom: 1px solid var(--border-muted);
  }
  .phase-tab {
    padding: 12px 24px;
    background: transparent;
    border: none;
    color: var(--text2);
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    cursor: pointer;
    border-bottom: 2px solid transparent;
    transition: color 0.15s, border-color 0.15s;
  }
  .phase-tab:hover { color: var(--text); }
  .phase-tab.active {
    color: var(--accent-bright);
    border-bottom-color: var(--accent);
  }
  .phase-tab .step {
    display: inline-block;
    width: 18px;
    height: 18px;
    line-height: 18px;
    text-align: center;
    background: var(--elevated);
    color: var(--text3);
    border-radius: 50%;
    font-size: 10px;
    font-weight: 700;
    margin-right: 8px;
  }
  .phase-tab.active .step {
    background: var(--accent);
    color: var(--bg);
  }

  .phase-panel { display: none; }
  .phase-panel.active { display: block; }

  /* ─── Review phase (storyboard) ──────────────────────────── */
  .finals {
    margin: 18px 32px 0;
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
  }
  .finals .card {
    flex: 1;
    min-width: 320px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 16px;
  }
  .finals h3 {
    margin: 0 0 8px;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.8px;
    color: var(--text3);
  }

  .timeline {
    display: flex;
    align-items: center;
    gap: 6px;
    background: var(--elevated);
    border: 1px solid var(--border);
    padding: 14px 18px;
    margin: 18px 32px;
    border-radius: 8px;
    overflow-x: auto;
    white-space: nowrap;
  }
  .tl-step {
    flex-shrink: 0;
    padding: 7px 13px;
    border-radius: 6px;
    background: var(--surface);
    border: 1px solid var(--border);
    font-size: 12px;
    color: var(--text);
    cursor: pointer;
    text-decoration: none;
    display: inline-flex;
    flex-direction: column;
    gap: 2px;
  }
  .tl-step.speedup { border-color: var(--purple); background: #1f1530; }
  .tl-step:hover { border-color: var(--accent-bright); }
  .tl-step .num { color: var(--text3); font-size: 10px; font-weight: 600; }
  .tl-step .mark { color: var(--purple); font-size: 10px; }

  .steps { padding: 0 32px 80px; }
  .step-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 24px;
    margin-top: 20px;
  }
  .step-card h2 {
    margin: 0 0 6px;
    font-size: 22px;
    color: var(--accent-bright);
    display: flex;
    align-items: center;
    gap: 12px;
  }
  .step-card h2 .badge {
    background: var(--accent);
    color: var(--bg);
    padding: 4px 10px;
    border-radius: 16px;
    font-size: 13px;
    font-weight: 700;
  }
  .step-card .desc { color: var(--text2); font-size: 13px; margin-bottom: 14px; }

  .speedup-pill {
    display: inline-block;
    padding: 4px 10px;
    border-radius: 12px;
    background: var(--purple);
    color: #fff;
    font-size: 12px;
    font-weight: 600;
    margin-left: 8px;
  }

  .actions-list {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 12px 16px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    line-height: 1.7;
    color: var(--text2);
    margin-top: 12px;
    max-height: 160px;
    overflow-y: auto;
  }
  .warnings {
    background: #2d2412;
    border: 1px solid #a8791e;
    color: #f0c260;
    border-radius: 6px;
    padding: 10px 14px;
    font-size: 12px;
    margin-top: 12px;
  }

  .clip-row { display: grid; grid-template-columns: 1fr; gap: 16px; margin-top: 18px; }
  .clip-block { display: flex; flex-direction: column; gap: 6px; }
  .clip-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 600;
  }
  .clip-label.raw { color: var(--accent-bright); }
  .clip-label.fast { color: var(--purple); }

  .video-wrap {
    position: relative;
    aspect-ratio: 16 / 10;
    background: #000;
    border-radius: 6px;
    border: 1px solid var(--border);
    overflow: hidden;
  }
  .video-wrap video {
    width: 100%;
    height: 100%;
    display: block;
    background: #000;
    object-fit: contain;
  }
  .video-wrap .expand {
    position: absolute;
    top: 8px;
    right: 8px;
    background: rgba(0,0,0,0.75);
    color: #fff;
    border: 1px solid rgba(255,255,255,0.3);
    padding: 6px 12px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
    font-family: inherit;
    z-index: 5;
    transition: background 0.15s;
  }
  .video-wrap .expand:hover {
    background: rgba(0,0,0,0.95);
    border-color: var(--accent-bright);
    color: var(--accent-bright);
  }

  /* ─── Narrate phase (voiceover) ─────────────────────────── */
  .narrate {
    padding: 24px 32px 80px;
    max-width: 1100px;
    margin: 0 auto;
  }
  .narrate .big-video {
    background: #000;
    border: 1px solid var(--border);
    border-radius: 10px;
    aspect-ratio: 16 / 10;
    overflow: hidden;
    margin-bottom: 18px;
  }
  .narrate .big-video video {
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  .narrate-controls {
    display: flex;
    gap: 12px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 16px;
  }
  .narrate button {
    background: var(--elevated);
    color: var(--text);
    border: 1px solid var(--border);
    padding: 10px 18px;
    border-radius: 8px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .narrate button:hover:not(:disabled) {
    border-color: var(--accent);
    background: rgba(0,184,176,0.08);
  }
  .narrate button:disabled { opacity: 0.4; cursor: not-allowed; }
  .narrate .btn-record {
    background: var(--red);
    color: #fff;
    border-color: var(--red);
    padding: 12px 22px;
    font-weight: 600;
    box-shadow: 0 4px 14px rgba(248,81,73,0.25);
  }
  .narrate .btn-record:hover:not(:disabled) {
    background: #ff5a52;
    color: #fff;
  }
  .narrate .btn-record.recording {
    background: var(--elevated);
    color: var(--red);
    border-color: var(--red);
    box-shadow: 0 0 0 3px rgba(248,81,73,0.15);
    animation: pulse 1.6s ease-in-out infinite;
  }
  .narrate .btn-record.recording::before { content: '⏹ '; }
  .narrate .btn-finalize {
    background: var(--accent);
    color: var(--bg);
    border-color: var(--accent);
    font-weight: 500;
  }
  .narrate .btn-finalize:hover:not(:disabled) {
    background: var(--accent-bright);
    border-color: var(--accent-bright);
    color: var(--bg);
  }
  @keyframes pulse {
    0%,100% { box-shadow: 0 0 0 3px rgba(248,81,73,0.15); }
    50%     { box-shadow: 0 0 0 6px rgba(248,81,73,0.35); }
  }

  .narrate .status {
    background: var(--elevated);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 14px 18px;
    color: var(--text2);
    font-size: 13px;
    line-height: 1.6;
    white-space: pre-wrap;
  }
  .narrate .not-ready {
    background: #2d2412;
    border: 1px solid #a8791e;
    color: #f0c260;
    border-radius: 8px;
    padding: 20px 24px;
    font-size: 13px;
    margin-bottom: 18px;
    line-height: 1.6;
  }
  .narrate .not-ready .prepare-btn {
    display: inline-block;
    margin-top: 14px;
    background: var(--accent);
    color: var(--bg);
    border: 1px solid var(--accent);
    padding: 11px 22px;
    border-radius: 8px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
  }
  .narrate .not-ready .prepare-btn:hover:not(:disabled) {
    background: var(--accent-bright);
    color: var(--bg);
  }
  .narrate .not-ready .prepare-btn:disabled {
    opacity: 0.55;
    cursor: wait;
  }
  .narrate .not-ready .prepare-hint {
    display: block;
    margin-top: 10px;
    color: #caa860;
    font-size: 11px;
    font-family: 'JetBrains Mono', monospace;
  }

  /* ─── Publish phase ─────────────────────────────────────── */
  .publish {
    padding: 32px;
    max-width: 780px;
    margin: 0 auto;
    color: var(--text2);
    font-size: 14px;
  }
  .publish h3 { color: var(--text); margin: 0 0 14px; font-size: 20px; font-weight: 700; }
  .publish p { margin: 0 0 14px; }
  .publish .inline-code {
    background: var(--elevated);
    padding: 2px 8px;
    border-radius: 4px;
    color: var(--accent-bright);
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
  }
  .publish form {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 20px 24px;
    margin-top: 18px;
  }
  .publish label {
    display: block;
    margin-bottom: 14px;
  }
  .publish label .lbl {
    display: block;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 600;
    color: var(--text3);
    margin-bottom: 6px;
  }
  .publish input[type="text"] {
    width: 100%;
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 10px 14px;
    font-family: inherit;
    font-size: 14px;
  }
  .publish input[type="text"]:focus {
    border-color: var(--accent);
    outline: none;
  }
  .publish .prompt-box {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 16px 18px;
    margin-top: 14px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 12px;
    line-height: 1.7;
    color: var(--text);
    white-space: pre-wrap;
    word-break: break-word;
  }
  .publish .prompt-actions {
    margin-top: 12px;
    display: flex;
    gap: 10px;
    align-items: center;
  }
  .publish .copy-btn {
    background: var(--accent);
    color: var(--bg);
    border: 1px solid var(--accent);
    padding: 10px 18px;
    border-radius: 8px;
    font-family: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s;
  }
  .publish .copy-btn:hover { background: var(--accent-bright); color: var(--bg); }
  .publish .copy-btn.copied {
    background: var(--green);
    border-color: var(--green);
    color: var(--bg);
  }
  .publish .publish-warning {
    background: #2d2412;
    border: 1px solid #a8791e;
    color: #f0c260;
    border-radius: 8px;
    padding: 14px 18px;
    font-size: 13px;
    margin-top: 16px;
  }

  /* Lightbox + video fullscreen — shared across phases */
  .lightbox {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: rgba(0,0,0,0.92);
    align-items: center;
    justify-content: center;
    padding: 40px;
    cursor: zoom-out;
  }
  .lightbox.open { display: flex; }
  .lightbox img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    box-shadow: 0 8px 32px rgba(0,0,0,0.6);
    border: 1px solid var(--border);
  }
  .lightbox .close {
    position: absolute;
    top: 20px;
    right: 32px;
    color: var(--text);
    font-size: 32px;
    cursor: pointer;
    background: none;
    border: none;
  }
  .video-fullscreen {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: #000;
    align-items: center;
    justify-content: center;
  }
  .video-fullscreen.open { display: flex; }
  .video-fullscreen video {
    max-width: 100%;
    max-height: 100%;
    width: auto;
    height: auto;
  }
  .video-fullscreen .close {
    position: absolute;
    top: 16px;
    right: 20px;
    z-index: 2001;
    background: rgba(0,0,0,0.85);
    border: 1px solid rgba(255,255,255,0.3);
    color: #fff;
    font-size: 20px;
    cursor: pointer;
    padding: 8px 14px;
    border-radius: 4px;
  }
</style>
</head>
<body>
<!-- Canonical Adom app-header — see gallia/skills/adom-app-header/SKILL.md.
     Single row + phase tabs row underneath. -->
<header>
  <div class="header-row">
    <div class="brand" data-tooltip-target="app-info" tabindex="0">
      <svg class="logo" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
        <path d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4zm2 2v2h2V6H5zm4 0v12h6V6H9zm8 0v2h2V6h-2zM5 10v2h2v-2H5zm12 0v2h2v-2h-2zM5 14v2h2v-2H5zm12 0v2h2v-2h-2zM5 18v0h2v0H5zm12 0v0h2v0h-2zM11 8v8h2V8h-2z"/>
      </svg>
      <span class="app-name">adom-video-post</span>
      <span class="sep">·</span>
      <span class="app-subtitle">review · narrate · publish</span>
    </div>
    <div class="vsep" aria-hidden="true"></div>
    <div class="subject">
      <span class="subject-name" id="title">Loading…</span>
      <span class="subject-meta" id="stats"></span>
    </div>
  </div>
  <div class="phase-tabs">
    <button class="phase-tab active" data-phase="review">
      <span class="step">1</span>Review
    </button>
    <button class="phase-tab" data-phase="narrate">
      <span class="step">2</span>Narrate
    </button>
    <button class="phase-tab" data-phase="publish">
      <span class="step">3</span>Publish
    </button>
  </div>

  <div class="hover-popover" id="app-info" role="dialog" aria-label="App information">
    <div class="popover-title">adom-video-post</div>
    <div class="popover-row"><span class="k">about</span>
      <span class="v">Per-clip review, voiceover narration, and final concat for Adom feature-tour demos.</span></div>
    <div class="popover-row"><span class="k">version</span><span class="v">v0.3.0</span></div>
    <div class="popover-row"><span class="k">author</span><span class="v">Adom Inc · adom.inc</span></div>
    <div class="popover-row"><span class="k">tts</span>
      <span class="v"><a class="popover-link" href="https://wiki-ufypy5dpx93o.adom.cloud/apps/adom-tts" target="_blank" rel="noopener">adom-tts ↗</a> — house TTS engine + pronunciation overrides shared by every Adom tool that speaks. Mispronounced terms? Add them once there.</span></div>
    <div class="popover-actions">
      <a class="popover-link" href="https://wiki-ufypy5dpx93o.adom.cloud/apps/adom-video-post" target="_blank" rel="noopener">Open wiki ↗</a>
      <a class="popover-link" href="https://github.com/adom-inc/adom-video-post" target="_blank" rel="noopener">GitHub ↗</a>
    </div>
  </div>
</header>

<script>
// SHOW_DELAY = 500 ms per human-ui-patterns §1b; HIDE_GRACE = 220 ms.
(function attachHeaderPopovers(){
  const SHOW_DELAY = 500;
  const HIDE_GRACE = 220;
  const wire = () => {
    document.querySelectorAll('header [data-tooltip-target]').forEach(trigger => {
      if (trigger.__popoverWired) return;
      trigger.__popoverWired = true;
      const target = document.getElementById(trigger.dataset.tooltipTarget);
      if (!target) return;
      let showTimer = null, hideTimer = null;
      const show = () => {
        clearTimeout(hideTimer);
        if (target.classList.contains('show')) return;
        clearTimeout(showTimer);
        showTimer = setTimeout(() => target.classList.add('show'), SHOW_DELAY);
      };
      const queueHide = () => {
        clearTimeout(showTimer);
        clearTimeout(hideTimer);
        hideTimer = setTimeout(() => target.classList.remove('show'), HIDE_GRACE);
      };
      trigger.addEventListener('mouseenter', show);
      trigger.addEventListener('mouseleave', queueHide);
      trigger.addEventListener('focus', show);
      trigger.addEventListener('blur', queueHide);
      target.addEventListener('mouseenter', show);
      target.addEventListener('mouseleave', queueHide);
      trigger.addEventListener('click', e => {
        clearTimeout(showTimer);
        if (target.classList.contains('show')) target.classList.remove('show');
        else target.classList.add('show');
        e.stopPropagation();
      });
    });
  };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);
  else wire();
})();
</script>

<!-- ─── Review phase ─────────────────────────────────────── -->
<section id="phase-review" class="phase-panel active">
  <div id="finals" class="finals" style="display:none;"></div>
  <div id="timeline" class="timeline" style="display:none;"></div>
  <div id="steps" class="steps"></div>
</section>

<!-- ─── Narrate phase ────────────────────────────────────── -->
<section id="phase-narrate" class="phase-panel">
  <div class="narrate">
    <div id="narrateNotReady" class="not-ready" style="display:none;">
      <strong>Narration isn't ready yet.</strong> The manifest has no
      <code>final_fast_path</code> — your sped-up clips need to be
      concatenated into a single video first.
      <div>
        <button id="prepareBtn" class="prepare-btn">Prepare for narration</button>
        <span class="prepare-hint">
          Runs <code>ffmpeg concat</code> over every clip's
          <code>fast_path</code> and writes the result back into the
          manifest. Re-enables this phase as soon as it's done.
        </span>
      </div>
    </div>
    <div class="big-video">
      <video id="narrateVideo" controls preload="metadata" src="voiceover/video"></video>
    </div>
    <div class="narrate-controls">
      <button id="narrateRecord" class="btn-record">Record voiceover</button>
      <button id="narrateDiscard" disabled>Discard &amp; redo</button>
      <button id="narrateFinalize" class="btn-finalize" disabled>Finalize &amp; mux</button>
    </div>
    <div id="narrateStatus" class="status">Click Record voiceover for a 3-2-1 countdown, then the video plays silently while Hydrogen captures your mic. Click Finalize &amp; mux when done.</div>
  </div>
</section>

<!-- ─── Publish phase ────────────────────────────────────── -->
<section id="phase-publish" class="phase-panel">
  <div class="publish">
    <h3>Publish to the Adom wiki</h3>
    <p>
      Fill in the target wiki page and a caption, then copy the
      generated prompt and paste it into Claude Code. Claude will
      use the <span class="inline-code">adom-video-post</span> +
      <span class="inline-code">adom-wiki</span> skills to upload
      the narrated final video.
    </p>
    <form onsubmit="event.preventDefault(); return false;">
      <label>
        <span class="lbl">Wiki page ref</span>
        <input type="text" id="pubPage" placeholder="apps/adom-desktop" value="apps/adom-desktop">
      </label>
      <label>
        <span class="lbl">Caption</span>
        <input type="text" id="pubCaption" placeholder="Fusion 360 deep dive: BQ25792 manufacturing pipeline">
      </label>
      <div class="prompt-box" id="pubPrompt">(fill in the fields above to see the prompt)</div>
      <div class="prompt-actions">
        <button class="copy-btn" id="pubCopyBtn">Copy prompt</button>
        <span id="pubPromptHint" style="color:var(--text3);font-size:11px;"></span>
      </div>
      <div id="pubNoVideo" class="publish-warning" style="display:none;">
        ⚠ No narrated video available yet. Finalize a voiceover in
        the <strong>Narrate</strong> phase first, or paste the prompt
        to Claude Code and it will pick up whatever final video
        exists when it runs.
      </div>
    </form>
  </div>
</section>

<!-- Shared overlays -->
<div id="lightbox" class="lightbox" onclick="closeLightbox(event)">
  <button class="close" onclick="closeLightbox()">&times;</button>
  <img id="lightbox-img" alt="">
</div>
<div id="video-fs" class="video-fullscreen" onclick="closeVideoFs(event)">
  <button class="close" onclick="closeVideoFs()">&times; Close</button>
  <video id="video-fs-player" controls autoplay></video>
</div>

<!-- EVAL_CLIENT -->

<script>
window.__appCtx = window.__appCtx || {};
window.__appCtx.subcommand = 'app';
window.__appCtx.phase = 'review';
window.__appCtx.manifest = null;
window.__appCtx.version = 0;

// ─── console forwarding ──────────────────────────────────
(function() {
  const orig = { log: console.log, warn: console.warn, error: console.error };
  function forward(level, args) {
    orig[level].apply(console, args);
    try {
      fetch('console', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          level: level,
          message: args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' '),
          ts: Date.now(),
        }),
      }).catch(() => {});
    } catch (_) {}
  }
  console.log = function() { forward('log', arguments); };
  console.warn = function() { forward('warn', arguments); };
  console.error = function() { forward('error', arguments); };
})();

function esc(s) {
  return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

// ─── phase selector ──────────────────────────────────────
document.querySelectorAll('.phase-tab').forEach(btn => {
  btn.addEventListener('click', () => {
    const phase = btn.dataset.phase;
    switchPhase(phase);
  });
});
function switchPhase(phase) {
  window.__appCtx.phase = phase;
  document.querySelectorAll('.phase-tab').forEach(b => {
    b.classList.toggle('active', b.dataset.phase === phase);
  });
  document.querySelectorAll('.phase-panel').forEach(p => {
    p.classList.toggle('active', p.id === 'phase-' + phase);
  });
  console.log('phase switched:', phase);
}

// ─── lightbox + fullscreen helpers ───────────────────────
window.closeLightbox = function(e) {
  if (e && e.target && e.target.tagName === 'IMG') return;
  document.getElementById('lightbox').classList.remove('open');
};
window.expandVideo = function(btn) {
  const wrap = btn.parentElement;
  const src = wrap.querySelector('video');
  if (!src) return;
  const fs = document.getElementById('video-fs');
  const player = document.getElementById('video-fs-player');
  player.src = src.getAttribute('src') || src.src;
  player.currentTime = src.currentTime || 0;
  fs.classList.add('open');
};
window.closeVideoFs = function(e) {
  if (e && e.target && e.target.tagName === 'VIDEO') return;
  const fs = document.getElementById('video-fs');
  const player = document.getElementById('video-fs-player');
  player.pause();
  player.src = '';
  fs.classList.remove('open');
};
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') { closeLightbox(); closeVideoFs(); }
});

// ─── Review phase rendering ──────────────────────────────
function renderReview(manifest) {
  const clipCount = manifest.clips.length;
  const totalSpeedups = manifest.clips.reduce((n, c) => n + (c.speedup_events?.length || 0), 0);
  const withFast = manifest.clips.filter(c => c.fast_path).length;
  const parts = [`${clipCount} clip${clipCount === 1 ? '' : 's'}`];
  if (totalSpeedups > 0) parts.push(`${totalSpeedups} speedup${totalSpeedups === 1 ? '' : 's'}`);
  if (withFast > 0) parts.push(`${withFast} sped-up`);
  document.getElementById('stats').innerHTML = parts.map(esc).join('<span class="sep">·</span>');
  document.getElementById('title').textContent = manifest.title || 'Adom Video Post';

  // Finals
  const finalsEl = document.getElementById('finals');
  let finalsHtml = '';
  if (manifest.final_raw_path) {
    finalsHtml += `<div class="card"><h3>Raw concat</h3>
      <div class="video-wrap">
        <button class="expand" onclick="expandVideo(this)">⛶ Expand</button>
        <video controls preload="metadata" src="final-video/raw"></video>
      </div></div>`;
  }
  if (manifest.final_fast_path) {
    finalsHtml += `<div class="card"><h3>Fast concat</h3>
      <div class="video-wrap">
        <button class="expand" onclick="expandVideo(this)">⛶ Expand</button>
        <video controls preload="metadata" src="final-video/fast"></video>
      </div></div>`;
  }
  finalsEl.innerHTML = finalsHtml;
  finalsEl.style.display = finalsHtml ? 'flex' : 'none';

  // Timeline strip
  const timelineEl = document.getElementById('timeline');
  if (clipCount > 0) {
    timelineEl.style.display = 'flex';
    timelineEl.innerHTML = manifest.clips.map((c, i) => {
      const n = i + 1;
      const hasSpeedup = (c.speedup_events || []).length > 0;
      const cls = hasSpeedup ? 'tl-step speedup' : 'tl-step';
      const markHtml = hasSpeedup
        ? `<span class="mark">⚡ ${esc(c.speedup_events[0].speed)}x</span>`
        : '';
      return `<a class="${cls}" href="#clip-${esc(c.id)}">
        <span class="num">${n}</span>
        <span>${esc(c.title || c.id).slice(0, 30)}</span>
        ${markHtml}
      </a>`;
    }).join('');
  } else {
    timelineEl.style.display = 'none';
  }

  // Step cards
  const stepsEl = document.getElementById('steps');
  if (clipCount === 0) {
    stepsEl.innerHTML = `<div style="padding:40px 32px;color:var(--text3);text-align:center;font-family:'JetBrains Mono',monospace;">
      <strong style="color:var(--text);">No clips yet.</strong><br><br>
      Your demo script should call <code>adom-video-post manifest add</code> once per step.<br>
      The UI live-updates as clips are added.
    </div>`;
    return;
  }
  stepsEl.innerHTML = manifest.clips.map((c, i) => renderClip(c, i + 1)).join('');
}

function renderClip(c, n) {
  const speedupPill = (c.speedup_events || []).length > 0
    ? `<span class="speedup-pill">⚡ ${esc(c.speedup_events[0].speed)}x ${esc(c.speedup_events[0].label)}</span>`
    : '';
  const description = c.description ? `<div class="desc">${esc(c.description)}</div>` : '';
  const warnings = (c.warnings || []).length > 0
    ? `<div class="warnings">⚠ ${c.warnings.map(esc).join('<br>⚠ ')}</div>`
    : '';
  const actions = (c.actions || []).length > 0
    ? `<div class="actions-list">${c.actions.map(a => esc(a)).map(a => '• ' + a).join('<br>')}</div>`
    : '';
  let rawBlock = '';
  if (c.raw_path) {
    rawBlock = `<div class="clip-block">
      <div class="clip-label raw">Raw (as recorded)</div>
      <div class="video-wrap">
        <button class="expand" onclick="expandVideo(this)">⛶ Expand</button>
        <video controls preload="metadata" poster="poster/${esc(c.id)}" src="video/${esc(c.id)}/raw"></video>
      </div>
    </div>`;
  }
  let fastBlock = '';
  if (c.fast_path) {
    fastBlock = `<div class="clip-block">
      <div class="clip-label fast">Fast (after speedup)</div>
      <div class="video-wrap">
        <button class="expand" onclick="expandVideo(this)">⛶ Expand</button>
        <video controls preload="metadata" poster="poster/${esc(c.id)}" src="video/${esc(c.id)}/fast"></video>
      </div>
    </div>`;
  }
  return `<div class="step-card" id="clip-${esc(c.id)}">
    <h2><span class="badge">${n}</span>${esc(c.title || c.id)}${speedupPill}</h2>
    ${description}
    ${warnings}
    <div class="clip-row">${rawBlock}${fastBlock}</div>
  </div>`;
}

// ─── Narrate phase ───────────────────────────────────────
const narrateRecord   = document.getElementById('narrateRecord');
const narrateDiscard  = document.getElementById('narrateDiscard');
const narrateFinalize = document.getElementById('narrateFinalize');
const narrateStatus   = document.getElementById('narrateStatus');
const narrateNotReady = document.getElementById('narrateNotReady');
const narrateVideo    = document.getElementById('narrateVideo');
const prepareBtn      = document.getElementById('prepareBtn');

// ─── Prepare for narration: POST /concat ─────────────────
prepareBtn.addEventListener('click', async () => {
  prepareBtn.disabled = true;
  const originalText = prepareBtn.textContent;
  prepareBtn.textContent = 'Concatenating clips…';
  try {
    const r = await fetch('concat', { method: 'POST' });
    const result = await r.json();
    if (result.ok) {
      prepareBtn.textContent = `Concatenated ${result.clipCount} clips ✓`;
      console.log('concat complete:', result.path);
      // The backend bumps the version counter, so the next pollLoop
      // tick will re-fetch /manifest, see the new final_fast_path,
      // and auto-unhide the record button + hide this card.
    } else {
      prepareBtn.textContent = originalText;
      prepareBtn.disabled = false;
      narrateStatus.textContent = 'Concat failed: ' + (result.error || 'unknown');
      console.warn('concat failed:', result);
    }
  } catch (e) {
    prepareBtn.textContent = originalText;
    prepareBtn.disabled = false;
    narrateStatus.textContent = 'Concat failed: ' + e.message;
    console.warn('concat error:', e.message);
  }
});

narrateRecord.addEventListener('click', async () => {
  if (narrateRecord.classList.contains('recording')) {
    // Stop
    narrateVideo.pause();
    narrateRecord.disabled = true;
    narrateStatus.textContent = 'Stopping recording…';
    try {
      const r = await fetch('voiceover/stop-recording', { method: 'POST' });
      const result = await r.json();
      if (result.ok) {
        narrateStatus.textContent = 'Recorded. Click Finalize & mux to produce the narrated video.';
        narrateRecord.classList.remove('recording');
        narrateRecord.textContent = 'Record voiceover';
        narrateRecord.disabled = false;
        narrateFinalize.disabled = false;
        narrateDiscard.disabled = false;
      } else {
        narrateStatus.textContent = 'Stop failed: ' + (result.error || 'unknown');
        narrateRecord.disabled = false;
      }
    } catch (e) {
      narrateStatus.textContent = 'Stop failed: ' + e.message;
      narrateRecord.disabled = false;
    }
  } else {
    // Start
    narrateRecord.disabled = true;
    narrateStatus.textContent = 'Starting recording…';
    try {
      const r = await fetch('voiceover/start-recording', { method: 'POST' });
      const result = await r.json();
      if (result.ok) {
        narrateRecord.classList.add('recording');
        narrateRecord.textContent = 'Stop recording';
        narrateRecord.disabled = false;
        narrateVideo.currentTime = 0;
        narrateVideo.play().catch(e => console.warn('play:', e.message));
        narrateStatus.textContent = 'Recording — narrate while the video plays.';
      } else {
        narrateStatus.textContent = 'Start failed: ' + (result.error || 'unknown');
        narrateRecord.disabled = false;
      }
    } catch (e) {
      narrateStatus.textContent = 'Start failed: ' + e.message;
      narrateRecord.disabled = false;
    }
  }
});
narrateFinalize.addEventListener('click', async () => {
  narrateFinalize.disabled = true;
  narrateStatus.textContent = 'Running ffmpeg mux…';
  try {
    const r = await fetch('voiceover/finalize', { method: 'POST' });
    const result = await r.json();
    if (result.ok) {
      narrateStatus.textContent = 'Done. Final path: ' + (result.rawPath || result.path || 'unknown');
    } else {
      narrateStatus.textContent = 'Finalize failed: ' + (result.error || 'unknown');
    }
  } catch (e) {
    narrateStatus.textContent = 'Finalize failed: ' + e.message;
  }
  narrateFinalize.disabled = false;
});
narrateDiscard.addEventListener('click', async () => {
  await fetch('voiceover/discard', { method: 'POST' }).catch(() => {});
  narrateRecord.classList.remove('recording');
  narrateRecord.textContent = 'Record voiceover';
  narrateFinalize.disabled = true;
  narrateDiscard.disabled = true;
  narrateStatus.textContent = 'Discarded. Ready for a fresh take.';
});

// ─── Publish phase: build a Claude Code prompt ────────────
const pubPage = document.getElementById('pubPage');
const pubCaption = document.getElementById('pubCaption');
const pubPrompt = document.getElementById('pubPrompt');
const pubCopyBtn = document.getElementById('pubCopyBtn');
const pubPromptHint = document.getElementById('pubPromptHint');
const pubNoVideo = document.getElementById('pubNoVideo');

function renderPublishPrompt() {
  const m = window.__appCtx.manifest;
  const title = (m && m.title) ? m.title : 'this demo';
  const finalFast = (m && m.final_fast_path) || '';
  // The voiceover subcommand writes the narrated result to
  // <input>.webm → <input>-narrated.webm per its default naming.
  // Derive that here so Claude's prompt references the actual
  // narrated output rather than the silent concat.
  let narratedPath = '';
  if (finalFast) {
    narratedPath = finalFast.replace(/\.webm$/, '-take-1-norm.webm');
  }
  const page = (pubPage.value || 'apps/adom-desktop').trim();
  const caption = (pubCaption.value || title).trim();

  pubNoVideo.style.display = finalFast ? 'none' : 'block';

  const videoLine = narratedPath
    ? `The narrated final video is at \`${narratedPath}\` (if that's missing, use whatever the latest narrated take is under the same directory).`
    : 'The narrated final video is whatever came out of the Narrate phase of the Adom Video Post app — look in the same directory as the manifest for the most recent `*-narrated*.webm` or `*-take-*-norm.webm`.';

  const prompt = `Publish my narrated "${title}" demo video to the Adom wiki.

${videoLine}

Upload it to the \`${page}\` wiki page with the caption:
"${caption}"

Use the \`adom-video-post\` and \`adom-wiki\` skills. Confirm the upload succeeded by returning the wiki asset URL.`;
  pubPrompt.textContent = prompt;
}

pubPage.addEventListener('input', renderPublishPrompt);
pubCaption.addEventListener('input', renderPublishPrompt);

pubCopyBtn.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText(pubPrompt.textContent);
    pubCopyBtn.textContent = 'Copied ✓';
    pubCopyBtn.classList.add('copied');
    pubPromptHint.textContent = 'Paste it into your Claude Code chat.';
    setTimeout(() => {
      pubCopyBtn.textContent = 'Copy prompt';
      pubCopyBtn.classList.remove('copied');
    }, 2000);
  } catch (e) {
    pubPromptHint.textContent = 'Copy failed: ' + e.message + ' — select the text above manually.';
    console.warn('clipboard.writeText failed:', e.message);
  }
});

// ─── fetch + live watch ──────────────────────────────────
async function fetchManifest() {
  const r = await fetch('manifest');
  if (!r.ok) throw new Error('GET /manifest ' + r.status);
  return r.json();
}
async function fetchState() {
  const r = await fetch('state');
  if (!r.ok) throw new Error('GET /state ' + r.status);
  return r.json();
}

async function pollLoop() {
  let lastFinalFast = null;
  while (true) {
    try {
      const s = await fetchState();
      if (s.version !== window.__appCtx.version) {
        const m = await fetchManifest();
        window.__appCtx.manifest = m;
        window.__appCtx.version = s.version;
        renderReview(m);
        // Narrate phase availability depends on final_fast_path
        const narrateReady = !!m.final_fast_path;
        narrateNotReady.style.display = narrateReady ? 'none' : 'block';
        narrateRecord.disabled = !narrateReady;
        // If final_fast_path just appeared (or changed), tell the
        // narrate video element to reload. Without this, the
        // element keeps its NETWORK_NO_SOURCE state from the
        // initial 404 when the concat hadn't run yet, and clicking
        // Play shows an empty black player.
        if (m.final_fast_path !== lastFinalFast) {
          lastFinalFast = m.final_fast_path;
          if (narrateReady) {
            narrateVideo.load();
            console.log('narrate video reloaded for new final_fast_path:', m.final_fast_path);
          }
        }
        // Re-render the Publish-phase prompt with fresh context
        // (title, derived narrated path, etc.) each time the
        // manifest changes.
        renderPublishPrompt();
        console.log('manifest loaded: version', s.version, 'clips', m.clips.length, 'narrateReady', narrateReady);
      }
    } catch (e) {
      console.warn('pollLoop error:', e.message);
    }
    await new Promise(r => setTimeout(r, 800));
  }
}

window.addEventListener('load', pollLoop);
</script>
</body>
</html>