123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Adom Desktop Recorder</title>
  <!--
    Brand: Adom. Sources reconciled:
      • gallia/skills/brand/SKILL.md      — operational rules + canonical font URLs
      • gallia/brand/BRAND-GUIDELINES.md  — fonts, palette, weights
      • gallia/viewer/adom-theme.js       — token names/values, button + card patterns

    Fonts: Familjen Grotesk + Satoshi from adom.inc (canonical Adom CDN per
    SKILL.md), JetBrains Mono from Google Fonts (per SKILL.md font-sources
    table). All adom.inc URLs verified to return 200.

    Icons: monochrome white SVGs in icons/ directory per the SKILL.md rule
    "all icons must be monochrome white". No emoji, no multi-color icons.

    App icon: recorder-icon.svg — a red variant of favicon-color.svg, used
    instead of the regular Adom teal favicon so the recorder window is
    visually distinct from the main Adom Desktop app in the Windows taskbar.
  -->
  <!--
    v1.8.131: crisp raster favicon. The recorder runs as a Chrome `--app`
    window, whose Windows taskbar icon is derived from this favicon. An
    SVG-only favicon (the previous `recorder-icon.svg` link) gets rasterized
    by Chrome at a small fixed size and then upscaled by the taskbar → blurry.
    A multi-size `.ico` (16→256, incl. a 256 entry) + a 256px PNG give the
    taskbar a sharp bitmap at every DPI. `recorder-icon.svg` remains the
    source of truth; `recorder-icon.ico` / `recorder-icon-256.png` are
    generated from it (the red record-state variant) and ship in the bundle.
  -->
  <link rel="icon" type="image/x-icon" href="recorder-icon.ico">
  <link rel="icon" type="image/png" sizes="256x256" href="recorder-icon-256.png">
  <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-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: 'Satoshi';
      src: url('https://adom.inc/fonts/satoshi/satoshi-700-normal.woff2') format('woff2');
      font-weight: 700; font-display: swap;
    }
    @font-face {
      font-family: 'JetBrains Mono';
      src: url('https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPx3cwhsk.woff2') format('woff2');
      font-weight: 400; font-display: swap;
    }
    @font-face {
      font-family: 'JetBrains Mono';
      src: url('https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxTcwhsk.woff2') format('woff2');
      font-weight: 500; font-display: swap;
    }
  </style>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    /* Semantic tokens follow gallia/viewer/adom-theme.js naming (--gv-*).
       Same names = future-proof if recorder is later swapped to import
       from adom-theme.js directly. */
    :root {
      --gv-bg:           #0d1117;
      --gv-bg-surface:   #161b22;
      --gv-bg-elevated:  #1c2128;
      --gv-bg-overlay:   #21262d;
      --gv-border:       #30363d;
      --gv-border-muted: #21262d;
      --gv-border-accent: rgba(0, 184, 176, 0.3);
      --gv-text:         #e6edf3;
      --gv-text-secondary: #8b949e;
      --gv-text-muted:   #484f58;
      --gv-accent:       #00b8b0;
      --gv-accent-bright: #00e6dc;
      --gv-accent-muted: rgba(0, 184, 176, 0.12);
      --gv-accent-border: rgba(0, 184, 176, 0.25);
      --gv-danger:       #f85149;     /* adom-theme.js semantic danger */
      --gv-pin-power:    #ef5350;     /* used for the REC pulse dot per brand pin-power red */
      --gv-radius-sm:    4px;
      --gv-radius-md:    6px;
      --gv-radius-lg:    8px;
      /* Brand fonts. Per BRAND-GUIDELINES.md weights:
         Familjen Grotesk: headlines (Bold 700) or sub-headlines (Medium 500)
         Satoshi: body (Regular 400 or Medium 500)
         JetBrains Mono: code/monospace (per SKILL.md font-sources table) */
      --gv-font:         'Satoshi', system-ui, -apple-system, sans-serif;
      --gv-font-display: 'Familjen Grotesk', 'Satoshi', system-ui, sans-serif;
      --gv-font-mono:    'JetBrains Mono', 'SF Mono', 'Cascadia Code', Consolas, monospace;
    }
    html, body {
      min-height: 100%;
      background: var(--gv-bg);
      color: var(--gv-text);
      font-family: var(--gv-font);
      font-weight: 400;            /* Satoshi Regular — body default per BRAND-GUIDELINES.md */
      font-size: 13px;
      user-select: none;
      overflow: auto;              /* graceful degradation on user-resize */
    }
    body {
      padding: 14px 16px;
      display: flex; flex-direction: column; gap: 12px;
      min-height: 100vh;
    }
    /* min-height: 0 lets flex children shrink below their content size
       (CSS default for flex items is min-height: auto). Without this, a
       tall child like an oversized SVG forces the column to grow past
       the viewport and pushes later siblings — Stop button, action row —
       below the fold. */
    .state.active {
      flex: 1; display: flex; flex-direction: column; gap: 12px;
      min-height: 0; min-width: 0;
    }
    /* Action controls + their wrappers must NEVER shrink — they're the
       primary affordance and clipping them is the worst-case failure. */
    .btn, .source-card, .rec-bar, .actions, .saved-info { flex-shrink: 0; }

    .header {
      display: flex; align-items: center; gap: 10px;
      font-family: var(--gv-font-display);   /* Familjen Grotesk Bold */
      font-weight: 700; font-size: 14.5px;
      letter-spacing: -0.012em;              /* Familjen Grotesk reads tighter at headline weight */
    }
    /* `.logo` (NOT scoped to `.header`) so the constraint applies wherever
       the logo appears. Earlier this was `.header .logo`, which meant the
       same <img class="logo"> placed inside `.rec-bar` got no sizing rule
       — SVGs with viewBox but no intrinsic px size then render at the
       browser default (300×150 stretched to flex parent), dominating the
       Recording state and pushing the Stop button below the fold. */
    .logo {
      width: 22px; height: 22px;
      flex-shrink: 0;
      border-radius: 5px;
      box-shadow: 0 0 0 1px var(--gv-border);
    }
    .reason {
      color: var(--gv-text-secondary); font-size: 11.5px;
      line-height: 1.45; word-wrap: break-word;
    }
    .reason b { color: var(--gv-text); font-weight: 500; }
    .divider { height: 1px; background: var(--gv-border); }
    .helper { color: var(--gv-text-secondary); font-size: 11.5px; line-height: 1.45; }
    .helper code {
      font-family: var(--gv-font-mono); font-size: 11px;
      color: var(--gv-text); background: var(--gv-bg-overlay);
      padding: 1px 5px; border-radius: var(--gv-radius-sm);
    }
    .label-section {
      font-family: var(--gv-font-display); font-size: 10px;
      font-weight: 700; letter-spacing: 0.06em;
      color: var(--gv-text-secondary); margin-top: 2px;
    }

    /* Source-cards follow the .card pattern from gallia/viewer/adom-theme.js
       cardCSS(): bg-surface, border, radius-lg (8px). Hover lifts to elevated
       + accent border. */
    .source-card {
      background: var(--gv-bg-surface);
      border: 1px solid var(--gv-border);
      border-radius: var(--gv-radius-lg);
      padding: 12px 14px;
      display: flex; align-items: center; gap: 12px;
      cursor: pointer;
      transition: all 0.15s;
    }
    .source-card:hover {
      border-color: var(--gv-accent);
      background: var(--gv-bg-elevated);
    }
    /* Monochrome white SVG icons (per SKILL.md "all icons must be monochrome
       white"). currentColor on the SVG path inherits this color. */
    .source-card .icon {
      width: 18px; height: 18px; flex-shrink: 0;
      color: var(--gv-text-secondary);
      transition: color 0.15s;
    }
    .source-card:hover .icon { color: var(--gv-text); }
    .source-card .icon svg { width: 100%; height: 100%; display: block; }
    .source-card .label { flex: 1; }
    .source-card .label .name {
      font-family: var(--gv-font-display); font-weight: 500; font-size: 13px;
    }
    .source-card .label .sub { color: var(--gv-text-secondary); font-size: 11px; margin-top: 1px; }
    .badge {
      font-family: var(--gv-font-display); font-size: 10px;
      font-weight: 700; letter-spacing: 0.05em;
      padding: 3px 7px; border-radius: var(--gv-radius-sm);
    }
    /* ACTIVE: brand teal accent (matches toolbarCSS button.active) */
    .badge-active { background: var(--gv-accent-muted); color: var(--gv-accent-bright); }
    .badge-off    { background: var(--gv-bg-overlay);  color: var(--gv-text-muted); }

    /* Buttons follow gallia/viewer/adom-theme.js buttonCSS() ghost pattern:
       transparent surface, secondary text, border. Hover bumps border + text.
       transitions all 0.15s (per SKILL.md "always 0.15s ease"). */
    .btn {
      background: var(--gv-bg-surface); color: var(--gv-text-secondary);
      border: 1px solid var(--gv-border);
      border-radius: var(--gv-radius-md);
      padding: 10px 12px; cursor: pointer;
      font-family: var(--gv-font-display);
      font-size: 13px; font-weight: 500;
      width: 100%;
      display: flex; align-items: center; justify-content: center; gap: 8px;
      transition: all 0.15s;
    }
    .btn:hover {
      background: var(--gv-bg-elevated);
      border-color: var(--gv-text-secondary);
      color: var(--gv-text);
    }
    /* Start/Stop = danger action (REC red). Same ghost pattern with
       brand danger color tones. */
    .btn-start, .btn-stop {
      color: var(--gv-danger);
      border-color: rgba(248, 81, 73, 0.35);
    }
    .btn-start:hover, .btn-stop:hover {
      background: rgba(248, 81, 73, 0.10);
      border-color: var(--gv-danger);
      color: var(--gv-danger);
    }
    .btn .dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
    .btn .square { width: 8px; height: 8px; background: currentColor; }

    /* State 2 — Recording */
    .rec-bar {
      display: flex; align-items: center; gap: 10px;
      font-family: var(--gv-font-display);
      font-size: 14px; font-weight: 700;
    }
    .rec-bar .rec-dot {
      width: 10px; height: 10px; border-radius: 50%;
      background: var(--gv-pin-power);   /* brand pin-power red, slightly warmer */
      animation: pulse 1.4s ease-in-out infinite;
      box-shadow: 0 0 8px var(--gv-pin-power);
    }
    .rec-bar .duration {
      font-family: var(--gv-font-mono); font-weight: 500; letter-spacing: 0.04em;
    }
    .rec-bar .size {
      margin-left: auto; color: var(--gv-text-secondary);
      font-family: var(--gv-font-mono); font-weight: 400; font-size: 11px;
    }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50%      { opacity: 0.4; }
    }

    /* State 3 — Saved */
    .saved-header {
      display: flex; align-items: center; gap: 8px;
      color: var(--gv-accent-bright);
      font-family: var(--gv-font-display);
      font-weight: 700; font-size: 14px;
    }
    .saved-info { font-size: 11.5px; color: var(--gv-text-secondary); line-height: 1.5; }
    .saved-info code {
      font-family: var(--gv-font-mono); font-size: 11px; color: var(--gv-text);
    }

    .actions { display: flex; gap: 8px; }
    .actions .btn { flex: 1; }

    /* Hide-by-default */
    .state { display: none; flex-direction: column; gap: 12px; }
    .state.active { display: flex; }
  </style>
