<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OPHI-118 / onboard</title>
<link rel="stylesheet" href="{{ asset('css/hud.css') }}">
<style>
  /* Onboard wizard is the only page that's not an OBS browser_source — it
     renders in a real desktop browser at arbitrary viewport sizes. So we
     break from the canvas-pixel design language and use a centered
     terminal-card layout with breathing room. */
  body.overlay-body {
    padding: 24px;
    display: grid;
    grid-template-rows: auto 1fr auto;
    gap: 24px;
    min-height: 100vh;
    height: auto;
    overflow: auto;
  }
  .wizard {
    width: min(960px, 100%);
    margin: 0 auto;
    display: grid;
    grid-template-columns: 1fr 360px;
    gap: 22px;
    align-items: start;
  }
  @media (max-width: 800px) {
    .wizard { grid-template-columns: 1fr; }
  }

  .panel {
    position: relative;
    background: var(--term-bg);
    border: 2px solid var(--term-edge);
    border-radius: 4px;
    padding: 28px 30px;
    color: var(--term-fg);
    text-shadow: 0 0 6px var(--term-glow);
    box-shadow:
      0 0 60px rgba(80, 220, 100, 0.10),
      inset 0 0 90px rgba(0, 30, 0, 0.55);
    font-family: var(--mono);
    overflow: visible;
  }
  .panel::after {
    content: '';
    position: absolute; inset: 0;
    pointer-events: none;
    background: repeating-linear-gradient(
      to bottom,
      rgba(0,0,0,0)    0px,
      rgba(0,0,0,0)    2px,
      rgba(0,0,0,0.22) 3px,
      rgba(0,0,0,0.22) 4px
    );
    opacity: 0.45;
    mix-blend-mode: multiply;
    border-radius: 2px;
  }
  .panel > .pane-tab {
    position: absolute;
    top: -14px;
    left: 28px;
    padding: 2px 14px;
    background: var(--bg);
    color: var(--term-fg-bright);
    font-size: 14px;
    letter-spacing: 0.32em;
    text-shadow: 0 0 6px var(--term-glow);
    z-index: 1;
  }
  .panel > * { position: relative; z-index: 0; }

  .form-row {
    display: grid;
    grid-template-columns: 14em 1fr;
    gap: 14px;
    align-items: start;
    margin-bottom: 18px;
  }
  .form-row > label {
    color: var(--term-fg-dim);
    letter-spacing: 0.18em;
    text-transform: uppercase;
    font-size: 14px;
    padding-top: 8px;
  }
  .form-row > .field { min-width: 0; }
  .form-row .hint {
    grid-column: 2;
    color: var(--term-fg-dim);
    font-size: 12px;
    letter-spacing: 0.08em;
    margin-top: 6px;
  }
  .form-row.error > label { color: var(--accent); }
  .form-row.error .field input,
  .form-row.error .field .toggle { border-color: var(--accent); }

  .prompt-line {
    display: flex;
    align-items: center;
    gap: 8px;
    border: 1px solid var(--term-edge);
    background: rgba(0, 30, 0, 0.45);
    border-radius: 3px;
    padding: 8px 12px;
    transition: border-color .15s ease, box-shadow .15s ease;
  }
  .prompt-line:focus-within {
    border-color: var(--term-fg-bright);
    box-shadow: 0 0 0 1px var(--term-fg-bright), 0 0 18px rgba(95, 220, 98, 0.25);
  }
  .prompt-line .caret {
    color: var(--term-fg-bright);
    font-size: 14px;
  }
  .prompt-line input {
    flex: 1 1 auto;
    background: transparent;
    border: 0;
    outline: 0;
    color: var(--term-fg-bright);
    font: inherit;
    font-size: 15px;
    padding: 2px 0;
    text-shadow: 0 0 6px var(--term-glow);
  }
  .prompt-line input::placeholder {
    color: var(--term-fg-dim);
    text-shadow: none;
  }

  /* Twitch search dropdown — appears below the game input. */
  .search-wrap { position: relative; }
  .search-results {
    position: absolute;
    top: calc(100% + 4px);
    left: 0; right: 0;
    background: var(--term-bg);
    border: 1px solid var(--term-edge);
    border-radius: 3px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    z-index: 10;
    max-height: 260px;
    overflow-y: auto;
    display: none;
  }
  .search-results.on { display: block; }
  .search-results .row {
    display: grid;
    grid-template-columns: 36px 1fr auto;
    gap: 12px;
    align-items: center;
    padding: 8px 12px;
    cursor: pointer;
    transition: background .1s ease;
    border-bottom: 1px solid rgba(80, 220, 100, 0.10);
  }
  .search-results .row:last-child { border-bottom: 0; }
  .search-results .row:hover,
  .search-results .row.active {
    background: rgba(95, 220, 98, 0.10);
  }
  .search-results .row img {
    width: 36px;
    height: 48px;
    object-fit: cover;
    background: rgba(0,0,0,0.4);
    border: 1px solid var(--term-edge);
  }
  .search-results .row .name { color: var(--term-fg-bright); }
  .search-results .row .id   { color: var(--term-fg-dim); font-size: 12px; }
  .search-results .row .empty,
  .search-results .row .error { color: var(--term-fg-dim); grid-column: 1 / -1; padding: 4px 0; }
  .search-results .row .error { color: var(--accent); }

  /* Number input — chevron buttons styled like terminal buttons. */
  .stepper {
    display: inline-grid;
    grid-template-columns: 32px 5ch 32px;
    align-items: stretch;
    border: 1px solid var(--term-edge);
    border-radius: 3px;
    overflow: hidden;
    background: rgba(0, 30, 0, 0.45);
  }
  .stepper button {
    background: transparent;
    border: 0;
    color: var(--term-fg-bright);
    font: inherit;
    font-size: 16px;
    cursor: pointer;
    transition: background .12s ease;
  }
  .stepper button:hover { background: rgba(95, 220, 98, 0.15); }
  .stepper input {
    background: transparent;
    border: 0;
    outline: 0;
    color: var(--term-fg-bright);
    font: inherit;
    font-size: 15px;
    text-align: center;
    text-shadow: 0 0 6px var(--term-glow);
    -moz-appearance: textfield;
  }
  .stepper input::-webkit-outer-spin-button,
  .stepper input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

  /* Toggle for camera / microphone — labeled ENABLED / DISABLED, swaps color. */
  .toggle {
    display: inline-flex;
    border: 1px solid var(--term-edge);
    border-radius: 3px;
    overflow: hidden;
    background: rgba(0, 30, 0, 0.45);
    user-select: none;
  }
  .toggle button {
    background: transparent;
    border: 0;
    color: var(--term-fg-dim);
    font: inherit;
    font-size: 14px;
    letter-spacing: 0.18em;
    padding: 8px 16px;
    cursor: pointer;
    transition: background .12s ease, color .12s ease;
  }
  .toggle button.on {
    background: rgba(95, 220, 98, 0.18);
    color: var(--term-fg-bright);
    text-shadow: 0 0 6px var(--term-glow);
  }
  .toggle button.on.off-state {
    background: rgba(230, 58, 46, 0.18);
    color: var(--accent);
    text-shadow: 0 0 6px rgba(230, 58, 46, 0.6);
  }

  /* Twitch push checkbox — boxy custom-styled. */
  .check-row {
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--term-fg);
  }
  .check-row input[type=checkbox] { display: none; }
  .check-row .checkbox {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px; height: 18px;
    border: 1px solid var(--term-edge);
    border-radius: 2px;
    background: rgba(0, 30, 0, 0.45);
    font-size: 14px;
    color: var(--term-fg-bright);
    cursor: pointer;
    transition: border-color .12s ease;
  }
  .check-row.on .checkbox { border-color: var(--term-fg-bright); }
  .check-row.on .checkbox::before { content: '✓'; }
  .check-row.disabled { color: var(--term-fg-dim); }
  .check-row.disabled .checkbox { cursor: not-allowed; opacity: 0.5; }

  /* Submit row — primary action plus a small "save only" secondary. */
  .actions {
    display: flex;
    justify-content: flex-end;
    gap: 14px;
    margin-top: 8px;
  }
  .btn {
    background: rgba(0, 30, 0, 0.55);
    color: var(--term-fg-bright);
    font: inherit;
    font-size: 14px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    padding: 12px 22px;
    border: 1px solid var(--term-fg-bright);
    border-radius: 3px;
    cursor: pointer;
    text-shadow: 0 0 6px var(--term-glow);
    box-shadow: 0 0 14px rgba(95, 220, 98, 0.12);
    transition: background .12s ease, box-shadow .12s ease;
  }
  .btn:hover {
    background: rgba(95, 220, 98, 0.18);
    box-shadow: 0 0 22px rgba(95, 220, 98, 0.28);
  }
  .btn.secondary {
    color: var(--term-fg);
    border-color: var(--term-edge);
    box-shadow: none;
  }
  .btn:disabled {
    color: var(--term-fg-dim);
    border-color: var(--term-edge);
    box-shadow: none;
    cursor: not-allowed;
    background: rgba(0, 30, 0, 0.3);
  }
  .btn .spinner {
    display: inline-block;
    width: 1ch;
    text-align: center;
    margin-right: 6px;
  }

  /* Preview pane (right column) renders the same lines the loading
     overlay would type, so the streamer can see exactly what will show up. */
  .preview-pane .terminal-mock {
    background: rgba(0, 0, 0, 0.4);
    border: 1px solid var(--term-edge);
    border-radius: 3px;
    padding: 14px 16px;
    font-size: 13px;
    line-height: 1.55;
    min-height: 240px;
    color: var(--term-fg);
    text-shadow: 0 0 6px var(--term-glow);
  }
  .preview-pane .terminal-mock .prompt { color: var(--term-fg-bright); }
  .preview-pane .terminal-mock .dim    { color: var(--term-fg-dim); }
  .preview-pane .terminal-mock .gap    { height: 0.5em; }
  .preview-pane h3 {
    color: var(--hud);
    letter-spacing: 0.22em;
    font-size: 13px;
    margin-bottom: 12px;
    text-transform: uppercase;
  }
  .preview-pane .footnote {
    margin-top: 14px;
    font-size: 12px;
    color: var(--term-fg-dim);
    letter-spacing: 0.08em;
  }
  .preview-pane .footnote a { color: var(--hud); text-decoration: none; }
  .preview-pane .footnote a:hover { text-decoration: underline; }

  /* Toast / result line shown after submit. */
  #toast {
    margin-top: 12px;
    padding: 12px 14px;
    border: 1px solid var(--term-edge);
    border-radius: 3px;
    background: rgba(0, 30, 0, 0.45);
    color: var(--term-fg);
    font-size: 13px;
    display: none;
  }
  #toast.on   { display: block; }
  #toast.err  { border-color: var(--accent); color: var(--accent); }
  #toast.ok   { border-color: var(--term-fg-bright); }
  #toast pre  { white-space: pre-wrap; margin: 6px 0 0; color: var(--term-fg-dim); }

  /* Header strip mirrors the on-stream HUD chrome. */
  .hud {
    font-size: 16px;
    padding: 0 4px;
  }
  .hud .group { gap: 18px; }

  /* Footer note. */
  .foot-note {
    text-align: center;
    color: var(--term-fg-dim);
    font-size: 12px;
    letter-spacing: 0.18em;
  }
