import {
  waitForLegacySession,
  levelBus,
  LEVEL_EVENT,
  MultiTrackRecorder,
  CloudUploadCoordinator,
  bridgeLegacyMeters,
  monitorTrackLevel,
} from '../core/index.js';

const STUDIO_ROOT_ID = 'podcast-root';
const ROSTER_REFRESH_MS = 1500;
const PREFLIGHT_STORAGE_KEY = 'podcastStudio.preflightState';
const PREFLIGHT_CACHE_MS = 6 * 60 * 60 * 1000;
const PREFLIGHT_MIN_MANDATORY_MS = 5 * 60 * 1000;
const DROPBOX_GUIDE_URL = '/cloud.html#dropbox';
const CLOUD_STATUS_STORAGE_KEY = 'podcastStudio.cloudStatus';
const CLOUD_STATUS_STALE_MS = 30 * 60 * 1000;
const DISK_RECORDING_STORAGE_KEY = 'podcastStudio.diskRecordingState';
const DISK_DB_NAME = 'podcastStudio.disk';
const DISK_DB_STORE = 'handles';
const PODCAST_CLOUD_EVENT = 'podcast-cloud-status';
const PODCAST_DISK_EVENT = 'podcast-disk-state';
const PODCAST_RECORD_PLAN_EVENT = 'podcast-record-plan';
const PODCAST_RECORD_STATUS_EVENT = 'podcast-record-status';
const UPLOAD_TRACKER_COOLDOWN_MS = 15000;
const DRIVE_PROGRESS_EVENT = 'vdoninja:gdrive-progress';
const DRIVE_STATUS_RESET_MS = 8000;
const DRIVE_STATUS_MESSAGES = {
  idle: 'Drive idle',
  pending: 'Drive readying…',
  uploading: 'Drive uploading…',
  done: 'Drive upload complete',
  error: 'Drive upload error',
};
const STUDIO_DISK_FEATURE_FLAG = (() => {
  let enabled = true;
  if (typeof urlParams !== 'undefined' && urlParams) {
    const hasParam = typeof urlParams.has === 'function' ? urlParams.has('studioiso') : false;
    if (hasParam) {
      const rawValue = typeof urlParams.get === 'function' ? urlParams.get('studioiso') : null;
      const normalized = (rawValue || '1').toString().toLowerCase();
      enabled = !['0', 'false', 'off', 'no'].includes(normalized);
    }
  }
  return enabled;
})();

function injectStylesheet() {
  if (document.getElementById('podcast-studio-style')) {
    return;
  }
  const link = document.createElement('link');
  link.id = 'podcast-studio-style';
  link.rel = 'stylesheet';
  link.href = new URL('./studio.css?v=4', import.meta.url).toString();
  document.head.appendChild(link);
}

function createElement(tag, className, attrs = {}) {
  const el = document.createElement(tag);
  if (className) {
    el.className = className;
  }
  Object.entries(attrs).forEach(([key, value]) => {
    if (value === undefined || value === null) {
      return;
    }
    if (key === 'text') {
      el.textContent = value;
    } else {
      el.setAttribute(key, value);
    }
  });
  return el;
}

function makeCollapsible(panel, title, storageKey = null) {
  panel.dataset.collapsible = 'true';

  // Add title h2 if provided and panel doesn't already have one
  if (title && !panel.querySelector('h2')) {
    const h2 = createElement('h2', '', { text: title });
    panel.insertBefore(h2, panel.firstChild);
  }

  // Create toggle button (will be positioned absolute in top right via CSS)
  const toggle = createElement('button', 'panel-collapse-toggle', { type: 'button', text: '−', title: 'Collapse section' });

  // Load saved state
  let collapsed = false;
  if (storageKey) {
    try {
      collapsed = localStorage.getItem(storageKey) === 'true';
    } catch (e) {}
  }

  const updateState = () => {
    panel.dataset.collapsed = collapsed ? 'true' : 'false';
    toggle.textContent = collapsed ? '+' : '−';
    toggle.title = collapsed ? 'Expand section' : 'Collapse section';
    if (storageKey) {
      try {
        localStorage.setItem(storageKey, collapsed ? 'true' : 'false');
      } catch (e) {}
    }
  };

  toggle.addEventListener('click', () => {
    collapsed = !collapsed;
    updateState();
  });

  panel.appendChild(toggle);
  updateState();

  return { toggle };
}

function dispatchStudioEvent(name, detail = {}) {
  if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') {
    return;
  }
  try {
    window.dispatchEvent(new CustomEvent(name, { detail }));
  } catch (error) {
    console.warn('Unable to dispatch studio event', name, error);
  }
}

const SPECTROGRAM_GRADIENT = [
  { stop: 0, color: [4, 5, 13] }, // floor
  { stop: 0.25, color: [24, 60, 140] },
  { stop: 0.45, color: [47, 231, 163] }, // studio green accent
  { stop: 0.7, color: [255, 153, 68] }, // warning orange
  { stop: 1, color: [255, 255, 255] },
];

const DEFAULT_SPECTROGRAM_OPTIONS = {
  fps: 24,
  pixelStep: 1,
  decay: 0.008,
  noiseFloor: 2,
  gamma: 0.65,
  frequencyExponent: 0.95,
  lowFrequencyCutoff: 0.55,
  lowFrequencyGain: 1.1,
  lowFrequencySpread: 2,
};

function lerpColorChannel(start, end, ratio) {
  return Math.round(start + (end - start) * ratio);
}

function pickSpectrogramColor(value) {
  const clamped = Math.min(1, Math.max(0, value));
  for (let i = 1; i < SPECTROGRAM_GRADIENT.length; i += 1) {
    const prev = SPECTROGRAM_GRADIENT[i - 1];
    const next = SPECTROGRAM_GRADIENT[i];
    if (clamped <= next.stop) {
      const span = next.stop - prev.stop || 1;
      const ratio = (clamped - prev.stop) / span;
      return [
        lerpColorChannel(prev.color[0], next.color[0], ratio),
        lerpColorChannel(prev.color[1], next.color[1], ratio),
        lerpColorChannel(prev.color[2], next.color[2], ratio),
      ];
    }
  }
  const fallback = SPECTROGRAM_GRADIENT[SPECTROGRAM_GRADIENT.length - 1];
  return [...fallback.color];
}

class SpectrogramRenderer {
  constructor(canvas, options = {}) {
    this.canvas = canvas;
    this.ctx = canvas?.getContext ? canvas.getContext('2d', { alpha: true }) : null;
    this.options = { ...DEFAULT_SPECTROGRAM_OPTIONS, ...options };
    this.pixelStepBase = Math.max(1, this.options.pixelStep);
    this.frameInterval = this.options.fps > 0 ? 1000 / this.options.fps : 0;
    this.lastFrame = 0;
    this.animationFrame = null;
    this.resizeObserver = null;
    this.resizeListener = null;
    this.columnBuffer = null;
    this.analyser = null;
    this.frequencyData = null;
    this.width = 0;
    this.height = 0;
    this.pixelStep = this.pixelStepBase;
    this.noiseFloor = Math.max(0, this.options.noiseFloor);
    this.gamma = Math.max(0.25, Math.min(1.5, this.options.gamma));
    this.frequencyExponent = Math.max(0.4, Math.min(2.4, this.options.frequencyExponent));
    this.lowFrequencyCutoff = Math.min(0.9, Math.max(0.05, this.options.lowFrequencyCutoff || 0.3));
    this.lowFrequencyGain = Math.max(1, this.options.lowFrequencyGain || 1.2);
    this.lowFrequencySpread = Math.max(1, Math.round(this.options.lowFrequencySpread || 2));
    this.baseFillStyle = 'rgb(4, 5, 13)';
    this.boundResize = () => this.handleResize();
    this.renderLoop = (timestamp) => this.tick(timestamp);
    if (this.ctx && this.canvas) {
      this.ctx.imageSmoothingEnabled = false;
      this.observeResize();
      this.handleResize();
    }
  }

  observeResize() {
    if (!this.canvas) {
      return;
    }
    if (typeof ResizeObserver === 'function') {
      this.resizeObserver = new ResizeObserver(this.boundResize);
      this.resizeObserver.observe(this.canvas);
    } else {
      this.resizeListener = this.boundResize;
      window.addEventListener('resize', this.resizeListener);
    }
  }

  handleResize() {
    if (!this.canvas || !this.ctx) {
      return;
    }
    const rect = this.canvas.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
    const nextWidth = Math.max(10, Math.floor(rect.width * dpr) || 10);
    const nextHeight = Math.max(10, Math.floor(rect.height * dpr) || 10);
    if (nextWidth === this.width && nextHeight === this.height) {
      return;
    }
    this.width = nextWidth;
    this.height = nextHeight;
    this.pixelStep = Math.max(1, Math.round(this.pixelStepBase * dpr));
    this.canvas.width = nextWidth;
    this.canvas.height = nextHeight;
    this.columnBuffer = this.ctx.createImageData(this.pixelStep, this.height);
    this.ctx.fillStyle = this.baseFillStyle;
    this.ctx.fillRect(0, 0, this.width, this.height);
  }

  ensureColumnBuffer() {
    if (!this.ctx) {
      return null;
    }
    if (!this.columnBuffer || this.columnBuffer.height !== this.height || this.columnBuffer.width !== this.pixelStep) {
      this.columnBuffer = this.ctx.createImageData(this.pixelStep, this.height);
    }
    return this.columnBuffer;
  }

  normalizeMagnitude(rawValue) {
    if (!Number.isFinite(rawValue)) {
      return 0;
    }
    const adjusted = Math.max(0, rawValue - this.noiseFloor);
    const normalized = Math.min(1, adjusted / (255 - this.noiseFloor));
    return Math.pow(normalized, this.gamma);
  }

  setAnalyser(analyser) {
    if (this.analyser === analyser) {
      return;
    }
    this.analyser = analyser || null;
    this.frequencyData = this.analyser ? new Uint8Array(this.analyser.frequencyBinCount) : null;
    if (this.analyser) {
      this.startLoop();
    } else {
      this.stopLoop();
    }
  }

  startLoop() {
    if (this.animationFrame || !this.analyser) {
      return;
    }
    this.lastFrame = 0;
    this.animationFrame = requestAnimationFrame(this.renderLoop);
  }

  stopLoop() {
    if (this.animationFrame) {
      cancelAnimationFrame(this.animationFrame);
      this.animationFrame = null;
    }
  }

  tick(timestamp) {
    if (!this.analyser || !this.frequencyData || !this.ctx || !this.canvas) {
      this.stopLoop();
      return;
    }
    if (this.frameInterval && timestamp - this.lastFrame < this.frameInterval) {
      this.animationFrame = requestAnimationFrame(this.renderLoop);
      return;
    }
    this.lastFrame = timestamp;
    this.drawColumn();
    this.animationFrame = requestAnimationFrame(this.renderLoop);
  }

  drawColumn() {
    if (!this.analyser || !this.frequencyData || !this.ctx) {
      return;
    }
    try {
      this.analyser.getByteFrequencyData(this.frequencyData);
    } catch (error) {
      console.warn('Spectrogram analyser unavailable', error);
      this.frequencyData = null;
      return;
    }
    const width = this.canvas.width;
    const height = this.canvas.height;
    const shift = Math.min(this.pixelStep, Math.max(1, width - 1));
    if (!width || !height || !shift) {
      return;
    }
    this.ctx.drawImage(this.canvas, shift, 0, width - shift, height, 0, 0, width - shift, height);
    const fadeStrength = Math.max(0, Math.min(1, this.options.decay));
    if (fadeStrength > 0 && width - shift > 0) {
      this.ctx.save();
      this.ctx.globalAlpha = fadeStrength;
      this.ctx.fillStyle = this.baseFillStyle;
      this.ctx.fillRect(0, 0, width - shift, height);
      this.ctx.restore();
    }
    // clear the area reserved for the new samples
    this.ctx.fillStyle = this.baseFillStyle;
    this.ctx.fillRect(width - shift, 0, shift, height);
    const column = this.ensureColumnBuffer();
    if (!column) {
      return;
    }
    const bins = this.frequencyData.length;
    for (let y = 0; y < height; y += 1) {
      const ratio = 1 - y / height;
      const curved = Math.pow(ratio, this.frequencyExponent); // slower exponent keeps low freqs visible
      const baseIndex = Math.max(0, Math.min(bins - 1, Math.floor(curved * (bins - 1))));
      let accumulator = 0;
      let samples = 0;
      const isLowBand = curved <= this.lowFrequencyCutoff;
      const spread = isLowBand ? this.lowFrequencySpread : 1;
      for (let i = 0; i < spread; i += 1) {
        const idx = Math.min(bins - 1, baseIndex + i);
        accumulator += this.frequencyData[idx];
        samples += 1;
      }
      let magnitude = this.normalizeMagnitude(accumulator / Math.max(1, samples));
      if (isLowBand) {
        magnitude = Math.min(1, magnitude * this.lowFrequencyGain);
      }
      const [r, g, b] = pickSpectrogramColor(magnitude);
      const alpha = Math.round(35 + magnitude * 220);
      for (let x = 0; x < shift; x += 1) {
        const offset = (y * shift + x) * 4;
        column.data[offset] = r;
        column.data[offset + 1] = g;
        column.data[offset + 2] = b;
        column.data[offset + 3] = alpha;
      }
    }
    this.ctx.putImageData(column, width - shift, 0);
  }

  destroy() {
    this.stopLoop();
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }
    if (this.resizeListener) {
      window.removeEventListener('resize', this.resizeListener);
      this.resizeListener = null;
    }
    this.analyser = null;
    this.frequencyData = null;
    if (this.ctx && this.canvas) {
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
  }
}

const ROOM_QUERY_KEYS = ['room', 'roomid', 'r'];
const DIRECTOR_QUERY_KEYS = ['director', 'dir'];
const ROOM_STATE_STORAGE_KEY = 'podcastStudio.lastRoom';

function sanitizeRoomSlug(value) {
  if (!value) {
    return '';
  }
  const trimmed = String(value).trim();
  if (!trimmed) {
    return '';
  }
  try {
    if (typeof window.sanitizeRoomName === 'function') {
      return window.sanitizeRoomName(trimmed);
    }
  } catch (error) {
    console.warn('sanitizeRoomName unavailable', error);
  }
  return trimmed.replace(/[^a-zA-Z0-9_\-]/g, '').slice(0, 64);
}

function getRoomSlugFromParams(params = new URLSearchParams(window.location.search)) {
  for (const key of DIRECTOR_QUERY_KEYS) {
    if (params.has(key)) {
      const slug = sanitizeRoomSlug(params.get(key));
      if (slug) {
        return slug;
      }
    }
  }
  for (const key of ROOM_QUERY_KEYS) {
    if (params.has(key)) {
      const slug = sanitizeRoomSlug(params.get(key));
      if (slug) {
        return slug;
      }
    }
  }
  return '';
}

function readStoredRoomState() {
  try {
    const raw = window.localStorage.getItem(ROOM_STATE_STORAGE_KEY);
    if (!raw) {
      return {};
    }
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      return {
        room: typeof parsed.room === 'string' ? parsed.room : '',
        password: typeof parsed.password === 'string' ? parsed.password : '',
      };
    }
  } catch (error) {
    console.warn('Unable to read stored room state', error);
  }
  return {};
}

function persistStoredRoomState(state) {
  try {
    window.localStorage.setItem(ROOM_STATE_STORAGE_KEY, JSON.stringify(state || {}));
  } catch (error) {
    console.warn('Unable to store room state', error);
  }
}

function readPreflightState() {
  try {
    const raw = window.localStorage.getItem(PREFLIGHT_STORAGE_KEY);
    if (!raw) {
      return {};
    }
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      return parsed;
    }
  } catch (error) {
    console.warn('Unable to read preflight cache', error);
  }
  return {};
}

function writePreflightState(state) {
  try {
    window.localStorage.setItem(PREFLIGHT_STORAGE_KEY, JSON.stringify(state || {}));
  } catch (error) {
    console.warn('Unable to persist preflight cache', error);
  }
}

function isPreflightFresh(timestamp) {
  if (!timestamp) {
    return false;
  }
  return Date.now() - timestamp < PREFLIGHT_CACHE_MS;
}

function formatRelativeTime(timestamp) {
  if (!timestamp) {
    return '';
  }
  const deltaSeconds = Math.max(0, Math.round((Date.now() - timestamp) / 1000));
  if (deltaSeconds < 45) {
    return 'just now';
  }
  if (deltaSeconds < 90) {
    return 'about a minute ago';
  }
  if (deltaSeconds < 45 * 60) {
    const minutes = Math.round(deltaSeconds / 60);
    return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
  }
  if (deltaSeconds < 90 * 60) {
    return 'about an hour ago';
  }
  if (deltaSeconds < 36 * 3600) {
    const hours = Math.round(deltaSeconds / 3600);
    return `${hours} hour${hours === 1 ? '' : 's'} ago`;
  }
  const days = Math.round(deltaSeconds / 86400);
  return `${days} day${days === 1 ? '' : 's'} ago`;
}