</head>
<body>
  <!-- ================ STATE 1 — READY ================ -->
  <div id="state-ready" class="state active">
    <div class="header">
      <img class="logo" src="recorder-icon.svg" alt="Adom Desktop Recorder" />
      <span>Adom Desktop Recorder</span>
    </div>
    <div class="reason"><b>Reason:</b> <span id="reason-ready">—</span></div>
    <div class="divider"></div>
    <div class="helper">Ready to record. Toggle sources below, or wait for Claude.</div>

    <div class="label-section">SOURCES</div>
    <div class="source-card">
      <span class="icon">
        <!-- Monitor icon (MDI 'monitor' equivalent), monochrome white per brand rule -->
        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z"/></svg>
      </span>
      <div class="label">
        <div class="name">Screen</div>
        <div class="sub">Entire desktop</div>
      </div>
      <span class="badge badge-active">ACTIVE</span>
    </div>
    <div class="source-card">
      <span class="icon">
        <!-- Microphone icon (MDI 'microphone' equivalent), monochrome white per brand rule -->
        <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2c-1.66 0-3 1.34-3 3v6c0 1.66 1.34 3 3 3s3-1.34 3-3V5c0-1.66-1.34-3-3-3zm5.91 9c-.49 0-.9.36-.98.85C16.52 14.2 14.47 16 12 16s-4.52-1.8-4.93-4.15c-.08-.49-.49-.85-.98-.85-.61 0-1.09.54-1 1.14.49 3 2.89 5.35 5.91 5.78V20c0 .55.45 1 1 1s1-.45 1-1v-2.08c3.02-.43 5.42-2.78 5.91-5.78.1-.6-.39-1.14-1-1.14z"/></svg>
      </span>
      <div class="label">
        <div class="name">Microphone</div>
        <div class="sub" id="mic-sub">Not enabled</div>
      </div>
      <span class="badge badge-off" id="mic-badge">OFF</span>
    </div>

    <div class="helper" id="clip-summary" style="display:none">Clips this session: <b id="clip-count">0</b><br>Last: <code id="last-clip">—</code></div>

    <button class="btn btn-start" id="start-btn">
      <span class="dot"></span>
      <span>Start recording</span>
    </button>
  </div>

  <!-- ================ STATE 2 — RECORDING ================ -->
  <div id="state-recording" class="state">
    <!-- No logo here: the OS title-bar favicon provides brand identity
         in this compact state, and the REC pulse dot is the dominant
         indicator. Adding the SVG logo here was the bug — it had no
         applied size and dominated the layout. -->
    <div class="rec-bar">
      <span class="rec-dot"></span>
      <span>REC</span>
      <span class="duration" id="duration-el">0:00</span>
      <span class="size" id="size-el">0 KB</span>
    </div>
    <div class="reason" id="reason-recording">—</div>
    <div class="divider"></div>
    <div class="helper">Recording entire desktop. Claude can call <code>desktop_record_stop</code>, or click:</div>
    <button class="btn btn-stop" id="stop-btn">
      <span class="square"></span>
      <span>Stop recording</span>
    </button>
  </div>

  <!-- ================ STATE 3 — SAVED ================ -->
  <div id="state-saved" class="state">
    <div class="saved-header">
      <span>✓</span>
      <span>Clip saved</span>
    </div>
    <div class="reason" id="reason-saved">—</div>
    <div class="divider"></div>
    <div class="saved-info">
      <code id="saved-name">—</code><br>
      <span id="saved-meta">—</span>
    </div>
    <div class="actions">
      <button class="btn" id="open-folder-btn">Open folder</button>
      <button class="btn" id="another-btn">Record another</button>
    </div>
    <div class="helper" id="auto-ready">Auto-ready in <b id="auto-ready-count">3</b>…</div>
  </div>