</style>
</head>
<body class="overlay-body">

<header class="hud hud--onair">
  <div class="group">
    <span><span class="led"></span>ONBOARD</span>
    <span>OPHI-118 // PROJECT MANIFEST</span>
  </div>
  <div class="group">
    <span class="dim">RIG</span>
    <span id="rig-name">{{ $telemetry['rig'] ?? '—' }}</span>
  </div>
  <div class="group">
    <span class="dim">UTC</span>
    <span id="clock">--:--:--</span>
  </div>
</header>

<main class="wizard">
  <section class="panel" id="form-panel">
    <span class="pane-tab">┤ PROJECT LOAD ├</span>

    <form id="onboard-form" novalidate>
      <div class="form-row" id="row-game">
        <label for="f-game">Game</label>
        <div class="field search-wrap">
          <div class="prompt-line">
            <span class="caret">▶</span>
            <input
              type="text"
              id="f-game"
              name="game"
              autocomplete="off"
              spellcheck="false"
              placeholder="start typing — e.g. Half-Life 2"
              value="{{ $manifest['game'] ?? '' }}"
              data-game-id="{{ $manifest['gameId'] ?? '' }}"
            >
          </div>
          <div class="search-results" id="search-results" role="listbox"></div>
          <div class="hint" id="game-hint">
            @if (!$twitchEnabled)
              <span style="color: var(--warn);">⚠ TWITCH_CLIENT_ID / TWITCH_TOKEN not set — game search disabled. Manifest will still save locally.</span>
            @else
              Twitch search runs as you type. Pick the matching category to lock the game id.
            @endif
          </div>
        </div>
      </div>

      <div class="form-row">
        <label for="f-subtitle">Subtitle</label>
        <div class="field">
          <div class="prompt-line">
            <span class="caret">▶</span>
            <input
              type="text"
              id="f-subtitle"
              name="subtitle"
              autocomplete="off"
              spellcheck="false"
              placeholder="mode / episode / note (optional)"
              value="{{ $manifest['subtitle'] ?? '' }}"
            >
          </div>
          <div class="hint">Becomes the Twitch stream title when "push to Twitch" is on (else falls back to the game name).</div>
        </div>
      </div>

      <div class="form-row">
        <label for="f-countdown">Countdown</label>
        <div class="field">
          <div class="stepper" role="group" aria-label="countdown minutes">
            <button type="button" data-step="-1" aria-label="−1">−</button>
            <input
              type="number" id="f-countdown" name="countdownMin" min="0" max="120"
              value="{{ $manifest['countdownMin'] ?? 5 }}"
            >
            <button type="button" data-step="+1" aria-label="+1">+</button>
          </div>
          <span style="margin-left: 12px; color: var(--term-fg-dim); letter-spacing: 0.12em;">minutes</span>
          <div class="hint">"Starting in MM:00 — Transmission Incoming" countdown on the Project Loading scene.</div>
        </div>
      </div>

      <div class="form-row">
        <label>Camera</label>
        <div class="field">
          <div class="toggle" data-toggle="camera"
               data-value="{{ ($manifest['camera'] ?? true) ? 'true' : 'false' }}">
            <button type="button" data-val="true" class="{{ ($manifest['camera'] ?? true) ? 'on' : '' }}">ENABLED</button>
            <button type="button" data-val="false" class="off-state {{ ($manifest['camera'] ?? true) ? '' : 'on' }}">DISABLED</button>
          </div>
          <input type="hidden" name="camera" value="{{ ($manifest['camera'] ?? true) ? '1' : '0' }}">
        </div>
      </div>

      <div class="form-row">
        <label>Microphone</label>
        <div class="field">
          <div class="toggle" data-toggle="microphone"
               data-value="{{ ($manifest['microphone'] ?? true) ? 'true' : 'false' }}">
            <button type="button" data-val="true" class="{{ ($manifest['microphone'] ?? true) ? 'on' : '' }}">ENABLED</button>
            <button type="button" data-val="false" class="off-state {{ ($manifest['microphone'] ?? true) ? '' : 'on' }}">DISABLED</button>
          </div>
          <input type="hidden" name="microphone" value="{{ ($manifest['microphone'] ?? true) ? '1' : '0' }}">
        </div>
      </div>

      <div class="form-row">
        <label>Push to Twitch</label>
        <div class="field">
          <label class="check-row {{ $twitchEnabled ? 'on' : 'disabled' }}" id="push-row">
            <input type="checkbox" name="pushTwitch" {{ $twitchEnabled ? 'checked' : '' }} {{ $twitchEnabled ? '' : 'disabled' }}>
            <span class="checkbox"></span>
            <span>Update channel title + category via Helix on save</span>
          </label>
          <div class="hint">Uses TWITCH_CLIENT_ID / TWITCH_TOKEN with the channel:manage:broadcast scope.</div>
        </div>
      </div>

      <div class="actions">
        <button type="submit" class="btn" id="submit-btn">
          <span class="spinner" id="spinner"></span>WRITE MANIFEST
        </button>
      </div>

      <div id="toast" role="status" aria-live="polite"></div>
    </form>
  </section>

  <aside class="panel preview-pane">
    <span class="pane-tab">┤ PREVIEW ├</span>
    <h3>terminal output (loading scene)</h3>
    <div class="terminal-mock" id="preview"></div>
    <div class="footnote">
      Rendered live as you edit. Mirrors the
      <a href="/loading" target="_blank">/loading</a> overlay's terminal block.
      Tracks in library: <strong>{{ $trackCount }}</strong>.
    </div>
  </aside>