function createRecordingSessionId() {
  try {
    if (typeof crypto !== 'undefined' && crypto.randomUUID) {
      return crypto.randomUUID();
    }
  } catch (error) {
    console.warn('randomUUID unavailable', error);
  }
  return `rec-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}

function snapshotHighResClock() {
  if (typeof performance === 'undefined' || typeof performance.now !== 'function') {
    return null;
  }
  const now = performance.now();
  const origin =
    typeof performance.timeOrigin === 'number'
      ? performance.timeOrigin
      : Date.now() - now;
  return {
    perfNow: now,
    timeOrigin: origin,
    wallClockMs: Math.round(origin + now),
  };
}

function readCloudLinkStatus() {
  try {
    const raw = window.localStorage.getItem(CLOUD_STATUS_STORAGE_KEY);
    if (!raw) {
      return {};
    }
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === 'object' ? parsed : {};
  } catch (error) {
    console.warn('Unable to read cloud link status', error);
    return {};
  }
}

function writeCloudLinkStatus(nextState) {
  const snapshot = nextState || {};
  try {
    window.localStorage.setItem(CLOUD_STATUS_STORAGE_KEY, JSON.stringify(snapshot));
  } catch (error) {
    console.warn('Unable to persist cloud link status', error);
    return;
  }
  dispatchStudioEvent(PODCAST_CLOUD_EVENT, { state: snapshot });
}

function isCloudLinkFresh(entry) {
  if (!entry?.linkedAt) {
    return false;
  }
  return Date.now() - entry.linkedAt < CLOUD_STATUS_STALE_MS;
}

function markCloudLinked(service, details = {}) {
  if (!service) {
    return;
  }
  const state = readCloudLinkStatus();
  state[service] = {
    linkedAt: Date.now(),
    ...details,
  };
  writeCloudLinkStatus(state);
}

function markCloudUnlinked(service) {
  if (!service) {
    return;
  }
  const state = readCloudLinkStatus();
  if (state[service]) {
    delete state[service];
    writeCloudLinkStatus(state);
  }
}

function readDiskRecordingState() {
  try {
    const raw = window.localStorage.getItem(DISK_RECORDING_STORAGE_KEY);
    if (!raw) {
      return {};
    }
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === 'object' ? parsed : {};
  } catch (error) {
    console.warn('Unable to read disk recording state', error);
    return {};
  }
}

function isDiskRecordingEnabled() {
  const state = readDiskRecordingState();
  return Boolean(state.folderName && state.enabled);
}

function setDiskRecordingEnabled(enabled) {
  const current = readDiskRecordingState();
  const next = {
    ...current,
    enabled: Boolean(enabled) && Boolean(current.folderName),
    updatedAt: Date.now(),
  };
  writeDiskRecordingState(next);
  return next;
}

function writeDiskRecordingState(state) {
  const snapshot = state || {};
  try {
    window.localStorage.setItem(DISK_RECORDING_STORAGE_KEY, JSON.stringify(snapshot));
  } catch (error) {
    console.warn('Unable to persist disk recording state', error);
    return;
  }
  dispatchStudioEvent(PODCAST_DISK_EVENT, { state: snapshot });
}

function openDiskHandleDatabase() {
  return new Promise((resolve, reject) => {
    if (!window.indexedDB) {
      reject(new Error('IndexedDB unavailable'));
      return;
    }
    const request = window.indexedDB.open(DISK_DB_NAME, 1);
    request.onerror = () => reject(request.error || new Error('Unable to open disk handle database'));
    request.onupgradeneeded = () => {
      const db = request.result;
      if (!db.objectStoreNames.contains(DISK_DB_STORE)) {
        db.createObjectStore(DISK_DB_STORE);
      }
    };
    request.onsuccess = () => resolve(request.result);
  });
}

async function saveDiskDirectoryHandle(handle) {
  if (!handle) {
    return;
  }
  const db = await openDiskHandleDatabase();
  await new Promise((resolve, reject) => {
    const tx = db.transaction(DISK_DB_STORE, 'readwrite');
    tx.oncomplete = () => {
      db.close();
      resolve();
    };
    tx.onerror = () => {
      db.close();
      reject(tx.error || new Error('Unable to store disk handle'));
    };
    tx.objectStore(DISK_DB_STORE).put(handle, 'primary');
  });
}

async function readDiskDirectoryHandle() {
  const db = await openDiskHandleDatabase();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(DISK_DB_STORE, 'readonly');
    tx.oncomplete = () => {
      db.close();
    };
    tx.onerror = () => {
      db.close();
      reject(tx.error || new Error('Unable to read disk handle'));
    };
    const request = tx.objectStore(DISK_DB_STORE).get('primary');
    request.onsuccess = () => resolve(request.result || null);
  });
}

async function verifyStoredDiskRecordingDirectory({ requestPermission = false } = {}) {
  try {
    const handle = await readDiskDirectoryHandle();
    if (!handle) {
      return { ok: false, message: 'No folder selected yet.' };
    }
    let permission = await handle.queryPermission({ mode: 'readwrite' });
    if (permission === 'prompt' && requestPermission) {
      permission = await handle.requestPermission({ mode: 'readwrite' });
    }
    if (permission !== 'granted') {
      return { ok: false, message: 'Access to the selected folder was denied.' };
    }
    const meta = readDiskRecordingState();
    writeDiskRecordingState({
      ...meta,
      lastVerifiedAt: Date.now(),
      folderName: meta.folderName || handle.name || 'Selected folder',
      lastError: null,
    });
    return { ok: true, folderName: meta.folderName || handle.name || 'Selected folder' };
  } catch (error) {
    console.warn('Failed to verify disk folder', error);
    const meta = readDiskRecordingState();
    writeDiskRecordingState({
      ...meta,
      lastError: error?.message || 'Unable to verify folder access.',
    });
    return { ok: false, message: error?.message || 'Unable to verify folder access.' };
  }
}

async function chooseDiskRecordingDirectory() {
  if (typeof window.showDirectoryPicker !== 'function') {
    throw new Error('This browser does not support the file-system directory picker yet.');
  }
  const handle = await window.showDirectoryPicker({ mode: 'readwrite' });
  if (!handle) {
    throw new Error('Folder selection was cancelled.');
  }
  await saveDiskDirectoryHandle(handle);
  const meta = readDiskRecordingState();
  writeDiskRecordingState({
    ...meta,
    folderName: handle.name || 'Recording folder',
    lastVerifiedAt: Date.now(),
    lastError: null,
  });
  return { handle, folderName: handle.name || 'Recording folder' };
}

function buildRoomGate(defaults = {}) {
  injectStylesheet();

  const gate = createElement('div', '', { id: 'podcast-room-gate' });
  const panel = createElement('div', 'podcast-room-gate__panel');
  const title = createElement('h1', 'podcast-room-gate__title', { text: 'Start a Control Room' });
  const subtitle = createElement('p', 'podcast-room-gate__subtitle', {
    text: 'Name your room to invite talent and capture their tracks. This matches the “&director=” link you share with guests.',
  });

  const form = createElement('form', 'podcast-room-gate__form');
  const roomLabel = createElement('label');
  roomLabel.append(createElement('span', '', { text: 'Room name' }));
  const roomInput = createElement('input');
  roomInput.name = 'room';
  roomInput.placeholder = defaults.roomPlaceholder || 'podcast-hq';
  roomInput.title = 'This name becomes the room slug used in guest links.';
  roomInput.autocomplete = 'off';
  roomInput.autocapitalize = 'off';
  roomInput.spellcheck = false;
  if (defaults.room) {
    roomInput.value = defaults.room;
  }
  roomLabel.append(roomInput);

  const passwordLabel = createElement('label');
  passwordLabel.append(createElement('span', '', { text: 'Room password (optional)' }));
  const passwordInput = createElement('input');
  passwordInput.name = 'password';
  passwordInput.placeholder = 'Leave blank to skip';
  passwordInput.title = 'Optional room password for guests and directors.';
  passwordInput.type = 'text';
  passwordInput.autocomplete = 'off';
  passwordInput.autocapitalize = 'off';
  passwordInput.spellcheck = false;
  if (defaults.password) {
    passwordInput.value = defaults.password;
  }
  passwordLabel.append(passwordInput);

  const errorNode = createElement('div', 'podcast-room-gate__error');

  const actions = createElement('div', 'podcast-room-gate__actions');
  const cancelButton = createElement('button', 'podcast-room-gate__cancel', {
    type: 'button',
    text: 'Back to classic',
    title: 'Return to the classic VDO.Ninja interface.',
  });
  const submitButton = createElement('button', 'podcast-room-gate__submit', {
    type: 'submit',
    text: 'Enter studio',
    title: 'Enter the podcast studio for this room.',
  });
  actions.append(submitButton, cancelButton);

  form.append(roomLabel, passwordLabel, errorNode, actions);
  panel.append(title, subtitle, form);
  gate.append(panel);
  document.body.append(gate);

  document.body.classList.remove('hidden');
  document.body.classList.add('podcast-studio-mode');

  setTimeout(() => {
    roomInput.focus();
    roomInput.select();
  }, 0);

  return {
    gate,
    form,
    roomInput,
    passwordInput,
    errorNode,
    submitButton,
    cancelButton,
  };
}

async function ensureRoomSelection() {
  const params = new URLSearchParams(window.location.search);
  const existing = getRoomSlugFromParams(params);
  if (existing) {
    injectStylesheet();
    const preflight = await runPreflightChecklist({ roomSlug: existing });
    if (preflight?.redirect) {
      return preflight;
    }
    return { roomSlug: preflight?.roomSlug || existing };
  }

  const stored = readStoredRoomState();
  const gateElements = buildRoomGate(stored);

  return new Promise((resolve) => {
    function redirectToClassic() {
      const base = window.location.pathname;
      gateElements.cancelButton.disabled = true;
      gateElements.submitButton.disabled = true;
      window.location.href = base || '/';
      resolve({ redirect: true });
    }

    function handleSubmit(event) {
      event.preventDefault();
      const slug = sanitizeRoomSlug(gateElements.roomInput.value);
      if (!slug) {
        gateElements.errorNode.textContent = 'Room name is required.';
        return;
      }
      gateElements.errorNode.textContent = '';
      gateElements.submitButton.disabled = true;
      gateElements.cancelButton.disabled = true;

      const updatedParams = new URLSearchParams(window.location.search);
      updatedParams.set('studio', 'podcast');
      updatedParams.set('director', slug);
      for (const key of DIRECTOR_QUERY_KEYS) {
        if (key !== 'director') {
          updatedParams.delete(key);
        }
      }
      for (const key of ROOM_QUERY_KEYS) {
        updatedParams.delete(key);
      }

      const password = gateElements.passwordInput.value.trim();
      if (password) {
        updatedParams.set('password', password);
      } else {
        updatedParams.delete('password');
      }

      persistStoredRoomState({ room: slug, password });
      window.location.search = updatedParams.toString();
      resolve({ redirect: true });
    }

    gateElements.form.addEventListener('submit', (event) => handleSubmit(event));
    gateElements.cancelButton.addEventListener('click', (event) => {
      event.preventDefault();
      redirectToClassic();
    });
    // Rely on form submit for enter/return handling.
  });
}

function describePreflightStatus(status) {
  switch (status) {
    case 'ready':
      return 'Ready';
    case 'testing':
      return 'Testing…';
    case 'error':
      return 'Needs attention';
    default:
      return 'Pending';
  }
}

function createPreflightRow(label, description, options = {}) {
  const {
    initialStatus = 'pending',
    actionLabel = 'Test',
    showAction = true,
  } = options;
  const row = createElement('div', 'preflight-row');
  row.dataset.status = initialStatus;

  const info = createElement('div', 'preflight-row__info');
  const labelNode = createElement('div', 'preflight-row__label', { text: label });
  const descriptionNode = createElement('div', 'preflight-row__description', { text: description });
  const messageNode = createElement('div', 'preflight-row__message');
  info.append(labelNode, descriptionNode, messageNode);

  const controls = createElement('div', 'preflight-row__controls');
  const statusNode = createElement('span', 'preflight-row__status', { text: describePreflightStatus(initialStatus) });
  controls.append(statusNode);

  let actionButton = null;
  if (showAction) {
    actionButton = createElement('button', 'preflight-row__action', { type: 'button', text: actionLabel, title: `Run: ${label}` });
    controls.append(actionButton);
  }

  row.append(info, controls);
  return {
    row,
    info,
    statusNode,
    messageNode,
    actionButton,
  };
}

function setPreflightRowState(rowParts, status, message = '') {
  if (!rowParts || !rowParts.row) {
    return;
  }
  rowParts.row.dataset.status = status;
  if (rowParts.statusNode) {
    rowParts.statusNode.textContent = describePreflightStatus(status);
  }
  if (rowParts.messageNode) {
    rowParts.messageNode.textContent = message || '';
  }
  if (rowParts.actionButton) {
    if (status === 'testing') {
      rowParts.actionButton.disabled = true;
    } else {
      rowParts.actionButton.disabled = false;
    }
    if (status === 'ready') {
      rowParts.actionButton.textContent = 'Retest';
    } else if (status === 'testing') {
      rowParts.actionButton.textContent = 'Testing…';
    } else if (status === 'error') {
      rowParts.actionButton.textContent = 'Retry';
    } else {
      rowParts.actionButton.textContent = rowParts.actionButton.dataset.initialLabel || 'Test';
    }
  }
}

async function runPreflightChecklist({ roomSlug } = {}) {
  const stored = readPreflightState();
  const now = Date.now();
  const micFresh = isPreflightFresh(stored.micSuccessAt);
  const camFresh = isPreflightFresh(stored.cameraSuccessAt);

  // If the user just completed the preflight moments ago, allow immediate pass-through.
  if (stored.completedAt && now - stored.completedAt < PREFLIGHT_MIN_MANDATORY_MS) {
    document.body.classList.remove('hidden');
    document.body.classList.add('podcast-studio-mode');
    return { roomSlug, skipped: true };
  }

  const overlay = createElement('div', 'podcast-preflight-backdrop');
  overlay.dataset.podcastOverlay = 'true';
  // Inline styles ensure overlay is styled before external CSS loads
  overlay.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.85);z-index:9999';
  const panel = createElement('div', 'podcast-preflight-panel');
  panel.style.cssText = 'background:#1a1d24;padding:32px;border-radius:12px;color:#fff;max-width:480px;width:90%';
  panel.setAttribute('role', 'dialog');
  panel.setAttribute('aria-modal', 'true');
  panel.setAttribute('aria-label', 'Podcast studio preflight checklist');

  const heading = createElement('h2', 'preflight-title', { text: 'Check Your Setup' });
  const subtitleText = roomSlug
    ? `Confirm your gear before directing room “${roomSlug}”.`
    : 'Confirm your gear before directing a session.';
  const subtitle = createElement('p', 'preflight-subtitle', { text: subtitleText });

  const checklist = createElement('div', 'preflight-list');
  const micRow = createPreflightRow('Microphone access', 'Verify your preferred mic is available and browser permission is granted.', {
    initialStatus: micFresh ? 'ready' : 'pending',
    actionLabel: micFresh ? 'Retest' : 'Test mic',
  });
  if (micRow.actionButton) {
    micRow.actionButton.dataset.initialLabel = micFresh ? 'Retest' : 'Test mic';
  }
  if (micFresh) {
    setPreflightRowState(micRow, 'ready', `Last checked ${formatRelativeTime(stored.micSuccessAt)}.`);
  }

  const camRow = createPreflightRow('Camera access', 'Optional but useful if you plan to capture video.', {
    initialStatus: camFresh ? 'ready' : 'pending',
    actionLabel: camFresh ? 'Retest' : 'Test camera',
  });
  if (camRow.actionButton) {
    camRow.actionButton.dataset.initialLabel = camFresh ? 'Retest' : 'Test camera';
  }
  if (camFresh) {
    setPreflightRowState(camRow, 'ready', `Last checked ${formatRelativeTime(stored.cameraSuccessAt)}.`);
  }

  const diskRow = createPreflightRow(
    'Local disk recording',
    'Select a destination folder for ISO files (optional but recommended).',
    {
      initialStatus: 'pending',
      showAction: Boolean(window.showDirectoryPicker),
      actionLabel: window.showDirectoryPicker ? 'Choose folder' : 'Unavailable',
    }
  );
  if (!window.showDirectoryPicker && diskRow.actionButton) {
    diskRow.actionButton.disabled = true;
  }
  setPreflightRowState(
    diskRow,
    window.showDirectoryPicker ? 'pending' : 'error',
    window.showDirectoryPicker ? 'No folder selected yet.' : 'Local disk recording requires the File System Access API (Chromium-based browsers).'
  );

  checklist.append(micRow.row, camRow.row, diskRow.row);

  const actions = createElement('div', 'preflight-actions');
  const continueButton = createElement('button', 'preflight-primary', { type: 'button', text: 'Enter Control Room', title: 'Enter the podcast studio.' });
  const skipButton = createElement('button', 'preflight-secondary', { type: 'button', text: 'Skip preflight', title: 'Skip these checks and enter the studio.' });
  actions.append(continueButton, skipButton);

  panel.append(heading, subtitle, checklist, actions);
  overlay.append(panel);
  document.body.append(overlay);

  document.body.classList.remove('hidden');
  document.body.classList.add('podcast-studio-mode');

  let micOk = Boolean(micFresh);
  let camOk = Boolean(camFresh);
  let diskReady = false;
  let destroyed = false;
  let diskStatusListener = null;
  let resolver;
  const completion = new Promise((resolve) => {
    resolver = resolve;
  });

  function closeOverlay(result = {}) {
    if (destroyed) {
      return;
    }
    destroyed = true;
    if (diskStatusListener) {
      window.removeEventListener(PODCAST_DISK_EVENT, diskStatusListener);
      diskStatusListener = null;
    }
    if (overlay && overlay.parentNode) {
      overlay.parentNode.removeChild(overlay);
    }
    const payload = { roomSlug, ...result };
    if (result.completed) {
      writePreflightState({
        ...stored,
        completedAt: Date.now(),
        micSuccessAt: micOk ? (stored.micSuccessAt || Date.now()) : stored.micSuccessAt,
        cameraSuccessAt: camOk ? (stored.cameraSuccessAt || Date.now()) : stored.cameraSuccessAt,
        roomSlug,
      });
    } else {
      writePreflightState({
        ...stored,
        micSuccessAt: micOk ? (stored.micSuccessAt || Date.now()) : stored.micSuccessAt,
        cameraSuccessAt: camOk ? (stored.cameraSuccessAt || Date.now()) : stored.cameraSuccessAt,
        roomSlug,
      });
    }
    if (typeof resolver === 'function') {
      resolver(payload);
      resolver = null;
    }
  }

  function updateContinueState() {
    continueButton.disabled = !micOk;
    continueButton.title = micOk ? '' : 'Run the microphone test to continue.';
  }

  updateContinueState();

  async function runMediaTest(kind) {
    if (!navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== 'function') {
      throw new Error('Browser does not support media tests.');
    }
    const constraints = kind === 'video' ? { video: true } : { audio: { echoCancellation: false } };
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    stream.getTracks().forEach((track) => {
      try {
        track.stop();
      } catch (error) {
        console.warn('Unable to stop track', error);
      }
    });
  }

  if (micRow.actionButton) {
    micRow.actionButton.addEventListener('click', async () => {
      setPreflightRowState(micRow, 'testing', 'Requesting microphone access…');
      try {
        await runMediaTest('audio');
        micOk = true;
        const timestamp = Date.now();
        stored.micSuccessAt = timestamp;
        setPreflightRowState(micRow, 'ready', 'Microphone ready to record.');
      } catch (error) {
        console.error('Microphone check failed', error);
        micOk = false;
        setPreflightRowState(
          micRow,
          'error',
          error?.message ? error.message : 'Unable to access microphone.'
        );
      }
      updateContinueState();
      writePreflightState({ ...stored, micSuccessAt: micOk ? Date.now() : stored.micSuccessAt, roomSlug });
    });
  }

  if (camRow.actionButton) {
    camRow.actionButton.addEventListener('click', async () => {
      setPreflightRowState(camRow, 'testing', 'Requesting camera access…');
      try {
        await runMediaTest('video');
        camOk = true;
        const timestamp = Date.now();
        stored.cameraSuccessAt = timestamp;
        setPreflightRowState(camRow, 'ready', 'Camera detected.');
        writePreflightState({ ...stored, cameraSuccessAt: timestamp, roomSlug });
      } catch (error) {
        console.error('Camera check failed', error);
        camOk = false;
        setPreflightRowState(
          camRow,
          'error',
          error?.message ? error.message : 'Unable to access camera.'
        );
        writePreflightState({ ...stored, roomSlug });
      }
    });
  }

  async function refreshDiskRowStatus({ interactive = false } = {}) {
    if (typeof window.showDirectoryPicker !== 'function') {
      diskReady = false;
      setPreflightRowState(
        diskRow,
        'error',
        'Local disk recording requires a Chromium-based browser with the File System Access API.'
      );
      if (diskRow.actionButton) {
        diskRow.actionButton.disabled = true;
      }
      return;
    }
    const diskState = readDiskRecordingState();
    if (!diskState.folderName) {
      diskReady = false;
      if (diskRow.actionButton) {
        diskRow.actionButton.textContent = 'Choose folder';
        diskRow.actionButton.disabled = false;
      }
      setPreflightRowState(diskRow, 'pending', 'Pick a folder to enable local ISO recording.');
      return;
    }
    setPreflightRowState(diskRow, 'testing', 'Validating folder permissions…');
    const result = await verifyStoredDiskRecordingDirectory({ requestPermission: interactive });
    if (result.ok) {
      diskReady = true;
      const meta = readDiskRecordingState();
      if (diskRow.actionButton) {
        diskRow.actionButton.textContent = 'Change folder';
        diskRow.actionButton.disabled = false;
      }
      const checked = meta.lastVerifiedAt ? `Last checked ${formatRelativeTime(meta.lastVerifiedAt)}.` : 'Ready to write.';
      setPreflightRowState(diskRow, 'ready', `Folder: ${result.folderName}. ${checked}`);
    } else {
      diskReady = false;
      if (diskRow.actionButton) {
        diskRow.actionButton.textContent = 'Choose folder';
        diskRow.actionButton.disabled = false;
      }
      setPreflightRowState(diskRow, 'error', result.message || 'Unable to access the selected folder.');
    }
  }

  refreshDiskRowStatus();
  diskStatusListener = () => refreshDiskRowStatus();
  window.addEventListener(PODCAST_DISK_EVENT, diskStatusListener);

  if (diskRow.actionButton) {
    diskRow.actionButton.addEventListener('click', async () => {
      if (diskRow.actionButton.disabled) {
        return;
      }
      try {
        setPreflightRowState(diskRow, 'testing', 'Waiting for folder selection…');
        await chooseDiskRecordingDirectory();
        await refreshDiskRowStatus({ interactive: true });
      } catch (error) {
        diskReady = false;
        const message =
          error?.name === 'AbortError' || /cancel/i.test(error?.message || '')
            ? 'Folder selection cancelled.'
            : error?.message || 'Unable to choose folder.';
        setPreflightRowState(diskRow, 'error', message);
      }
    });
  }

  continueButton.addEventListener('click', () => {
    if (!micOk) {
      setPreflightRowState(micRow, 'error', 'Microphone test is required before entering.');
      return;
    }
    closeOverlay({ completed: true });
  });

  skipButton.addEventListener('click', () => {
    closeOverlay({ skipped: true });
  });

  overlay.addEventListener('click', (event) => {
    if (event.target === overlay) {
      closeOverlay({ skipped: true });
    }
  });

  document.addEventListener(
    'keydown',
    (event) => {
      if (destroyed) {
        return;
      }
      if (event.key === 'Escape') {
        event.preventDefault();
        closeOverlay({ skipped: true });
      }
    },
    { once: true }
  );

  return completion;
}

function extractPeerAudioStats(peer) {
  const stats = peer?.stats || {};
  const candidates = [
    stats.audio_bitrate_kbps,
    stats.inbound_audio_bitrate_kbps,
    stats.total_audio_bitrate_kbps,
    stats.total_sending_bitrate_kbps,
  ];
  const audioBitrateKbps = candidates.find((value) => typeof value === 'number' && value >= 0);
  const codec =
    typeof stats.audio_codec === 'string'
      ? stats.audio_codec
      : typeof stats.audio_codec_in === 'string'
        ? stats.audio_codec_in
        : typeof stats.audio_codec_out === 'string'
          ? stats.audio_codec_out
          : '';
  return {
    audioBitrateKbps: audioBitrateKbps ?? null,
    audioCodec: codec || null,
  };
}

function collectParticipants(session) {
  const participants = [];

  Object.entries(session.rpcs || {}).forEach(([uuid, peer]) => {
    if (!peer) {
      return;
    }
    const audioTracks = peer.streamSrc?.getAudioTracks?.() || [];
    let level = 0;
    if (peer.stats && typeof peer.stats.Audio_Loudness === 'number') {
      level = peer.stats.Audio_Loudness;
    } else if (peer.audioMeter) {
      level = peer.audioMeter.level || 0;
    }
    const { audioBitrateKbps, audioCodec } = extractPeerAudioStats(peer);
    participants.push({
      uuid,
      label: peer.label || peer.streamID || `Guest ${uuid.substring(0, 4)}`,
      streamID: peer.streamID,
      status: (peer.streamSrc && audioTracks.length) ? 'connected' : 'connecting',
      audioLevel: level,
      isLocal: false,
      role: 'remote',
      audioBitrateKbps,
      audioCodec,
    });
  });

  return participants;
}

class PodcastStudioApp {
  constructor(options = {}) {
    this.options = options || {};
    this.roomHint = this.options.roomHint || '';
    this.session = null;
    this.cloud = null;
    this.recorder = null;
    this.audioContext = null;
    this.recording = false;
    this.rosterItems = new Map();
    this.rosterDriveButtons = new Map();
    this.rosterDriveStatuses = new Map();
    this.driveStatusResetTimers = new Map();
    this.driveProgressSnapshots = new Map();
    this.meterValues = new Map();
    this.outputIndicators = new Map();
    this.trackRuntimeStats = new Map();
    this.trackLevelNodes = new Map();
    this.spectrograms = new Map();
    this.participantMetrics = new Map();
    this.markers = [];
    this.markerActions = null;
    this.markerExportButton = null;
    this.markerCopyButton = null;
    this.markerCopyResetTimer = null;
    this.autoMarkerTimeout = null;
    this.rosterTimer = null;
    this.pendingDriveUploads = [];
    this.levelOff = null;
    this.recordStartedAt = null;
    this.driveStatusNode = null;
    this.dropboxStatusNode = null;
    this.abortUploadsController = null;
    this.activeDownloadUrls = [];
    this.stopMeterBridge = null;
    this.roomName = this.roomHint || '';
    this.virtualParticipants = new Map();
    this.hostMic = null;
    this.hostMicMeter = null;
    this.hostMicButton = null;
    this.hostMicStatusNode = null;
    this.hostMicErrorNode = null;
    this.hostMicBusy = false;
    this.hostMicMuted = false;
    this.hostMuteButton = null;
    this.cloudBusy = {
      drive: false,
      dropbox: false,
    };
    this.cloudLinkButtons = {
      drive: null,
      dropbox: null,
    };
    this.cloudLinkStatusNodes = {
      drive: null,
      dropbox: null,
    };
    this.cloudLinkMessages = {
      drive: null,
      dropbox: null,
    };
    this.cloudLinkMessageTextNodes = {
      drive: null,
      dropbox: null,
    };
    this.dropboxTokenInput = null;
    this.dropboxTokenRow = null;
    this.dropboxGuideRow = null;
    this.inviteLinkInput = null;
    this.inviteCopyButton = null;
    this.inviteStatusNode = null;
    this.inviteOptionNodes = {};
    this.inviteCopyTimer = null;
    this.remoteOverlay = null;
    this.remoteOverlayContent = null;
    this.remoteControlState = {
      activeUuid: null,
      element: null,
      placeholder: null,
      wrapper: null,
    };
    this.cloudProgressNodes = {
      drive: null,
      dropbox: null,
    };
    this.uploadTrackers = {
      drive: new Map(),
      dropbox: new Map(),
    };
    this.chatModule = null;
    this.chatPlaceholder = null;
    this.chatPanel = null;
    this.chatCollapseButton = null;
    this.chatPopoutButton = null;
    this.chatCollapsed = false;
    this.chatPopoutAnchor = null;
    this.chatCollapsedHint = null;
    this.diskControls = null;
    this.diskToggleButton = null;
    this.diskFolderButton = null;
    this.diskStatusNode = null;
    this.diskRecordingEnabled = isDiskRecordingEnabled();
    this.diskStateListener = null;
    this.cloudStateListener = null;
    this.cloudSummaryNode = null;
    this.recordingStatusNode = null;
    this.recordingPlan = null;
    this.recordingSessionId = null;
    this.boundDriveProgressHandler = null;
  }

  async init() {
    document.body.classList.remove('hidden');
    document.body.classList.add('podcast-studio-mode');
    injectStylesheet();

    this.session = await waitForLegacySession({ timeoutMs: 15000 });
    this.applyDirectorAudioDefaults();
    this.audioContext = this.session.audioCtx || this.session.audioCtxOutbound || this.createAudioContext();
    this.recorder = new MultiTrackRecorder({
      audioContext: this.audioContext,
      includeVideo: false,
      includeScreenshares: false,
      monitorLevels: true,
      timeslice: 1000,
    });
    this.cloud = new CloudUploadCoordinator(this.session);

    this.roomName = this.resolveRoomName();
    this.buildLayout();
    if (STUDIO_DISK_FEATURE_FLAG) {
      this.diskStateListener = () => {
        this.updateDiskRecordingUI();
        this.updateReadinessSummary();
      };
      window.addEventListener(PODCAST_DISK_EVENT, this.diskStateListener);
    }
    this.cloudStateListener = () => this.updateReadinessSummary();
    window.addEventListener(PODCAST_CLOUD_EVENT, this.cloudStateListener);
    this.boundDriveProgressHandler = (event) => this.handleDriveProgressEvent(event);
    window.addEventListener(DRIVE_PROGRESS_EVENT, this.boundDriveProgressHandler);
    this.updateRoomIndicator();
    this.updateCloudFooter();
    this.attachRecorderEvents();
    this.refreshRoster();
    this.startRosterLoop();
    this.levelOff = levelBus.on(LEVEL_EVENT, (payload) => this.updateMeterFromBus(payload));
    try {
      this.stopMeterBridge = await bridgeLegacyMeters();
    } catch (error) {
      console.warn('Failed to bridge legacy meter events', error);
    }
  }

  resolveRoomName() {
    if (this.session?.roomid && this.session.roomid !== true) {
      return sanitizeRoomSlug(this.session.roomid);
    }
    if (this.session?.director && this.session.director !== true) {
      return sanitizeRoomSlug(this.session.director);
    }
    if (this.roomHint) {
      return sanitizeRoomSlug(this.roomHint);
    }
    const paramsSlug = getRoomSlugFromParams();
    if (paramsSlug) {
      return paramsSlug;
    }
    return '';
  }

  updateRoomIndicator() {
    const latest = this.resolveRoomName();
    if (latest !== this.roomName) {
      this.roomName = latest;
      if (this.sessionInfo) {
        this.sessionInfo.textContent = 'Room: ' + (this.roomName || '—');
      }
      if (this.roomName) {
        const stored = readStoredRoomState();
        persistStoredRoomState({ room: this.roomName, password: stored?.password || '' });
      }
    } else if (!this.roomName && this.sessionInfo) {
      this.sessionInfo.textContent = 'Room: —';
    }
    this.updateInviteLink();
  }

  describeInviteOptions() {
    const labels = [];
    if (this.inviteOptionNodes.disableVideo?.checked) {
      labels.push('Audio only');
    } else {
      labels.push('Video preview');
    }
    if (this.inviteOptionNodes.proAudio?.checked) {
      labels.push('Pro audio');
    }
    if (this.inviteOptionNodes.disableAec?.checked) {
      labels.push('AEC off');
    }
    if (this.inviteOptionNodes.disableDenoise?.checked) {
      labels.push('Denoise off');
    }
    if (this.inviteOptionNodes.disableAgc?.checked) {
      labels.push('AGC off');
    }
    return labels.join(' • ');
  }

  applyDirectorAudioDefaults() {
    if (!this.session) {
      return;
    }
    if (this.session.stereo === undefined || this.session.stereo === null || this.session.stereo === false || this.session.stereo === 0) {
      this.session.stereo = 1;
    }
    if (!this.session.audiobitrate || this.session.audiobitrate < 192) {
      this.session.audiobitrate = 256;
    }
    if (!this.session.outboundAudioBitrate || this.session.outboundAudioBitrate < 192) {
      this.session.outboundAudioBitrate = 256;
    }
    if (typeof this.session.autoGainControl === 'undefined') {
      this.session.autoGainControl = false;
    }
    if (typeof this.session.noiseSuppression === 'undefined') {
      this.session.noiseSuppression = false;
    }
    if (typeof this.session.echoCancellation === 'undefined') {
      this.session.echoCancellation = false;
    }
    if (typeof this.session.applyStereoDefaults === 'function') {
      try {
        this.session.applyStereoDefaults();
      } catch (error) {
        console.warn('applyStereoDefaults failed', error);
      }
    }
  }

  updateInviteLink() {
    if (!this.inviteLinkInput) {
      return;
    }
    const room = this.resolveRoomName();
    if (!room) {
      this.inviteLinkInput.value = 'Set a room name to generate a guest link';
      this.inviteLinkInput.dataset.state = 'placeholder';
      if (this.inviteCopyButton) {
        this.inviteCopyButton.disabled = true;
      }
      if (this.inviteStatusNode) {
        this.inviteStatusNode.textContent = '';
      }
      return;
    }
    this.inviteLinkInput.dataset.state = 'ready';
    if (this.inviteCopyButton) {
      this.inviteCopyButton.disabled = false;
    }
    const guestUrl = new URL(window.location.href);
    guestUrl.search = '';
    guestUrl.hash = '';

    const params = new URLSearchParams();
    params.set('room', room);
    params.set('style', '2');
    params.set('showlabel', '1');
    params.set('tips', '1');
    params.set('label', '');

    const options = this.inviteOptionNodes || {};
    const summary = [];
    summary.push('Label prompt');
    summary.push('Name tag overlay');
    summary.push('Join tips');

    // Video is ON by default
    if (options.disableVideo?.checked) {
      params.set('miconly', '1');
      summary.push('Audio only');
    } else {
      summary.push('Video enabled');
    }

    if (options.proAudio?.checked) {
      params.set('proaudio', '1');
      params.set('stereo', '1');
      params.set('audiobitrate', '256');
      summary.push('Pro audio');
    } else {
      params.delete('proaudio');
      params.delete('stereo');
      params.delete('audiobitrate');
    }

    if (options.disableAec?.checked) {
      params.set('aec', '0');
      params.set('echocancellation', '0');
      summary.push('AEC off');
    } else {
      params.delete('aec');
      params.delete('echocancellation');
    }

    if (options.disableDenoise?.checked) {
      params.set('denoise', '0');
      summary.push('Denoise off');
    } else {
      params.delete('denoise');
    }

    if (options.disableAgc?.checked) {
      params.set('agc', '0');
      params.set('autogain', '0');
      summary.push('AGC off');
    } else {
      params.delete('agc');
      params.delete('autogain');
    }

    if (options.guestRecordBackup?.checked) {
      params.set('autorecordlocal', '-128');
      summary.push('Audio backup');
    } else {
      params.delete('autorecordlocal');
    }

    guestUrl.search = params.toString();
    const value = guestUrl.toString();
    this.inviteLinkInput.value = value;
    if (this.inviteStatusNode) {
      this.inviteStatusNode.textContent = summary.length ? summary.join(' • ') : 'Default settings';
    }
  }

  async copyInviteLink() {
    if (!this.inviteLinkInput || this.inviteLinkInput.dataset.state === 'placeholder') {
      return;
    }
    const value = this.inviteLinkInput.value;
    if (!value) {
      return;
    }
    const notify = (message, variant = 'info') => {
      if (!this.inviteStatusNode) {
        return;
      }
      this.inviteStatusNode.textContent = message;
      this.inviteStatusNode.dataset.variant = variant;
      if (this.inviteCopyTimer) {
        clearTimeout(this.inviteCopyTimer);
      }
      this.inviteCopyTimer = setTimeout(() => {
        this.inviteStatusNode.dataset.variant = '';
        this.updateInviteLink();
      }, 3500);
    };
    try {
      if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
        await navigator.clipboard.writeText(value);
        notify('Guest link copied.', 'success');
        return;
      }
    } catch (error) {
      console.warn('Navigator clipboard copy failed', error);
    }
    try {
      this.inviteLinkInput.focus();
      this.inviteLinkInput.select();
      const success = document.execCommand('copy');
      if (success) {
        notify('Guest link copied.', 'success');
      } else {
        notify('Select and copy the link manually.', 'warning');
      }
    } catch (error) {
      console.warn('Fallback copy failed', error);
      notify('Select and copy the link manually.', 'warning');
    }
  }

  getAdditionalRecordingParticipants() {
    const extras = [];
    this.virtualParticipants.forEach((participant) => {
      if (participant && participant.stream) {
        extras.push({
          ...participant,
        });
      }
    });
    return extras;
  }

  async ensureAudioContextResumed() {
    if (!this.audioContext) {
      this.audioContext = this.createAudioContext();
    }
    if (this.audioContext && typeof this.audioContext.resume === 'function' && this.audioContext.state === 'suspended') {
      try {
        await this.audioContext.resume();
      } catch (error) {
        console.warn('Failed to resume audio context', error);
      }
    }
  }

  setHostMicError(message) {
    if (this.hostMicErrorNode) {
      this.hostMicErrorNode.textContent = message || '';
    }
  }

  updateHostMicUI() {
    if (this.hostMicButton) {
      if (this.hostMicBusy || this.recording) {
        this.hostMicButton.disabled = true;
        const busyLabel = this.hostMic?.active ? 'Disabling…' : 'Enabling…';
        this.hostMicButton.textContent = this.recording ? 'Locked' : busyLabel;
      } else {
        this.hostMicButton.disabled = false;
        this.hostMicButton.textContent = this.hostMic?.active ? 'Disable' : 'Enable';
      }
      if (this.hostMic?.active) {
        this.hostMicButton.classList.add('active');
      } else {
        this.hostMicButton.classList.remove('active');
      }
    }
    if (this.hostMicStatusNode) {
      if (this.hostMic?.active) {
        this.hostMicStatusNode.textContent = 'Live';
        this.hostMicStatusNode.dataset.state = 'active';
      } else {
        this.hostMicStatusNode.textContent = 'Idle';
        this.hostMicStatusNode.dataset.state = 'idle';
      }
    }
    this.updateHostMuteUI();
  }

  async handleHostMicToggle() {
    if (this.recording) {
      this.setHostMicError('Stop the recording to change the host mic.');
      return;
    }
    if (this.hostMicBusy) {
      return;
    }
    if (this.hostMic?.active) {
      await this.disableHostMic();
    } else {
      await this.enableHostMic();
    }
  }

  handleHostMuteToggle() {
    if (!this.hostMic?.active) {
      return;
    }
    this.hostMicMuted = !this.hostMicMuted;
    if (this.hostMic.track) {
      this.hostMic.track.enabled = !this.hostMicMuted;
    }
    this.updateHostMuteUI();
  }

  updateHostMuteUI() {
    if (this.hostMuteButton) {
      if (this.hostMic?.active) {
        this.hostMuteButton.disabled = false;
        this.hostMuteButton.textContent = this.hostMicMuted ? '🔇 Unmute' : '🔊 Mute';
        this.hostMuteButton.classList.toggle('muted', this.hostMicMuted);
      } else {
        this.hostMuteButton.disabled = true;
        this.hostMuteButton.textContent = '🔊 Mute';
        this.hostMuteButton.classList.remove('muted');
      }
    }
  }

  async enableHostMic() {
    if (this.hostMic?.active) {
      this.updateHostMicUI();
      return;
    }
    if (!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) {
      this.setHostMicError('Browser does not support microphone capture.');
      return;
    }
    this.hostMicBusy = true;
    this.setHostMicError('');
    this.updateHostMicUI();
    try {
      await this.ensureAudioContextResumed();
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
      const [track] = stream.getAudioTracks();
      if (!track) {
        throw new Error('No audio track available.');
      }
      const label = this.session?.label ? `${this.session.label} (Host)` : 'Host Mic';
      const participant = {
        uuid: 'host-mic',
        label,
        stream,
        streamID: 'host-mic',
        status: 'connected',
        audioLevel: 0,
        isLocal: true,
        kind: 'local',
        role: 'host-mic',
      };
      track.addEventListener('ended', () => {
        if (this.hostMic?.track === track) {
          this.disableHostMic();
        }
      });
      this.hostMic = {
        active: true,
        stream,
        track,
        uuid: participant.uuid,
        label: participant.label,
        streamID: participant.streamID,
        participant,
      };
      this.virtualParticipants.set(participant.uuid, participant);
      if (this.audioContext && track) {
        try {
          this.hostMicMeter = await monitorTrackLevel(this.audioContext, track, {
            uuid: participant.uuid,
            trackType: 'audio',
            metadata: { label: participant.label, source: 'host' },
          });
        } catch (error) {
          console.warn('Failed to attach host mic meter', error);
        }
      }
      this.updateHostMicUI();
      this.refreshRoster();
    } catch (error) {
      console.error('Failed to enable host microphone', error);
      this.setHostMicError(error?.message || 'Unable to access microphone.');
      if (this.hostMic?.stream) {
        try {
          this.hostMic.stream.getTracks().forEach((mediaTrack) => mediaTrack.stop());
        } catch (stopError) {
          console.warn('Failed to stop host mic stream after error', stopError);
        }
      }
      this.hostMic = null;
      this.virtualParticipants.delete('host-mic');
      this.updateHostMicUI();
    } finally {
      this.hostMicBusy = false;
      this.updateHostMicUI();
    }
  }

  async disableHostMic() {
    if (!this.hostMic?.active && !this.virtualParticipants.has('host-mic')) {
      this.hostMic = null;
      this.updateHostMicUI();
      return;
    }
    this.hostMicBusy = true;
    this.updateHostMicUI();
    try {
      if (this.hostMicMeter) {
        try {
          this.hostMicMeter.disconnect({ stopTrack: false });
        } catch (error) {
          console.warn('Failed to disconnect host mic meter', error);
        }
        this.hostMicMeter = null;
      }
      if (this.hostMic?.stream) {
        this.hostMic.stream.getTracks().forEach((track) => {
          try {
            track.stop();
          } catch (error) {
            console.warn('Failed to stop host mic track', error);
          }
        });
      }
    } finally {
      this.virtualParticipants.delete('host-mic');
      this.hostMic = null;
      this.hostMicBusy = false;
      this.hostMicMuted = false;
      this.setHostMicError('');
      this.updateHostMicUI();
      this.applyMeterValue('host-mic', 0);
      this.refreshRoster();
    }
  }

  setCloudMessage(service, message, variant = 'info') {
    const container = this.cloudLinkMessages?.[service];
    if (!container) {
      return;
    }
    const target = this.cloudLinkMessageTextNodes?.[service] || container;
    target.textContent = message || '';
    container.dataset.variant = message ? variant : '';
  }

  updateCloudLinkUI() {
    const driveLinked = this.cloud?.hasDriveAccess();
    const dropboxLinked = this.cloud?.hasDropboxAccess();
    const cachedState = readCloudLinkStatus();
    if (!driveLinked && cachedState.drive && !isCloudLinkFresh(cachedState.drive)) {
      markCloudUnlinked('drive');
    }
    if (!dropboxLinked && cachedState.dropbox && !isCloudLinkFresh(cachedState.dropbox)) {
      markCloudUnlinked('dropbox');
    }

    if (this.cloudLinkButtons.drive) {
      this.cloudLinkButtons.drive.textContent = driveLinked ? 'Reauthorize Drive' : 'Link Google Drive';
      this.cloudLinkButtons.drive.disabled = Boolean(this.cloudBusy.drive) || this.recording;
      this.cloudLinkButtons.drive.dataset.state = driveLinked ? 'linked' : 'idle';
    }
    if (this.cloudLinkStatusNodes.drive) {
      this.cloudLinkStatusNodes.drive.textContent = driveLinked ? 'Linked' : 'Not linked';
      this.cloudLinkStatusNodes.drive.dataset.state = driveLinked ? 'linked' : 'idle';
    }
    if (driveLinked && this.pendingDriveUploads.length) {
      this.flushPendingDriveUploads().catch((error) => {
        console.warn('Pending Drive uploads failed to resume', error);
      });
    }

    if (this.cloudLinkButtons.dropbox) {
      this.cloudLinkButtons.dropbox.textContent = dropboxLinked ? 'Refresh Dropbox' : 'Link Dropbox';
      this.cloudLinkButtons.dropbox.disabled = Boolean(this.cloudBusy.dropbox) || this.recording;
      this.cloudLinkButtons.dropbox.dataset.state = dropboxLinked ? 'linked' : 'idle';
    }
    if (this.cloudLinkStatusNodes.dropbox) {
      this.cloudLinkStatusNodes.dropbox.textContent = dropboxLinked ? 'Linked' : 'Not linked';
      this.cloudLinkStatusNodes.dropbox.dataset.state = dropboxLinked ? 'linked' : 'idle';
    }
    if (this.dropboxTokenInput) {
      this.dropboxTokenInput.disabled = Boolean(this.cloudBusy.dropbox) || this.recording;
    }
    this.updateAllDriveActions();
  }

  ensureDropboxTokenFallbackVisible({ focus = false, select = false } = {}) {
    if (this.dropboxTokenRow) {
      this.dropboxTokenRow.hidden = false;
      this.dropboxTokenRow.classList.add('cloud-sync-token--visible');
    }
    if (this.dropboxGuideRow) {
      this.dropboxGuideRow.hidden = false;
      this.dropboxGuideRow.classList.add('cloud-sync-token__guide--visible');
    }
    if (focus && this.dropboxTokenInput) {
      this.dropboxTokenInput.focus();
      if (select && typeof this.dropboxTokenInput.select === 'function') {
        this.dropboxTokenInput.select();
      }
    }
  }

  hideDropboxTokenFallback() {
    if (this.dropboxTokenRow) {
      this.dropboxTokenRow.hidden = true;
      this.dropboxTokenRow.classList.remove('cloud-sync-token--visible');
    }
    if (this.dropboxGuideRow) {
      this.dropboxGuideRow.hidden = true;
      this.dropboxGuideRow.classList.remove('cloud-sync-token__guide--visible');
    }
    if (this.dropboxTokenInput) {
      this.dropboxTokenInput.value = '';
    }
  }

  async handleDiskFolderSelection({ autoEnable = false } = {}) {
    if (!STUDIO_DISK_FEATURE_FLAG || !this.diskFolderButton) {
      return;
    }
    if (typeof window.showDirectoryPicker !== 'function') {
      this.diskStatusNode.textContent = 'Local disk recording requires Chrome, Edge, or Arc.';
      this.diskStatusNode.dataset.state = 'error';
      return;
    }
    try {
      this.diskFolderButton.disabled = true;
      this.diskFolderButton.textContent = '…';
      await chooseDiskRecordingDirectory();
      const result = await verifyStoredDiskRecordingDirectory({ requestPermission: true });
      if (!result.ok) {
        throw new Error(result.message || 'Failed');
      }
      if (autoEnable || isDiskRecordingEnabled()) {
        setDiskRecordingEnabled(true);
        this.diskRecordingEnabled = true;
      }
    } catch (error) {
      console.warn('Disk folder selection failed', error);
      this.diskStatusNode.textContent =
        error?.name === 'AbortError' || /cancel/i.test(error?.message || '')
          ? 'Cancelled'
          : error?.message || 'Error';
      this.diskStatusNode.dataset.state = 'error';
    } finally {
      this.diskFolderButton.disabled = false;
      this.updateDiskRecordingUI();
    }
  }

  async handleDiskToggle() {
    if (!STUDIO_DISK_FEATURE_FLAG || !this.diskToggleButton) {
      return;
    }
    if (typeof window.showDirectoryPicker !== 'function') {
      this.diskStatusNode.textContent = 'Browser lacks File System Access API support.';
      this.diskStatusNode.dataset.state = 'error';
      return;
    }
    const meta = readDiskRecordingState();
    if (!meta.folderName) {
      await this.handleDiskFolderSelection({ autoEnable: true });
      return;
    }
    const nextEnabled = !isDiskRecordingEnabled();
    if (nextEnabled) {
      const result = await verifyStoredDiskRecordingDirectory({ requestPermission: true });
      if (!result.ok) {
        this.diskStatusNode.textContent = result.message || 'Unable to access the selected folder.';
        this.diskStatusNode.dataset.state = 'error';
        setDiskRecordingEnabled(false);
        this.diskRecordingEnabled = false;
        this.updateDiskRecordingUI();
        return;
      }
    }
    const finalState = setDiskRecordingEnabled(nextEnabled);
    this.diskRecordingEnabled = Boolean(finalState.enabled);
    this.updateDiskRecordingUI();
  }

  updateDiskRecordingUI() {
    if (!STUDIO_DISK_FEATURE_FLAG || !this.diskControls) {
      return;
    }
    const diskSupported = typeof window.showDirectoryPicker === 'function';
    const meta = readDiskRecordingState();
    const hasFolder = Boolean(meta.folderName);
    const enabled = Boolean(meta.enabled && hasFolder);
    this.diskRecordingEnabled = enabled;
    if (this.diskToggleButton) {
      this.diskToggleButton.disabled = !diskSupported;
      this.diskToggleButton.dataset.state = enabled ? 'enabled' : 'disabled';
      this.diskToggleButton.textContent = enabled ? 'Armed ✓' : 'Arm';
      this.diskToggleButton.setAttribute('aria-pressed', enabled ? 'true' : 'false');
    }
    if (this.diskFolderButton) {
      this.diskFolderButton.disabled = !diskSupported;
      this.diskFolderButton.textContent = hasFolder ? 'Change' : diskSupported ? 'Folder' : 'N/A';
    }
    if (this.diskStatusNode) {
      if (!diskSupported) {
        this.diskStatusNode.textContent = 'Requires Chrome/Edge';
        this.diskStatusNode.dataset.state = 'error';
      } else if (!hasFolder) {
        this.diskStatusNode.textContent = 'No folder selected';
        this.diskStatusNode.dataset.state = 'pending';
      } else if (meta.lastError) {
        this.diskStatusNode.textContent = `${meta.folderName} — error`;
        this.diskStatusNode.dataset.state = 'error';
      } else {
        this.diskStatusNode.textContent = `${meta.folderName} ✓`;
        this.diskStatusNode.dataset.state = meta.lastVerifiedAt ? 'ready' : 'pending';
      }
    }
    this.updateReadinessSummary();
  }

  async ensureDiskCaptureReadiness({ interactive = false } = {}) {
    if (!STUDIO_DISK_FEATURE_FLAG || !this.diskRecordingEnabled) {
      return { enabled: false, ready: false };
    }
    const result = await verifyStoredDiskRecordingDirectory({ requestPermission: interactive });
    if (!result.ok) {
      if (this.diskStatusNode) {
        this.diskStatusNode.textContent = result.message || 'Unable to access the selected folder.';
        this.diskStatusNode.dataset.state = 'error';
      }
      setDiskRecordingEnabled(false);
      this.diskRecordingEnabled = false;
      this.updateDiskRecordingUI();
      return {
        enabled: true,
        ready: false,
        error: new Error(result.message || 'Folder unavailable'),
      };
    }
    return {
      enabled: true,
      ready: true,
      folderName: result.folderName,
      verifiedAt: Date.now(),
    };
  }

  async handleDriveLink() {
    if (!this.cloud || this.cloudBusy.drive) {
      return;
    }
    this.cloudBusy.drive = true;
    this.updateCloudLinkUI();
    this.setCloudMessage('drive', 'Requesting Google authorization…', 'info');
    try {
      const client = this.cloud.ensureDriveClient();
      if (!client) {
        throw new Error('Google Drive integration is not available on this build.');
      }
      if (typeof client.ensureInitialized === 'function') {
        await client.ensureInitialized();
      }
      if (typeof client.requestAccessToken === 'function') {
        client.requestAccessToken();
      }
      if (client.promise && typeof client.promise.then === 'function') {
        await client.promise;
      } else {
        await new Promise((resolve) => setTimeout(resolve, 800));
      }
      if (this.cloud.hasDriveAccess()) {
        this.setCloudMessage('drive', 'Google Drive linked. Recordings will upload automatically.', 'success');
        const folder = this.session?.GDRIVE_FOLDERNAME || null;
        markCloudLinked('drive', { folder });
        await this.flushPendingDriveUploads();
      } else {
        this.setCloudMessage('drive', 'Check your popup blocker or try again.', 'warn');
        markCloudUnlinked('drive');
      }
    } catch (error) {
      console.error('Failed to link Google Drive', error);
      this.setCloudMessage('drive', error?.message || 'Failed to link Google Drive.', 'error');
      markCloudUnlinked('drive');
    } finally {
      this.cloudBusy.drive = false;
      this.updateCloudFooter();
    }
  }

  async handleDropboxLink() {
    if (!this.cloud || this.cloudBusy.dropbox) {
      return;
    }
    this.cloudBusy.dropbox = true;
    this.updateCloudLinkUI();
    const providedToken = (this.dropboxTokenInput?.value || '').trim();
    if (providedToken) {
      this.ensureDropboxTokenFallbackVisible();
    }
    const interactive = !providedToken;
    const hasExistingAccess = this.cloud?.hasDropboxAccess();
    const forceReauth = !providedToken && hasExistingAccess;
    const pendingMessage = providedToken
      ? 'Linking Dropbox with the provided token…'
      : hasExistingAccess
        ? 'Refreshing Dropbox session…'
        : 'Waiting for the Dropbox popup to complete…';
    this.setCloudMessage('dropbox', pendingMessage, 'info');
    try {
      if (typeof window.setupDropbox !== 'function') {
        throw new Error('Dropbox uploader is not available in this build.');
      }
      const client = await this.cloud.ensureDropboxClient(providedToken || undefined, { interactive, forceReauth });
      if (client) {
        this.setCloudMessage('dropbox', 'Dropbox linked. Recordings will upload automatically.', 'success');
        if (this.dropboxTokenInput) {
          this.dropboxTokenInput.value = '';
        }
        if (!providedToken) {
          this.hideDropboxTokenFallback();
        }
        markCloudLinked('dropbox');
      } else {
        markCloudUnlinked('dropbox');
        if (providedToken) {
          this.setCloudMessage('dropbox', 'Dropbox rejected the provided token. Double-check and try again.', 'error');
          this.ensureDropboxTokenFallbackVisible({ focus: true, select: true });
        } else {
          this.setCloudMessage('dropbox', 'Dropbox authorization was cancelled. Check your popup blocker and try again.', 'warn');
          this.ensureDropboxTokenFallbackVisible({ focus: true });
        }
      }
    } catch (error) {
      console.error('Failed to init Dropbox', error);
      this.setCloudMessage('dropbox', error?.message || 'Unable to initialise Dropbox.', 'error');
      this.ensureDropboxTokenFallbackVisible({ focus: true });
      markCloudUnlinked('dropbox');
    } finally {
      this.cloudBusy.dropbox = false;
      this.updateCloudFooter();
    }
  }

  createAudioContext() {
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    if (!AudioCtx) {
      return null;
    }
    return new AudioCtx();
  }

  buildLayout() {
    if (document.getElementById(STUDIO_ROOT_ID)) {
      return;
    }

    const root = createElement('div', '', { id: STUDIO_ROOT_ID });

    // Header
    const header = createElement('header', 'podcast-header');
    const title = createElement('h1', '', { text: 'Podcast Control Room' });
    const statusPill = createElement('div', 'podcast-status-pill');
    statusPill.innerHTML = '<span>Live-ready</span>';
    header.append(title, statusPill);

    // Main layout
    const main = createElement('main', 'podcast-main');

    // Left column (host input + roster + markers)
    const rosterColumn = createElement('div', 'podcast-roster');

    // Host Input panel (director's mic)
    const hostPanel = createElement('section', 'podcast-panel host-panel');
    hostPanel.append(createElement('h2', '', { text: '🎙️ Host Input' }));
    const hostControls = createElement('div', 'host-input-content');
    this.hostMicButton = createElement('button', 'host-input-toggle', { type: 'button', text: 'Enable', title: 'Toggle local host microphone capture (optional).'});
    this.hostMicButton.addEventListener('click', () => this.handleHostMicToggle());
    this.hostMuteButton = createElement('button', 'host-mute-toggle', { type: 'button', text: '🔊 Mute', title: 'Mute/unmute the host mic track.' });
    this.hostMuteButton.disabled = true;
    this.hostMuteButton.addEventListener('click', () => this.handleHostMuteToggle());
    this.hostMicStatusNode = createElement('div', 'host-input-status', { text: 'Idle' });
    hostControls.append(this.hostMicButton, this.hostMuteButton, this.hostMicStatusNode);
    this.hostMicErrorNode = createElement('div', 'host-input-error');
    hostPanel.append(hostControls, this.hostMicErrorNode);

    const rosterPanel = createElement('section', 'podcast-panel');
    this.rosterList = createElement('div', 'roster-list');
    rosterPanel.append(this.rosterList);
    makeCollapsible(rosterPanel, 'Talent Roster', 'podcastStudio.collapse.roster');

    const markersPanel = createElement('section', 'podcast-panel');
    this.markerLog = createElement('div', 'marker-log');
    const emptyMarkers = createElement('div', 'empty-state', { text: 'Tap “Marker” to drop cue points during recording.' });
    emptyMarkers.dataset.empty = 'true';
    this.markerLog.append(emptyMarkers);
    this.markerActions = createElement('div', 'cloud-sync-list__actions marker-actions');
    this.markerActions.style.display = 'none';
    this.markerExportButton = createElement('button', 'cloud-sync-list__button', { type: 'button', text: 'Export CSV', title: 'Download markers as a CSV file.' });
    this.markerExportButton.addEventListener('click', () => this.exportMarkersCsv());
    this.markerCopyButton = createElement('button', 'cloud-sync-list__button', { type: 'button', text: 'Copy CSV', title: 'Copy markers CSV to clipboard.' });
    this.markerCopyButton.addEventListener('click', () => this.copyMarkersCsv());
    this.markerActions.append(this.markerExportButton, this.markerCopyButton);
    markersPanel.append(this.markerLog, this.markerActions);
    makeCollapsible(markersPanel, 'Session Markers', 'podcastStudio.collapse.markers');

    rosterColumn.append(hostPanel, rosterPanel, markersPanel);

    // Right column (controls + timeline)
    const consoleColumn = createElement('div', 'podcast-console');
    const consoleGrid = createElement('div', 'podcast-console-grid');
    consoleColumn.append(consoleGrid);

    const invitePanel = createElement('section', 'podcast-panel invite-panel');
    invitePanel.classList.add('console-grid__span-2');
    const inviteIntro = createElement('p', 'invite-copy', {
      text: 'Share a pro audio-ready link with guests. Tweak processing flags before copying.',
    });
    const inviteLinkRow = createElement('div', 'invite-link-row');
    this.inviteLinkInput = createElement('input', 'invite-link-input', {
      type: 'text',
      readonly: 'true',
      value: '',
      title: 'Guest invite link (click to select).',
    });
    this.inviteLinkInput.addEventListener('focus', () => {
      try {
        this.inviteLinkInput.select();
      } catch (error) {
        console.warn('Invite link select failed', error);
      }
    });
    this.inviteCopyButton = createElement('button', 'invite-link-copy', { type: 'button', text: 'Copy link', title: 'Copy the guest invite link.' });
    this.inviteCopyButton.addEventListener('click', () => this.copyInviteLink());
    inviteLinkRow.append(this.inviteLinkInput, this.inviteCopyButton);
    this.inviteStatusNode = createElement('div', 'invite-status');

    const inviteOptions = createElement('div', 'invite-options');
    const optionDefs = [
      { key: 'disableVideo', label: 'Disable video preview', defaultChecked: false },
      { key: 'proAudio', label: 'Enable pro audio (stereo, 256 kbps)', defaultChecked: true },
      { key: 'disableAec', label: 'Disable echo cancellation', defaultChecked: true },
      { key: 'disableDenoise', label: 'Disable noise reduction', defaultChecked: true },
      { key: 'disableAgc', label: 'Disable auto gain control', defaultChecked: true },
      { key: 'guestRecordBackup', label: 'Guest-side audio record backup', defaultChecked: true },
    ];
    optionDefs.forEach((option) => {
      const optionLabel = createElement('label', 'invite-option');
      const checkbox = createElement('input', 'invite-option__checkbox', { type: 'checkbox' });
      checkbox.checked = option.defaultChecked;
      checkbox.title = 'Applies to the generated guest link.';
      optionLabel.title = option.label;
      checkbox.addEventListener('change', () => this.updateInviteLink());
      optionLabel.append(checkbox, createElement('span', 'invite-option__label', { text: option.label }));
      inviteOptions.append(optionLabel);
      this.inviteOptionNodes[option.key] = checkbox;
    });

    invitePanel.append(inviteIntro, inviteLinkRow, this.inviteStatusNode, inviteOptions);
    makeCollapsible(invitePanel, 'Guest Invites', 'podcastStudio.collapse.invites');

    const sessionToolsPanel = createElement('section', 'podcast-panel session-tools');
    sessionToolsPanel.classList.add('console-grid__span-2');
    const sessionToolsGrid = createElement('div', 'session-tools__grid');
    sessionToolsPanel.append(sessionToolsGrid);

    const controlCard = createElement('div', 'session-tool session-tool--control');
    controlCard.append(createElement('h2', 'session-tool__title', { text: '⏺ ISO Track Recording' }));

    // Buttons row
    const transportButtons = createElement('div', 'transport-buttons');
    this.recordButton = createElement('button', '', { type: 'button', text: '⏺ Record Audio', title: 'Start/stop ISO WAV recording (per-speaker tracks).' });
    this.recordButton.addEventListener('click', () => this.handleRecordToggle());
    this.markerButton = createElement('button', '', { type: 'button', text: 'Marker', title: 'Drop a cue marker at the current time.' });
    this.markerButton.disabled = true;
    this.markerButton.addEventListener('click', () => this.addMarker());
    transportButtons.append(this.recordButton, this.markerButton);

    // Video recording row
    const videoRecordSection = createElement('div', 'transport-strip transport-strip--video');
    this.recordShowButton = createElement('button', '', { type: 'button', text: '🎬 Record Group', title: 'Open a popup window with the combined scene for recording.' });
    this.recordShowButton.addEventListener('click', () => this.openRecordShowWindow());
    const videoHint = createElement('span', 'video-record-hint', { text: 'Opens popup with combined scene' });
    const videoIsoTip = createElement('a', 'video-iso-tip', {
      text: 'Individual video guide →',
      href: 'https://www.youtube.com/watch?v=s5shpEqLZbM',
      target: '_blank',
      rel: 'noopener',
      title: 'Open a guide for recording individual video tracks.',
    });
    videoRecordSection.append(this.recordShowButton, videoHint, videoIsoTip);

    // Status row
    const statusRow = createElement('div', 'recording-status-row');
    this.sessionInfo = createElement('div', 'session-info', { text: 'Room: ' + (this.roomName || '—') });
    this.recordingStatusNode = createElement('div', 'session-recording-status', { text: 'Idle' });
    this.recordingStatusNode.dataset.state = 'idle';
    statusRow.append(this.sessionInfo, this.recordingStatusNode);

    controlCard.append(transportButtons, videoRecordSection, statusRow);
    sessionToolsGrid.append(controlCard);

    // ISO Recording Configuration - unified destinations section
    const isoConfigCard = createElement('div', 'session-tool session-tool--iso-config');
    isoConfigCard.append(createElement('h2', 'session-tool__title', { text: '💾 ISO Recording Destinations' }));
    const isoConfigList = createElement('div', 'iso-config-list');

    // Google Drive row
    const driveRow = createElement('div', 'iso-config-row');
    driveRow.append(createElement('div', 'iso-config-row__label', { text: 'Google Drive' }));
    const driveActions = createElement('div', 'iso-config-row__actions');
    this.cloudLinkButtons.drive = createElement('button', 'iso-config-row__button', { type: 'button', text: 'Link', title: 'Authorize Google Drive uploads for ISO tracks.' });
    this.cloudLinkButtons.drive.addEventListener('click', () => this.handleDriveLink());
    this.cloudLinkButtons.drive.dataset.state = 'idle';
    this.cloudLinkStatusNodes.drive = createElement('span', 'iso-config-row__status', { text: 'Not linked' });
    this.cloudLinkStatusNodes.drive.dataset.state = 'idle';
    driveActions.append(this.cloudLinkButtons.drive, this.cloudLinkStatusNodes.drive);
    driveRow.append(driveActions);
    this.cloudLinkMessages.drive = createElement('div', 'iso-config-row__hint');
    this.cloudLinkMessages.drive.dataset.variant = '';
    const driveMessageText = createElement('span', 'iso-config-row__hint-text');
    this.cloudLinkMessages.drive.append(driveMessageText);
    this.cloudLinkMessageTextNodes.drive = driveMessageText;
    isoConfigList.append(driveRow, this.cloudLinkMessages.drive);

    // Dropbox row
    const dropboxRow = createElement('div', 'iso-config-row');
    dropboxRow.append(createElement('div', 'iso-config-row__label', { text: 'Dropbox' }));
    const dropboxActions = createElement('div', 'iso-config-row__actions');
    this.cloudLinkButtons.dropbox = createElement('button', 'iso-config-row__button', { type: 'button', text: 'Link', title: 'Authorize Dropbox uploads for ISO tracks.' });
    this.cloudLinkButtons.dropbox.addEventListener('click', () => this.handleDropboxLink());
    this.cloudLinkButtons.dropbox.dataset.state = 'idle';
    this.cloudLinkStatusNodes.dropbox = createElement('span', 'iso-config-row__status', { text: 'Not linked' });
    this.cloudLinkStatusNodes.dropbox.dataset.state = 'idle';
    dropboxActions.append(this.cloudLinkButtons.dropbox, this.cloudLinkStatusNodes.dropbox);
    dropboxRow.append(dropboxActions);
    this.cloudLinkMessages.dropbox = createElement('div', 'iso-config-row__hint');
    this.cloudLinkMessages.dropbox.dataset.variant = '';
    const dropboxMessageText = createElement('span', 'iso-config-row__hint-text');
    this.cloudLinkMessages.dropbox.append(dropboxMessageText);
    this.cloudLinkMessageTextNodes.dropbox = dropboxMessageText;
    const tokenFieldId = 'podcast-dropbox-token';
    const dropboxTokenRow = createElement('div', 'cloud-sync-token');
    this.dropboxTokenRow = dropboxTokenRow;
    const tokenLabel = createElement('label', 'cloud-sync-token__label', { text: 'Access token' });
    tokenLabel.setAttribute('for', tokenFieldId);
    this.dropboxTokenInput = createElement('input', 'cloud-sync-token__input', {
      type: 'password',
      placeholder: 'Paste Dropbox personal access token',
      id: tokenFieldId,
      spellcheck: 'false',
      autocapitalize: 'none',
      autocomplete: 'off',
      title: 'Fallback: paste a Dropbox token if the Link popup is unavailable.',
    });
    dropboxTokenRow.append(tokenLabel, this.dropboxTokenInput);
    const dropboxGuideRow = createElement('div', 'cloud-sync-token__guide');
    this.dropboxGuideRow = dropboxGuideRow;
    const guideLink = createElement('a', 'cloud-sync-guide-link', {
      text: 'Open the Dropbox setup guide',
      href: DROPBOX_GUIDE_URL,
      target: '_blank',
      rel: 'noopener',
      title: 'Open the Dropbox setup guide in a new tab.',
    });
    dropboxGuideRow.append('Need a token? ', guideLink);
    this.cloudLinkMessages.dropbox.append(dropboxTokenRow, dropboxGuideRow);
    this.hideDropboxTokenFallback();
    isoConfigList.append(dropboxRow, this.cloudLinkMessages.dropbox);

    // Local Disk row
    if (STUDIO_DISK_FEATURE_FLAG) {
      const diskSupported = typeof window.showDirectoryPicker === 'function';
      const diskRow = createElement('div', 'iso-config-row');
      diskRow.append(createElement('div', 'iso-config-row__label', { text: 'Local Disk' }));
      const diskActions = createElement('div', 'iso-config-row__actions');
      this.diskControls = diskActions;
      this.diskToggleButton = createElement('button', 'iso-config-row__button', {
        type: 'button',
        text: 'Arm',
        title: 'Arm/disarm recording ISO files to local disk.',
      });
      this.diskToggleButton.addEventListener('click', () => this.handleDiskToggle());
      this.diskFolderButton = createElement('button', 'iso-config-row__button iso-config-row__button--secondary', {
        type: 'button',
        text: diskSupported ? 'Folder' : 'N/A',
        title: diskSupported ? 'Choose the destination folder for disk recording.' : 'Local disk recording is not supported in this browser.',
      });
      this.diskFolderButton.disabled = !diskSupported;
      if (diskSupported) {
        this.diskFolderButton.addEventListener('click', () => this.handleDiskFolderSelection());
      }
      this.diskStatusNode = createElement('span', 'iso-config-row__status', {
        text: diskSupported ? 'No folder' : 'Not supported',
      });
      diskActions.append(this.diskToggleButton, this.diskFolderButton, this.diskStatusNode);
      diskRow.append(diskActions);
      isoConfigList.append(diskRow);
      this.updateDiskRecordingUI();
    }

    // Summary section
    const isoSummary = createElement('div', 'iso-config-summary');
    this.cloudSummaryNode = createElement('div', 'iso-config-summary__item', { text: 'Status: checking...' });
    this.cloudSummaryNode.dataset.state = 'pending';
    isoSummary.append(this.cloudSummaryNode);

    isoConfigCard.append(isoConfigList, isoSummary);
    sessionToolsGrid.append(isoConfigCard);
    makeCollapsible(sessionToolsPanel, 'Recording Controls', 'podcastStudio.collapse.recording');

    const timelinePanel = createElement('section', 'podcast-panel timeline-shell');
    timelinePanel.classList.add('console-grid__span-2');
    this.outputsContainer = createElement('div', 'timeline-surface');
    timelinePanel.append(this.outputsContainer);
    this.showOutputsMessage('Recordings and cue points will appear here.');
    makeCollapsible(timelinePanel, 'Timeline & Outputs', 'podcastStudio.collapse.timeline');

    const chatPanel = createElement('section', 'podcast-panel chat-panel');
    chatPanel.dataset.collapsed = 'false';
    this.chatPanel = chatPanel;
    const chatHeaderRow = createElement('div', 'chat-panel__header');
    const chatTitle = createElement('h2', '', { text: 'Control Room Chat' });
    const chatActions = createElement('div', 'chat-panel__actions');
    this.chatPopoutButton = createElement('button', 'chat-panel__action', { type: 'button', text: 'Pop out', title: 'Open chat in a separate window.' });
    this.chatPopoutButton.addEventListener('click', () => this.handleChatPopout());
    this.chatCollapseButton = createElement('button', 'panel-collapse-toggle', {
      type: 'button',
      text: '−',
    });
    this.chatCollapseButton.title = 'Collapse section';
    this.chatCollapseButton.setAttribute('aria-expanded', 'true');
    this.chatCollapseButton.addEventListener('click', () => this.toggleChatPanel());
    chatActions.append(this.chatPopoutButton, this.chatCollapseButton);
    chatHeaderRow.append(chatTitle, chatActions);
    chatPanel.append(chatHeaderRow);

    const chatBody = createElement('div', 'chat-panel__body');
    const legacyChat = document.getElementById('chatModule');
    if (legacyChat) {
      this.chatPlaceholder = document.createElement('div');
      this.chatPlaceholder.dataset.podcastPlaceholder = 'chat-module';
      if (legacyChat.parentNode) {
        legacyChat.parentNode.insertBefore(this.chatPlaceholder, legacyChat);
      }
      legacyChat.classList.remove('hidden');
      legacyChat.dataset.podcastOverlay = 'true';
      const legacyHeader = legacyChat.querySelector('.chat-header');
      if (legacyHeader) {
        const popLink = legacyHeader.querySelector('#popOutChat');
        if (popLink) {
          this.chatPopoutAnchor = popLink;
          popLink.style.display = 'none';
        }
        const closeLink = legacyHeader.querySelector('#closeChat');
        if (closeLink) {
          closeLink.style.display = 'none';
        }
        legacyHeader.dataset.podcastDisplay = legacyHeader.style.display || '';
        legacyHeader.style.display = 'none';
      }
      const legacyResizer = legacyChat.querySelector('.resizer');
      if (legacyResizer) {
        legacyResizer.dataset.podcastDisplay = legacyResizer.style.display || '';
        legacyResizer.style.display = 'none';
      }
      legacyChat.style.position = 'relative';
      legacyChat.style.right = 'auto';
      legacyChat.style.left = 'auto';
      legacyChat.style.bottom = 'auto';
      legacyChat.style.top = 'auto';
      legacyChat.style.zIndex = 'auto';
      legacyChat.style.maxWidth = '100%';
      legacyChat.style.width = '100%';
      legacyChat.style.height = 'auto';
      legacyChat.style.maxHeight = '320px';
      legacyChat.style.overflow = 'hidden';
      legacyChat.style.margin = '0';
      chatBody.append(legacyChat);
      this.chatModule = legacyChat;
    } else {
      chatBody.append(createElement('div', 'chat-panel__empty', { text: 'Chat initialising…' }));
    }
    if (this.chatPopoutButton) {
      const hasPopout = Boolean(this.chatPopoutAnchor || typeof window.createPopoutChat === 'function');
      this.chatPopoutButton.disabled = !hasPopout;
      if (!hasPopout) {
        this.chatPopoutButton.textContent = 'Pop out unavailable';
      }
    }
    chatPanel.append(chatBody);
    this.chatCollapsedHint = createElement('div', 'chat-panel__collapsed-hint', {
      text: 'Chat hidden. Click “Show chat” to reopen.',
    });
    this.chatCollapsedHint.style.display = 'none';
    chatPanel.append(this.chatCollapsedHint);

    if (this.chatModule) {
      this.chatModule.classList.remove('hidden');
      this.chatModule.style.display = '';
      this.chatModule.dataset.podcastEmbedded = 'true';
    }

    consoleGrid.append(invitePanel);
    consoleGrid.append(sessionToolsPanel);
    consoleGrid.append(timelinePanel);
    chatPanel.classList.add('console-grid__span-2');
    consoleGrid.append(chatPanel);

    main.append(rosterColumn, consoleColumn);

    // Footer
    const footer = createElement('footer', 'podcast-footer');
    footer.innerHTML = `
      <div>Powered by VDO.Ninja • Low-latency P2P backbone intact • <span class="podcast-help-link" id="podcast-help-link" role="button" tabindex="0">Guide</span></div>
      <div class="cloud-status">
        <span id="podcast-cloud-drive">${this.cloud?.hasDriveAccess() ? 'Google Drive linked' : 'Drive link pending'}</span>
        <span id="podcast-cloud-dropbox">${this.cloud?.hasDropboxAccess() ? 'Dropbox linked' : 'Dropbox link pending'}</span>
      </div>
    `;

    root.append(header, main, footer);
    document.body.appendChild(root);

    // Help link click handler (must be after appendChild)
    const helpLink = document.getElementById('podcast-help-link');
    if (helpLink) {
      helpLink.addEventListener('click', () => this.openHelpModal());
      helpLink.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          this.openHelpModal();
        }
      });
    }

    this.driveStatusNode = document.getElementById('podcast-cloud-drive');
    this.dropboxStatusNode = document.getElementById('podcast-cloud-dropbox');
    this.updateHostMicUI();
    this.setHostMicError('');
    this.updateCloudLinkUI();
    this.updateReadinessSummary();
    this.setCloudMessage('drive', '');
    this.setCloudMessage('dropbox', '');
    this.updateInviteLink();
    this.toggleChatPanel(false);
    requestAnimationFrame(() => this.toggleChatPanel(false));
  }

  handleChatPopout() {
    if (this.chatPopoutAnchor && typeof this.chatPopoutAnchor.click === 'function') {
      try {
        this.chatPopoutAnchor.click();
        return;
      } catch (error) {
        console.warn('Legacy chat pop-out click failed', error);
      }
    }
    if (typeof window.createPopoutChat === 'function') {
      try {
        window.createPopoutChat();
        return;
      } catch (error) {
        console.warn('createPopoutChat invocation failed', error);
      }
    }
    try {
      window.open(window.location.href, '_blank', 'noopener');
    } catch (error) {
      console.warn('Fallback chat pop-out failed', error);
    }
  }

  toggleChatPanel(forceCollapsed) {
    const shouldCollapse = typeof forceCollapsed === 'boolean' ? forceCollapsed : !this.chatCollapsed;
    this.chatCollapsed = Boolean(shouldCollapse);
    if (this.chatPanel) {
      this.chatPanel.dataset.collapsed = this.chatCollapsed ? 'true' : 'false';
    }
    if (this.chatCollapseButton) {
      this.chatCollapseButton.textContent = this.chatCollapsed ? '+' : '−';
      this.chatCollapseButton.title = this.chatCollapsed ? 'Expand section' : 'Collapse section';
      this.chatCollapseButton.setAttribute('aria-expanded', this.chatCollapsed ? 'false' : 'true');
    }
    if (this.chatCollapsedHint) {
      this.chatCollapsedHint.style.display = this.chatCollapsed ? '' : 'none';
    }
    if (this.chatModule) {
      if (this.chatCollapsed) {
        this.chatModule.classList.add('hidden');
      } else {
        this.chatModule.classList.remove('hidden');
      }
    }
    if (this.session && typeof this.session.chat !== 'undefined') {
      this.session.chat = !this.chatCollapsed;
    }
  }

  attachRecorderEvents() {
    this.recorder.addEventListener('start', (event) => {
      if (this.abortUploadsController) {
        this.abortUploadsController.abort();
      }
      this.abortUploadsController = new AbortController();
      this.cleanupDownloadUrls();
      this.trackRuntimeStats.clear();
      this.trackLevelNodes.clear();
      this.teardownSpectrograms();
      this.outputIndicators.clear();
      this.recording = true;
      this.recordStartedAt = event?.detail?.startedAt || Date.now();
      this.markers = [];
      this.renderMarkers();
      this.scheduleAutoSyncMarker();
      this.recordButton.classList.add('recording');
      this.recordButton.textContent = 'Stop Recording';
      this.markerButton.disabled = false;
      this.showOutputsMessage('Recording… tracks will appear as audio arrives.');
      this.updateHostMicUI();
      this.setUploadProgressPending(true);
      if (this.recordingPlan?.sync) {
        this.recordingPlan.sync.start = {
          wallClock: this.recordStartedAt,
          highRes: snapshotHighResClock(),
        };
      }
      this.logRecordingEvent('record:start', { sessionId: this.recordingSessionId });
      this.updateRecordingPlanStatus('started', { events: this.recordingPlan?.events || [] });
      this.setRecordingStatus('Recording in progress', 'active');
    });

    this.recorder.addEventListener('chunk', (event) => {
      const { participant, trackType, channelIndex } = event.detail || {};
      if (!participant || !trackType) {
        return;
      }
      const channelKey = typeof channelIndex === 'number' ? channelIndex : 0;
      const key = this.buildTrackKey(participant.uuid, trackType, channelKey);
      if (!key) {
        return;
      }
      const indicator = this.ensureOutputIndicator(key, participant, trackType, channelKey);
      if (!indicator) {
        return;
      }
      indicator.badge.textContent = 'Recording';
      indicator.wrapper.dataset.state = 'recording';
      this.updateRecordingRuntimeMetrics(key, indicator, event.detail);
      this.trackManifestChunk(event.detail);
    });

    this.recorder.addEventListener('meter-ready', (event) => {
      const { participant, trackType, channelIndex, meter } = event.detail || {};
      if (!participant?.uuid || trackType !== 'audio') {
        return;
      }
      const key = this.buildTrackKey(participant.uuid, trackType, channelIndex);
      if (!key) {
        return;
      }
      const indicator = this.outputIndicators.get(key);
      if (!indicator) {
        return;
      }
      this.attachSpectrogram(key, indicator, participant, trackType, channelIndex, meter);
    });

    this.recorder.addEventListener('participant-added', (event) => {
      const { participant, trackCount, startOffsetSeconds } = event.detail || {};
      if (!participant) {
        return;
      }
      // Create output indicators immediately for the new participant's tracks
      for (let i = 0; i < (trackCount || 1); i += 1) {
        const key = this.buildTrackKey(participant.uuid, 'audio', i);
        if (key) {
          const indicator = this.ensureOutputIndicator(key, participant, 'audio', i);
          if (indicator?.badge) {
            indicator.badge.textContent = 'Late join';
            indicator.badge.title = `Joined ${startOffsetSeconds?.toFixed(1) || '?'}s into recording`;
          }
        }
      }
    });

    this.recorder.addEventListener('error', (event) => {
      console.error('Recorder error', event.detail);
      this.setStatusMessage('Recorder error: ' + (event.detail?.message || 'unknown'));
    });

    this.recorder.addEventListener('stop', (event) => {
      this.recording = false;
      this.recordButton.classList.remove('recording');
      this.recordButton.textContent = '⏺ Record Audio Tracks';
      this.markerButton.disabled = true;
      if (this.autoMarkerTimeout) {
        clearTimeout(this.autoMarkerTimeout);
        this.autoMarkerTimeout = null;
      }
      this.showOutputsMessage('Finalising recordings…');
      this.trackLevelNodes.clear();
      this.teardownSpectrograms();
      this.presentRecordings(event.detail?.files);
      this.outputIndicators.clear();
      this.trackRuntimeStats.clear();
      this.updateHostMicUI();
      if (this.recordingPlan?.sync) {
        this.recordingPlan.sync.stop = {
          wallClock: Date.now(),
          highRes: snapshotHighResClock(),
        };
      }
      if (this.recordingPlan) {
        this.recordingPlan.files = this.summariseRecordingFiles(event.detail?.files);
        this.logRecordingEvent('record:stop', { fileCount: this.recordingPlan?.files?.length || 0 });
        this.updateRecordingPlanStatus('stopped', {
          files: this.recordingPlan.files,
          events: this.recordingPlan.events,
        });
      }
      this.setRecordingStatus('Recording idle', 'idle');
    });
  }

  ensureOutputIndicator(key, participant, trackType, channelIndex = 0) {
    if (!this.outputsContainer) {
      return null;
    }
    if (this.outputIndicators.has(key)) {
      return this.outputIndicators.get(key);
    }

    this.prepareTracklistSurface();

    if (!this.outputsContainer.dataset.hasTracks) {
      this.outputsContainer.innerHTML = '';
      this.outputsContainer.dataset.hasTracks = 'true';
    }

    const wrapper = createElement('div', 'timeline-track');
    wrapper.dataset.key = key;
    wrapper.dataset.trackType = trackType;
    wrapper.dataset.participant = participant.uuid || '';
    wrapper.dataset.state = 'armed';

    const header = createElement('div', 'timeline-track__header');
    const titleGroup = createElement('div', 'timeline-track__title-group');
    const title = createElement('div', 'timeline-track__title', { text: participant.label || participant.uuid || 'Guest' });
    const descriptorParts = [];
    if (participant.external || participant.uuid === 'host-mic') {
      descriptorParts.push('Local input');
    } else if (participant.streamID) {
      descriptorParts.push(`Stream ${participant.streamID}`);
    }
    descriptorParts.push(trackType ? trackType.toUpperCase() : 'AUDIO');
    descriptorParts.push(`Channel ${channelIndex + 1}`);
    const subtitle = createElement('div', 'timeline-track__subtitle', {
      text: descriptorParts.filter(Boolean).join(' • '),
    });
    titleGroup.append(title, subtitle);
    const badge = createElement('span', 'timeline-track__badge', { text: 'Arming' });
    header.append(titleGroup, badge);

    const metrics = createElement('div', 'timeline-track__metrics');
    const inboundMetric = createElement('span', 'timeline-track__metric timeline-track__metric--inbound', {
      text: participant.external || participant.uuid === 'host-mic' ? 'Inbound: Local capture' : 'Inbound: pending…',
    });
    const recordMetric = createElement('span', 'timeline-track__metric timeline-track__metric--recording', {
      text: 'Recording: waiting…',
    });
    metrics.append(inboundMetric, recordMetric);

    const waveform = createElement('div', 'timeline-track__waveform');
    const spectrogramCanvas = document.createElement('canvas');
    spectrogramCanvas.className = 'timeline-track__spectrogram';
    const waveFill = createElement('div', 'timeline-track__wavefill');
    waveform.append(spectrogramCanvas, waveFill);

    wrapper.append(header, metrics, waveform);
    this.outputsContainer.append(wrapper);

    const indicator = {
      key,
      wrapper,
      badge,
      inboundMetric,
      recordMetric,
      waveFill,
      spectrogramCanvas,
      participant,
      trackType,
      channelIndex,
    };

    this.outputIndicators.set(key, indicator);
    this.registerTrackLevelNode(participant.uuid, waveFill);
    this.updateTrackInboundMetric(participant.uuid);
    this.attachSpectrogram(key, indicator, participant, trackType, channelIndex);
    return indicator;
  }

  setStatusMessage(message) {
    this.showOutputsMessage(message);
  }

  showOutputsMessage(text) {
    if (!this.outputsContainer) {
      return;
    }
    this.outputsContainer.dataset.mode = 'message';
    this.outputsContainer.dataset.hasTracks = '';
    this.outputsContainer.classList.remove('timeline-tracklist');
    this.outputsContainer.classList.remove('timeline-results');
    this.outputsContainer.innerHTML = '';
    if (typeof text === 'string' && text.trim()) {
      this.outputsContainer.append(createElement('div', 'timeline-placeholder', { text }));
    } else {
      this.outputsContainer.append(createElement('div', 'timeline-placeholder', { text: '' }));
    }
  }

  prepareTracklistSurface({ reset = false } = {}) {
    if (!this.outputsContainer) {
      return;
    }
    const switchingMode = this.outputsContainer.dataset.mode !== 'recording';
    if (switchingMode || reset) {
      this.outputsContainer.innerHTML = '';
      this.outputsContainer.dataset.hasTracks = '';
    }
    this.outputsContainer.dataset.mode = 'recording';
    this.outputsContainer.classList.add('timeline-tracklist');
    this.outputsContainer.classList.remove('timeline-results');
  }

  buildTrackKey(uuid, trackType, channelIndex = 0) {
    if (!uuid || !trackType) {
      return '';
    }
    const index = typeof channelIndex === 'number' ? channelIndex : 0;
    return `${uuid}-${trackType}-${index}`;
  }

  getMeterForTrack(uuid, trackType, channelIndex = 0) {
    if (!this.recorder || typeof this.recorder.getTrackMeter !== 'function') {
      return null;
    }
    return this.recorder.getTrackMeter(uuid, trackType, channelIndex);
  }

  attachSpectrogram(key, indicator, participant, trackType, channelIndex, meterOverride = null) {
    if (!key || trackType !== 'audio' || !indicator?.spectrogramCanvas) {
      return;
    }
    let renderer = this.spectrograms.get(key);
    if (!renderer) {
      renderer = new SpectrogramRenderer(indicator.spectrogramCanvas);
      this.spectrograms.set(key, renderer);
    }
    if (!participant?.uuid) {
      return;
    }
    const meter = meterOverride || this.getMeterForTrack(participant.uuid, trackType, channelIndex);
    if (meter?.analyser) {
      renderer.setAnalyser(meter.analyser);
    }
  }

  teardownSpectrograms() {
    if (!this.spectrograms) {
      return;
    }
    this.spectrograms.forEach((renderer) => {
      if (renderer && typeof renderer.destroy === 'function') {
        renderer.destroy();
      }
    });
    this.spectrograms.clear();
  }

  registerTrackLevelNode(uuid, node) {
    if (!uuid || !node) {
      return;
    }
    if (!this.trackLevelNodes.has(uuid)) {
      this.trackLevelNodes.set(uuid, new Set());
    }
    this.trackLevelNodes.get(uuid).add(node);
  }

  updateTrackLevelVisual(uuid, level) {
    if (!uuid) {
      return;
    }
    const nodes = this.trackLevelNodes.get(uuid);
    if (!nodes || !nodes.size) {
      return;
    }
    const normalized = Math.max(0.08, Math.min(1, (level || 0) / 100));
    nodes.forEach((node) => {
      if (!node) {
        return;
      }
      node.style.transform = `scaleY(${normalized})`;
      node.style.opacity = level > 3 ? '0.95' : '0.45';
    });
  }

  captureParticipantMetrics(participant) {
    if (!participant?.uuid) {
      return;
    }
    const next = { ...(this.participantMetrics.get(participant.uuid) || {}) };
    if (typeof participant.audioBitrateKbps === 'number' && participant.audioBitrateKbps >= 0) {
      next.audioBitrateKbps = participant.audioBitrateKbps;
    }
    if (participant.audioCodec) {
      next.audioCodec = participant.audioCodec;
    }
    if (participant.external || participant.uuid === 'host-mic') {
      next.local = true;
    }
    this.participantMetrics.set(participant.uuid, next);
    this.updateTrackInboundMetric(participant.uuid, next);
  }

  updateTrackInboundMetric(uuid, metrics = this.participantMetrics.get(uuid)) {
    if (!uuid) {
      return;
    }
    const resolvedMetrics = metrics || null;
    const indicators = this.outputIndicators || new Map();
    indicators.forEach((indicator) => {
      if (!indicator || !indicator.participant || indicator.participant.uuid !== uuid) {
        return;
      }
      const node = indicator.inboundMetric;
      if (!node) {
        return;
      }
      if (resolvedMetrics?.local) {
        node.textContent = 'Inbound: Local capture';
        return;
      }
      const parts = [];
      if (resolvedMetrics && typeof resolvedMetrics.audioBitrateKbps === 'number' && resolvedMetrics.audioBitrateKbps > 0) {
        const formatted = this.formatBitrate(resolvedMetrics.audioBitrateKbps);
        if (formatted) {
          parts.push(formatted);
        }
      }
      if (resolvedMetrics?.audioCodec) {
        parts.push(resolvedMetrics.audioCodec.toUpperCase());
      }
      node.textContent = parts.length ? `Inbound: ${parts.join(' • ')}` : 'Inbound: pending…';
    });
  }

  updateRecordingRuntimeMetrics(key, indicator, detail) {
    if (!indicator) {
      return;
    }
    const runtime = this.trackRuntimeStats.get(key) || {
      bytes: 0,
      startedAt: Date.now(),
      lastUpdate: Date.now(),
    };
    const chunk = detail?.data;
    if (chunk && typeof chunk.size === 'number') {
      runtime.bytes += chunk.size;
    }
    const now = Date.now();
    if (!runtime.startedAt) {
      runtime.startedAt = now;
    }
    runtime.lastUpdate = now;
    const elapsedMs = Math.max(1, now - runtime.startedAt);
    const kbps = runtime.bytes ? (runtime.bytes * 8) / elapsedMs : 0;
    const durationSeconds = (now - runtime.startedAt) / 1000;
    const sampleRate = this.recorder?.options?.targetSampleRate || 48000;
    const sampleRateLabel =
      sampleRate >= 1000
        ? `${(sampleRate / 1000).toFixed(sampleRate % 1000 === 0 ? 0 : 1)} kHz`
        : `${sampleRate} Hz`;
    const bitrateLabel = kbps > 0 ? `${Math.round(kbps)} kbps` : 'estimating…';
    const durationLabel = this.formatDuration(durationSeconds);
    indicator.recordMetric.textContent = `Recording: ${bitrateLabel} • WAV ${sampleRateLabel} • ${durationLabel}`;
    this.trackRuntimeStats.set(key, runtime);
  }

  buildRecordingPlanContext({ diskInfo } = {}) {
    const now = Date.now();
    const cloudSnapshot = readCloudLinkStatus();
    const plan = {
      sessionId: createRecordingSessionId(),
      conductor: 'studio',
      preparedAt: now,
      disk: {
        enabled: Boolean(diskInfo?.ready),
        folderName: diskInfo?.folderName || null,
        verifiedAt: diskInfo?.verifiedAt || null,
      },
      cloud: {
        driveLinked: Boolean(this.cloud?.hasDriveAccess() || cloudSnapshot.drive),
        dropboxLinked: Boolean(this.cloud?.hasDropboxAccess() || cloudSnapshot.dropbox),
        snapshot: cloudSnapshot,
      },
      sync: {
        prepared: snapshotHighResClock(),
        start: null,
        stop: null,
      },
      participants: {},
      files: [],
      events: [],
    };
    this.recordingPlan = plan;
    this.recordingSessionId = plan.sessionId;
    this.logRecordingEvent('record:plan', { sessionId: plan.sessionId });
    dispatchStudioEvent(PODCAST_RECORD_PLAN_EVENT, { plan });
    this.setRecordingStatus('Recording plan armed', 'armed');
    return plan;
  }

  updateRecordingPlanStatus(status, extra = {}) {
    if (!this.recordingPlan) {
      return;
    }
    const detail = {
      status,
      plan: this.recordingPlan,
      timestamp: Date.now(),
      ...extra,
    };
    dispatchStudioEvent(PODCAST_RECORD_STATUS_EVENT, detail);
  }

  trackManifestChunk(detail) {
    if (!this.recordingPlan || !detail?.participant?.uuid) {
      return;
    }
    const participantId = detail.participant.uuid;
    if (!this.recordingPlan.participants[participantId]) {
      this.recordingPlan.participants[participantId] = {
        participantId,
        label: detail.participant.label || participantId,
        tracks: {},
      };
    }
    const participantPlan = this.recordingPlan.participants[participantId];
    const trackKey = `${detail.trackType || 'audio'}:${typeof detail.channelIndex === 'number' ? detail.channelIndex : 0}`;
    if (!participantPlan.tracks[trackKey]) {
      participantPlan.tracks[trackKey] = {
        trackType: detail.trackType || 'audio',
        channelIndex: typeof detail.channelIndex === 'number' ? detail.channelIndex : 0,
        segments: [],
        totalBytes: 0,
        sequence: 0,
      };
    }
    const track = participantPlan.tracks[trackKey];
    const bytes = detail.data?.size || 0;
    track.sequence += 1;
    track.totalBytes += bytes;
    const timecodeMs = this.recordStartedAt ? Date.now() - this.recordStartedAt : 0;
    const segment = {
      sequence: track.sequence,
      bytes,
      receivedAt: Date.now(),
      timecodeMs,
    };
    if (track.segments.length > 48) {
      track.segments.shift();
    }
    track.segments.push(segment);
  }

  summariseRecordingFiles(filesMap) {
    if (!filesMap || typeof filesMap.forEach !== 'function') {
      return [];
    }
    const summaries = [];
    filesMap.forEach((meta) => {
      if (!meta) {
        return;
      }
      summaries.push({
        participant: meta.participant?.uuid || null,
        label: meta.participant?.label || null,
        trackType: meta.trackType,
        channelIndex: meta.channelIndex,
        filename: meta.filename,
        mimeType: meta.mimeType,
        size: meta.size,
        durationSeconds: meta.durationSeconds,
      });
    });
    return summaries;
  }

  logRecordingEvent(type, data = {}) {
    if (!type) {
      return;
    }
    if (!this.recordingPlan) {
      this.recordingPlan = {
        sessionId: createRecordingSessionId(),
        events: [],
      };
    }
    if (!Array.isArray(this.recordingPlan.events)) {
      this.recordingPlan.events = [];
    }
    const timestamp = Date.now();
    const timecodeMs = this.recordStartedAt ? Math.max(0, timestamp - this.recordStartedAt) : 0;
    this.recordingPlan.events.push({
      type,
      timestamp,
      timecodeMs,
      data,
    });
    if (this.recordingPlan.events.length > 2000) {
      this.recordingPlan.events.shift();
    }
  }

  setRecordingStatus(text, state = 'idle') {
    if (!this.recordingStatusNode) {
      return;
    }
    this.recordingStatusNode.textContent = text;
    this.recordingStatusNode.dataset.state = state;
  }

  formatBitrate(value) {
    if (!Number.isFinite(value) || value <= 0) {
      return null;
    }
    if (value >= 1000) {
      const megabits = value / 1000;
      return `${megabits.toFixed(megabits >= 10 ? 0 : 1)} Mbps`;
    }
    return `${Math.round(value)} kbps`;
  }

  formatDuration(seconds) {
    if (!Number.isFinite(seconds) || seconds < 0) {
      return '0:00';
    }
    const totalSeconds = Math.floor(seconds);
    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const secs = totalSeconds % 60;
    if (hours > 0) {
      return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
    }
    return `${minutes}:${secs.toString().padStart(2, '0')}`;
  }

  async handleRecordToggle() {
    if (this.recording) {
      this.showOutputsMessage('Wrapping up recording…');
      this.logRecordingEvent('record:stop:requested', { reason: 'host-toggle' });
      this.setRecordingStatus('Stopping recording…', 'stopping');
      if (this.markerButton) {
        this.markerButton.disabled = true;
      }
      if (this.autoMarkerTimeout) {
        clearTimeout(this.autoMarkerTimeout);
        this.autoMarkerTimeout = null;
      }
      const markerSnapshot = this.markers.map((marker) => ({ ...marker }));
      try {
        await this.recorder.stop({ markers: markerSnapshot });
      } catch (error) {
        console.error('Failed to stop recorder cleanly', error);
        this.setStatusMessage('Recording stop failed: ' + (error?.message || 'unknown error'));
      }
      return;
    }
    try {
      let diskInfo = null;
      if (STUDIO_DISK_FEATURE_FLAG && this.diskRecordingEnabled) {
        diskInfo = await this.ensureDiskCaptureReadiness({ interactive: true });
        if (diskInfo && diskInfo.error) {
          this.setStatusMessage(diskInfo.error.message || 'Disk folder not accessible.');
          return;
        }
      }
      this.buildRecordingPlanContext({ diskInfo });
      this.logRecordingEvent('record:arm', { source: 'host-toggle' });
      this.updateRecordingPlanStatus('armed', { events: this.recordingPlan?.events || [] });
      this.setRecordingStatus('Arming recorders…', 'arming');
      await this.recorder.start({
        includeVideo: false,
        includeLocal: false,
        extraParticipants: this.getAdditionalRecordingParticipants(),
      });
    } catch (error) {
      console.error('Failed to start recorder', error);
      this.setStatusMessage('Unable to start recording: ' + (error?.message || 'unknown error'));
      this.updateHostMicUI();
      this.logRecordingEvent('record:error', { stage: 'start', message: error?.message || 'unknown error' });
      this.setRecordingStatus('Recording idle', 'error');
      this.updateRecordingPlanStatus('error', { error: error?.message || 'start failed', events: this.recordingPlan?.events || [] });
      this.setUploadProgressPending(false);
    }
  }

  tryAddParticipantToRecording(participant) {
    if (!this.recording || !this.recorder) {
      return;
    }
    if (!participant?.stream) {
      return;
    }
    try {
      const result = this.recorder.addParticipant(participant);
      if (result?.added) {
        console.log(`Added late-joining participant to recording: ${participant.label || participant.uuid} (offset: ${result.startOffsetSeconds?.toFixed(1)}s)`);
        this.logRecordingEvent('participant:added-mid-recording', {
          uuid: participant.uuid,
          label: participant.label,
          startOffsetSeconds: result.startOffsetSeconds,
          trackCount: result.tracks,
        });
        // Drop a sync marker so the new track can be aligned with existing tracks
        this.addSyncMarkerForNewTrack(participant, result.startOffsetSeconds);
      }
    } catch (error) {
      console.warn('Failed to add participant to recording', error);
    }
  }

  addSyncMarkerForNewTrack(participant, startOffsetSeconds) {
    if (!this.recording || !this.recordStartedAt) {
      return;
    }
    // Wait 1 second after track starts, then drop a sync marker
    // This gives the track time to stabilize before the sync point
    setTimeout(() => {
      if (!this.recording) {
        return;
      }
      const timestamp = this.recordStartedAt ? (Date.now() - this.recordStartedAt) / 1000 : startOffsetSeconds + 1;
      const label = participant?.label || participant?.uuid || 'Guest';
      const note = {
        time: timestamp,
        label: `Sync: ${label} joined @ ${timestamp.toFixed(1)}s`,
        auto: true,
        joinSync: true,
      };
      this.markers.push(note);
      this.logRecordingEvent('marker', { label: note.label, timeSeconds: note.time, auto: true, joinSync: true });
      this.renderMarkers();
    }, 1000);
  }

  openRecordShowWindow() {
    const room = this.resolveRoomName();
    if (!room) {
      this.setStatusMessage('Set a room name before recording the show.');
      return;
    }
    // Build URL to the main VDO.ninja with scene + recordwindow
    const baseUrl = window.location.origin + window.location.pathname.replace(/\/podcast\/?.*/, '');
    const url = `${baseUrl}/?scene=0&room=${encodeURIComponent(room)}&recordwindow&chroma=000&locked=1.777`;
    const win = window.open(
      url,
      'recordShow',
      'toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=yes,width=1280,height=720'
    );
    if (win) {
      win.focus();
    }
  }

  async presentRecordings(filesMap) {
    const files = filesMap || this.recorder.getFiles();
    if (!files || files.size === 0) {
      this.showOutputsMessage('No media captured.');
      this.setUploadProgressPending(false);
      return;
    }
    this.cleanupDownloadUrls();
    this.outputsContainer.dataset.mode = 'results';
    this.outputsContainer.dataset.hasTracks = '';
    this.outputsContainer.classList.remove('timeline-tracklist');
    this.outputsContainer.classList.add('timeline-results');
    this.outputsContainer.innerHTML = '';
    const uploadPromises = [];
    this.setUploadProgressPending(false);
    files.forEach((meta, key) => {
      if (!meta?.blob) {
        return;
      }
      const wrapper = createElement('div', 'timeline-entry');
      wrapper.dataset.key = key;
      const label = `${meta.trackType.toUpperCase()} • ${meta.participant.label || meta.participant.uuid}`;
      const downloadUrl = URL.createObjectURL(meta.blob);
      this.activeDownloadUrls.push(downloadUrl);
      const linkLabel = meta.mimeType === 'audio/wav' ? 'Download WAV' : 'Download';
      const linkTitle =
        meta.mimeType === 'audio/wav'
          ? 'Download as WAV (includes embedded cue markers).'
          : 'Download captured media.';
      const link = createElement('a', 'marker-badge', { text: linkLabel, href: downloadUrl, title: linkTitle });
      const fallbackExtension = meta.mimeType?.split('/')?.[1] || 'webm';
      link.download = meta.filename || `${meta.participant.streamID || meta.participant.uuid}-${meta.trackType}.${fallbackExtension}`;
      const header = createElement('div', 'timeline-entry-header');
      header.append(createElement('span', 'timeline-entry-label', { text: label }), link);
      wrapper.append(header);
      const metaSummary = this.describeTrackMeta(meta) || 'Metadata pending';
      const metaLine = createElement('div', 'upload-meta', { text: metaSummary });
      if (meta.packagingError) {
        metaLine.textContent += metaSummary ? ' • fallback export' : 'Fallback export';
      }
      wrapper.append(metaLine);
      const statusContainer = createElement('div', 'upload-status');
      const driveLine = this.createServiceStatusLine('drive');
      const dropboxLine = this.createServiceStatusLine('dropbox');
      statusContainer.append(driveLine, dropboxLine);
      wrapper.append(statusContainer);
      this.outputsContainer.append(wrapper);
      uploadPromises.push(
        this.queueCloudUpload(meta, {
          drive: driveLine,
          dropbox: dropboxLine,
        }),
      );
    });
    if (uploadPromises.length) {
      try {
        await Promise.allSettled(uploadPromises);
      } catch (error) {
        console.warn('One or more uploads failed', error);
      }
    }
    this.updateCloudFooter();
  }

  addMarker() {
    if (!this.recording) {
      return;
    }
    const timestamp = this.recordStartedAt ? (Date.now() - this.recordStartedAt) / 1000 : 0;
    const note = {
      time: timestamp,
      label: `Marker @ ${timestamp.toFixed(1)}s`,
    };
    this.markers.push(note);
    this.logRecordingEvent('marker', { label: note.label, timeSeconds: note.time });
    this.renderMarkers();
  }

  scheduleAutoSyncMarker() {
    if (this.autoMarkerTimeout) {
      return;
    }
    this.autoMarkerTimeout = setTimeout(() => {
      this.autoMarkerTimeout = null;
      if (!this.recording) {
        return;
      }
      const timestamp = this.recordStartedAt ? (Date.now() - this.recordStartedAt) / 1000 : 1;
      const note = {
        time: timestamp,
        label: `Auto sync @ ${timestamp.toFixed(1)}s`,
        auto: true,
      };
      this.markers.push(note);
      this.logRecordingEvent('marker', { label: note.label, timeSeconds: note.time, auto: true });
      this.renderMarkers();
    }, 1000);
  }

  escapeCsvValue(value) {
    const raw = value === null || typeof value === 'undefined' ? '' : String(value);
    const escaped = raw.replace(/\"/g, '""');
    return `"${escaped}"`;
  }

  formatMarkerTimecode(seconds) {
    const safeSeconds = Number.isFinite(seconds) ? Math.max(0, seconds) : 0;
    const totalMs = Math.round(safeSeconds * 1000);
    const hours = Math.floor(totalMs / 3600000);
    const minutes = Math.floor((totalMs % 3600000) / 60000);
    const secs = Math.floor((totalMs % 60000) / 1000);
    const ms = totalMs % 1000;
    if (hours > 0) {
      return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
    }
    return `${minutes}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
  }

  buildMarkersCsv() {
    const markers = Array.isArray(this.markers) ? this.markers : [];
    const header = ['index', 'time_seconds', 'timecode', 'label', 'auto'].join(',');
    if (!markers.length) {
      return `${header}\n`;
    }
    const rows = markers.map((marker, index) => {
      const timeSeconds = Number.isFinite(marker?.time) ? marker.time : 0;
      const timecode = this.formatMarkerTimecode(timeSeconds);
      const label = marker?.label || `Marker #${index + 1}`;
      const auto = marker?.auto ? '1' : '0';
      return [
        index + 1,
        timeSeconds.toFixed(3),
        this.escapeCsvValue(timecode),
        this.escapeCsvValue(label),
        auto,
      ].join(',');
    });
    return `${header}\n${rows.join('\n')}\n`;
  }

  buildMarkersFilename() {
    const sessionId = this.recordingSessionId || this.recordingPlan?.sessionId || 'session';
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    return `vdo-ninja-markers-${sessionId}-${timestamp}.csv`;
  }

  exportMarkersCsv() {
    if (!this.markerExportButton) {
      return;
    }
    const csv = this.buildMarkersCsv();
    if (!csv.trim()) {
      return;
    }
    // Visual feedback while preparing download
    const originalText = this.markerExportButton.textContent;
    this.markerExportButton.textContent = 'Exporting…';
    this.markerExportButton.disabled = true;

    // Small delay to show feedback before download triggers
    setTimeout(() => {
      const blob = new Blob([csv], { type: 'text/csv' });
      const url = URL.createObjectURL(blob);
      try {
        const link = document.createElement('a');
        link.href = url;
        link.download = this.buildMarkersFilename();
        link.rel = 'noopener';
        link.click();
        this.markerExportButton.textContent = 'Exported';
      } catch (error) {
        console.warn('Failed to trigger CSV download', error);
        this.markerExportButton.textContent = 'Export failed';
      } finally {
        setTimeout(() => URL.revokeObjectURL(url), 100);
        // Restore button after a moment
        setTimeout(() => {
          if (this.markerExportButton) {
            this.markerExportButton.textContent = originalText;
            this.markerExportButton.disabled = false;
          }
        }, 1500);
      }
    }, 50);
  }

  async copyMarkersCsv() {
    if (!this.markerCopyButton) {
      return;
    }
    const csv = this.buildMarkersCsv();
    if (!csv.trim()) {
      return;
    }
    try {
      if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
        await navigator.clipboard.writeText(csv);
      } else {
        const textarea = document.createElement('textarea');
        textarea.value = csv;
        textarea.setAttribute('readonly', 'true');
        textarea.style.position = 'fixed';
        textarea.style.left = '-9999px';
        document.body.append(textarea);
        textarea.select();
        document.execCommand('copy');
        textarea.remove();
      }
      this.markerCopyButton.textContent = 'Copied';
      if (this.markerCopyResetTimer) {
        clearTimeout(this.markerCopyResetTimer);
      }
      this.markerCopyResetTimer = setTimeout(() => {
        this.markerCopyResetTimer = null;
        if (this.markerCopyButton) {
          this.markerCopyButton.textContent = 'Copy CSV';
        }
      }, 1500);
    } catch (error) {
      console.warn('Copy markers failed', error);
      this.markerCopyButton.textContent = 'Copy failed';
      if (this.markerCopyResetTimer) {
        clearTimeout(this.markerCopyResetTimer);
      }
      this.markerCopyResetTimer = setTimeout(() => {
        this.markerCopyResetTimer = null;
        if (this.markerCopyButton) {
          this.markerCopyButton.textContent = 'Copy CSV';
        }
      }, 2000);
    }
  }

  renderMarkers() {
    this.markerLog.innerHTML = '';
    if (this.markerActions) {
      this.markerActions.style.display = this.markers.length ? '' : 'none';
    }
    if (!this.markers.length) {
      const empty = createElement('div', 'empty-state', { text: 'Tap “Marker” to drop cue points during recording.' });
      empty.dataset.empty = 'true';
      this.markerLog.append(empty);
      return;
    }
    // Render newest markers first (reverse order) so they appear at the top
    for (let i = this.markers.length - 1; i >= 0; i -= 1) {
      const marker = this.markers[i];
      const timeSeconds = Number.isFinite(marker?.time) ? marker.time : 0;
      const timecode = this.formatMarkerTimecode(timeSeconds);
      const item = createElement('div', 'marker-item');
      item.title = `${marker?.auto ? 'Auto sync' : 'Marker'} @ ${timecode}`;
      item.append(createElement('span', '', { text: marker.label }));
      item.append(createElement('span', 'marker-badge', { text: `#${i + 1}` }));
      this.markerLog.append(item);
    }
  }

  startRosterLoop() {
    if (this.rosterTimer) {
      clearInterval(this.rosterTimer);
    }
    this.rosterTimer = setInterval(() => this.refreshRoster(), ROSTER_REFRESH_MS);
  }

  refreshRoster() {
    if (!this.session) {
      return;
    }
    this.updateRoomIndicator();
    const baseParticipants = collectParticipants(this.session);
    const participants = [...baseParticipants];
    this.virtualParticipants.forEach((participant) => {
      if (participant) {
        participants.push(participant);
      }
    });
    const activeIds = new Set();

    participants.forEach((participant) => {
      activeIds.add(participant.uuid);
      this.captureParticipantMetrics(participant);
      const existing = this.rosterItems.get(participant.uuid);
      if (existing) {
        this.updateRosterItem(existing, participant);
      } else {
        const item = this.createRosterItem(participant);
        this.rosterItems.set(participant.uuid, item);
        this.rosterList.append(item);
        // Add new participant to active recording
        this.tryAddParticipantToRecording(participant);
      }
    });

    Array.from(this.rosterItems.keys()).forEach((uuid) => {
      if (!activeIds.has(uuid)) {
        const node = this.rosterItems.get(uuid);
      if (node?.parentNode) {
        node.parentNode.removeChild(node);
      }
      this.rosterItems.delete(uuid);
      this.meterValues.delete(uuid);
      this.teardownDriveControl(uuid);
      if (this.remoteOverlay && this.remoteOverlay.dataset.activeUuid === uuid) {
        this.closeRemoteOverlay();
      }
    }
  });
  }

  createRosterItem(participant) {
    const item = createElement('div', 'roster-item');
    item.dataset.uuid = participant.uuid;
    item.dataset.status = participant.status || 'connecting';
    if (participant.role) {
      item.dataset.role = participant.role;
    }

    // Video thumbnail for guest preview
    const videoThumb = document.createElement('video');
    videoThumb.className = 'roster-item__video-thumb';
    videoThumb.muted = true;
    videoThumb.playsInline = true;
    videoThumb.autoplay = true;
    videoThumb.dataset.noVideo = 'true'; // hidden by default until video track available

    const meta = createElement('div', 'roster-meta');
    meta.append(createElement('div', 'roster-name', { text: participant.label }));
    const idText = participant.streamID ? `Stream: ${participant.streamID}` : 'Awaiting stream';
    meta.append(createElement('div', 'roster-id', { text: idText }));
    const descriptorText = this.describeParticipantRole(participant);
    if (descriptorText) {
      meta.append(createElement('div', 'roster-role', { text: descriptorText }));
    }

    const meter = createElement('div', 'meter-bar', { 'data-meter': participant.uuid });
    meter.append(createElement('div', 'meter-bar-fill'));

    const mediaRow = createElement('div', 'roster-item__media-row');
    mediaRow.append(videoThumb, meter);

    item.append(meta, mediaRow);

    const actions = createElement('div', 'roster-actions');
    const actionRow = createElement('div', 'roster-action-row');
    let hasActions = false;
    if (participant.role !== 'host-mic') {
      const controlButton = createElement('button', 'roster-action-button', {
        type: 'button',
        text: 'Remote Controls',
        title: 'Open legacy remote controls for this guest.',
      });
      controlButton.addEventListener('click', () => this.openRemoteControls(participant.uuid));
      actionRow.append(controlButton);
      hasActions = true;
    }
    const driveControls = this.createDriveControl(participant);
    if (driveControls) {
      actionRow.append(driveControls.button);
      hasActions = true;
    }
    if (hasActions) {
      if (driveControls?.status) {
        actionRow.append(driveControls.status);
      }
      actions.append(actionRow);
      item.append(actions);
    }

    this.updateRosterItem(item, participant);
    return item;
  }

  updateRosterItem(item, participant) {
    item.dataset.status = participant.status || 'connecting';
    const name = item.querySelector('.roster-name');
    if (name) {
      name.textContent = participant.label;
    }
    const id = item.querySelector('.roster-id');
    if (id) {
      id.textContent = participant.streamID ? `Stream: ${participant.streamID}` : 'Awaiting stream';
    }
    item.dataset.role = participant.role || '';
    const descriptor = item.querySelector('.roster-role');
    if (descriptor) {
      const descriptorText = this.describeParticipantRole(participant);
      descriptor.textContent = descriptorText || '';
      descriptor.style.display = descriptorText ? '' : 'none';
    }
    this.applyMeterValue(participant.uuid, participant.audioLevel || 0);
    this.updateDriveActionAvailability(participant.uuid);

    // Update video thumbnail if available
    const videoThumb = item.querySelector('.roster-item__video-thumb');
    if (videoThumb && this.session?.rpcs) {
      const peer = this.session.rpcs[participant.uuid];
      const videoTracks = peer?.streamSrc?.getVideoTracks?.() || [];
      if (videoTracks.length > 0) {
        if (!videoThumb.srcObject || videoThumb.srcObject.getVideoTracks()[0]?.id !== videoTracks[0].id) {
          videoThumb.srcObject = new MediaStream(videoTracks);
        }
        videoThumb.dataset.noVideo = 'false';
      } else {
        if (videoThumb.srcObject) {
          videoThumb.srcObject = null;
        }
        videoThumb.dataset.noVideo = 'true';
      }
    }
  }

  createDriveControl(participant) {
    if (!participant || participant.role === 'host-mic' || !participant.uuid) {
      return null;
    }
    if (typeof window === 'undefined' || typeof window.requestGoogleDriveRecord !== 'function') {
      return null;
    }
    const button = createElement('button', 'roster-action-button roster-action-button--drive', {
      type: 'button',
      text: 'Guest → Drive',
    });
    button.dataset.uuid = participant.uuid;
    button.addEventListener('click', () => this.handleDriveRecordToggle(participant.uuid));

    const status = createElement('div', 'roster-drive-status', { text: DRIVE_STATUS_MESSAGES.idle });
    status.dataset.state = 'idle';
    status.dataset.uuid = participant.uuid;

    this.rosterDriveButtons.set(participant.uuid, button);
    this.rosterDriveStatuses.set(participant.uuid, status);
    this.updateDriveActionAvailability(participant.uuid);
    this.applyDriveSnapshot(participant.uuid);

    return { button, status };
  }

  teardownDriveControl(uuid) {
    if (!uuid) {
      return;
    }
    if (this.driveStatusResetTimers.has(uuid)) {
      clearTimeout(this.driveStatusResetTimers.get(uuid));
      this.driveStatusResetTimers.delete(uuid);
    }
    this.rosterDriveButtons.delete(uuid);
    this.rosterDriveStatuses.delete(uuid);
    this.driveProgressSnapshots.delete(uuid);
  }

  findLegacyDriveButton(uuid) {
    if (!uuid || typeof document === 'undefined') {
      return null;
    }
    return document.querySelector('[data-action-type="recorder-google-drive-remote"][data--u-u-i-d="' + uuid + '"]');
  }

  canTriggerDriveUpload() {
    if (typeof window === 'undefined' || typeof window.requestGoogleDriveRecord !== 'function') {
      return false;
    }
    return Boolean(this.cloud?.hasDriveAccess());
  }

  async handleDriveRecordToggle(uuid) {
    if (!uuid) {
      return;
    }
    const button = this.rosterDriveButtons.get(uuid);
    if (!button) {
      return;
    }
    if (typeof window === 'undefined' || typeof window.requestGoogleDriveRecord !== 'function') {
      this.setRosterDriveStatus(uuid, 'error', 'Drive controls unavailable in this build.');
      return;
    }
    const legacyButton = this.findLegacyDriveButton(uuid);
    if (!legacyButton) {
      this.setRosterDriveStatus(uuid, 'pending', 'Guest controls preparing…');
      this.updateDriveActionAvailability(uuid);
      return;
    }
    const isActive = legacyButton.classList?.contains('pressed');
    if (!isActive && !this.canTriggerDriveUpload()) {
      this.setRosterDriveStatus(uuid, 'error', 'Link Google Drive above to enable uploads.');
      this.updateDriveActionAvailability(uuid);
      return;
    }
    button.dataset.pending = 'true';
    button.disabled = true;
    try {
      if (isActive) {
        await window.requestGoogleDriveRecord(legacyButton, false);
        this.setRosterDriveStatus(uuid, 'idle', DRIVE_STATUS_MESSAGES.idle);
      } else {
        this.setRosterDriveStatus(uuid, 'pending', 'Requesting Drive upload…');
        await window.requestGoogleDriveRecord(legacyButton);
      }
    } catch (error) {
      const message = error?.message || 'Drive request cancelled';
      this.setRosterDriveStatus(uuid, 'error', message);
    } finally {
      button.dataset.pending = 'false';
      this.updateDriveActionAvailability(uuid);
    }
  }

  updateDriveActionAvailability(uuid) {
    const button = this.rosterDriveButtons.get(uuid);
    if (!button) {
      return;
    }
    const hasRequestApi = typeof window !== 'undefined' && typeof window.requestGoogleDriveRecord === 'function';
    const legacyButton = this.findLegacyDriveButton(uuid);
    const hasLegacyControl = Boolean(legacyButton);
    const isActive = Boolean(legacyButton?.classList?.contains('pressed'));
    const pending = button.dataset.pending === 'true';

    let disabled = pending || !hasRequestApi;
    let title = '';

    if (!hasRequestApi) {
      title = 'Drive controls are not available in this build.';
    } else if (!hasLegacyControl) {
      title = 'Guest controls are still initialising.';
      disabled = true;
    } else if (isActive) {
      title = 'Stop this guest’s Drive upload.';
      disabled = pending;
    } else if (!this.canTriggerDriveUpload()) {
      title = 'Link Google Drive above to enable uploads.';
      disabled = true;
    } else {
      title = 'Ask this guest to upload to Drive.';
      disabled = pending;
    }

    button.disabled = disabled;
    button.textContent = isActive ? 'Stop Guest → Drive' : 'Guest → Drive';
    button.dataset.state = isActive ? 'active' : 'idle';
    if (title) {
      button.title = title;
    }
  }

  updateAllDriveActions() {
    this.rosterDriveButtons.forEach((_, uuid) => this.updateDriveActionAvailability(uuid));
  }

  setRosterDriveStatus(uuid, state = 'idle', text) {
    const node = this.rosterDriveStatuses.get(uuid);
    if (!node) {
      return;
    }
    if (this.driveStatusResetTimers.has(uuid)) {
      clearTimeout(this.driveStatusResetTimers.get(uuid));
      this.driveStatusResetTimers.delete(uuid);
    }
    const label = text || DRIVE_STATUS_MESSAGES[state] || DRIVE_STATUS_MESSAGES.idle;
    node.dataset.state = state;
    node.textContent = label;
    if (state === 'done') {
      const timer = setTimeout(() => {
        this.setRosterDriveStatus(uuid, 'idle', DRIVE_STATUS_MESSAGES.idle);
        this.driveStatusResetTimers.delete(uuid);
      }, DRIVE_STATUS_RESET_MS);
      this.driveStatusResetTimers.set(uuid, timer);
    }
  }

  applyDriveSnapshot(uuid) {
    const snapshot = this.driveProgressSnapshots.get(uuid);
    if (!snapshot) {
      return;
    }
    this.setRosterDriveStatusFromSnapshot(uuid, snapshot);
  }

  setRosterDriveStatusFromSnapshot(uuid, gdrive) {
    if (!gdrive) {
      this.setRosterDriveStatus(uuid, 'idle', DRIVE_STATUS_MESSAGES.idle);
      return;
    }
    if (gdrive.state === 2) {
      this.setRosterDriveStatus(uuid, 'done', DRIVE_STATUS_MESSAGES.done);
      return;
    }
    if (typeof gdrive.rec === 'number' && gdrive.rec > 0) {
      const percent = Math.min(100, Math.round((gdrive.up / Math.max(1, gdrive.rec)) * 100));
      this.setRosterDriveStatus(uuid, 'uploading', `Drive upload ${percent}%`);
    } else {
      this.setRosterDriveStatus(uuid, 'pending', DRIVE_STATUS_MESSAGES.pending);
    }
  }

  handleDriveProgressEvent(event) {
    const detail = event?.detail;
    if (!detail || !detail.UUID) {
      return;
    }
    const { UUID: uuid, gdrive } = detail;
    this.driveProgressSnapshots.set(uuid, gdrive || null);
    if (!this.rosterDriveStatuses.has(uuid)) {
      return;
    }
    this.setRosterDriveStatusFromSnapshot(uuid, gdrive || null);
    this.updateDriveActionAvailability(uuid);
  }

  ensureRemoteOverlay() {
    if (this.remoteOverlay && this.remoteOverlayContent) {
      return this.remoteOverlay;
    }
    const overlay = createElement('div', 'remote-overlay');
    overlay.dataset.podcastOverlay = 'true';
    overlay.dataset.visible = 'false';

    const panel = createElement('div', 'remote-overlay__panel');
    const header = createElement('div', 'remote-overlay__header');
    const title = createElement('h3', 'remote-overlay__title', { text: 'Remote controls' });
    const closeButton = createElement('button', 'remote-overlay__close', { type: 'button', text: 'Close', title: 'Close remote controls.' });
    closeButton.addEventListener('click', () => this.closeRemoteOverlay());
    header.append(title, closeButton);

    const body = createElement('div', 'remote-overlay__body');
    panel.append(header, body);
    overlay.append(panel);

    overlay.addEventListener('click', (event) => {
      if (event.target === overlay) {
        this.closeRemoteOverlay();
      }
    });

    document.body.appendChild(overlay);
    this.remoteOverlay = overlay;
    this.remoteOverlayContent = body;
    return overlay;
  }

  restoreRemoteControls() {
    const state = this.remoteControlState;
    if (!state || !state.element) {
      if (this.remoteOverlay) {
        delete this.remoteOverlay.dataset.activeUuid;
      }
      return;
    }
    const { element, placeholder, wrapper } = state;
    try {
      if (wrapper && wrapper.parentNode) {
        wrapper.parentNode.removeChild(wrapper);
      }
    } catch (error) {
      console.warn('Failed to remove remote controls wrapper', error);
    }
    if (placeholder && placeholder.parentNode) {
      try {
        placeholder.parentNode.insertBefore(element, placeholder);
        placeholder.parentNode.removeChild(placeholder);
      } catch (error) {
        console.warn('Failed to restore remote controls container', error);
      }
    }
    this.remoteControlState = {
      activeUuid: null,
      element: null,
      placeholder: null,
      wrapper: null,
    };
    if (this.remoteOverlay) {
      delete this.remoteOverlay.dataset.activeUuid;
    }
  }

  openRemoteControls(uuid) {
    if (!uuid) {
      return;
    }
    if (this.remoteControlState?.activeUuid && this.remoteControlState.activeUuid !== uuid) {
      this.restoreRemoteControls();
    }
    const overlay = this.ensureRemoteOverlay();
    const body = this.remoteOverlayContent;
    if (!overlay || !body) {
      return;
    }
    body.innerHTML = '';

    const rosterNode = this.rosterItems.get(uuid);
    let label = '';
    if (rosterNode) {
      const nameNode = rosterNode.querySelector('.roster-name');
      label = nameNode ? nameNode.textContent : '';
    }
    const headerTitle = overlay.querySelector('.remote-overlay__title');
    if (headerTitle) {
      headerTitle.textContent = label ? `Remote controls • ${label}` : 'Remote controls';
    }

    const existingState = this.remoteControlState || {};
    if (existingState.activeUuid && existingState.activeUuid === uuid && existingState.wrapper) {
      body.append(existingState.wrapper);
      overlay.dataset.visible = 'true';
      overlay.dataset.activeUuid = uuid;
      return;
    }

    const source = document.getElementById(`container_${uuid}`);
    if (!source) {
      body.append(
        createElement('div', 'remote-overlay__empty', {
          text: 'Legacy director controls are still loading. Try again once the guest is fully connected.',
        }),
      );
      overlay.dataset.visible = 'true';
      overlay.dataset.activeUuid = uuid;
      return;
    }

    const placeholder = document.createElement('div');
    placeholder.dataset.podcastPlaceholder = 'remote-controls';
    source.parentNode?.insertBefore(placeholder, source);

    source.classList.remove('hidden');

    const wrapper = createElement('div', 'remote-overlay__legacy');
    wrapper.dataset.uuid = uuid;
    wrapper.append(source);
    body.append(wrapper);

    this.remoteControlState = {
      activeUuid: uuid,
      element: source,
      placeholder,
      wrapper,
    };

    overlay.dataset.visible = 'true';
    overlay.dataset.activeUuid = uuid;
  }

  closeRemoteOverlay() {
    if (!this.remoteOverlay) {
      return;
    }
    this.restoreRemoteControls();
    this.remoteOverlay.dataset.visible = 'false';
    if (this.remoteOverlayContent) {
      this.remoteOverlayContent.innerHTML = '';
    }
  }

  openHelpModal() {
    if (this.helpOverlay) {
      this.helpOverlay.dataset.visible = 'true';
      return;
    }

    const overlay = createElement('div', 'help-overlay');
    overlay.dataset.visible = 'true';
    overlay.dataset.podcastOverlay = 'true'; // Prevent CSS from hiding it
    this.helpOverlay = overlay;

    const panel = createElement('div', 'help-overlay__panel');

    const header = createElement('div', 'help-overlay__header');
    const title = createElement('h2', 'help-overlay__title', { text: 'Podcast Studio Guide' });
    const closeButton = createElement('button', 'help-overlay__close', { type: 'button', text: '✕', title: 'Close' });
    closeButton.addEventListener('click', () => this.closeHelpModal());
    header.append(title, closeButton);

    const content = createElement('div', 'help-overlay__content');

    const sections = [
      {
        title: 'Getting Started',
        content: `
          <p>The Podcast Studio is a specialized interface for recording multi-track audio from remote guests.</p>
          <ul>
            <li><strong>Create a room</strong> — Enter a room name and optional password</li>
            <li><strong>Share the invite link</strong> — Guests join via the generated link</li>
            <li><strong>Hit Record</strong> — Each guest's audio is captured as a separate WAV file</li>
          </ul>
          <p>All audio is recorded locally in your browser — nothing is uploaded unless you link cloud storage.</p>
        `,
      },
      {
        title: 'Session Markers',
        content: `
          <p>Markers are cue points you can drop during recording to mark important moments.</p>
          <ul>
            <li><strong>Manual markers</strong> — Click "Marker" to drop a cue at the current time</li>
            <li><strong>Auto sync markers</strong> — Dropped automatically ~1 second into recording for alignment</li>
            <li><strong>Join sync markers</strong> — Created when a guest joins mid-recording</li>
          </ul>
          <p><strong>WAV cue points:</strong> Markers are embedded directly in the WAV files as standard cue chunks. Compatible with:</p>
          <ul>
            <li>Adobe Audition, Audacity, Reaper, Pro Tools</li>
            <li>Most DAWs that support WAV cue/region markers</li>
          </ul>
          <p><strong>CSV export:</strong> Use "Export CSV" or "Copy CSV" to get markers in spreadsheet format for reference or importing into editors that don't read WAV cues.</p>
        `,
      },
      {
        title: 'Late Joiners & Reconnects',
        content: `
          <p>If a guest joins or reconnects while recording is in progress:</p>
          <ul>
            <li>Their audio is automatically added to the recording</li>
            <li>A sync marker is dropped ~1 second after they join</li>
            <li>Their track appears in the timeline with a "Late join" badge</li>
          </ul>
          <p><strong>Syncing in post:</strong> Each track's markers are adjusted relative to when that track started. Use the shared sync markers to align tracks in your editor.</p>
          <p>Screen shares added mid-session are also captured if video recording is enabled.</p>
        `,
      },
      {
        title: 'Cloud Backup',
        content: `
          <p>Link Google Drive or Dropbox to automatically upload recordings.</p>
          <p><strong>Google Drive:</strong></p>
          <ul>
            <li>Uploads complete files after recording stops</li>
            <li>Files appear in a "VDO.Ninja Recordings" folder</li>
          </ul>
          <p><strong>Dropbox:</strong></p>
          <ul>
            <li>Supports chunked uploads for large files</li>
            <li>More reliable for longer recordings</li>
            <li>Can paste a token manually if popup is blocked</li>
          </ul>
          <p>Both services are optional — recordings are always available for local download.</p>
        `,
      },
      {
        title: 'Video Recording',
        content: `
          <p>The studio focuses on audio ISO recording, but video options exist:</p>
          <ul>
            <li><strong>Record Group</strong> — Opens a popup with the combined scene for screen recording</li>
            <li><strong>Individual video ISOs</strong> — <a href="https://www.youtube.com/watch?v=s5shpEqLZbM" target="_blank" rel="noopener">See video guide ↗</a></li>
          </ul>
          <p>For individual video tracks, guests can use <code>&record</code> in their URL to self-record, or use the remote recording features in the classic VDO.Ninja interface.</p>
        `,
      },
      {
        title: 'Live Captions',
        content: `
          <p>VDO.Ninja supports real-time speech-to-text captions:</p>
          <ul>
            <li><strong>Enable captions</strong> — Add <code>&transcribe</code> to a guest's URL to enable browser-based speech recognition</li>
            <li><strong>Display captions</strong> — Use <code>&showcc</code> on the viewer/scene URL to display incoming captions</li>
            <li><strong>Overlay in OBS</strong> — Captions can be displayed as a text overlay in your stream</li>
          </ul>
          <p>Captions are processed locally in the browser using the Web Speech API — no third-party services required.</p>
        `,
      },
      {
        title: 'Tips & Troubleshooting',
        content: `
          <ul>
            <li><strong>No audio?</strong> — Ensure guests have granted microphone permission</li>
            <li><strong>Tracks missing?</strong> — Check that guests joined before hitting Record, or they'll appear as late joiners</li>
            <li><strong>Large files?</strong> — Use Dropbox for chunked uploads, or download locally</li>
            <li><strong>Browser support:</strong> — Chrome/Edge recommended. Firefox/Safari may have limitations</li>
          </ul>
        `,
      },
    ];

    sections.forEach((section) => {
      const item = createElement('details', 'help-section');
      const summary = createElement('summary', 'help-section__title', { text: section.title });
      const body = createElement('div', 'help-section__body');
      body.innerHTML = section.content;
      item.append(summary, body);
      content.append(item);
    });

    // Open first section by default
    const firstSection = content.querySelector('details');
    if (firstSection) {
      firstSection.open = true;
    }

    panel.append(header, content);
    overlay.append(panel);

    overlay.addEventListener('click', (event) => {
      if (event.target === overlay) {
        this.closeHelpModal();
      }
    });

    document.body.appendChild(overlay);
  }

  closeHelpModal() {
    if (this.helpOverlay) {
      this.helpOverlay.dataset.visible = 'false';
    }
  }

  describeParticipantRole(participant) {
    if (!participant) {
      return '';
    }
    if (participant.role === 'host-mic') {
      return 'Local recording input';
    }
    return '';
  }

  applyMeterValue(uuid, value) {
    const percent = Math.min(100, Math.max(0, value));
    this.meterValues.set(uuid, percent);
    const meter = this.rosterList.querySelector(`[data-meter="${uuid}"] .meter-bar-fill`);
    if (meter) {
      meter.style.width = `${percent}%`;
    }
  }

  updateMeterFromBus(payload) {
    if (!payload?.uuid) {
      return;
    }
    const peak = payload.peak || 0;
    const level = Math.min(100, Math.round(peak * 120));
    this.applyMeterValue(payload.uuid, level);
    this.updateTrackLevelVisual(payload.uuid, level);
  }

  updateCloudFooter() {
    if (this.driveStatusNode) {
      const driveText = this.cloud?.hasDriveAccess()
        ? 'Google Drive linked'
        : 'Drive link pending';
      this.driveStatusNode.textContent = driveText;
    }
    if (this.dropboxStatusNode) {
      const dropboxText = this.cloud?.hasDropboxAccess()
        ? 'Dropbox linked'
        : 'Dropbox link pending';
      this.dropboxStatusNode.textContent = dropboxText;
    }
    this.updateCloudLinkUI();
    this.updateReadinessSummary();
  }

  updateReadinessSummary() {
    if (this.cloudSummaryNode) {
      const driveActive = Boolean(this.cloud?.hasDriveAccess());
      const dropboxActive = Boolean(this.cloud?.hasDropboxAccess());
      const driveStatus = driveActive ? 'Drive ready' : 'Drive not linked';
      const dropboxStatus = dropboxActive ? 'Dropbox ready' : 'Dropbox not linked';
      this.cloudSummaryNode.textContent = `Cloud uploads: ${driveStatus} • ${dropboxStatus}`;
      this.cloudSummaryNode.dataset.state = driveActive || dropboxActive ? 'ready' : 'pending';
    }
  }

  formatFileSize(bytes) {
    if (!bytes && bytes !== 0) {
      return '';
    }
    const thresh = 1024;
    if (bytes < thresh) {
      return `${bytes} B`;
    }
    const units = ['KB', 'MB', 'GB', 'TB'];
    let unitIndex = -1;
    let value = bytes;
    do {
      value /= thresh;
      unitIndex += 1;
    } while (value >= thresh && unitIndex < units.length - 1);
    return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
  }

  describeTrackMeta(meta) {
    const parts = [];
    if (meta?.mimeType) {
      parts.push(meta.mimeType.toUpperCase());
    }
    if (meta?.size) {
      parts.push(this.formatFileSize(meta.size));
    }
    if (meta?.durationSeconds) {
      parts.push(`${meta.durationSeconds.toFixed(1)}s`);
    }
    return parts.join(' • ');
  }

  describeService(service) {
    if (service === 'drive') {
      return 'Drive';
    }
    if (service === 'dropbox') {
      return 'Dropbox';
    }
    return service || 'Service';
  }

  createServiceStatusLine(service) {
    const line = createElement('div', 'upload-status-line');
    line.dataset.service = service;
    const ready =
      service === 'drive'
        ? this.cloud?.hasDriveAccess()
        : service === 'dropbox'
          ? this.cloud?.hasDropboxAccess()
          : false;
    const hint = ready ? 'ready' : 'link to upload';
    line.textContent = `${this.describeService(service)}: ${hint}`;
    line.title = ready
      ? `${this.describeService(service)} is linked; uploads will start when queued.`
      : `Link ${this.describeService(service)} above to enable uploads.`;
    return line;
  }

  setUploadProgressPending(pending) {
    ['drive', 'dropbox'].forEach((service) => {
      const node = this.cloudProgressNodes?.[service];
      if (!node) {
        return;
      }
      if (pending) {
        node.dataset.state = 'pending';
        node.textContent = `${this.describeService(service)} uploads pending (recording in progress)`;
      } else if (!this.uploadTrackers?.[service]?.size) {
        node.dataset.state = 'idle';
        node.textContent = `${this.describeService(service)} uploads idle`;
      }
    });
  }

  registerUploadTask(service, meta) {
    if (!service || !this.uploadTrackers?.[service]) {
      return null;
    }
    const tracker = this.uploadTrackers[service];
    const key = `${service}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
    const bytesTotal = meta?.blob?.size || 0;
    tracker.set(key, {
      key,
      label: meta?.participant?.label || meta?.filename || 'Track',
      bytesUploaded: 0,
      bytesTotal,
      status: 'pending',
      startedAt: Date.now(),
    });
    this.refreshUploadProgress(service);
    return key;
  }

  updateUploadTask(service, key, { uploaded, total, status } = {}) {
    if (!service || !key || !this.uploadTrackers?.[service]) {
      return;
    }
    const tracker = this.uploadTrackers[service];
    const entry = tracker.get(key);
    if (!entry) {
      return;
    }
    if (typeof uploaded === 'number') {
      entry.bytesUploaded = uploaded;
    }
    if (typeof total === 'number' && total >= 0) {
      entry.bytesTotal = total;
    }
    if (status) {
      entry.status = status;
    }
    this.refreshUploadProgress(service);
  }

  finalizeUploadTask(service, key, status = 'uploaded') {
    if (!service || !key || !this.uploadTrackers?.[service]) {
      return;
    }
    const tracker = this.uploadTrackers[service];
    const entry = tracker.get(key);
    if (!entry) {
      return;
    }
    entry.status = status;
    if (!entry.bytesTotal) {
      entry.bytesTotal = entry.bytesUploaded;
    }
    tracker.set(key, entry);
    this.refreshUploadProgress(service);
    const ttl = status === 'error' ? UPLOAD_TRACKER_COOLDOWN_MS * 2 : UPLOAD_TRACKER_COOLDOWN_MS;
    setTimeout(() => {
      const current = tracker.get(key);
      if (current && current.status === status) {
        tracker.delete(key);
        this.refreshUploadProgress(service);
      }
    }, ttl);
  }

  refreshUploadProgress(service) {
    const node = this.cloudProgressNodes?.[service];
    const tracker = this.uploadTrackers?.[service];
    if (!node || !tracker) {
      return;
    }
    if (!tracker.size) {
      node.textContent = `${this.describeService(service)} uploads idle`;
      node.dataset.state = 'idle';
      return;
    }
    const entries = Array.from(tracker.values());
    const errors = entries.filter((entry) => entry.status === 'error');
    const active = entries.filter((entry) => entry.status === 'pending' || entry.status === 'uploading');
    const completed = entries.filter((entry) => entry.status === 'uploaded');
    const skipped = entries.filter((entry) => entry.status === 'skipped');
    const uploadedBytes = entries.reduce((total, entry) => total + Math.min(entry.bytesUploaded || 0, entry.bytesTotal || entry.bytesUploaded || 0), 0);
    const totalBytes = entries.reduce((total, entry) => total + (entry.bytesTotal || entry.bytesUploaded || 0), 0);
    const percentage = totalBytes ? Math.min(100, Math.round((uploadedBytes / totalBytes) * 100)) : 0;
    if (errors.length) {
      node.textContent = `${this.describeService(service)} upload error (${errors.length})`;
      node.dataset.state = 'error';
      return;
    }
    if (active.length) {
      node.textContent = `${this.describeService(service)} uploading ${active.length} file${active.length === 1 ? '' : 's'} • ${percentage}%`;
      node.dataset.state = 'uploading';
      return;
    }
    if (completed.length || skipped.length) {
      node.textContent = `${this.describeService(service)} uploads complete`;
      node.dataset.state = 'complete';
      return;
    }
    node.textContent = `${this.describeService(service)} uploads idle`;
    node.dataset.state = 'idle';
  }

 applyUploadResult(element, result) {
    if (!element || !result) {
      return;
    }
    const label = this.describeService(result.service || element.dataset.service);
    element.dataset.status = result.status || 'unknown';
    if (result.status === 'uploaded') {
      const sizeText = result.bytes ? ` (${this.formatFileSize(result.bytes)})` : '';
      element.textContent = `${label}: uploaded${sizeText}`;
    } else if (result.status === 'skipped') {
      element.textContent = `${label}: ${result.reason || 'skipped'}`;
    } else if (result.status === 'error') {
      const message = result.error?.message || result.error?.toString() || 'failed';
      element.textContent = `${label}: ${message}`;
      element.dataset.status = 'error';
    } else {
      element.textContent = `${label}: ${result.status || 'unknown'}`;
    }
  }

  enqueuePendingDriveUpload(meta, driveElement) {
    if (!meta || !meta.blob) {
      return;
    }
    const existing = this.pendingDriveUploads.find((entry) => entry.meta === meta);
    if (existing) {
      if (driveElement) {
        existing.driveElement = driveElement;
      }
      return;
    }
    this.pendingDriveUploads.push({
      meta,
      driveElement: driveElement || null,
    });
    if (driveElement) {
      driveElement.dataset.status = 'pending';
      driveElement.textContent = `${this.describeService('drive')}: waiting for link…`;
    }
  }

  async flushPendingDriveUploads() {
    if (!this.pendingDriveUploads.length || !this.cloud?.hasDriveAccess()) {
      return;
    }
    const pending = [...this.pendingDriveUploads];
    this.pendingDriveUploads = [];
    for (const entry of pending) {
      try {
        await this.queueCloudUpload(
          entry.meta,
          {
            drive: entry.driveElement,
          },
          { driveOnly: true },
        );
      } catch (error) {
        console.warn('Deferred Drive upload failed', error);
      }
    }
  }

  async queueCloudUpload(meta, serviceElements = {}, options = {}) {
    if (!this.cloud || !meta?.blob) {
      if (serviceElements?.drive) {
        serviceElements.drive.textContent = 'Drive: unavailable';
      }
      if (serviceElements?.dropbox) {
        serviceElements.dropbox.textContent = 'Dropbox: unavailable';
      }
      return;
    }

    const driveOnly = options.driveOnly === true;

    let driveClient = null;
    let driveReady = false;
    try {
      driveClient = this.cloud.ensureDriveClient();
      driveReady = Boolean(this.cloud?.hasDriveAccess());
    } catch (error) {
      console.warn('Drive client unavailable; continuing without Drive uploads', error);
    }
    let canDropbox = !driveOnly && Boolean(this.cloud?.hasDropboxAccess());
    if (!driveOnly && !canDropbox) {
      try {
        const dropboxClient = await this.cloud.ensureDropboxClient();
        canDropbox = Boolean(dropboxClient);
      } catch (error) {
        console.warn('Dropbox client unavailable; continuing without Dropbox uploads', error);
      }
    }

    if (serviceElements?.drive) {
      const driveHint = driveReady ? 'preparing upload…' : 'link to upload';
      serviceElements.drive.textContent = `${this.describeService('drive')}: ${driveHint}`;
      serviceElements.drive.dataset.status = driveReady ? 'pending' : 'idle';
    }
    if (!driveOnly && serviceElements?.dropbox) {
      serviceElements.dropbox.textContent = `${this.describeService('dropbox')}: ${canDropbox ? 'preparing upload…' : 'link to upload'}`;
      serviceElements.dropbox.dataset.status = canDropbox ? 'pending' : 'idle';
    }

    const uploadKeys = {};
    const allowDriveUpload = driveReady && Boolean(driveClient);
    if (allowDriveUpload) {
      uploadKeys.drive = this.registerUploadTask('drive', meta);
    } else if (!driveOnly && driveClient) {
      this.enqueuePendingDriveUpload(meta, serviceElements?.drive || null);
    }
    if (!driveOnly && canDropbox) {
      uploadKeys.dropbox = this.registerUploadTask('dropbox', meta);
    }

    try {
      const results = await this.cloud.uploadBlob(meta.blob, {
        filename: meta.filename,
        drive: allowDriveUpload,
        dropbox: !driveOnly && canDropbox,
        onProgress: (progress) => {
          if (!progress?.service) {
            return;
          }
          const label = this.describeService(progress.service);
          if (progress.service === 'drive' && serviceElements?.drive) {
            serviceElements.drive.textContent = `${label}: ${progress.percentage || 0}%`;
            if (uploadKeys.drive) {
              this.updateUploadTask('drive', uploadKeys.drive, {
                uploaded: progress.uploaded,
                total: progress.total,
                status: 'uploading',
              });
            }
          } else if (progress.service === 'dropbox' && serviceElements?.dropbox) {
            serviceElements.dropbox.textContent = `${label}: ${progress.percentage || 0}%`;
            if (uploadKeys.dropbox) {
              this.updateUploadTask('dropbox', uploadKeys.dropbox, {
                uploaded: progress.uploaded,
                total: progress.total,
                status: 'uploading',
              });
            }
          }
        },
        signal: this.abortUploadsController?.signal,
      });
      if (allowDriveUpload) {
        this.applyUploadResult(serviceElements?.drive, results.drive);
      }
      if (!driveOnly) {
        this.applyUploadResult(serviceElements?.dropbox, results.dropbox);
      }
      if (uploadKeys.drive) {
        this.finalizeUploadTask('drive', uploadKeys.drive, results.drive?.status || 'unknown');
      }
      if (uploadKeys.dropbox) {
        this.finalizeUploadTask('dropbox', uploadKeys.dropbox, results.dropbox?.status || 'unknown');
      }
    } catch (error) {
      console.error('Cloud upload failed', error);
      if (allowDriveUpload && serviceElements?.drive) {
        serviceElements.drive.textContent = 'Drive: upload failed';
        serviceElements.drive.dataset.status = 'error';
      }
      if (!driveOnly && serviceElements?.dropbox) {
        serviceElements.dropbox.textContent = 'Dropbox: upload failed';
        serviceElements.dropbox.dataset.status = 'error';
      }
      if (uploadKeys.drive) {
        this.finalizeUploadTask('drive', uploadKeys.drive, 'error');
      }
      if (uploadKeys.dropbox) {
        this.finalizeUploadTask('dropbox', uploadKeys.dropbox, 'error');
      }
    } finally {
      this.updateCloudFooter();
    }
  }

  cleanupDownloadUrls() {
    if (!this.activeDownloadUrls || !this.activeDownloadUrls.length) {
      return;
    }
    this.activeDownloadUrls.forEach((url) => {
      try {
        URL.revokeObjectURL(url);
      } catch (error) {
        console.warn('Failed to revoke object URL', error);
      }
    });
    this.activeDownloadUrls = [];
  }

  dispose() {
    if (this.rosterTimer) {
      clearInterval(this.rosterTimer);
      this.rosterTimer = null;
    }
    if (this.diskStateListener) {
      window.removeEventListener(PODCAST_DISK_EVENT, this.diskStateListener);
      this.diskStateListener = null;
    }
    if (this.cloudStateListener) {
      window.removeEventListener(PODCAST_CLOUD_EVENT, this.cloudStateListener);
      this.cloudStateListener = null;
    }
    if (this.boundDriveProgressHandler) {
      window.removeEventListener(DRIVE_PROGRESS_EVENT, this.boundDriveProgressHandler);
      this.boundDriveProgressHandler = null;
    }
    if (this.levelOff) {
      this.levelOff();
      this.levelOff = null;
    }
    if (this.abortUploadsController) {
      this.abortUploadsController.abort();
      this.abortUploadsController = null;
    }
    if (this.hostMic?.active || this.virtualParticipants.size) {
      this.disableHostMic().catch((error) => {
        console.warn('Failed to disable host microphone during dispose', error);
      });
    }
    this.restoreRemoteControls();
    if (this.chatModule) {
      try {
        if (this.chatModule.dataset) {
          delete this.chatModule.dataset.podcastOverlay;
        }
        if (this.chatPlaceholder?.parentNode) {
          this.chatPlaceholder.parentNode.insertBefore(this.chatModule, this.chatPlaceholder);
          this.chatPlaceholder.parentNode.removeChild(this.chatPlaceholder);
        }
        const legacyHeader = this.chatModule.querySelector('.chat-header');
        if (legacyHeader) {
          if (legacyHeader.dataset && Object.prototype.hasOwnProperty.call(legacyHeader.dataset, 'podcastDisplay')) {
            legacyHeader.style.display = legacyHeader.dataset.podcastDisplay || '';
            delete legacyHeader.dataset.podcastDisplay;
          } else {
            legacyHeader.style.display = '';
          }
        }
        const legacyResizer = this.chatModule.querySelector('.resizer');
        if (legacyResizer) {
          if (legacyResizer.dataset && Object.prototype.hasOwnProperty.call(legacyResizer.dataset, 'podcastDisplay')) {
            legacyResizer.style.display = legacyResizer.dataset.podcastDisplay || '';
            delete legacyResizer.dataset.podcastDisplay;
          } else {
            legacyResizer.style.display = '';
          }
        }
        const popLink = this.chatModule.querySelector('#popOutChat');
        if (popLink) {
          popLink.style.display = '';
        }
        const closeLink = this.chatModule.querySelector('#closeChat');
        if (closeLink) {
          closeLink.style.display = '';
        }
        if (this.chatModule.style) {
          this.chatModule.style.position = '';
          this.chatModule.style.right = '';
          this.chatModule.style.left = '';
          this.chatModule.style.bottom = '';
          this.chatModule.style.top = '';
          this.chatModule.style.zIndex = '';
          this.chatModule.style.maxWidth = '';
          this.chatModule.style.width = '';
          this.chatModule.style.height = '';
          this.chatModule.style.maxHeight = '';
          this.chatModule.style.overflow = '';
          this.chatModule.style.margin = '';
        }
        this.chatModule.classList.add('hidden');
      } catch (error) {
        console.warn('Failed to restore chat module', error);
      }
      this.chatModule = null;
      this.chatPlaceholder = null;
    }
    this.chatPanel = null;
    this.chatCollapseButton = null;
    this.chatPopoutButton = null;
    this.chatPopoutAnchor = null;
    this.chatCollapsed = false;
    this.chatCollapsedHint = null;
    this.cleanupDownloadUrls();
    if (this.stopMeterBridge) {
      this.stopMeterBridge();
      this.stopMeterBridge = null;
    }
    if (this.inviteCopyTimer) {
      clearTimeout(this.inviteCopyTimer);
      this.inviteCopyTimer = null;
    }
    if (this.remoteOverlay && this.remoteOverlay.parentNode) {
      this.remoteOverlay.parentNode.removeChild(this.remoteOverlay);
    }
    this.remoteOverlay = null;
    this.remoteOverlayContent = null;
    this.rosterDriveButtons.clear();
    this.rosterDriveStatuses.clear();
    this.driveStatusResetTimers.forEach((timer) => clearTimeout(timer));
    this.driveStatusResetTimers.clear();
  }
}

async function bootstrap() {
  try {
    const preflight = await ensureRoomSelection();
    if (preflight?.redirect) {
      return;
    }
    const app = new PodcastStudioApp({ roomHint: preflight?.roomSlug });
    await app.init();
    window.podcastStudioApp = app;
  } catch (error) {
    console.error('Failed to initialise podcast studio', error);
  }
}

bootstrap();