<script>
// ─── State & DOM ──────────────────────────────────────────────────────
const states = {
  ready: document.getElementById('state-ready'),
  recording: document.getElementById('state-recording'),
  saved: document.getElementById('state-saved'),
};
const ui = {
  reasonReady:     document.getElementById('reason-ready'),
  reasonRecording: document.getElementById('reason-recording'),
  reasonSaved:     document.getElementById('reason-saved'),
  micBadge:        document.getElementById('mic-badge'),
  micSub:          document.getElementById('mic-sub'),
  startBtn:        document.getElementById('start-btn'),
  stopBtn:         document.getElementById('stop-btn'),
  durationEl:      document.getElementById('duration-el'),
  sizeEl:          document.getElementById('size-el'),
  clipSummary:     document.getElementById('clip-summary'),
  clipCount:       document.getElementById('clip-count'),
  lastClip:        document.getElementById('last-clip'),
  savedName:       document.getElementById('saved-name'),
  savedMeta:       document.getElementById('saved-meta'),
  openFolderBtn:   document.getElementById('open-folder-btn'),
  anotherBtn:      document.getElementById('another-btn'),
  autoReady:       document.getElementById('auto-ready'),
  autoReadyCount:  document.getElementById('auto-ready-count'),
};

let mediaRecorder = null;
let stream = null;
let sentBytes = 0;
let startedAt = 0;
let durationTimer = null;
let autoReadyTimer = null;
let session = {
  reason: '—',
  audioRequested: false,
  clipCount: 0,
  lastClip: null,
};