</main>

<footer class="foot-note">
  saves to webapp/storage/data/loading.json · OBS browser_source refresh-cache picks it up
</footer>

<script src="{{ asset('js/hud.js') }}"></script>
<script>
  'use strict';

  // ─── small utilities ───────────────────────────────────────────────────
  const $ = sel => document.querySelector(sel);
  const $$ = sel => Array.from(document.querySelectorAll(sel));
  const debounce = (fn, ms) => {
    let t;
    return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
  };

  // ─── form state ────────────────────────────────────────────────────────
  const form        = $('#onboard-form');
  const gameInput   = $('#f-game');
  const subInput    = $('#f-subtitle');
  const cdInput     = $('#f-countdown');
  const camHidden   = form.querySelector('input[name="camera"]');
  const micHidden   = form.querySelector('input[name="microphone"]');
  const pushInput   = form.querySelector('input[name="pushTwitch"]');
  const submitBtn   = $('#submit-btn');
  const spinner     = $('#spinner');
  const toast       = $('#toast');
  const preview     = $('#preview');
  const resultsEl   = $('#search-results');
  const rigName     = $('#rig-name').textContent || 'UNKNOWN';

  const TWITCH_ENABLED = @json($twitchEnabled);

  // ─── toggle (camera / microphone) ──────────────────────────────────────
  $$('.toggle').forEach(group => {
    const hidden = form.querySelector(`input[name="${group.dataset.toggle}"]`);
    group.querySelectorAll('button').forEach(btn => {
      btn.addEventListener('click', () => {
        const val = btn.dataset.val;
        group.dataset.value = val;
        group.querySelectorAll('button').forEach(b => {
          b.classList.toggle('on', b.dataset.val === val);
        });
        hidden.value = val === 'true' ? '1' : '0';
        renderPreview();
      });
    });
  });

  // ─── stepper (countdown) ───────────────────────────────────────────────
  $$('.stepper button').forEach(btn => {
    btn.addEventListener('click', () => {
      const step = parseInt(btn.dataset.step, 10) || 0;
      const cur = parseInt(cdInput.value, 10) || 0;
      cdInput.value = Math.max(0, Math.min(120, cur + step));
      renderPreview();
    });
  });
  cdInput.addEventListener('input', renderPreview);
  subInput.addEventListener('input', renderPreview);

  // ─── push-to-twitch checkbox ───────────────────────────────────────────
  const pushRow = $('#push-row');
  pushInput.addEventListener('change', () => {
    pushRow.classList.toggle('on', pushInput.checked);
  });

  // ─── twitch search ─────────────────────────────────────────────────────
  let activeIdx = -1;
  let matches = [];

  function setGameId(id) {
    gameInput.dataset.gameId = id || '';
  }
  function clearGameId() {
    if (gameInput.dataset.gameId) setGameId('');
  }

  function renderMatches(rows) {
    matches = rows;
    activeIdx = -1;
    if (!rows.length) {
      resultsEl.innerHTML = '<div class="row"><span class="empty">no matches</span></div>';
      resultsEl.classList.add('on');
      return;
    }
    resultsEl.innerHTML = rows.map((r, i) => {
      const box = (r.boxArt || '').replace(/-{width}x{height}/, '-72x96');
      return `
        <div class="row" data-i="${i}" role="option">
          ${box ? `<img src="${box}" alt="">` : '<span></span>'}
          <div>
            <div class="name">${escapeHtml(r.name)}</div>
            <div class="id">id ${escapeHtml(r.id)}</div>
          </div>
          <span class="dim" style="color: var(--term-fg-dim); font-size: 11px;">↵ select</span>
        </div>
      `;
    }).join('');
    resultsEl.classList.add('on');
    resultsEl.querySelectorAll('.row').forEach(row => {
      row.addEventListener('click', () => pickMatch(parseInt(row.dataset.i, 10)));
    });
  }

  function pickMatch(i) {
    const m = matches[i];
    if (!m) return;
    gameInput.value = m.name;
    setGameId(m.id);
    resultsEl.classList.remove('on');
    renderPreview();
  }

  function escapeHtml(s) {
    return String(s).replace(/[&<>"']/g, c => ({
      '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
    })[c]);
  }

  const runSearch = debounce(async (q) => {
    if (!TWITCH_ENABLED) { resultsEl.classList.remove('on'); return; }
    if (q.length < 2) { resultsEl.classList.remove('on'); return; }
    try {
      const r = await fetch(`/onboard/search?q=${encodeURIComponent(q)}`);
      if (!r.ok) {
        const err = await r.json().catch(() => ({}));
        resultsEl.innerHTML = `<div class="row"><span class="error">${escapeHtml(err.error || 'search failed')}</span></div>`;
        resultsEl.classList.add('on');
        return;
      }
      const body = await r.json();
      renderMatches(body.matches || []);
    } catch (e) {
      resultsEl.innerHTML = `<div class="row"><span class="error">${escapeHtml(e.message)}</span></div>`;
      resultsEl.classList.add('on');
    }
  }, 220);

  gameInput.addEventListener('input', () => {
    clearGameId();
    runSearch(gameInput.value.trim());
    renderPreview();
  });
  gameInput.addEventListener('keydown', (e) => {
    if (!resultsEl.classList.contains('on')) return;
    const rows = resultsEl.querySelectorAll('.row[data-i]');
    if (!rows.length) return;
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      activeIdx = (activeIdx + 1) % rows.length;
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      activeIdx = (activeIdx - 1 + rows.length) % rows.length;
    } else if (e.key === 'Enter' && activeIdx >= 0) {
      e.preventDefault();
      pickMatch(activeIdx);
      return;
    } else if (e.key === 'Escape') {
      resultsEl.classList.remove('on');
      return;
    } else {
      return;
    }
    rows.forEach((r, i) => r.classList.toggle('active', i === activeIdx));
  });
  document.addEventListener('click', (e) => {
    if (!e.target.closest('.search-wrap')) resultsEl.classList.remove('on');
  });

  // ─── live preview ──────────────────────────────────────────────────────
  function renderPreview() {
    const game = gameInput.value.trim();
    const sub  = subInput.value.trim();
    const cd   = parseInt(cdInput.value, 10) || 0;
    const cam  = camHidden.value === '1';
    const mic  = micHidden.value === '1';
    const onOff = b => b ? 'ENABLED' : 'DISABLED';
    const pad  = n => String(n).padStart(2, '0');
    const rig  = (rigName || '?').toUpperCase();
    const PROMPT = 'OPHI-118://> ';

    const rows = [
      { prompt: PROMPT, text: 'loadproject --manifest' },
      { text: `detected rig :: ${rig} - initializing transmission...` },
      { gap: true },
      { text: `target       :: ${(game || '—').toUpperCase()}` },
      ...(sub ? [{ text: `mode         :: ${sub.toUpperCase()}` }] : []),
      { text: `camera       :: ${onOff(cam)}` },
      { text: `microphone   :: ${onOff(mic)}` },
      { text: `countdown    :: ${pad(cd)}:00` },
      { gap: true },
      { text: 'all systems nominal', dim: true },
      { text: 'READY.' },
    ];
    preview.innerHTML = rows.map(r => {
      if (r.gap) return '<div class="gap"></div>';
      const cls = r.dim ? ' dim' : '';
      if (r.prompt) {
        return `<div><span class="prompt">${escapeHtml(r.prompt)}</span><span class="${cls.trim()}">${escapeHtml(r.text)}</span></div>`;
      }
      return `<div><span class="prompt">&gt; </span><span class="${cls.trim()}">${escapeHtml(r.text)}</span></div>`;
    }).join('');
  }

  // ─── submit ────────────────────────────────────────────────────────────
  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    toast.classList.remove('on', 'ok', 'err');
    submitBtn.disabled = true;
    spinner.textContent = '⟳';
    const payload = {
      game:         gameInput.value.trim(),
      gameId:       gameInput.dataset.gameId || null,
      subtitle:     subInput.value.trim() || null,
      countdownMin: parseInt(cdInput.value, 10) || 0,
      camera:       camHidden.value === '1',
      microphone:   micHidden.value === '1',
      pushTwitch:   pushInput.checked,
    };
    try {
      const r = await fetch('/onboard', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
        body: JSON.stringify(payload),
      });
      const body = await r.json().catch(() => ({}));
      if (!r.ok) {
        const msg = body.message
          ? body.message + (body.errors ? '\n' + JSON.stringify(body.errors, null, 2) : '')
          : `HTTP ${r.status}`;
        toast.className = 'on err';
        toast.innerHTML = `✗ submit failed<pre>${escapeHtml(msg)}</pre>`;
        return;
      }
      const lines = [`✓ wrote ${body.path}`];
      if (body.twitch) lines.push(`twitch: ${body.twitch}`);
      lines.push('→ refresh the Project Loading browser source in OBS.');
      toast.className = 'on ok';
      toast.innerHTML = `<strong>manifest saved</strong><pre>${escapeHtml(lines.join('\n'))}</pre>`;
    } catch (err) {
      toast.className = 'on err';
      toast.innerHTML = `✗ network error<pre>${escapeHtml(err.message)}</pre>`;
    } finally {
      submitBtn.disabled = false;
      spinner.textContent = '';
    }
  });

  // ─── boot ──────────────────────────────────────────────────────────────
  renderPreview();
  pushRow.classList.toggle('on', pushInput.checked);
</script>
</body>
</html>