function showState(name) {
  for (const s of Object.values(states)) s.classList.remove('active');
  states[name].classList.add('active');
}

function fmtDuration(ms) {
  const totalSec = Math.floor(ms / 1000);
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  return `${m}:${String(s).padStart(2, '0')}`;
}
function fmtSize(bytes) {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024*1024) return `${(bytes/1024).toFixed(0)} KB`;
  return `${(bytes/(1024*1024)).toFixed(1)} MB`;
}

function updateReasonText(reason) {
  session.reason = reason || '—';
  ui.reasonReady.textContent = session.reason;
  ui.reasonRecording.textContent = session.reason;
  ui.reasonSaved.textContent = session.reason;
}

function updateMicUi(enabled) {
  if (enabled) {
    ui.micBadge.textContent = 'ON';
    ui.micBadge.className = 'badge badge-active';
    ui.micSub.textContent = 'Will be captured';
  } else {
    ui.micBadge.textContent = 'OFF';
    ui.micBadge.className = 'badge badge-off';
    ui.micSub.textContent = 'Not enabled';
  }
}

function updateClipSummary() {
  if (session.clipCount === 0) {
    ui.clipSummary.style.display = 'none';
    return;
  }
  ui.clipSummary.style.display = 'block';
  ui.clipCount.textContent = String(session.clipCount);
  ui.lastClip.textContent = session.lastClip || '—';
}

// ─── Public: bridge → page (called via page.evaluate) ──────────────────
window.setSessionReason = (reason) => updateReasonText(reason);

window.startRecording = async (opts = {}) => {
  if (mediaRecorder && mediaRecorder.state === 'recording') {
    throw new Error('Already recording');
  }
  if (opts.reason) updateReasonText(opts.reason);
  session.audioRequested = !!opts.audio;
  updateMicUi(session.audioRequested);

  try {
    stream = await navigator.mediaDevices.getDisplayMedia({
      video: { frameRate: opts.fps || 30, displaySurface: 'monitor' },
      audio: opts.audio === true,
    });
  } catch (e) {
    throw new Error('getDisplayMedia failed: ' + e.message);
  }

  let mimeType = 'video/webm;codecs=vp9';
  if (!MediaRecorder.isTypeSupported(mimeType)) {
    mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp8') ? 'video/webm;codecs=vp8' : 'video/webm';
  }

  mediaRecorder = new MediaRecorder(stream, {
    mimeType,
    videoBitsPerSecond: opts.videoBitsPerSecond || 4_000_000,
  });
  sentBytes = 0;
  startedAt = Date.now();

  mediaRecorder.ondataavailable = async (e) => {
    if (!e.data || !e.data.size) return;
    const buf = await e.data.arrayBuffer();
    let binary = '';
    const bytes = new Uint8Array(buf);
    const chunkSize = 0x8000;
    for (let i = 0; i < bytes.length; i += chunkSize) {
      binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
    }
    const b64 = btoa(binary);
    try { await window._saveChunk(b64); } catch (err) { console.error('saveChunk:', err); }
    sentBytes += buf.byteLength;
    ui.sizeEl.textContent = fmtSize(sentBytes);
  };

  mediaRecorder.onstop = async () => {
    if (stream) { stream.getTracks().forEach(t => t.stop()); stream = null; }
    if (durationTimer) { clearInterval(durationTimer); durationTimer = null; }
    const durationMs = Date.now() - startedAt;
    try {
      await window._recordingStopped({ sentBytes, durationMs });
    } catch (e) {
      console.error('recordingStopped:', e);
    }
    mediaRecorder = null;
  };

  mediaRecorder.start(1000);
  showState('recording');
  durationTimer = setInterval(() => {
    ui.durationEl.textContent = fmtDuration(Date.now() - startedAt);
  }, 250);
  // No dynamic window resize — window stays at its launch size for the
  // life of the session. Each state's content fits within the same
  // window comfortably; switching states only swaps which .state.active
  // div is visible. Eliminates the resize-clipping bug where the Stop
  // button got cut off when the window shrank to fit Recording state.
};

window.stopRecording = async () => {
  if (!mediaRecorder || mediaRecorder.state !== 'recording') return;
  return new Promise((resolve) => {
    mediaRecorder.addEventListener('stop', resolve, { once: true });
    mediaRecorder.stop();
  });
};

// Bridge → page: switch to Saved state with clip metadata
window.showSaved = (clip) => {
  session.clipCount += 1;
  if (clip && clip.filename) session.lastClip = clip.filename + ' · ' + (clip.sizeKBStr || '');
  ui.savedName.textContent = clip && clip.filename ? clip.filename : '—';
  ui.savedMeta.textContent = clip
    ? `${fmtSize(clip.sizeBytes || 0)} · ${fmtDuration(clip.durationMs || 0)}`
    : '—';
  showState('saved');

  // Auto-transition back to Ready after 3s
  if (autoReadyTimer) clearInterval(autoReadyTimer);
  let countdown = 3;
  ui.autoReadyCount.textContent = countdown;
  autoReadyTimer = setInterval(() => {
    countdown -= 1;
    if (countdown <= 0) {
      clearInterval(autoReadyTimer); autoReadyTimer = null;
      goToReady();
    } else {
      ui.autoReadyCount.textContent = countdown;
    }
  }, 1000);
};

window.goToReady = goToReady;
function goToReady() {
  if (autoReadyTimer) { clearInterval(autoReadyTimer); autoReadyTimer = null; }
  updateClipSummary();
  showState('ready');
  // No dynamic window resize — see comment in startRecording above.
}

// ─── User → bridge: in-window button clicks ────────────────────────────
ui.startBtn.addEventListener('click', async () => {
  // Default audio off; user can toggle by clicking the mic card (see below)
  try {
    await window.startRecording({ audio: session.audioRequested });
  } catch (e) {
    alert('Recording failed: ' + e.message);
  }
});
ui.stopBtn.addEventListener('click', async () => {
  await window.stopRecording();
});
ui.openFolderBtn.addEventListener('click', async () => {
  try { await window._openRecordingsFolder(); } catch {}
});
ui.anotherBtn.addEventListener('click', () => goToReady());

// Mic toggle by clicking the source card
document.querySelectorAll('.source-card')[1].addEventListener('click', () => {
  session.audioRequested = !session.audioRequested;
  updateMicUi(session.audioRequested);
});

// Window-close → bridge can detect via beforeunload
window.addEventListener('beforeunload', () => {
  try {
    if (mediaRecorder && mediaRecorder.state === 'recording') {
      mediaRecorder.stop();
    }
    window._recorderClosed && window._recorderClosed();
  } catch {}
});

// Initialize from query string if launched with ?reason=...
const params = new URLSearchParams(location.search);
if (params.get('reason')) updateReasonText(params.get('reason'));
</script>
</body>
</html>