<?php
/* HOTFIX MODAL BUNDLE — 2026-03-07 | FILE: coproducer.php */
/* DB ASSET PATCH BUILD — 2026-03-07 | project_assets media persistence */
/* V10 BUILD MARKER — 2026-03-07 | FILE: coproducer.php */
// --------------------
// Load local .env (same directory) so STREAM_API_URL/TOKEN etc work on shared hosting
// --------------------
function load_local_env(string $path): void {
  if (!is_file($path) || !is_readable($path)) return;
  $lines = @file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  if (!is_array($lines)) return;
  foreach ($lines as $line) {
    $line = trim($line);
    if ($line === '' || $line[0] === '#') continue;
    if (stripos($line, 'export ') === 0) $line = trim(substr($line, 7));
    $pos = strpos($line, '=');
    if ($pos === false) continue;
    $key = trim(substr($line, 0, $pos));
    $val = trim(substr($line, $pos + 1));
    if ($key === '') continue;
    if ((strlen($val) >= 2) && (($val[0] === '"' && $val[strlen($val)-1] === '"') || ($val[0] === "'" && $val[strlen($val)-1] === "'"))) {
      $val = substr($val, 1, -1);
    }
    $existing = getenv($key);
    if ($existing !== false && $existing !== '') continue;
    @putenv($key . '=' . $val);
    $_ENV[$key] = $val;
  }
}
load_local_env(__DIR__ . '/.env');


/* === SESSION/AUTH BOOTSTRAP v28 (prevents login redirect loops) ===
   - Start session before ANY output
   - If dashboard login stored user id under common keys, normalize to $_SESSION['user_id']
   - If link passes ?user_id=..., hydrate session from it (so subsequent checks pass)
   - Never hard-redirect to login from this file; UI can still load for debugging
*/
if (!headers_sent()) {
  if (PHP_VERSION_ID >= 70300) {
    @session_set_cookie_params([
      'lifetime' => 0,
      'path' => '/',
      'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
      'httponly' => true,
      'samesite' => 'Lax',
    ]);
  } else {
    @session_set_cookie_params(0, '/; samesite=Lax', '', (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'), true);
  }
}

if (session_status() !== PHP_SESSION_ACTIVE && !headers_sent()) {
  @session_start();
}

$__uid_candidates = [
  $_SESSION['user_id'] ?? null,
  $_SESSION['uid'] ?? null,
  $_SESSION['id'] ?? null,
  (isset($_SESSION['user']) && is_array($_SESSION['user']) ? ($_SESSION['user']['id'] ?? null) : null),
  $_SESSION['auth_user_id'] ?? null,
];

$__uid = null;
foreach ($__uid_candidates as $__c) {
  if ($__c === null) continue;
  $v = is_numeric($__c) ? (string)$__c : trim((string)$__c);
  if ($v !== '') { $__uid = $v; break; }
}

if (isset($_GET['user_id']) && trim((string)$_GET['user_id']) !== '') {
  $__uid = trim((string)$_GET['user_id']);
  if (session_status() === PHP_SESSION_ACTIVE) {
    $_SESSION['user_id'] = $__uid;
    if (!isset($_SESSION['uid'])) $_SESSION['uid'] = $__uid;
  }
}

$GLOBALS['__COPRODUCER_USER_ID'] = $__uid;

/**
 * Co-Producer — Safety bootstrap (prevents "white page" on fatal errors)
 * - Default: errors are NOT displayed to users (production-safe)
 * - Add ?debug=1 to show the full error panel in-browser (temporary)
 * - All errors are logged to a writable file in sys temp dir
 */
$__CP_DEBUG = (isset($_GET['debug']) && $_GET['debug'] === '1');

error_reporting(E_ALL);
ini_set('log_errors', '1');

// Use a writable log location on cPanel/shared hosting.
$__cp_log = rtrim(sys_get_temp_dir(), '/\\') . '/coproducer_error.log';
@ini_set('error_log', $__cp_log);

if ($__CP_DEBUG) {
  ini_set('display_errors', '1');
  ini_set('display_startup_errors', '1');
} else {
  ini_set('display_errors', '0');
  ini_set('display_startup_errors', '0');
}

// Buffer output so shutdown handler can still print a meaningful panel.
if (!ob_get_level()) { @ob_start(); }

// Convert warnings/notices into log entries (do not interrupt page).
set_error_handler(function($severity, $message, $file, $line) use ($__cp_log, $__CP_DEBUG) {
  // Respect @ operator
  if (!(error_reporting() & $severity)) return false;
  $level = $severity;
  $msg = "[PHP] {$message} in {$file}:{$line} (sev={$level})";
  @error_log($msg);
  return true; // handled
});

// Catch uncaught exceptions and show a panel (or generic message in production)
set_exception_handler(function($ex) use ($__cp_log, $__CP_DEBUG) {
  $msg = "[EXCEPTION] " . $ex->getMessage() . " in " . $ex->getFile() . ":" . $ex->getLine();
  @error_log($msg . "\n" . $ex->getTraceAsString());

  if (!headers_sent()) {
    header('Content-Type: text/html; charset=UTF-8', true, 500);
  }
  @ob_clean();
  $msg = htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    $file = htmlspecialchars($e->getFile(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    $line = (int)$e->getLine();
    $trace = htmlspecialchars($e->getTraceAsString(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    http_response_code(200);
    header('Content-Type: text/html; charset=utf-8');
    $__cp_inline_css = '';
    $__cp_inline_js = '';
    $__cp_css_path = __DIR__ . '/assets/css/coproducer.core.css';
    $__cp_js_path  = __DIR__ . '/assets/js/modules/stream.module.js';
    if (is_file($__cp_css_path) && is_readable($__cp_css_path)) $__cp_inline_css = file_get_contents($__cp_css_path);
    if (is_file($__cp_js_path)  && is_readable($__cp_js_path))  $__cp_inline_js  = file_get_contents($__cp_js_path);
    echo <<<HTML
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Co-Producer Error</title>



<!-- Embedded assets -->
<style id=\"__INLINE_CSS__\">{$__cp_inline_css}
/* V8 BUILD MARKER — 2026-03-07 | overlay composer */
.overlayComposerBar{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 12px 0}
.overlayComposerNote{font-size:12px;opacity:.72}
.overlayComposerHead{margin:14px 0 8px 0;font-size:12px;letter-spacing:.12em;text-transform:uppercase;opacity:.78}
.ocModal{z-index:10040;background:rgba(3,6,14,.72);backdrop-filter:blur(10px)}
.ocShell{width:min(1480px,96vw);height:min(900px,92vh);margin:auto;background:linear-gradient(180deg,rgba(21,27,41,.98),rgba(10,14,23,.98));border:1px solid rgba(255,255,255,.12);border-radius:18px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 28px 90px rgba(0,0,0,.48)}
.ocHead{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,.08)}
.ocTitleWrap{display:flex;align-items:center;gap:12px}.ocTitleWrap b{font-size:18px}.ocSub{font-size:12px;opacity:.72}
.ocHeadActions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.ocHeadActions input{min-width:220px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:10px;color:#fff;padding:8px 10px}
.ocBody{display:grid;grid-template-columns:280px 1fr 330px;gap:0;min-height:0;flex:1}
.ocLeft,.ocRight{padding:14px;border-right:1px solid rgba(255,255,255,.08);overflow:auto}.ocRight{border-right:none;border-left:1px solid rgba(255,255,255,.08)}
.ocCenter{padding:14px;display:flex;flex-direction:column;gap:12px;min-height:0}
.ocSectionLabel{font-size:11px;letter-spacing:.14em;text-transform:uppercase;opacity:.72;margin:4px 0 10px}
.ocTemplates,.ocSceneList,.ocLayerList{display:grid;gap:8px}
.ocTpl,.ocSceneItem,.ocLayerItem{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:10px;cursor:pointer}.ocTpl:hover,.ocSceneItem:hover,.ocLayerItem:hover{border-color:rgba(245,158,11,.55)}.ocSceneItem.active,.ocLayerItem.active{outline:2px solid rgba(245,158,11,.86)}
.ocCanvasTools{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.ocToggle{display:flex;align-items:center;gap:6px;padding:0 8px}
.ocStageWrap{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,#0b1321,#05070b);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:16px;overflow:hidden}
.ocStage{position:relative;width:min(100%,1120px);aspect-ratio:16/9;background:#101318;border-radius:16px;overflow:hidden;box-shadow:inset 0 0 0 1px rgba(255,255,255,.06)}
.ocStage::before,.ocStage::after{content:"";position:absolute;pointer-events:none;inset:5%;border:1px dashed rgba(255,255,255,.08)}
.ocStage::after{inset:10%;border-color:rgba(245,158,11,.18)}
.ocEl{position:absolute;box-sizing:border-box;user-select:none;touch-action:none}.ocEl.selected{outline:2px solid rgba(245,158,11,.95);outline-offset:1px}.ocElText{color:#fff;font-weight:800;letter-spacing:.01em;text-shadow:0 8px 20px rgba(0,0,0,.35)}
.ocElPanel{background:rgba(0,0,0,.42);border:1px solid rgba(255,255,255,.08)}
.ocElImage img{width:100%;height:100%;display:block;object-fit:contain}
.ocInspector{display:grid;gap:10px}.ocField{display:grid;gap:6px}.ocField label{font-size:12px;opacity:.8}.ocField input,.ocField select,.ocField textarea{width:100%;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:10px;color:#fff;padding:8px 10px}.ocGrid2{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.ocMini{font-size:11px;opacity:.7}
.ocThumbPill{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:2px 8px;background:rgba(245,158,11,.14);font-size:11px}
@keyframes ocInFadeUp{from{opacity:0;transform:translateY(28px)}to{opacity:1;transform:translateY(0)}}
@keyframes ocInSlideLeft{from{opacity:0;transform:translateX(-48px)}to{opacity:1;transform:translateX(0)}}
@keyframes ocInZoom{from{opacity:0;transform:scale(.82)}to{opacity:1;transform:scale(1)}}
@keyframes ocLoopPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.04)}}
@keyframes ocLoopFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
@keyframes ocLoopTicker{0%{transform:translateX(0)}100%{transform:translateX(-24px)}}
@keyframes ocOutFadeDown{from{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(28px)}}
@keyframes ocOutSlideRight{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(48px)}}
@keyframes ocOutZoom{from{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.85)}}

.previewGuideTools{display:flex;align-items:center;gap:8px}
.cpModal[hidden]{display:none!important}
.cpModal{position:fixed;inset:0;z-index:10060;background:rgba(2,6,14,.62);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:24px}
.cpModalCard{width:min(560px,96vw);max-height:88vh;overflow:auto;border-radius:18px;background:linear-gradient(180deg,rgba(19,24,35,.98),rgba(11,14,21,.98));color:#f5f7fb;border:1px solid rgba(255,255,255,.09);box-shadow:0 30px 80px rgba(0,0,0,.48)}
.cpModalHead,.cpModalFoot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px}
.cpModalHead{border-bottom:1px solid rgba(255,255,255,.08)}
.cpModalFoot{border-top:1px solid rgba(255,255,255,.08)}
.cpModalTitle{font-size:16px;font-weight:800;letter-spacing:.02em}
.cpModalBody{padding:16px;display:grid;gap:12px}
.cpModal .fxRow{display:grid;grid-template-columns:180px 1fr;gap:12px;align-items:center}
.cpModal .fxRow label{font-size:13px;opacity:.86}
.cpModal .fxRow input[type="number"],.cpModal .fxRow select,.cpModal .fxRow input[type="color"],.cpModal .fxRow input[type="range"]{width:100%}
.cpModal .fxRow input[type="checkbox"]{justify-self:start;transform:scale(1.1)}

/* Modal core refactor v1 */
.appModalRoot,.overlay,.lkModal,.cpModal,#streamModalV3{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(2,6,14,.62);backdrop-filter:blur(8px);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility .16s ease;z-index:10050}
.appModalRoot.show,.overlay.show,.lkModal.show,.cpModal.show,#streamModalV3.show{opacity:1;visibility:visible;pointer-events:auto}
.overlay[aria-hidden="false"],.lkModal[aria-hidden="false"],.cpModal[aria-hidden="false"],#streamModalV3[aria-hidden="false"]{opacity:1;visibility:visible;pointer-events:auto}
.overlay[aria-hidden="true"],.lkModal[aria-hidden="true"],.cpModal[aria-hidden="true"],#streamModalV3[aria-hidden="true"]{opacity:0;visibility:hidden;pointer-events:none}
.overlay>.sheet,.overlay>.modalSheet,.overlay>.modalCard,.overlay>.ocShell,.overlay>.cpModalCard,.overlay>.streamCard,
.lkModal__panel,.cpModalCard,#streamModalV3>.streamCard{transform:translateY(14px) scale(.985);transition:transform .16s ease,opacity .16s ease;opacity:.98}
.overlay.show>.sheet,.overlay.show>.modalSheet,.overlay.show>.modalCard,.overlay.show>.ocShell,.overlay.show>.cpModalCard,.overlay.show>.streamCard,
.lkModal.show .lkModal__panel,.cpModal.show>.cpModalCard,#streamModalV3.show>.streamCard,
.overlay[aria-hidden="false"]>.sheet,.overlay[aria-hidden="false"]>.modalSheet,.overlay[aria-hidden="false"]>.modalCard,.overlay[aria-hidden="false"]>.ocShell,.overlay[aria-hidden="false"]>.cpModalCard,.overlay[aria-hidden="false"]>.streamCard,
.lkModal[aria-hidden="false"] .lkModal__panel,.cpModal[aria-hidden="false"]>.cpModalCard,#streamModalV3[aria-hidden="false"]>.streamCard{transform:translateY(0) scale(1);opacity:1}
.lkModal{z-index:10070}
.lkModal__backdrop{position:absolute;inset:0}
.lkModal__panel{position:relative;z-index:1;width:min(780px,96vw);max-height:88vh;overflow:auto;border-radius:18px;background:linear-gradient(180deg,rgba(19,24,35,.98),rgba(11,14,21,.98));color:#f5f7fb;border:1px solid rgba(255,255,255,.09);box-shadow:0 30px 80px rgba(0,0,0,.48)}
.lkModal__hdr{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,.08)}
.lkModal__ttl{font-size:16px;font-weight:800;letter-spacing:.02em}
.lkModal__x{appearance:none;border:0;border-radius:10px;padding:8px 10px;background:rgba(255,255,255,.08);color:#fff;cursor:pointer}
.lkModal__body{padding:16px}
.appModalNoScroll{overflow:hidden}
.studioLaunchBar{display:flex;justify-content:flex-end;align-items:center;gap:10px;margin:0 0 10px}

.guidesCard .miniBtn{min-width:110px}

</style>
<script id=\"__INLINE_JS__\">{$__cp_inline_js}</script>
<!-- Embedded assets -->
<style id=\"__INLINE_CSS__\">{$__cp_inline_css}</style>
<script id=\"__INLINE_JS__\">{$__cp_inline_js}</script>

<link rel="stylesheet" href="assets/css/modules/overlay-composer.css">
<link rel="stylesheet" href="assets/css/modules/feature-modal-core.css">
<link rel="stylesheet" href="assets/css/modules/media-studio.css">
<link rel="stylesheet" href="assets/css/modules/virtual-studio.css">
<link rel="stylesheet" href="assets/css/modules/transition-studio.css">

<style id="studio-launcher-thumb-styles">
.studioThumbLaunchWrap{ margin:0 0 12px 0; }
.studioThumbLaunch{
  cursor:pointer; user-select:none;
}
.studioThumbLaunch .thumbArt{
  display:flex; align-items:center; justify-content:center; text-align:center;
  font-weight:800; letter-spacing:.05em; line-height:1.15;
}
.studioThumbLaunch .thumbMeta{ display:flex; flex-direction:column; gap:2px; }
.studioThumbLaunch .thumbMeta span{ font-size:11px; opacity:.82; }
.studioThumbLaunch--media{ box-shadow: inset 0 0 0 1px rgba(0,188,212,.48); }
.studioThumbLaunch--virtual{ box-shadow: inset 0 0 0 1px rgba(142,68,255,.48); }
.studioThumbLaunch--transition{ box-shadow: inset 0 0 0 1px rgba(255,166,0,.52); }
</style>


<link rel="stylesheet" href="assets/css/modules/guest-studio.css">
<link rel="stylesheet" href="assets/css/modules/feature-studio-manager.css">
</head><body>
<div class="wrap">
  <div class="card">
    <div class="hdr"><div class="dot"></div><div><div style="font-weight:700">Co‑Producer crashed (PHP)</div><div class="muted">Fix the fatal error below to restore UI.</div></div></div>
    <div><b>Message:</b> <span class="muted">{$msg}</span></div>
    <div class="muted" style="margin-top:6px">{$file}:{$line}</div>
    <div style="margin-top:10px"><b>Trace</b></div>
    <pre>{$trace}</pre>
  </div>
</div>

<script>
(function(){
  function prep(el){
    if (!el) return;
    if (!el.hasAttribute('aria-hidden')) el.setAttribute('aria-hidden','true');
    if ('hidden' in el && el.getAttribute('aria-hidden') !== 'false') el.hidden = true;
  }
  document.addEventListener('DOMContentLoaded', function(){
    document.querySelectorAll('.overlay,.lkModal,.cpModal,#streamModalV3').forEach(prep);
    if(!root.__featureStudioDelegatedBound){
      root.__featureStudioDelegatedBound=true;
      root.addEventListener('dblclick', function(ev){
        var mediaItem = ev.target && ev.target.closest ? ev.target.closest('[data-studio-launch="media"]') : null;
        if(mediaItem && window.MediaStudio && typeof window.MediaStudio.open==='function'){ ev.preventDefault(); ev.stopPropagation(); window.MediaStudio.open({ projectId: (window.projectId || window.CURRENT_PROJECT_ID || '') }); return; }
        var virtItem = ev.target && ev.target.closest ? ev.target.closest('[data-studio-launch="virtual"]') : null;
        if(virtItem && window.VirtualStudio && typeof window.VirtualStudio.open==='function'){ ev.preventDefault(); ev.stopPropagation(); window.VirtualStudio.open({ projectId: (window.projectId || window.CURRENT_PROJECT_ID || '') }); return; }
        var transItem = ev.target && ev.target.closest ? ev.target.closest('[data-studio-launch="transition"]') : null;
        if(transItem && window.TransitionStudio && typeof window.TransitionStudio.open==='function'){ ev.preventDefault(); ev.stopPropagation(); window.TransitionStudio.open({ projectId: (window.projectId || window.CURRENT_PROJECT_ID || '') }); return; }
      }, true);
    }

    document.addEventListener('keydown', function(ev){
      if (ev.key !== 'Escape') return;
      const open = Array.from(document.querySelectorAll('.overlay.show,.lkModal.show,.cpModal.show,#streamModalV3.show,[aria-hidden="false"].overlay,[aria-hidden="false"].lkModal,[aria-hidden="false"].cpModal,[aria-hidden="false"]#streamModalV3'));
      const top = open[open.length-1];
      if (top && typeof window.__modalHide === 'function') window.__modalHide(top);
    });
  });
})();
</script>


<script id="cp-overlay-composer-hard-modal-patch">
(function(){
  function q(id){ return document.getElementById(id); }
  function qq(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }

  var MODAL_ID = 'cpOverlayHardModal';
  var MOUNT_ID = 'cpOverlayHardModalMount';
  var TITLE_ID = 'cpOverlayHardModalTitle';

  function ensureStyle(){
    if (q('cpOverlayHardModalStyle')) return;
    var css = document.createElement('style');
    css.id = 'cpOverlayHardModalStyle';
    css.textContent = [
      '#' + MODAL_ID + '{position:fixed;inset:0;z-index:999999;display:none;}',
      '#' + MODAL_ID + '.show{display:block;}',
      '#' + MODAL_ID + ' .cp-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(3px);}',
      '#' + MODAL_ID + ' .cp-frame{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(1600px,calc(100vw - 28px));height:min(900px,calc(100vh - 28px));display:grid;grid-template-rows:54px minmax(0,1fr);border:1px solid rgba(255,255,255,.14);border-radius:18px;overflow:hidden;background:linear-gradient(180deg,#132033,#08111b);box-shadow:0 24px 70px rgba(0,0,0,.55);}',
      '#' + MODAL_ID + ' .cp-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 14px;border-bottom:1px solid rgba(255,255,255,.08);background:linear-gradient(180deg,rgba(255,255,255,.08),rgba(255,255,255,.015)),linear-gradient(180deg,#1b2738,#101824);}',
      '#' + MODAL_ID + ' .cp-head h3{margin:0;font:700 13px/1.1 Arial,Helvetica,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#eef4ff;}',
      '#' + MODAL_ID + ' .cp-actions{display:flex;gap:8px;align-items:center;}',
      '#' + MODAL_ID + ' .cp-btn{padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,.10);background:linear-gradient(180deg,rgba(255,255,255,.08),rgba(255,255,255,.02));cursor:pointer;color:#eef4ff;font:700 11px/1 Arial,Helvetica,sans-serif;text-transform:uppercase;}',
      '#' + MODAL_ID + ' .cp-body{min-height:0;overflow:auto;padding:10px;background:linear-gradient(180deg,#08121d,#050912);}',
      '#' + MOUNT_ID + '{min-height:100%;}',
      '#' + MOUNT_ID + ' > *{display:block !important;}',
      '.cp-overlay-lifted-panel{display:block !important;height:auto !important;min-height:100% !important;visibility:visible !important;opacity:1 !important;overflow:auto !important;}',
      '[data-cp-overlay-placeholder="1"]{display:none !important;}',
      '.cp-force-hide-inline-overlay{display:none !important;}'
    ].join("\\n");
    document.head.appendChild(css);
  }

  function ensureModal(){
    if (q(MODAL_ID)) return q(MODAL_ID);

    var modal = document.createElement('div');
    modal.id = MODAL_ID;
    modal.setAttribute('aria-hidden', 'true');
    modal.innerHTML = [
      '<div class="cp-backdrop" data-close-overlay-modal="1"></div>',
      '<div class="cp-frame">',
        '<div class="cp-head">',
          '<h3 id="' + TITLE_ID + '">Overlay Composer</h3>',
          '<div class="cp-actions">',
            '<button class="cp-btn" id="cpOverlayHardModalClose" type="button">Close</button>',
          '</div>',
        '</div>',
        '<div class="cp-body"><div id="' + MOUNT_ID + '"></div></div>',
      '</div>'
    ].join('');
    document.body.appendChild(modal);

    q('cpOverlayHardModalClose').addEventListener('click', function(ev){
      ev.preventDefault();
      ev.stopPropagation();
      closeOverlayModal();
    }, true);

    modal.querySelector('[data-close-overlay-modal="1"]').addEventListener('click', function(ev){
      ev.preventDefault();
      ev.stopPropagation();
      closeOverlayModal();
    }, true);

    document.addEventListener('keydown', function(ev){
      if(ev.key === 'Escape' && modal.classList.contains('show')){
        closeOverlayModal();
      }
    }, true);

    return modal;
  }

  function findOverlayComposerRoot(){
    var exactIds = [
      'overlayComposerModal',
      'overlayComposer',
      'graphicsComposer',
      'graphicsComposerModal',
      'featureStudioModal'
    ];

    for (var i=0; i<exactIds.length; i++){
      var byId = q(exactIds[i]);
      if (byId) return byId;
    }

    var selectors = [
      '[data-overlay-composer-root]',
      '.overlay-composer',
      '.overlayComposer',
      '.graphics-composer',
      '.graphicsComposer',
      '.oc-shell',
      '.oc-root',
      '.composer-shell'
    ];

    for (var j=0; j<selectors.length; j++){
      var el = document.querySelector(selectors[j]);
      if (el) return el;
    }

    var textHits = qq('section,div');
    for (var k=0; k<textHits.length; k++){
      var t = (textHits[k].textContent || '').trim();
      if (t.indexOf('Overlay Composer v10') >= 0 || t.indexOf('Graphics Composer') >= 0){
        return textHits[k];
      }
    }

    return null;
  }

  function ensurePlaceholder(panel){
    if (!panel || !panel.parentNode) return null;
    var ph = document.createElement('div');
    ph.setAttribute('data-cp-overlay-placeholder', '1');
    panel.parentNode.insertBefore(ph, panel);
    return ph;
  }

  function showModal(){
    var modal = ensureModal();
    modal.classList.add('show');
    modal.setAttribute('aria-hidden', 'false');
    document.body.classList.add('appModalNoScroll');
  }

  function hideModal(){
    var modal = ensureModal();
    modal.classList.remove('show');
    modal.setAttribute('aria-hidden', 'true');
    document.body.classList.remove('appModalNoScroll');
  }

  function liftOverlayComposer(){
    ensureStyle();
    var modal = ensureModal();
    var mount = q(MOUNT_ID);

    if (mount.__currentPanel) {
      showModal();
      return true;
    }

    var panel = findOverlayComposerRoot();
    if (!panel) {
      console.warn('Overlay composer root not found.');
      return false;
    }

    if (panel.id === MODAL_ID || panel.closest('#' + MODAL_ID)) {
      showModal();
      return true;
    }

    if (panel === modal) return false;

    // prevent legacy inline modal shell from showing behind ours
    var legacy = q('overlayComposerModal');
    if (legacy && legacy !== panel) {
      legacy.classList.remove('show', 'is-open');
      legacy.hidden = true;
      legacy.style.display = 'none';
    }

    var placeholder = ensurePlaceholder(panel);
    mount.innerHTML = '';
    mount.appendChild(panel);

    panel.classList.add('cp-overlay-lifted-panel');
    panel.removeAttribute('hidden');
    panel.style.display = 'block';
    panel.style.position = 'relative';
    panel.style.inset = 'auto';
    panel.style.left = 'auto';
    panel.style.top = 'auto';
    panel.style.transform = 'none';
    panel.style.width = '100%';
    panel.style.height = 'auto';
    panel.style.maxWidth = 'none';
    panel.style.maxHeight = 'none';

    mount.__currentPanel = panel;
    mount.__currentPlaceholder = placeholder;

    var title = q(TITLE_ID);
    if (title) title.textContent = 'Overlay Composer';

    showModal();
    return true;
  }

  function closeOverlayModal(){
    var modal = ensureModal();
    var mount = q(MOUNT_ID);

    if (mount.__currentPanel && mount.__currentPlaceholder) {
      try {
        mount.__currentPlaceholder.parentNode.insertBefore(mount.__currentPanel, mount.__currentPlaceholder);
      } catch(e) {
        console.warn('Failed to restore overlay composer panel', e);
      }

      mount.__currentPanel.classList.remove('cp-overlay-lifted-panel');
      mount.__currentPanel.style.display = 'none';

      mount.__currentPanel = null;
      mount.__currentPlaceholder = null;
      mount.innerHTML = '';
    }

    hideModal();
  }

  function suppressInlineOpeners(){
    var ids = [
      'overlayComposerLauncher',
      'btnOpenOverlayComposerStudio',
      'btnOverlayComposer',
      'btnOpenGraphicsComposer'
    ];

    ids.forEach(function(id){
      var el = q(id);
      if (!el || el.__cpOverlayBound) return;
      el.__cpOverlayBound = true;

      var opener = function(ev){
        ev.preventDefault();
        ev.stopPropagation();
        ev.stopImmediatePropagation && ev.stopImmediatePropagation();
        liftOverlayComposer();
        return false;
      };

      el.addEventListener('click', opener, true);
      el.addEventListener('dblclick', opener, true);
    });

    document.addEventListener('dblclick', function(ev){
      var row = ev.target.closest('.selectable-overlay,[data-overlay],[data-composer-scene],.fxThumb,.overlay-row,.overlay-item');
      if (!row) return;
      ev.preventDefault();
      ev.stopPropagation();
      ev.stopImmediatePropagation && ev.stopImmediatePropagation();
      liftOverlayComposer();
    }, true);
  }

  function suppressLegacyInlineVisibility(){
    var candidates = [
      q('overlayComposerModal'),
      q('overlayComposer'),
      document.querySelector('.overlay-composer'),
      document.querySelector('.graphics-composer')
    ].filter(Boolean);

    candidates.forEach(function(el){
      if (!el) return;
      if (el.closest('#' + MODAL_ID)) return;
      if (el.__cpSuppressed) return;
      el.__cpSuppressed = true;
      el.classList.add('cp-force-hide-inline-overlay');
    });
  }

  function removeDedicatedWorkspaceText(){
    qq('*').forEach(function(el){
      if (!el || !el.childNodes || el.childNodes.length !== 1) return;
      var txt = (el.textContent || '').trim();
      if (txt === 'Dedicated Workspace') {
        el.textContent = 'Studio';
      }
    });
  }

  function boot(){
    ensureStyle();
    ensureModal();
    suppressInlineOpeners();
    removeDedicatedWorkspaceText();

    // keep trying briefly in case legacy modules mount late
    var tries = 0;
    var timer = setInterval(function(){
      suppressLegacyInlineVisibility();
      removeDedicatedWorkspaceText();
      tries++;
      if (tries > 20) clearInterval(timer);
    }, 500);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot, {once:true});
  } else {
    boot();
  }

  window.openOverlayComposerHardModal = liftOverlayComposer;
  window.closeOverlayComposerHardModal = closeOverlayModal;
})();
</script>

</body></html>
HTML;
    exit;
  });

// Catch fatal errors (E_ERROR / parse/runtime fatals) and avoid blank page
register_shutdown_function(function() use ($__cp_log, $__CP_DEBUG) {
  $err = error_get_last();
  if (!$err) return;

  $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
  if (!in_array($err['type'], $fatalTypes, true)) return;

  $msg = "[FATAL] {$err['message']} in {$err['file']}:{$err['line']} (type={$err['type']})";
  @error_log($msg);

  if (!headers_sent()) {
    header('Content-Type: text/html; charset=UTF-8', true, 500);
  }
  @ob_clean();
  $msg = htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    $file = htmlspecialchars($e->getFile(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    $line = (int)$e->getLine();
    $trace = htmlspecialchars($e->getTraceAsString(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    http_response_code(200);
    header('Content-Type: text/html; charset=utf-8');
    $__cp_inline_css = '';
    $__cp_inline_js = '';
    $__cp_css_path = __DIR__ . '/assets/css/coproducer.core.css';
    $__cp_js_path  = __DIR__ . '/assets/js/modules/stream.module.js';
    if (is_file($__cp_css_path) && is_readable($__cp_css_path)) $__cp_inline_css = file_get_contents($__cp_css_path);
    if (is_file($__cp_js_path)  && is_readable($__cp_js_path))  $__cp_inline_js  = file_get_contents($__cp_js_path);
    echo <<<HTML
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Co-Producer Error</title>

</head><body>
<div class="wrap">
  <div class="card">
    <div class="hdr"><div class="dot"></div><div><div style="font-weight:700">Co‑Producer crashed (PHP)</div><div class="muted">Fix the fatal error below to restore UI.</div></div></div>
    <div><b>Message:</b> <span class="muted">{$msg}</span></div>
    <div class="muted" style="margin-top:6px">{$file}:{$line}</div>
    <div style="margin-top:10px"><b>Trace</b></div>
    <pre>{$trace}</pre>
  </div>
</div>

<script>
(function(){
  function prep(el){
    if (!el) return;
    if (!el.hasAttribute('aria-hidden')) el.setAttribute('aria-hidden','true');
    if ('hidden' in el && el.getAttribute('aria-hidden') !== 'false') el.hidden = true;
  }
  document.addEventListener('DOMContentLoaded', function(){
    document.querySelectorAll('.overlay,.lkModal,.cpModal,#streamModalV3').forEach(prep);
    document.addEventListener('keydown', function(ev){
      if (ev.key !== 'Escape') return;
      const open = Array.from(document.querySelectorAll('.overlay.show,.lkModal.show,.cpModal.show,#streamModalV3.show,[aria-hidden="false"].overlay,[aria-hidden="false"].lkModal,[aria-hidden="false"].cpModal,[aria-hidden="false"]#streamModalV3'));
      const top = open[open.length-1];
      if (top && typeof window.__modalHide === 'function') window.__modalHide(top);
    });
  });
})();
</script>


<script id="cp-overlay-composer-hard-modal-patch">
(function(){
  function q(id){ return document.getElementById(id); }
  function qq(sel, root){ return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }

  var MODAL_ID = 'cpOverlayHardModal';
  var MOUNT_ID = 'cpOverlayHardModalMount';
  var TITLE_ID = 'cpOverlayHardModalTitle';

  function ensureStyle(){
    if (q('cpOverlayHardModalStyle')) return;
    var css = document.createElement('style');
    css.id = 'cpOverlayHardModalStyle';
    css.textContent = [
      '#' + MODAL_ID + '{position:fixed;inset:0;z-index:999999;display:none;}',
      '#' + MODAL_ID + '.show{display:block;}',
      '#' + MODAL_ID + ' .cp-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(3px);}',
      '#' + MODAL_ID + ' .cp-frame{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(1600px,calc(100vw - 28px));height:min(900px,calc(100vh - 28px));display:grid;grid-template-rows:54px minmax(0,1fr);border:1px solid rgba(255,255,255,.14);border-radius:18px;overflow:hidden;background:linear-gradient(180deg,#132033,#08111b);box-shadow:0 24px 70px rgba(0,0,0,.55);}',
      '#' + MODAL_ID + ' .cp-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 14px;border-bottom:1px solid rgba(255,255,255,.08);background:linear-gradient(180deg,rgba(255,255,255,.08),rgba(255,255,255,.015)),linear-gradient(180deg,#1b2738,#101824);}',
      '#' + MODAL_ID + ' .cp-head h3{margin:0;font:700 13px/1.1 Arial,Helvetica,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#eef4ff;}',
      '#' + MODAL_ID + ' .cp-actions{display:flex;gap:8px;align-items:center;}',
      '#' + MODAL_ID + ' .cp-btn{padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,.10);background:linear-gradient(180deg,rgba(255,255,255,.08),rgba(255,255,255,.02));cursor:pointer;color:#eef4ff;font:700 11px/1 Arial,Helvetica,sans-serif;text-transform:uppercase;}',
      '#' + MODAL_ID + ' .cp-body{min-height:0;overflow:auto;padding:10px;background:linear-gradient(180deg,#08121d,#050912);}',
      '#' + MOUNT_ID + '{min-height:100%;}',
      '#' + MOUNT_ID + ' > *{display:block !important;}',
      '.cp-overlay-lifted-panel{display:block !important;height:auto !important;min-height:100% !important;visibility:visible !important;opacity:1 !important;overflow:auto !important;}',
      '[data-cp-overlay-placeholder="1"]{display:none !important;}',
      '.cp-force-hide-inline-overlay{display:none !important;}'
    ].join("\\n");
    document.head.appendChild(css);
  }

  function ensureModal(){
    if (q(MODAL_ID)) return q(MODAL_ID);

    var modal = document.createElement('div');
    modal.id = MODAL_ID;
    modal.setAttribute('aria-hidden', 'true');
    modal.innerHTML = [
      '<div class="cp-backdrop" data-close-overlay-modal="1"></div>',
      '<div class="cp-frame">',
        '<div class="cp-head">',
          '<h3 id="' + TITLE_ID + '">Overlay Composer</h3>',
          '<div class="cp-actions">',
            '<button class="cp-btn" id="cpOverlayHardModalClose" type="button">Close</button>',
          '</div>',
        '</div>',
        '<div class="cp-body"><div id="' + MOUNT_ID + '"></div></div>',
      '</div>'
    ].join('');
    document.body.appendChild(modal);

    q('cpOverlayHardModalClose').addEventListener('click', function(ev){
      ev.preventDefault();
      ev.stopPropagation();
      closeOverlayModal();
    }, true);

    modal.querySelector('[data-close-overlay-modal="1"]').addEventListener('click', function(ev){
      ev.preventDefault();
      ev.stopPropagation();
      closeOverlayModal();
    }, true);

    document.addEventListener('keydown', function(ev){
      if(ev.key === 'Escape' && modal.classList.contains('show')){
        closeOverlayModal();
      }
    }, true);

    return modal;
  }

  function findOverlayComposerRoot(){
    var exactIds = [
      'overlayComposerModal',
      'overlayComposer',
      'graphicsComposer',
      'graphicsComposerModal',
      'featureStudioModal'
    ];

    for (var i=0; i<exactIds.length; i++){
      var byId = q(exactIds[i]);
      if (byId) return byId;
    }

    var selectors = [
      '[data-overlay-composer-root]',
      '.overlay-composer',
      '.overlayComposer',
      '.graphics-composer',
      '.graphicsComposer',
      '.oc-shell',
      '.oc-root',
      '.composer-shell'
    ];

    for (var j=0; j<selectors.length; j++){
      var el = document.querySelector(selectors[j]);
      if (el) return el;
    }

    var textHits = qq('section,div');
    for (var k=0; k<textHits.length; k++){
      var t = (textHits[k].textContent || '').trim();
      if (t.indexOf('Overlay Composer v10') >= 0 || t.indexOf('Graphics Composer') >= 0){
        return textHits[k];
      }
    }

    return null;
  }

  function ensurePlaceholder(panel){
    if (!panel || !panel.parentNode) return null;
    var ph = document.createElement('div');
    ph.setAttribute('data-cp-overlay-placeholder', '1');
    panel.parentNode.insertBefore(ph, panel);
    return ph;
  }

  function showModal(){
    var modal = ensureModal();
    modal.classList.add('show');
    modal.setAttribute('aria-hidden', 'false');
    document.body.classList.add('appModalNoScroll');
  }

  function hideModal(){
    var modal = ensureModal();
    modal.classList.remove('show');
    modal.setAttribute('aria-hidden', 'true');
    document.body.classList.remove('appModalNoScroll');
  }

  function liftOverlayComposer(){
    ensureStyle();
    var modal = ensureModal();
    var mount = q(MOUNT_ID);

    if (mount.__currentPanel) {
      showModal();
      return true;
    }

    var panel = findOverlayComposerRoot();
    if (!panel) {
      console.warn('Overlay composer root not found.');
      return false;
    }

    if (panel.id === MODAL_ID || panel.closest('#' + MODAL_ID)) {
      showModal();
      return true;
    }

    if (panel === modal) return false;

    // prevent legacy inline modal shell from showing behind ours
    var legacy = q('overlayComposerModal');
    if (legacy && legacy !== panel) {
      legacy.classList.remove('show', 'is-open');
      legacy.hidden = true;
      legacy.style.display = 'none';
    }

    var placeholder = ensurePlaceholder(panel);
    mount.innerHTML = '';
    mount.appendChild(panel);

    panel.classList.add('cp-overlay-lifted-panel');
    panel.removeAttribute('hidden');
    panel.style.display = 'block';
    panel.style.position = 'relative';
    panel.style.inset = 'auto';
    panel.style.left = 'auto';
    panel.style.top = 'auto';
    panel.style.transform = 'none';
    panel.style.width = '100%';
    panel.style.height = 'auto';
    panel.style.maxWidth = 'none';
    panel.style.maxHeight = 'none';

    mount.__currentPanel = panel;
    mount.__currentPlaceholder = placeholder;

    var title = q(TITLE_ID);
    if (title) title.textContent = 'Overlay Composer';

    showModal();
    return true;
  }

  function closeOverlayModal(){
    var modal = ensureModal();
    var mount = q(MOUNT_ID);

    if (mount.__currentPanel && mount.__currentPlaceholder) {
      try {
        mount.__currentPlaceholder.parentNode.insertBefore(mount.__currentPanel, mount.__currentPlaceholder);
      } catch(e) {
        console.warn('Failed to restore overlay composer panel', e);
      }

      mount.__currentPanel.classList.remove('cp-overlay-lifted-panel');
      mount.__currentPanel.style.display = 'none';

      mount.__currentPanel = null;
      mount.__currentPlaceholder = null;
      mount.innerHTML = '';
    }

    hideModal();
  }

  function suppressInlineOpeners(){
    var ids = [
      'overlayComposerLauncher',
      'btnOpenOverlayComposerStudio',
      'btnOverlayComposer',
      'btnOpenGraphicsComposer'
    ];

    ids.forEach(function(id){
      var el = q(id);
      if (!el || el.__cpOverlayBound) return;
      el.__cpOverlayBound = true;

      var opener = function(ev){
        ev.preventDefault();
        ev.stopPropagation();
        ev.stopImmediatePropagation && ev.stopImmediatePropagation();
        liftOverlayComposer();
        return false;
      };

      el.addEventListener('click', opener, true);
      el.addEventListener('dblclick', opener, true);
    });

    document.addEventListener('dblclick', function(ev){
      var row = ev.target.closest('.selectable-overlay,[data-overlay],[data-composer-scene],.fxThumb,.overlay-row,.overlay-item');
      if (!row) return;
      ev.preventDefault();
      ev.stopPropagation();
      ev.stopImmediatePropagation && ev.stopImmediatePropagation();
      liftOverlayComposer();
    }, true);
  }

  function suppressLegacyInlineVisibility(){
    var candidates = [
      q('overlayComposerModal'),
      q('overlayComposer'),
      document.querySelector('.overlay-composer'),
      document.querySelector('.graphics-composer')
    ].filter(Boolean);

    candidates.forEach(function(el){
      if (!el) return;
      if (el.closest('#' + MODAL_ID)) return;
      if (el.__cpSuppressed) return;
      el.__cpSuppressed = true;
      el.classList.add('cp-force-hide-inline-overlay');
    });
  }

  function removeDedicatedWorkspaceText(){
    qq('*').forEach(function(el){
      if (!el || !el.childNodes || el.childNodes.length !== 1) return;
      var txt = (el.textContent || '').trim();
      if (txt === 'Dedicated Workspace') {
        el.textContent = 'Studio';
      }
    });
  }

  function boot(){
    ensureStyle();
    ensureModal();
    suppressInlineOpeners();
    removeDedicatedWorkspaceText();

    // keep trying briefly in case legacy modules mount late
    var tries = 0;
    var timer = setInterval(function(){
      suppressLegacyInlineVisibility();
      removeDedicatedWorkspaceText();
      tries++;
      if (tries > 20) clearInterval(timer);
    }, 500);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot, {once:true});
  } else {
    boot();
  }

  window.openOverlayComposerHardModal = liftOverlayComposer;
  window.closeOverlayComposerHardModal = closeOverlayModal;
})();
</script>

</body></html>
HTML;
    exit;
  });

// --------------------
// Session bootstrap (must run BEFORE any includes that may emit output)
// Prevents: "session_start(): Session cannot be started after headers have already been sent"
// --------------------
if (session_status() !== PHP_SESSION_ACTIVE) {
  if (!headers_sent()) {
    // Some shared hosts emit warnings; suppress and rely on logs.
    @session_start();
  } else {
    // If headers already sent (e.g., BOM/whitespace in included files), continue without session.
    // coproducer will fall back to user_id from query string.
  }
}

require_once __DIR__ . '/../includes/layout.php';
require_once __DIR__ . '/../includes/config.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/db.php';

$user = require_login();
$pdo = db();
// Accept either project public id (?project= / ?id= / ?project_id=) OR join code (?code= / ?join_code=).
$pid = $_GET['project'] ?? ($_GET['id'] ?? ($_GET['project_id'] ?? ($_GET['proj'] ?? '')));
$pid = is_string($pid) ? trim($pid) : '';
$jcode = $_GET['code'] ?? ($_GET['join_code'] ?? ($_GET['join'] ?? ($_GET['j'] ?? '')));
$jcode = is_string($jcode) ? trim($jcode) : '';

$p = null;
if ($jcode !== '') {
  $st = $pdo->prepare("SELECT * FROM projects WHERE join_code=? AND created_by=? LIMIT 1");
  $st->execute([$jcode, $user['id']]);
  $p = $st->fetch(PDO::FETCH_ASSOC);
} elseif ($pid !== '') {
  $st = $pdo->prepare("SELECT * FROM projects WHERE project_id=? AND created_by=? LIMIT 1");
  $st->execute([$pid, $user['id']]);
  $p = $st->fetch(PDO::FETCH_ASSOC);
} else {
  http_response_code(400); echo "Missing project id or join code"; exit;
}

if(!$p){ http_response_code(404); echo "Not found"; exit; }

// Room mapping: connect LiveKit using per-project room_name (fallbacks: livekit_room, project_id).
$roomName = $p['room_name'] ?? ($p['livekit_room'] ?? ($p['project_id'] ?? 'demo'));
$roomName = is_string($roomName) ? trim($roomName) : 'demo';
if ($roomName === '') $roomName = 'demo';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<?php
$__cp_css_path = __DIR__ . '/assets/css/coproducer.core.css';
$__cp_css = '';
if (is_file($__cp_css_path) && is_readable($__cp_css_path)) {
  $__cp_css = file_get_contents($__cp_css_path);
}
?>
<style id="__COPRODUCER_CSS__">
<?php echo $__cp_css; ?>

</style>
<style id="guest-studio-v4-css">
/* Guest Studio / Participants / Messages merge v4 */
.tabHdr{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));}
.tabBtn{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#tab-gueststudio .guestStudioShell{
  height:100%;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:12px;
}
#tab-gueststudio .guestStudioGrid{
  min-height:0;overflow:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;
}
#tab-gueststudio .guestStudioCard{
  min-height:180px;border:1px solid rgba(255,255,255,.08);
  background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.01));
  display:grid;grid-template-rows:84px auto auto auto;border-radius:12px;overflow:hidden;
}
#tab-gueststudio .guestStudioCard .head{padding:10px;font-weight:800;text-transform:uppercase;border-bottom:1px solid rgba(255,255,255,.06)}
#tab-gueststudio .guestStudioCard .state{padding:6px 10px;color:rgba(255,255,255,.72);font-size:11px;text-transform:uppercase}
#tab-gueststudio .guestStudioCard .row{padding:0 10px 10px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}
#tab-gueststudio .guestStudioCard .row button{
  min-width:0;padding:7px 0;font-size:10px;font-weight:800;text-transform:uppercase;border:1px solid rgba(255,255,255,.10);
  background:#121b28;color:#fff;cursor:pointer;border-radius:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
#tab-gueststudio .tb{box-shadow:inset 0 0 0 1px rgba(208,88,88,.55);color:#ffc9c9}
#tab-gueststudio .solo{box-shadow:inset 0 0 0 1px rgba(75,116,199,.55);color:#d9e7ff}
#tab-gueststudio .live{box-shadow:inset 0 0 0 1px rgba(67,195,107,.55);color:#c8ffd7}
#tab-gueststudio .mute{box-shadow:inset 0 0 0 1px rgba(201,150,46,.55);color:#ffe1a8}
#tab-gueststudio .kick{box-shadow:inset 0 0 0 1px rgba(208,88,88,.55);color:#ffc9c9}
#tab-gueststudio .promote{box-shadow:inset 0 0 0 1px rgba(67,195,107,.55);color:#c8ffd7}
#tab-gueststudio .guestInspector{
  min-height:0;overflow:auto;border:1px solid rgba(255,255,255,.08);border-radius:12px;
  background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.01));
}
#tab-gueststudio .guestInspector .title{
  display:flex;align-items:center;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.08);text-transform:uppercase;font-weight:800;
}
#tab-gueststudio .guestInspector .body{padding:12px;display:grid;gap:10px}
#tab-gueststudio .fRow{display:grid;grid-template-columns:110px 1fr;gap:8px;align-items:center}
#tab-gueststudio .fRow input,#tab-gueststudio .fRow select{width:100%;padding:8px;border:1px solid rgba(255,255,255,.10);background:#0f1621;color:#fff;border-radius:8px}
.audienceListShell{
  width:100%;height:100%;min-height:0;display:grid;grid-template-rows:42px minmax(0,1fr);overflow:hidden;
  border:1px solid rgba(255,255,255,.08);border-radius:12px;
}
.audienceToolbar{
  display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;padding:6px;border-bottom:1px solid rgba(255,255,255,.08)
}
.audienceToolbar input,.audienceToolbar select{
  min-width:0;padding:8px;border:1px solid rgba(255,255,255,.10);background:#0f1621;color:#fff;border-radius:8px;
}
.audienceListBody{min-height:0;overflow:auto;display:grid;gap:8px;padding:8px}
.audienceRow{
  min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;align-items:center;
  border:1px solid rgba(255,255,255,.08);background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.01));
  border-radius:10px;padding:8px 10px;
}
.audienceMain b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.audienceMain span{font-size:10px;text-transform:uppercase;color:rgba(255,255,255,.7)}
.audienceActions{display:flex;gap:6px}
.audienceActions button{
  padding:6px 8px;font-size:10px;font-weight:800;text-transform:uppercase;border:1px solid rgba(255,255,255,.10);
  background:#121b28;color:#fff;cursor:pointer;border-radius:8px;
}
.guestOverviewMini{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;height:100%}
.guestOverviewMini .ovCard{
  min-height:0;border:1px solid rgba(255,255,255,.08);background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.01));
  display:grid;grid-template-rows:minmax(0,1fr) auto auto;border-radius:10px;overflow:hidden;
}
.guestOverviewMini .ovScreen{position:relative;min-height:0}
.guestOverviewMini .ovScreen:before{
  content:"OFFLINE";position:absolute;left:8px;top:8px;padding:4px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.08);
  background:rgba(0,0,0,.28);color:rgba(255,255,255,.75);font-size:10px;font-weight:700;
}
.guestOverviewMini .ovName{padding:6px 8px;border-top:1px solid rgba(255,255,255,.06);font-weight:800;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.guestOverviewMini .ovMeta{padding:6px 8px;display:flex;justify-content:space-between;gap:6px;border-top:1px solid rgba(255,255,255,.06)}
.guestOverviewMini .miniPill{display:inline-flex;align-items:center;justify-content:center;padding:3px 6px;border:1px solid rgba(255,255,255,.10);font-size:10px;text-transform:uppercase;border-radius:999px}
.guestOverviewMini .mic{box-shadow:inset 0 0 0 1px rgba(67,195,107,.55);color:#c8ffd7}
.guestOverviewMini .cam{box-shadow:inset 0 0 0 1px rgba(75,116,199,.55);color:#d9e7ff}
.guestOverviewMini .cue{box-shadow:inset 0 0 0 1px rgba(201,150,46,.55);color:#ffe1a8}
@media (max-width: 1200px){
  .tabHdr{grid-template-columns:repeat(4,minmax(0,1fr));}
  #tab-gueststudio .guestStudioShell{grid-template-columns:1fr}
}
</style>

<meta charset="utf-8" />
<link rel="icon" href="data:," />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Co-Producer</title>
<script>
window.__STREAM_CFG = window.__STREAM_CFG || {};
window.__STREAM_CFG.endpoint = <?php echo json_encode(getenv('STREAM_API_URL') ?: ''); ?>;
window.__STREAM_CFG.token = <?php echo json_encode(getenv('STREAM_API_TOKEN') ?: ''); ?>;
window.STREAM_API_URL = window.__STREAM_CFG.endpoint;
window.STREAM_API_TOKEN = window.__STREAM_CFG.token;

// Project/user context for Stream API (required so add_channel and start/stop hit the same DB rows)
(function(){
  try{
    const cfg = window.__STREAM_CFG || (window.__STREAM_CFG = {});
    cfg.project = cfg.project || <?php
      $streamProject = $p['project_id'] ?? ($_GET['project'] ?? $_GET['project_id'] ?? ($_SESSION['current_project'] ?? $_SESSION['project'] ?? ''));
      echo json_encode((string)$streamProject);
    ?>;
    cfg.userId = cfg.userId || <?php
      $uid = 0;
      if (isset($_GET['user_id']) && $_GET['user_id'] !== '') { $uid = (int)$_GET['user_id']; }
      else {
        foreach (['user_id','uid','auth_user_id','id'] as $k){
          if (isset($_SESSION[$k]) && $_SESSION[$k] !== '') { $uid = (int)$_SESSION[$k]; break; }
        }
      }
      echo json_encode((int)$uid);
    ?>;
  }catch(e){}
})();


/* ===== Itahuka Guards: prevent missing-function crashes & normalize LK URL ===== */
(function(){
  const w = window;

  // No-op build badge (some builds still reference it)
  if (typeof w.addBadge !== 'function') w.addBadge = function(){};

  // Ensure openHtmlModal exists (real modal, not alert)
  w.__modalShow = function(el){
    if (!el) return false;
    if ('hidden' in el) el.hidden = false;
    try { el.removeAttribute('hidden'); } catch(_){ }
    el.style.display = 'flex';
    el.setAttribute('aria-hidden','false');
    el.classList.add('show');
    document.documentElement.classList.add('appModalNoScroll');
    return true;
  };
  w.__modalHide = function(el){
    if (!el) return false;
    el.classList.remove('show');
    el.setAttribute('aria-hidden','true');
    if ('hidden' in el) el.hidden = true;
    try { el.setAttribute('hidden','hidden'); } catch(_){ }
    el.style.display = '';
    if (!document.querySelector('.overlay.show,.lkModal.show,.cpModal.show,#streamModalV3.show,[aria-hidden="false"].overlay,[aria-hidden="false"].lkModal,[aria-hidden="false"].cpModal,[aria-hidden="false"]#streamModalV3')) {
      document.documentElement.classList.remove('appModalNoScroll');
    }
    return true;
  };
  w.__bindModalChrome = function(root, closeFn){
    if (!root || root.__modalChromeBound) return root;
    root.__modalChromeBound = true;
    root.addEventListener('click', function(ev){ if (ev.target === root) closeFn ? closeFn() : w.__modalHide(root); });
    return root;
  };

  if (typeof w.openHtmlModal !== 'function') {
    w.openHtmlModal = function(modalIdOrTitle, titleOrHtml, htmlMaybe, onMount){
      let modalId = 'globalModal';
      let title = '';
      let html = '';
      let cb = null;

      if (typeof htmlMaybe === 'undefined') {
        title = String(modalIdOrTitle || '');
        html = String(titleOrHtml || '');
      } else {
        modalId = String(modalIdOrTitle || 'globalModal');
        title = String(titleOrHtml || '');
        html = String(htmlMaybe || '');
        cb = (typeof onMount === 'function') ? onMount : null;
      }

      let root = document.getElementById(modalId);
      if (!root) {
        root = document.createElement('div');
        root.id = modalId;
        root.className = 'lkModal';
        root.setAttribute('aria-hidden','true');
        root.hidden = true;
        root.innerHTML =
          '<div class="lkModal__backdrop"></div>' +
          '<div class="lkModal__panel" role="dialog" aria-modal="true">' +
            '<div class="lkModal__hdr">' +
              '<div class="lkModal__ttl"></div>' +
              '<button type="button" class="lkModal__x" aria-label="Close">✕</button>' +
            '</div>' +
            '<div class="lkModal__body"></div>' +
          '</div>';
        document.body.appendChild(root);
      }
      const close = ()=> w.__modalHide(root);
      w.__bindModalChrome(root, close);
      const x = root.querySelector('.lkModal__x');
      if (x && !x.__boundClose){ x.__boundClose = true; x.addEventListener('click', close); }
      root.querySelector('.lkModal__ttl').textContent = title;
      const body = root.querySelector('.lkModal__body');
      body.innerHTML = html;
      w.__modalShow(root);
      if (cb) cb(root);
      return root;
    };
  }

  // Wrap openBasicModal to use openHtmlModal instead of alert (if present later)
  if (typeof w.openBasicModal === 'function') {
    const prev = w.openBasicModal;
    w.openBasicModal = function(title, html){
      try { return w.openHtmlModal('basic', title, html); } catch(e){ return prev(title, html); }
    };
  }

  // Required hooks referenced by UI
  if (typeof w.openFxSettings !== 'function') w.openFxSettings = function(){};
  if (typeof w.updateOverlaysAfterToggle !== 'function') w.updateOverlaysAfterToggle = function(){};
  if (typeof w.refreshProgramAudioRouting !== 'function') w.refreshProgramAudioRouting = function(){};
  if (typeof w.openStreamModal !== 'function') w.openStreamModal = function(){};

  // Normalize token URL to ws(s)
  w.__normalizeWsUrl = function(u){
    if (!u) return u;
    if (u.startsWith('https://')) return 'wss://' + u.slice(8);
    if (u.startsWith('http://')) return 'ws://' + u.slice(7);
    return u;
  };
})();


// Apply Width Cap (0=Fluid) immediately
function applyLayoutCaps(){
  const wrap = document.querySelector(".wrap");
  if (!wrap) return;
  const raw = getComputedStyle(document.documentElement).getPropertyValue("--frame-max").trim();
  const v = parseFloat(raw);
  if (!v || v <= 0){
    wrap.style.width = "100vw";
    wrap.style.maxWidth = "none";
  } else {
    wrap.style.width = "100%";
    wrap.style.maxWidth = v + "px";
  }
}


// Keep width-cap behavior consistent when skin sliders change CSS variables
(function(){
  const root = document.documentElement;
  let t = null;
  const mo = new MutationObserver(()=>{
    clearTimeout(t);
    t = setTimeout(()=>{ try{ applyLayoutCaps(); }catch(e){} }, 50);
  });
  mo.observe(root, { attributes:true, attributeFilter:['style'] });
})();


// Global safety guards (avoid missing-function crashes that block modals)
(function(){
  const w = window;
  if (typeof w.updateOverlaysAfterToggle !== "function") w.updateOverlaysAfterToggle = function(){};
  if (typeof w.openStreamModal !== "function") w.openStreamModal = function(){};
  if (typeof w.refreshProgramAudioRouting !== "function") w.refreshProgramAudioRouting = function(){};
})();

</script>




<!-- STREAM UI CLEAN v2 -->
</div>


    <div class="overlay ocModal" id="overlayComposerModal" aria-hidden="true">
      <div class="ocShell">
        <div class="ocHeader">
          <div class="ocHeaderTitle">
            <div class="ocEyebrow">Graphics Composer</div>
            <div class="ocTitleRow"><b>Overlay Composer v10</b><span class="ocSub">Templates, timeline, keyframes, bindings, preview/program</span></div>
          </div>
          <div class="ocHeaderActions">
            <input id="ocSceneName" type="text" placeholder="Scene name" />
            <button class="miniBtn" id="ocNewSceneBtn" type="button">New</button>
            <button class="miniBtn" id="ocSaveSceneBtn" type="button">Save</button>
            <button class="miniBtn" id="ocToPreviewBtn" type="button">To Preview</button>
            <button class="miniBtn ok" id="ocToProgramBtn" type="button">To Program</button>
            <button class="miniBtn danger" id="ocCloseBtn" type="button">Close</button>
          </div>
        </div>

        <div class="ocToolbar">
          <div class="ocToolbarGroup" id="ocTemplates"></div>
          <div class="ocToolbarGroup ocToolbarRight">
            <label class="ocToggle"><input id="ocSnapToggle" type="checkbox" checked /> Snap Grid</label>
            <label class="ocToggle"><input id="ocSafeToggle" type="checkbox" checked /> Safe Area</label>
            <label class="ocToggle"><input id="ocStageAnimToggle" type="checkbox" checked /> Animate</label>
          </div>
        </div>

        <div class="ocBody">
          <aside class="ocPanel ocLeft">
            <div class="ocPanelTitle">Scenes</div>
            <div class="ocSceneList" id="ocSceneList"></div>
            <div class="ocPanelTitle">Layers</div>
            <div class="ocLayerList" id="ocLayerList"></div>
          </aside>

          <main class="ocCenter">
            <div class="ocCanvasTools">
              <label class="miniBtn" for="ocImageInput">Import Image</label>
              <input id="ocImageInput" type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml" hidden />
              <button class="miniBtn" id="ocAddTextBtn" type="button">Add Text</button>
              <button class="miniBtn" id="ocAddRectBtn" type="button">Add Panel</button>
              <button class="miniBtn" id="ocDuplicateLayerBtn" type="button">Duplicate Layer</button>
              <button class="miniBtn danger" id="ocDeleteLayerBtn" type="button">Delete Layer</button>
              <button class="miniBtn" id="ocPreviewAnimBtn" type="button">Preview Anim</button>
            </div>
            <div class="ocStageWrap">
              <div class="ocStage chromeCard" id="ocStage">
                <div class="ocStageGrid"></div>
                <div class="ocSafeArea"></div>
              </div>
            </div>
            <div class="ocTimeline chromeCard">
              <div class="ocTimelineHead">
                <div class="ocPanelTitle">Timeline / Animation</div>
                <div class="ocTimelineLegend">Entrance · Continuous · Outrace · Keyframes</div>
              </div>
              <div class="ocTimelineRows" id="ocTimelineRows"></div>
            </div>
          </main>

          <aside class="ocPanel ocRight">
            <div class="ocPanelTitle">Inspector</div>
            <div class="ocInspector" id="ocInspector"></div>
          </aside>
        </div>
      </div>
    </div>



<script src="assets/js/modules/guest-studio.js"></script>
<script src="assets/js/modules/feature-studio-manager.js"></script>

<script src="https://cdn.jsdelivr.net/npm/livekit-client@2.17.1/dist/livekit-client.umd.min.js"></script>


<script>

// Hard-disable Notification permission prompts (some browsers show it on user gestures)
try{
  if (window.Notification && typeof Notification.requestPermission === 'function') {
    Notification.requestPermission = () => Promise.resolve(Notification.permission || 'default');
  }
}catch(e){}
window.ITAHUKA_PROJECT = {
  id: <?php echo json_encode($project_id); ?>,
  user_id: <?php echo json_encode($user['id'] ?? null); ?>,
  room: <?php echo json_encode($p['room_name'] ?? ''); ?>,
  name: <?php echo json_encode($p['name'] ?? ''); ?>
};
</script>

<div class="frame">
    <div class="grain"></div>
    <div class="vignette"></div>

    <div class="topbar">
      <div class="brand"><span class="dot"></span>Co-Producer / Pre-Studio Control — 2026-02-19-b3</div>

      <div class="chip">Project: <span class="mono" id="projLbl"><?= htmlspecialchars($pid ?: 'COPRO01') ?></span></div>
      <div class="chip" id="statusChip">Status: <span id="statusTxt" class="pill bad">DISCONNECTED</span></div>

      <button class="btnTop" id="btnRecord">RECORD</button>
      <button class="btnTop" id="btnStream">STREAM</button>
      <button class="btnTop" id="btnSkin">SKIN</button>
      <a class="btnTop" id="btnDashboard" href="dashboard.php">Dashboard</a>
      <a class="btnTop danger" id="btnLogout" href="logout.php">Logout</a>
    </div>

    <div class="main">
      <!-- Left column -->
      <div class="left">
        <div class="panel" style="height: 62%;">
          <div class="panelHeader">
            <div class="title">Guests Overview</div>
            <div class="sub">
              <span class="pill" id="pillGuests">0/5</span>
              <span class="pill" id="pillPreviewSrc">PREVIEW: —</span>
            </div>
          </div>
          <div class="panelBody">
            <div class="guests" id="guestsList"></div>
          </div>
        </div>

        <div style="height: calc(38% - var(--gap)); display:flex; flex-direction:column; gap: var(--gap); min-height:0;">
          <div class="bank" style="flex:1; min-height:0;">
            <div class="bankHdr">
              <div class="title">Switch Cue</div>
              <div class="sub">1–5</div>
            </div>
            <div class="bankBody" id="cueBank"></div>
          </div>

          <div class="bank" style="flex:1; min-height:0;">
            <div class="bankHdr">
              <div class="title">Quick Actions</div>
              <div class="sub"></div>
            </div>
            <div class="bankBody small">
              <button class="btnBig" id="qaPreview">PREVIEW</button>
              <button class="btnBig" id="qaRecord">RECORD</button>
              <button class="btnBig" id="qaStream">STREAM</button>
            </div>
          </div>
        </div>
      </div>

      <!-- Right column -->
      <div class="right">
        <div class="videoRow">
          <div class="tv">
            <div class="tvBezel">
              <div class="title">Preview</div>
              <div class="sub">
                <span class="tvBadge" id="previewBadge">ON-DECK</span>
                <span class="pill" id="previewSrcLbl">—</span>
                <div class="previewGuideTools">
                <button class="iconBtn" id="btnGuides" type="button" title="Guides" aria-pressed="false">
                  <svg viewBox="0 0 24 24"><path d="M4 4h2v2H4V4zm7 0h2v2h-2V4zm7 0h2v2h-2V4zM4 11h2v2H4v-2zm7 0h2v2h-2v-2zm7 0h2v2h-2v-2zM4 18h2v2H4v-2zm7 0h2v2h-2v-2zm7 0h2v2h-2v-2z"/></svg>
                </button>
                <button class="iconBtn" id="btnGuidesSettings" type="button" title="Guides Settings" aria-haspopup="dialog">
                  <svg viewBox="0 0 24 24"><path d="M12 8.8a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4Zm9 3.2-.95-.55a7.95 7.95 0 0 0-.52-1.25l.27-1.06a1 1 0 0 0-.25-.98l-1.41-1.41a1 1 0 0 0-.98-.25l-1.06.27c-.4-.2-.82-.37-1.25-.52L14 3a1 1 0 0 0-.86-.5h-2.28A1 1 0 0 0 10 3l-.55.95c-.43.15-.85.32-1.25.52l-1.06-.27a1 1 0 0 0-.98.25L4.75 5.86a1 1 0 0 0-.25.98l.27 1.06c-.2.4-.37.82-.52 1.25L3 10a1 1 0 0 0-.5.86v2.28A1 1 0 0 0 3 14l.95.55c.15.43.32.85.52 1.25l-.27 1.06a1 1 0 0 0 .25.98l1.41 1.41a1 1 0 0 0 .98.25l1.06-.27c.4.2.82.37 1.25.52L10 21a1 1 0 0 0 .86.5h2.28A1 1 0 0 0 14 21l.55-.95c.43-.15.85-.32 1.25-.52l1.06.27a1 1 0 0 0 .98-.25l1.41-1.41a1 1 0 0 0 .25-.98l-.27-1.06c.2-.4.37-.82.52-1.25L21 14a1 1 0 0 0 .5-.86v-2.28A1 1 0 0 0 21 12ZM12 17a5 5 0 1 1 0-10 5 5 0 0 1 0 10Z"/></svg>
                </button>
              </div>
              </div>
            </div>
            <div class="tvScreen" id="previewScreen"></div>
          </div>
          <div class="midCol">
            <button class="btnTake" id="btnTake" type="button" title="TAKE (Preview -> Program)">TAKE</button>
            <button class="btnMon" id="btnMonitor" type="button" title="Monitor source">MONITOR: ON AIR</button>
          </div>
<div class="tv">
            <div class="tvBezel">
              <div class="title">On Air</div>
              <div class="sub">
                <span class="tvBadge" id="onAirBadge">LIVE</span>
                <span class="pill" id="onAirStatusLbl">RETURN</span>
              </div>
            </div>
            <div class="tvScreen" id="onAirScreen">
</div>
          </div>
        </div>

        <div class="tabs">
          <div class="tabHdr">
            <button class="tabBtn active" data-tab="mixer">Audio Mixer</button>
            <button class="tabBtn" data-tab="transitions">Transitions</button>
            <button class="tabBtn" data-tab="overlays">Overlays</button>
            <button class="tabBtn" data-tab="media">Media</button>
            <button class="tabBtn" data-tab="virtuals">Virtuals / Multiwindows</button>
            <button class="tabBtn" data-tab="gueststudio">Guest Studio</button>
            <button class="tabBtn" data-tab="participants">Participants</button>
            <button class="tabBtn" data-tab="messages">Messages</button>
          </div>
          <div class="tabBody">
            <!-- Mixer -->
            <div class="tabPane active" id="tab-mixer" data-module="mixer">
              <div class="mixerWrap">
                <div class="mixerRow" id="stripRow"></div>
              </div>
            </div>

            <!-- Transitions -->
            <div class="tabPane" id="tab-transitions">
              <div class="thumbs fxThumbs" id="fxTransitions"></div>
            </div>

            <!-- Overlays -->
            <div class="tabPane" id="tab-overlays" data-module="overlays">
              <div class="thumbs fxThumbs" id="fxOverlays"></div>
              <div class="overlayComposerHead">Composer Scenes</div>
              <div class="thumbs fxThumbs" id="fxOverlayComposerThumbs"></div>
            </div>

            <!-- Media -->
            <div class="tabPane" id="tab-media" data-module="media">
              <div class="mediaWrap">
                <div class="mediaTop">
                  <div class="left">Local Media</div>
                  <div style="display:flex; gap:10px; align-items:center;">
                    <label class="fileBtn" for="fileInput">Add Media</label>
                    <input id="fileInput" type="file" multiple accept="video/mp4,audio/mpeg,image/png,image/jpeg,application/pdf,*/*" />
                    <button class="miniBtn" id="btnRemoveMedia" disabled>REMOVE</button>
                    <button class="miniBtn danger" id="btnClearMedia" disabled>CLEAR</button>
                  </div>
                </div>
                <div class="thumbs" id="thumbs"></div>
              </div>
            </div>

            <!-- Virtuals -->
            <div class="tabPane" id="tab-virtuals">
              <div class="thumbs fxThumbs" id="fxVirtuals"></div>
            </div>

            <!-- Guest Studio -->
            <div class="tabPane" id="tab-gueststudio">
              <div class="guestStudioShell">
                <div class="guestStudioGrid" id="guestStudioGrid"></div>
                <div class="guestInspector">
                  <div class="title"><span class="pill ready">SELECTED GUEST</span></div>
                  <div class="body">
                    <div class="fRow"><label>Name</label><input id="guestStudioName" value="—"></div>
                    <div class="fRow"><label>Role</label><select id="guestStudioRole"><option>Host</option><option>Guest</option><option>Backstage</option></select></div>
                    <div class="fRow"><label>Talkback</label><select id="guestStudioTalkback"><option>Private</option><option>Group</option><option>All</option></select></div>
                    <div class="fRow"><label>Return Feed</label><select id="guestStudioReturn"><option>Program</option><option>Preview</option><option>Muted</option></select></div>
                    <div class="fRow"><label>IFB Gain</label><input id="guestStudioIfb" type="range" min="0" max="100" value="72"></div>
                    <div class="fRow"><label>Notes</label><input id="guestStudioNotes" value=""></div>
                    <button class="miniBtn ok" id="btnApplyGuestStudio" type="button">APPLY GUEST UPDATE</button>
                  </div>
                </div>
              </div>
            </div>

            <!-- Participants -->
            <div class="tabPane" id="tab-participants">
              <div class="audienceListShell">
                <div class="audienceToolbar">
                  <input type="text" placeholder="Search participants" />
                  <select><option>All</option><option>Raised Hands</option><option>Invited</option></select>
                  <button class="miniBtn ok" type="button">INVITE QUEUE</button>
                </div>
                <div class="audienceListBody">
                  <div class="audienceRow"><div class="audienceMain"><b>Listener A</b><span>Raised Hand • Tanzania</span></div><span class="pill ready">AUDIENCE</span><div class="audienceActions"><button>INVITE</button><button>MUTE</button></div></div>
                  <div class="audienceRow"><div class="audienceMain"><b>Listener B</b><span>Listening • Kenya</span></div><span class="pill">AUDIENCE</span><div class="audienceActions"><button>INVITE</button><button>BLOCK</button></div></div>
                  <div class="audienceRow"><div class="audienceMain"><b>Listener C</b><span>Listening • USA</span></div><span class="pill">AUDIENCE</span><div class="audienceActions"><button>INVITE</button><button>BLOCK</button></div></div>
                  <div class="audienceRow"><div class="audienceMain"><b>Listener D</b><span>Raised Hand • Uganda</span></div><span class="pill ready">AUDIENCE</span><div class="audienceActions"><button>INVITE</button><button>MUTE</button></div></div>
                </div>
              </div>
            </div>

            <!-- Messages -->
            <div class="tabPane" id="tab-messages">
              <div class="audienceListShell">
                <div class="audienceToolbar">
                  <input type="text" placeholder="Search messages" />
                  <select><option>All Messages</option><option>Questions</option><option>Polls</option><option>Pinned</option></select>
                  <button class="miniBtn" type="button">UNREAD</button>
                </div>
                <div class="audienceListBody">
                  <div class="audienceRow"><div class="audienceMain"><b>Listener A</b><span>Question • What camera is used?</span></div><span class="pill ready">QUESTION</span><div class="audienceActions"><button>PIN</button><button>SHOW</button></div></div>
                  <div class="audienceRow"><div class="audienceMain"><b>Listener B</b><span>Message • Hello from Kenya</span></div><span class="pill">CHAT</span><div class="audienceActions"><button>PIN</button><button>HIDE</button></div></div>
                  <div class="audienceRow"><div class="audienceMain"><b>Poll Update</b><span>Poll • 68% voted Yes</span></div><span class="pill live">POLL</span><div class="audienceActions"><button>PIN</button><button>SHOW</button></div></div>
                  <div class="audienceRow"><div class="audienceMain"><b>Listener C</b><span>Question • Can I join the stage?</span></div><span class="pill ready">QUESTION</span><div class="audienceActions"><button>PIN</button><button>ANSWER</button></div></div>
                </div>
              </div>
            </div>

          </div>
        </div>
      </div>
    </div>

    <!-- Skin Tuner Overlay -->
    <div class="overlay" id="skinOverlay" aria-hidden="true">
      <div class="tuner">
        <div class="tHdr">
          <b>Skin Tuner</b>
          <div class="tHdrBtns">
  <button id="btnResetSkin" class="miniBtn danger">Reset</button>
  <button id="btnSaveSkin" class="miniBtn ok">Save</button>
  <button id="btnCloseSkin">Close</button>
</div>
        </div>
        <div class="tBody" id="tBody"></div>
      </div>
    
    </div>
</div>

  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/livekit-client@2.17.1/dist/livekit-client.umd.min.js"></script>

<script>
/* =========================================================
   Co-Producer / Pre-Studio Control — 2026-02-19-b3 Panel
   - Single file: HTML+CSS+JS
   - Joins LiveKit room as a subscriber (subscribe-only)
   - Guests list: up to 5 non-host UIDs
   - Host (YOLOBOX) feed: subscribe fixed UID (default 1 via ?host=1)
   - Preview can be guest or media; selecting media overrides guest preview
   - Audio Mixer: 6 channels (1-5 guests, 6 media audio)
   - Skin tuner: overlay (toggle S or button)
========================================================= */

/* ---------------------------
   Config from URL
--------------------------- */
const qs = new URLSearchParams(location.search);
const PROJECT = <?php echo json_encode($p['project_id'] ?? ''); ?>;
const ROOM_DEFAULT = <?php echo json_encode($roomName ?? 'demo'); ?>;
const ROOM = ((qs.get("room") || qs.get("channel") || "").trim() || ROOM_DEFAULT);
const NAME = qs.get("name") || "CoProducer";
const PROJECT_NAME = <?php echo json_encode($p['name'] ?? ''); ?>;
const PROJECT_JOIN_CODE = <?php echo json_encode($p['join_code'] ?? ''); ?>;

/* LiveKit config
   - LiveKit URL defaults to https://livekit.itahukamedia.com (from your Caddyfile)
   - Token endpoint: livekit_token.php (recommended) with fallbacks.
   Query params:
     ?room=camA
     ?name=CoProducer
     ?host=mixer            (optional fixed host identity; otherwise AUTO-pick)
     ?lk=https://livekit.itahukamedia.com   (override LiveKit URL)
     ?token=livekit_token.php               (override token endpoint)
     ?identity=coproducer-01                (optional identity override)
*/
const DEFAULT_LIVEKIT_URL = "https://livekit.itahukamedia.com";
const LIVEKIT_URL = (()=>{
  const raw = (qs.get("lk") || qs.get("url") || DEFAULT_LIVEKIT_URL || "").trim();
  if (!raw) return DEFAULT_LIVEKIT_URL;
  if (/^wss?:\/\//i.test(raw) || /^https?:\/\//i.test(raw)) return raw;
  return "https://" + raw.replace(/^\/\//, "");
})();

const IDENTITY = (()=>{
  const raw = (qs.get("identity") || qs.get("id") || "").trim();
  if (raw) return raw;
  // Stable identity required so server-side LiveKit egress can target the PROGRAM return participant.
  // Default to studio-return unless explicitly overridden in the URL.
  return "studio-return";
})();

/* On-Air (host) selection:
   - If ?host is provided, it is treated as a LiveKit participant identity to lock on.
   - Otherwise, the first remote participant that publishes a video track is treated as host (AUTO).
*/
let HOST_UID = (qs.get("host") || "mixer").trim();
const HOST_FIXED = qs.has("host");
let HOST_AUTO_UID = null;

// LiveKit room object
let room = null;
let ACTIVE_LIVEKIT_URL = LIVEKIT_URL;
let ACTIVE_LIVEKIT_ROOM = ROOM;


/* Host feed tracks */
let hostUser = null;
let hostVideoTrack = null;
let hostAudioTrack = null;

/* Slots: 5 guests (non-host) */
const SLOTS = 5;
const slots = Array.from({length:SLOTS}, (_,i)=>({
  idx:i,
  uid:null,
  connected:false,
  hasVideo:false,
  hasAudio:false,
  camPreview:true,
  micMonitorOn:true, /* controls subscription playback volume (local monitor) */
  cue:false,
  videoTrack:null,
  audioTrack:null,
}));

/* Preview state */
let previewSource = null; // {type:"guest", uid} | {type:"media", id}

const MEDIA_STORE_KEY = "coproducer_media_settings_v1";
const MEDIA_LIBRARY_STORE_KEY = "coproducer_media_library_v1";
const UI_STATE_STORE_KEY = "coproducer_ui_state_v1";
const PROJECT_ASSETS_API = "project_assets_api.php";
function projectAssetApiUrl(bucket){
  return PROJECT_ASSETS_API + "?project=" + encodeURIComponent(PROJECT || "") + "&bucket=" + encodeURIComponent(bucket || "media");
}
let mediaSettings = {}; // id -> settings
let pendingUiRestore = null;

function saveUiState(){
  try{
    const payload = {
      previewSource: previewSource ? JSON.parse(JSON.stringify(previewSource)) : null,
      programLocked: !!programLocked,
      programSnapshot: programSnapshot ? JSON.parse(JSON.stringify(programSnapshot)) : null,
      virtualSelected: fxState && fxState.virtuals ? String(fxState.virtuals.selected || 'single') : 'single'
    };
    localStorage.setItem(UI_STATE_STORE_KEY, JSON.stringify(payload));
  }catch(err){ console.warn('saveUiState failed', err); }
}
function loadUiState(){
  try{
    const raw = localStorage.getItem(UI_STATE_STORE_KEY);
    if (!raw) return;
    const obj = JSON.parse(raw);
    if (!obj || typeof obj !== 'object') return;
    pendingUiRestore = obj;
  }catch(err){ console.warn('loadUiState failed', err); }
}
function applyUiState(){
  try{
    if (!pendingUiRestore) return;
    const s = pendingUiRestore;
    previewSource = s.previewSource || null;
    if (fxState && fxState.virtuals && s.virtualSelected) fxState.virtuals.selected = String(s.virtualSelected || 'single');
    programLocked = !!s.programLocked;
    programSnapshot = s.programSnapshot || null;
    pendingUiRestore = null;
  }catch(err){ console.warn('applyUiState failed', err); }
}

function readFileAsDataURL(file){
  return new Promise((resolve,reject)=>{
    try{
      const fr = new FileReader();
      fr.onload = ()=> resolve(String(fr.result || ""));
      fr.onerror = ()=> reject(fr.error || new Error("read failed"));
      fr.readAsDataURL(file);
    }catch(err){ reject(err); }
  });
}

function saveMediaLibrary(){
  try{
    const payload = mediaItems.map(m => ({
      id: m.id,
      name: m.name,
      kind: m.kind,
      dataUrl: String(m.persistedDataUrl || (String(m.url||"").startsWith("data:") ? m.url : "") || ""),
      thumb: String(m.thumb || ""),
      fileRelPath: String(m.fileRelPath || ""),
      thumbRelPath: String(m.thumbRelPath || ""),
      meta: (m.meta && typeof m.meta === "object") ? m.meta : {},
    }));
    localStorage.setItem(MEDIA_LIBRARY_STORE_KEY, JSON.stringify(payload));
  }catch(err){ console.warn("saveMediaLibrary failed", err); }
}
async function loadMediaLibrary(){
  // Prefer DB-backed project assets
  try{
    if (PROJECT){
      const r = await fetch(projectAssetApiUrl("media"), { credentials:"include" });
      const j = await r.json().catch(()=>null);
      if (r.ok && j && j.ok && Array.isArray(j.items)){
        mediaItems = j.items.map((m, idx)=>({
          id: String(m.id || ("m"+(idx+1))),
          name: String(m.name || "Media"),
          kind: String(m.kind || "file"),
          url: String(m.url || m.file_url || m.fileRelPath || ""),
          persistedDataUrl: "",
          thumb: String(m.thumb || m.thumb_url || ""),
          fileRelPath: String(m.fileRelPath || m.file_rel_path || ""),
          thumbRelPath: String(m.thumbRelPath || m.thumb_rel_path || ""),
          meta: (m.meta && typeof m.meta === "object") ? m.meta : {},
          audioNode: null,
          audioEl: null,
        })).filter(m => m.url);
        let maxSeq = 0;
        for (const m of mediaItems){
          const n = parseInt(String(m.id||"").replace(/^m/,""),10);
          if (Number.isFinite(n)) maxSeq = Math.max(maxSeq, n);
        }
        mediaSeq = Math.max(mediaSeq, maxSeq + 1);
        try{ renderMediaThumbs(); }catch(_){}
        try{ renderUI(); }catch(_){}
        try{ updateMediaButtons(); }catch(_){}
        return;
      }
    }
  }catch(err){ console.warn("loadMediaLibrary DB failed", err); }

  // Fallback to localStorage
  try{
    const raw = localStorage.getItem(MEDIA_LIBRARY_STORE_KEY);
    if (!raw) return;
    const arr = JSON.parse(raw);
    if (!Array.isArray(arr)) return;
    mediaItems = arr.map((m, idx)=>({
      id: String(m.id || ("m"+(idx+1))),
      name: String(m.name || "Media"),
      kind: String(m.kind || "file"),
      url: String(m.dataUrl || m.url || ""),
      persistedDataUrl: String(m.dataUrl || ""),
      thumb: (String(m.kind||"")==="image") ? String(m.dataUrl || m.thumb || "") : (m.thumb || null),
      fileRelPath: String(m.fileRelPath || ""),
      thumbRelPath: String(m.thumbRelPath || ""),
      meta: (m.meta && typeof m.meta === "object") ? m.meta : {},
      audioNode: null,
      audioEl: null,
    })).filter(m => m.url);
    let maxSeq = 0;
    for (const m of mediaItems){
      const n = parseInt(String(m.id||"").replace(/^m/,""),10);
      if (Number.isFinite(n)) maxSeq = Math.max(maxSeq, n);
    }
    mediaSeq = Math.max(mediaSeq, maxSeq + 1);
  }catch(err){ console.warn("loadMediaLibrary failed", err); }
}

function loadMediaSettings(){
  try{
    const raw = localStorage.getItem(MEDIA_STORE_KEY);
    if (!raw) return;
    const obj = JSON.parse(raw);
    if (obj && typeof obj === "object") mediaSettings = obj;
  }catch(_){}
}
function saveMediaSettings(){
  try{ localStorage.setItem(MEDIA_STORE_KEY, JSON.stringify(mediaSettings)); }catch(_){}
}
function getMediaSettings(id){
  const s = mediaSettings[id] || {};
  // defaults
  return {
    rotate: Number(s.rotate ?? 0),            // 0,90,180,270
    scale: Number(s.scale ?? 1),
    offX: Number(s.offX ?? 0),
    offY: Number(s.offY ?? 0),
    fit: String(s.fit ?? "cover"),           // cover|contain|fill
    cropT: Number(s.cropT ?? 0),             // % inset
    cropR: Number(s.cropR ?? 0),
    cropB: Number(s.cropB ?? 0),
    cropL: Number(s.cropL ?? 0),
    flipH: !!s.flipH,
    flipV: !!s.flipV,

    playback: String(s.playback ?? "loop"),  // loop|resume|restart|once
    trimStart: Number(s.trimStart ?? 0),
    trimEnd: Number(s.trimEnd ?? 0),
    rate: Number(s.rate ?? 1),               // playbackRate
    vol: Number(s.vol ?? 1),                 // 0..1

    lastPos: Number(s.lastPos ?? 0)
  };
}
function setMediaSettings(id, patch){
  mediaSettings[id] = Object.assign(getMediaSettings(id), patch || {});
  saveMediaSettings();
}
let mediaItems = []; // {id,name,kind,url,thumb,audioNode?}
let mediaSeq = 1;

/* Audio context for metering */
let audioCtx = null;
let masterGain = null;
let meterNodes = []; // per channel: {analyserL, analyserR, split, gain}
let rafMeters = null;

/* Elements */
const roomLbl = document.getElementById("roomLbl");
const hostLbl = document.getElementById("hostLbl");
const statusTxt = document.getElementById("statusTxt");
const statusChip = document.getElementById("statusChip");
const guestsList = document.getElementById("guestsList");
const pillGuests = document.getElementById("pillGuests");
const pillPreviewSrc = document.getElementById("pillPreviewSrc");

const cueBank = document.getElementById("cueBank");
const qaPreview = document.getElementById("qaPreview");
const qaRecord = document.getElementById("qaRecord");
const qaStream = document.getElementById("qaStream");

const previewScreen = document.getElementById("previewScreen");
const previewBadge = document.getElementById("previewBadge");
const previewSrcLbl = document.getElementById("previewSrcLbl");

const onAirScreen = document.getElementById("onAirScreen");
const onAirBadge = document.getElementById("onAirBadge");

const tabBtns = Array.from(document.querySelectorAll(".tabBtn"));
const tabPanes = {
  mixer: document.getElementById("tab-mixer"),
  transitions: document.getElementById("tab-transitions"),
  overlays: document.getElementById("tab-overlays"),
  media: document.getElementById("tab-media"),
  virtuals: document.getElementById("tab-virtuals"),
  gueststudio: document.getElementById("tab-gueststudio"),
  participants: document.getElementById("tab-participants"),
  messages: document.getElementById("tab-messages"),
};
const guestStudioGrid = document.getElementById("guestStudioGrid");
const guestStudioName = document.getElementById("guestStudioName");
const guestStudioRole = document.getElementById("guestStudioRole");
const guestStudioTalkback = document.getElementById("guestStudioTalkback");
const guestStudioReturn = document.getElementById("guestStudioReturn");
const guestStudioIfb = document.getElementById("guestStudioIfb");
const guestStudioNotes = document.getElementById("guestStudioNotes");
const btnApplyGuestStudio = document.getElementById("btnApplyGuestStudio");

const stripRow = document.getElementById("stripRow");
const thumbs = document.getElementById("thumbs");
const fileInput = document.getElementById("fileInput");
const btnRemoveMedia = document.getElementById("btnRemoveMedia");
const btnClearMedia  = document.getElementById("btnClearMedia");

const btnTake = document.getElementById("btnTake");
const btnGuides = document.getElementById("btnGuides");
const btnGuidesSettings = document.getElementById("btnGuidesSettings");
const guidesModal = document.getElementById("guidesModal");
const btnCloseGuidesModal = document.getElementById("btnCloseGuidesModal");
const btnGuidesSave = document.getElementById("btnGuidesSave");
const btnGuidesReset = document.getElementById("btnGuidesReset");
const guidesStandard = document.getElementById("guidesStandard");
const guidesSafe = document.getElementById("guidesSafe");
const guidesGrid = document.getElementById("guidesGrid");
const guidesRuler = document.getElementById("guidesRuler");
const guidesPixels = document.getElementById("guidesPixels");
const guidesPxw = document.getElementById("guidesPxw");
const guidesPxh = document.getElementById("guidesPxh");
const guidesColor = document.getElementById("guidesColor");
const guidesOpacity = document.getElementById("guidesOpacity");
const guidesThickness = document.getElementById("guidesThickness");

function getGuidesModalEls(){
  return {
    modal: document.getElementById("guidesModal"),
    btnClose: document.getElementById("btnCloseGuidesModal"),
    btnSave: document.getElementById("btnGuidesSave"),
    btnReset: document.getElementById("btnGuidesReset"),
    standard: document.getElementById("guidesStandard"),
    safe: document.getElementById("guidesSafe"),
    grid: document.getElementById("guidesGrid"),
    ruler: document.getElementById("guidesRuler"),
    pixels: document.getElementById("guidesPixels"),
    pxw: document.getElementById("guidesPxw"),
    pxh: document.getElementById("guidesPxh"),
    color: document.getElementById("guidesColor"),
    opacity: document.getElementById("guidesOpacity"),
    thickness: document.getElementById("guidesThickness")
  };
}
const btnMonitor = document.getElementById("btnMonitor");
const onAirStatusLbl = document.getElementById("onAirStatusLbl");
const fxTransitions = document.getElementById("fxTransitions");
const fxOverlays = document.getElementById("fxOverlays");
const fxVirtuals = document.getElementById("fxVirtuals");


/* Skin tuner */
const skinOverlay = document.getElementById("skinOverlay");
const btnSkin = document.getElementById("btnSkin");
const btnStream = document.getElementById("btnStream");
const btnCloseSkin = document.getElementById("btnCloseSkin");
const tBody = document.getElementById("tBody");

/* ---------------------------
   Helpers
--------------------------- */
function safeUid(u){ return (u === null || u === undefined) ? "" : String(u); }


// --- LiveKit data/control channel (operator -> guest) ---
const CTRL_TOPIC = "itahuka.ctrl.v1";
function encodeJSON(obj){
  try{ return new TextEncoder().encode(JSON.stringify(obj)); }catch(_){ return new Uint8Array(); }
}
function sendCtrlTo(uid, msg){
  try{
    if (!room || !room.localParticipant) return false;
    const data = encodeJSON(msg);
    // Destination: set to participant identity if supported by SDK; otherwise broadcast and guests ignore non-target.
    const opts = { reliable:true, topic: CTRL_TOPIC };
    if (uid) opts.destinationIdentities = [String(uid)];
    room.localParticipant.publishData(data, opts);
    return true;
  }catch(e){
    console.warn("sendCtrlTo failed", e);
    return false;
  }
}
function sendGuestCmd(uid, cmd, value){
  return sendCtrlTo(uid, { t:"guest_cmd", cmd:String(cmd||""), v:value, uid:String(uid||""), ts:Date.now() });
}

// --- Guest framing editor (zoom/pan/fit) ---
const GUEST_VID_STORE_KEY = "coproducer_guest_video_settings_v1";
let guestVidSettings = {}; // slotIdx -> {fit, scale, x, y}
(function(){
  try{
    const raw = localStorage.getItem(GUEST_VID_STORE_KEY);
    if (raw) guestVidSettings = JSON.parse(raw) || {};
  }catch(_){ guestVidSettings = {}; }
})();
function saveGuestVidSettings(){
  try{ localStorage.setItem(GUEST_VID_STORE_KEY, JSON.stringify(guestVidSettings||{})); }catch(_){}
}
function clampNum(v, min, max, fallback){
  const n = Number(v);
  if (!isFinite(n)) return fallback;
  return Math.max(min, Math.min(max, n));
}
function getGuestVidCfg(slotIdx){
  const def = { fit:"cover", scale:100, x:0, y:0 };
  const v = guestVidSettings && guestVidSettings[String(slotIdx)] ? guestVidSettings[String(slotIdx)] : null;
  if (!v || typeof v !== "object") return def;
  return {
    fit: (v.fit === "contain" ? "contain" : "cover"),
    scale: clampNum(v.scale, 50, 200, 100),
    x: clampNum(v.x, -100, 100, 0),
    y: clampNum(v.y, -100, 100, 0),
  };
}
function setGuestVidCfg(slotIdx, cfg){
  guestVidSettings = guestVidSettings || {};
  guestVidSettings[String(slotIdx)] = {
    fit: (cfg.fit === "contain" ? "contain" : "cover"),
    scale: clampNum(cfg.scale, 50, 200, 100),
    x: clampNum(cfg.x, -100, 100, 0),
    y: clampNum(cfg.y, -100, 100, 0),
  };
  saveGuestVidSettings();
}

// Apply transform to a container's <video> element (guest tile screen + preview)
function applyVideoFramingToContainer(container, cfg){
  if (!container) return;
  const v = container.querySelector("video");
  if (!v) return;
  const fit = (cfg && cfg.fit === "contain") ? "contain" : "cover";
  const scale = clampNum(cfg?.scale, 50, 200, 100);
  const x = clampNum(cfg?.x, -100, 100, 0);
  const y = clampNum(cfg?.y, -100, 100, 0);
  v.style.objectFit = fit;
  v.style.transformOrigin = "50% 50%";
  v.style.transform = `translate(${x}%, ${y}%) scale(${scale/100})`;
}

function openGuestFramingModal(slotIdx){
  try{
    const s = slots && slots[slotIdx] ? slots[slotIdx] : null;
    if (!s) return;

    const cfg = getGuestVidCfg(slotIdx);

    const html = `
      <div class="lkHint">Guest: <b>G${slotIdx+1}</b> • Controls apply to the guest tile and PREVIEW (when this guest is in PREVIEW). Saved locally.</div>

      <div class="lkFormRow">
        <label>Fit</label>
        <select id="gf_fit">
          <option value="cover">Cover (crop)</option>
          <option value="contain">Contain (letterbox)</option>
        </select>
      </div>

      <div class="lkFormRow">
        <label>Zoom</label>
        <input id="gf_zoom" type="range" min="50" max="200" step="1" />
        <input id="gf_zoom_n" type="number" min="50" max="200" step="1" style="width:88px" />
      </div>

      <div class="lkFormRow">
        <label>Pan X</label>
        <input id="gf_x" type="range" min="-100" max="100" step="1" />
        <input id="gf_x_n" type="number" min="-100" max="100" step="1" style="width:88px" />
      </div>

      <div class="lkFormRow">
        <label>Pan Y</label>
        <input id="gf_y" type="range" min="-100" max="100" step="1" />
        <input id="gf_y_n" type="number" min="-100" max="100" step="1" style="width:88px" />
      </div>

      <div class="lkFormRow" style="justify-content:flex-end">
        <button class="lkBtn lkBtn--secondary" id="gf_reset" type="button">Reset</button>
      </div>
    `;

    openHtmlModal("guest-framing", "Guest Framing / Crop", html, (root)=>{
      const $ = (id)=>root.querySelector("#"+id);
      const fitEl=$("gf_fit"), zoomEl=$("gf_zoom"), zoomN=$("gf_zoom_n"), xEl=$("gf_x"), xN=$("gf_x_n"), yEl=$("gf_y"), yN=$("gf_y_n");
      fitEl.value = cfg.fit;
      zoomEl.value = zoomN.value = String(cfg.scale);
      xEl.value = xN.value = String(cfg.x);
      yEl.value = yN.value = String(cfg.y);

      function applyAndSave(){
        const next = {
          fit: fitEl.value === "contain" ? "contain" : "cover",
          scale: clampNum(zoomEl.value, 50, 200, 100),
          x: clampNum(xEl.value, -100, 100, 0),
          y: clampNum(yEl.value, -100, 100, 0),
        };
        zoomN.value = String(next.scale);
        xN.value = String(next.x);
        yN.value = String(next.y);

        setGuestVidCfg(slotIdx, next);

        // Apply to slot screen container
        applyVideoFramingToContainer(s.scrEl, next);

        // Apply to preview if this guest is currently in preview
        if (previewSource?.type==="guest" && safeUid(previewSource.uid)===safeUid(s.uid)){
          applyVideoFramingToContainer(previewVideoWrap, next);
        }
      }

      function syncFromNumber(){
        zoomEl.value = zoomN.value;
        xEl.value = xN.value;
        yEl.value = yN.value;
        applyAndSave();
      }

      [fitEl, zoomEl, xEl, yEl].forEach(el=>el.addEventListener("input", applyAndSave));
      [zoomN, xN, yN].forEach(el=>el.addEventListener("change", syncFromNumber));

      root.querySelector("#gf_reset").addEventListener("click", ()=>{
        fitEl.value = "cover";
        zoomEl.value = zoomN.value = "100";
        xEl.value = xN.value = "0";
        yEl.value = yN.value = "0";
        applyAndSave();
      });

      // initial apply
      applyAndSave();
    });
  }catch(e){
    console.warn("openGuestFramingModal failed", e);
  }
}



function slotConnState(s){
  if (!s || !s.connected) return "OFFLINE";
  if (s.hasVideo) return "LIVE";
  if (s.hasAudio) return "AUDIO";
  return "OFFLINE";
}
function slotIsMuted(s){
  return !!(s && s.connected && s.hasAudio && !s.micMonitorOn);
}
function hostConnState(){
  if (hostVideoTrack) return "LIVE";
  if (hostAudioTrack) return "AUDIO";
  return "OFFLINE";
}
function deriveOnAirBadgeState(){
  if (programLocked && programSnapshot){
    if (programSnapshot.source) return "PROGRAM";
    return "PROGRAM";
  }
  return hostConnState();
}

/* Icons */
function svgIcon(kind){
  const icons = {
    cam: `<svg viewBox="0 0 24 24"><path d="M4 7a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.2l3.4-2a1.2 1.2 0 0 1 1.8 1V16a1.2 1.2 0 0 1-1.8 1L16 15.8V18a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7z"/></svg>`,
    mic: `<svg viewBox="0 0 24 24"><path d="M12 14a3 3 0 0 0 3-3V6a3 3 0 0 0-6 0v5a3 3 0 0 0 3 3zm5-3a1 1 0 1 1 2 0 7 7 0 0 1-6 6.93V21h2a1 1 0 1 1 0 2H9a1 1 0 1 1 0-2h2v-3.07A7 7 0 0 1 5 11a1 1 0 1 1 2 0 5 5 0 0 0 10 0z"/></svg>`,
    bell:`<svg viewBox="0 0 24 24"><path d="M12 22a2.2 2.2 0 0 0 2.2-2.2H9.8A2.2 2.2 0 0 0 12 22zm7-6.2v-5.1A7 7 0 0 0 13 3.1V2a1 1 0 1 0-2 0v1.1A7 7 0 0 0 5 10.7v5.1l-1.4 1.4A1 1 0 0 0 4.3 19h15.4a1 1 0 0 0 .7-1.7L19 15.8z"/></svg>`,
  };
  return icons[kind] || "";
}

function setStatus(connected){
  statusTxt.className = "pill " + (connected ? "ok" : "bad");
  statusTxt.textContent = connected ? "CONNECTED" : "DISCONNECTED";
}

/* ---------------------------
   Build cue bank buttons
--------------------------- */
for (let i=0;i<SLOTS;i++){
  const b = document.createElement("button");
  b.className = "btnBig";
  b.textContent = String(i+1);
  b.id = "cueBtn"+i;
  b.disabled = true;
  b.onclick = () => selectPreviewBySlot(i);
  cueBank.appendChild(b);
}

qaPreview.onclick = ()=>{ /* wired by TAKE/preview system */ }; 
qaRecord.onclick = ()=>{ /* wired later */ }; 
if (qaStream) qaStream.onclick = ()=>{ openStreamModal(); }; 
{ const __btnRecord = document.getElementById("btnRecord"); if (__btnRecord && qaRecord) __btnRecord.onclick = ()=>qaRecord.click(); }
{ const __btnStream = document.getElementById("btnStream"); if (__btnStream) __btnStream.onclick = ()=>{ openStreamModal(); }; }

/* ---------------------------
   Tabs
--------------------------- */
tabBtns.forEach(btn=>{
  btn.addEventListener("click", ()=>{
    tabBtns.forEach(b=>b.classList.toggle("active", b===btn));
    const key = btn.dataset.tab;
    Object.entries(tabPanes).forEach(([k,el])=>{
      el.classList.toggle("active", k===key);
    });
  });
});

if (window.GuestStudio && typeof window.GuestStudio.init === "function") {
  window.GuestStudio.init({
    gridEl: guestStudioGrid,
    nameEl: guestStudioName,
    roleEl: guestStudioRole,
    talkbackEl: guestStudioTalkback,
    returnEl: guestStudioReturn,
    ifbEl: guestStudioIfb,
    notesEl: guestStudioNotes,
    applyBtn: btnApplyGuestStudio,
    slotsProvider: () => (Array.isArray(window.slots) ? window.slots : []),
    getConnState: (slot) => (typeof slotConnState === "function" ? slotConnState(slot) : ((slot && slot.connected) ? "LIVE" : "OFFLINE")),
    isMuted: (slot) => (typeof slotIsMuted === "function" ? slotIsMuted(slot) : false)
  });
if (typeof window.renderGuestStudio !== "function" && window.GuestStudio && typeof window.GuestStudio.render === "function") {
  window.renderGuestStudio = function(){ return window.GuestStudio.render(); };
}

}

/* ---------------------------
   Skin tuner (overlay)
--------------------------- */
const tunerDefs = [
  {key:"--frame-max",          label:"Width Cap (0=Fluid)", min:0, max:4096, step:32},
  {key:"--guests-width",       label:"Guests Panel Width",  min:420, max:1400, step:10},
  {key:"--guest-icon-size",    label:"Guest Icon Size",     min:28,  max:64,  step:1},
  {key:"--guest-icon-radius",  label:"Guest Icon Radius",   min:0,   max:24,  step:1},
  {key:"--frame-radius",   label:"Frame Radius",   min:0, max:40, step:1},
  {key:"--panel-radius",   label:"Panel Radius",   min:0, max:40, step:1},
  {key:"--control-radius", label:"Control Radius", min:0, max:30, step:1},
  {key:"--meter-radius",   label:"VU Radius",      min:0, max:24, step:1},
  {key:"--thumb",          label:"Thumb Size",      min:110, max:240, step:2},
  {key:"--take-radius",    label:"TAKE Radius",      min:0, max:26, step:1},
  {key:"--fader-radius",   label:"Fader Cap Radius", min:0, max:24, step:1},
  {key:"--flare",          label:"Flare",          min:0, max:.30, step:.01},
  {key:"--noise",          label:"Noise",          min:0, max:.35, step:.01},
  {key:"--emboss",         label:"Emboss",         min:0, max:1, step:.01},
  {key:"--engrave",        label:"Engrave",        min:0, max:1, step:.01},
    {key:"--vu-led-shift-x", label:"VU LED Shift X", min:-10, max:24, step:1},
  {key:"--media-btn-radius", label:"Media Btn Radius", min:0, max:22, step:1},
];

function openSkin(){
  if (!skinOverlay) return;
  if (typeof window.__modalShow === "function") window.__modalShow(skinOverlay);
  else { skinOverlay.classList.add("show"); skinOverlay.setAttribute("aria-hidden","false"); }
}
function closeSkin(){
  if (!skinOverlay) return;
  if (typeof window.__modalHide === "function") window.__modalHide(skinOverlay);
  else { skinOverlay.classList.remove("show"); skinOverlay.setAttribute("aria-hidden","true"); }
}
if (btnSkin) btnSkin.onclick = openSkin;
const btnSaveSkin = document.getElementById("btnSaveSkin");
const btnResetSkin = document.getElementById("btnResetSkin");
if (btnSaveSkin) btnSaveSkin.disabled = true;

async function persistSkinToDB(){
  if (!PROJECT) return;
  const vars = collectSkinVars();
  const body = JSON.stringify({ vars: vars });
  const r = await fetch("settings_api.php?project="+encodeURIComponent(PROJECT), {
    method:"POST",
    headers:{ "Content-Type":"application/json" },
    body,
    credentials:"include"
  });
  const j = await r.json().catch(()=>null);
  if (!r.ok || !j || !j.ok) throw new Error((j && j.error) ? j.error : ("HTTP "+r.status));
}
async function resetSkinInDB(){
  if (!PROJECT) return;
  const r = await fetch("settings_api.php?project="+encodeURIComponent(PROJECT), {
    method:"DELETE",
    credentials:"include"
  });
  const j = await r.json().catch(()=>null);
  if (!r.ok || !j || !j.ok) throw new Error((j && j.error) ? j.error : ("HTTP "+r.status));
}

if (btnSaveSkin){
  btnSaveSkin.onclick = async ()=>{
    try{
      btnSaveSkin.disabled = true;
      await persistSkinToDB();
      setSkinDirty(false);
    }catch(e){
      console.error("[SKIN] save failed", e);
      btnSaveSkin.disabled = false;
      alert("Save failed: "+(e && e.message ? e.message : e));
    }
  };
}
if (btnResetSkin){
  btnResetSkin.onclick = async ()=>{
    if (!confirm("Reset skin to defaults for this project?")) return;
    try{
      // remove inline vars so :root defaults take over
      for (const d of tunerDefs){
        document.documentElement.style.removeProperty(d.key);
      }
      await resetSkinInDB();
      setSkinDirty(false);
      buildTuner();
    }catch(e){
      console.error("[SKIN] reset failed", e);
      alert("Reset failed: "+(e && e.message ? e.message : e));
    }
  };
}

if (btnCloseSkin) btnCloseSkin.onclick = closeSkin;
if (skinOverlay) skinOverlay.addEventListener("click",(e)=>{
  if (e.target === skinOverlay) closeSkin();
});
document.addEventListener("keydown",(e)=>{
  if (e.key.toLowerCase()==="s"){
    if (skinOverlay.classList.contains("show")) closeSkin();
    else openSkin();
  }
});

function buildTuner(){
  tBody.innerHTML = "";
  for (const d of tunerDefs){
    const row = document.createElement("div");
    row.className="row";
    const lab = document.createElement("label");
    lab.textContent = d.label;
    const input = document.createElement("input");
    input.type="range";
    input.min = String(d.min);
    input.max = String(d.max);
    input.step = String(d.step);
    const out = document.createElement("output");

    const cur = getComputedStyle(document.documentElement).getPropertyValue(d.key).trim();
    let val = parseFloat(cur);
    if (Number.isNaN(val)) val = d.min;

    input.value = String(val);
    out.value = String(val);

    input.oninput = ()=>{
      const v = input.value;
      out.value = v;
      document.documentElement.style.setProperty(d.key, (d.key==="--flare"||d.key==="--noise"||d.key==="--emboss"||d.key==="--engrave") ? v : (v+"px"));
      saveSkin();
      // cap radius affects cap visuals immediately
      for (let i=0;i<6;i++) updateFaderCap(i);
    };

    row.appendChild(lab);
    row.appendChild(input);
    row.appendChild(out);
    tBody.appendChild(row);
  }
}

let skinDirty = false;

function collectSkinVars(){
  const obj = {};
  for (const d of tunerDefs){
    obj[d.key] = getComputedStyle(document.documentElement).getPropertyValue(d.key).trim();
  }
  return obj;
}
function applySkinVars(obj){
  if (!obj) return;
  for (const d of tunerDefs){
    if (obj[d.key] != null && obj[d.key] !== ""){
      document.documentElement.style.setProperty(d.key, String(obj[d.key]).trim());
    }
  }
}
function setSkinDirty(v){
  skinDirty = !!v;
  try{
    const b = document.getElementById("btnSaveSkin");
    if (b) b.disabled = !skinDirty;
  }catch(e){}
}

// called on every slider change
function saveSkin(){
  setSkinDirty(true);
}

// Load from DB (per user + project)
async function loadSkin(){
  setSkinDirty(false);
  if (!PROJECT) return;
  try{
    const r = await fetch("settings_api.php?project="+encodeURIComponent(PROJECT), { credentials:"include" });
    if (!r.ok) return;
    const j = await r.json();
    if (j && j.ok && j.vars){
      applySkinVars(j.vars);
    }
  }catch(e){}
}



/* ---------------------------
   Auto-fit (prevents overlap)
--------------------------- */
function applyFit(){
  // Base design box this UI was built for (do not change layout; we only compute scale)
  const BASE_W = 1700;
  const BASE_H = 980;

  // Use clientWidth/Height so browser zoom/scrollbars don't cause a 1–2px overflow at 100%
  const vw = Math.max(320, document.documentElement.clientWidth || window.innerWidth);
  const vh = Math.max(320, document.documentElement.clientHeight || window.innerHeight);

  // Safe padding (accounts for body padding + rounding errors)
  const pad = 40;
  const availW = Math.max(320, vw - pad);
  const availH = Math.max(320, vh - pad);

  // Small safety factor avoids overlap at exact 1.0 scale
  const safety = 0.995;

  const scale = Math.min(1, (availW / BASE_W) * safety, (availH / BASE_H) * safety);
  document.documentElement.style.setProperty("--fit-scale", scale.toFixed(4));
}
window.addEventListener("resize", applyFit);
applyFit();
loadSkin().then(()=>{ buildTuner(); });

/* ---------------------------
   LiveKit join
--------------------------- */
if (roomLbl) roomLbl.textContent = ROOM;
if (hostLbl) hostLbl.textContent = HOST_FIXED ? String(HOST_UID) : "AUTO";

// Debug surface for browser console / bridge diagnostics
window.__LK_DEBUG = {
  project: PROJECT,
  projectName: PROJECT_NAME,
  roomDefault: ROOM_DEFAULT,
  room: ROOM,
  activeRoom: ACTIVE_LIVEKIT_ROOM,
  identity: IDENTITY,
  livekitUrl: LIVEKIT_URL,
  activeLivekitUrl: ACTIVE_LIVEKIT_URL,
  tokenEndpoint: null,
  tokenRole: null,
  connectionState: "idle",
  reconnecting: false,
  lastJoinTs: null,
  lastError: null
};
window.currentProject = { id: PROJECT, name: PROJECT_NAME, room: ROOM, joinCode: PROJECT_JOIN_CODE || null };
window.streamState = { channels: [], summary: "IDLE", lastError: null, lastStatusAt: null };
window.appState = { programLocked: false, onAirBadge: "OFFLINE", previewSource: null };
function syncDebugState(){
  try{
    window.currentProject = { id: PROJECT, name: PROJECT_NAME, room: ROOM, joinCode: PROJECT_JOIN_CODE || null };
    window.appState = Object.assign({}, window.appState || {}, {
      programLocked: !!programLocked,
      onAirBadge: (onAirBadge && onAirBadge.textContent) ? String(onAirBadge.textContent).trim() : "OFFLINE",
      previewSource: previewSource ? JSON.parse(JSON.stringify(previewSource)) : null
    });
  }catch(_){ }
}

let joinInFlight = null;
let reconnectTimer = null;
let intentionalLeave = false;
let pageIsUnloading = false;
let studioReturnWatchdog = null;
let bindSeq = 0;

function clearReconnectTimer(){
  if (reconnectTimer){
    try{ clearTimeout(reconnectTimer); }catch(_){}
    reconnectTimer = null;
  }
}
function scheduleReconnect(delay=1200){
  if (pageIsUnloading || intentionalLeave) return;
  clearReconnectTimer();
  reconnectTimer = setTimeout(()=>{
    reconnectTimer = null;
    try{ join(true); }catch(_){ }
  }, Math.max(250, Number(delay)||1200));
}
function stopStudioReturnWatchdog(){
  if (studioReturnWatchdog){
    try{ clearInterval(studioReturnWatchdog); }catch(_){}
    studioReturnWatchdog = null;
  }
}
function startStudioReturnWatchdog(){
  stopStudioReturnWatchdog();
  studioReturnWatchdog = setInterval(()=>{
    try{
      if (!room || room.state !== "connected") return;
      ensureStudioReturnPublish();
    }catch(_){ }
  }, 3000);
}
window.addEventListener("beforeunload", ()=>{
  pageIsUnloading = true;
  intentionalLeave = true;
  stopStudioReturnWatchdog();
  clearReconnectTimer();
});
window.addEventListener("pagehide", ()=>{
  pageIsUnloading = true;
  intentionalLeave = true;
  stopStudioReturnWatchdog();
  clearReconnectTimer();
});
document.addEventListener("visibilitychange", ()=>{
  try{
    if (document.visibilityState === "visible" && !pageIsUnloading && !intentionalLeave && room && room.state !== "connected"){
      scheduleReconnect(300);
    }
  }catch(_){ }
});

/* Token fetch */
async function getToken(){
  const endpoints = [];
  const override = (qs.get("token") || qs.get("tokenEndpoint") || "").trim();
  if (override) endpoints.push(override);

  // Common defaults (try in this order)
  endpoints.push("livekit_token.php", "token_livekit.php", "token.php");

  const params = new URLSearchParams();
  params.set("name", NAME);
  params.set("identity", IDENTITY);
  params.set("role", "operator");
  params.set("publish", "1");
  params.set("subscribe", "1");

  if ((PROJECT || "").trim()){
    params.set("project", PROJECT);
  }else if ((PROJECT_JOIN_CODE || "").trim()){
    params.set("code", PROJECT_JOIN_CODE);
  }else{
    params.set("room", ROOM);
  }

  let lastErr = null;
  for (const ep of endpoints){
    try{
      const url = new URL(ep, location.href);
      for (const [k,v] of params.entries()) url.searchParams.set(k, v);

      const r = await fetch(url.toString(), { credentials: "same-origin" });
      if (!r.ok) throw new Error(`token endpoint ${url.pathname} failed: HTTP ${r.status}`);

      const ct = (r.headers.get("content-type") || "").toLowerCase();
      let data;
      if (ct.includes("application/json")){
        data = await r.json();
      }else{
        const txt = (await r.text()).trim();
        try{ data = JSON.parse(txt); }catch(_){ data = txt; }
      }

      if (data && typeof data === "object" && data.ok === false){
        throw new Error(data.error || `token endpoint ${ep} returned error`);
      }

      let token = null;
      if (typeof data === "string"){
        token = data;
      }else if (data && typeof data === "object"){
        token = data.token || data.accessToken || data.jwt || (data.data && (data.data.token || data.data.accessToken)) || null;
      }
      if (token && typeof token === "string"){
        const resolvedUrl = (data && typeof data === "object" && typeof data.url === "string" && data.url.trim()) ? data.url.trim() : LIVEKIT_URL;
        const resolvedRoom = (data && typeof data === "object" && typeof data.room === "string" && data.room.trim()) ? data.room.trim() : ROOM;
        const resolvedIdentity = (data && typeof data === "object" && typeof data.identity === "string" && data.identity.trim()) ? data.identity.trim() : IDENTITY;
        return {
          token,
          endpoint: ep,
          url: resolvedUrl,
          room: resolvedRoom,
          identity: resolvedIdentity,
          role: (data && typeof data === "object" && data.role) ? data.role : "operator"
        };
      }
      throw new Error(`token endpoint ${ep} returned no token`);
    }catch(err){
      lastErr = err;
      window.__LK_DEBUG.lastError = String(err && err.message ? err.message : err);
      console.warn("[LiveKit] token fetch failed:", ep, err);
    }
  }
  throw lastErr || new Error("Unable to fetch LiveKit token.");
}


async function seedExistingRemoteUsers(){
  if (!room) return;

  const remotes = Array.from(room.remoteParticipants?.values?.() || []);
  if (!remotes.length){
    updateOnAirPanel();
    return;
  }

  // AUTO host selection: pick first participant with any video publication if possible
  if (!HOST_FIXED && HOST_AUTO_UID === null){
    const cand = remotes.find(p => (p.videoTrackPublications && p.videoTrackPublications.size > 0));
    if (cand){
      HOST_AUTO_UID = cand.identity;
      if (hostLbl) hostLbl.textContent = String(cand.identity);
    }
  }

  // Allocate slots first so tracks can attach to known channels
  for (const p of remotes){
    const uid = p.identity;
    if (!uid) continue;
    if (isHostUid(uid)){
      hostUser = p;
    }else{
      allocateSlotForUser({ uid });
    }
  }

  // Pull any already-subscribed tracks
  for (const p of remotes){
    const uid = p.identity;
    if (!uid) continue;

    const vPub = (p.getTrackPublication && p.getTrackPublication(LivekitClient.Track.Source.Camera))
                 || (p.getTrackPublication && p.getTrackPublication(LivekitClient.Track.Source.ScreenShare))
                 || null;
    const aPub = (p.getTrackPublication && p.getTrackPublication(LivekitClient.Track.Source.Microphone))
                 || (p.getTrackPublication && p.getTrackPublication(LivekitClient.Track.Source.ScreenShareAudio))
                 || null;

    const vTrack = vPub?.track || null;
    const aTrack = aPub?.track || null;

    if (isHostUid(uid)){
      hostUser = p;
      if (vTrack){ hostVideoTrack = vTrack; }
      if (aTrack){
        hostAudioTrack = aTrack;
        mountHostAudio();
      }
    }else{
      const s = findSlotByUid(uid) || allocateSlotForUser({ uid });
      if (!s) continue;

      if (vTrack){
        s.videoTrack = vTrack;
        s.hasVideo = true;
      }
      if (aTrack){
        s.audioTrack = aTrack;
        s.hasAudio = true;

        resumeAudioContext();
        ensureAudioNodes();
        connectTrackToChannel(s.idx, aTrack);
        const vol = Number(mixer.strips[s.idx]?.fader?.value || mixer.strips[s.idx]?.val || 80);
        try{
          if (typeof aTrack.setVolume === "function"){
            aTrack.setVolume(s.micMonitorOn ? Math.max(0,Math.min(1, vol/100)) : 0);
          }
        }catch(_){}
      }
    }
  }

  renderUI();
  if (previewSource?.type==="guest"){
    updatePreviewPanel();
  }
  updateOnAirPanel();
}


async function join(force=false){
  if (joinInFlight && !force) return joinInFlight;
  if (!force && room && room.state === "connected"){
    try{ window.lkRoom = room; }catch(_){ }
    return room;
  }

  const run = (async()=>{
    // Reconnect support: cleanly disconnect existing session if requested
    if (force && room){
      try{ room.disconnect(); }catch(_){ }
      try{ room.removeAllListeners && room.removeAllListeners(); }catch(_){ }
      room = null;
    }

    try{
      intentionalLeave = false;
      clearReconnectTimer();
      setStatus(false);
      statusChip.style.opacity = 1;
      window.__LK_DEBUG.connectionState = "connecting";
      window.__LK_DEBUG.reconnecting = !!force;

      if (!window.LivekitClient){
        throw new Error("LiveKit client SDK not found. Ensure livekit-client.umd.min.js is loaded.");
      }

      const t = await getToken();
      const token = t.token;
      const connectUrl = (t.url || LIVEKIT_URL || "").trim();
      const connectRoom = (t.room || ROOM || "").trim();
      const connectIdentity = (t.identity || IDENTITY || "").trim();

      ACTIVE_LIVEKIT_URL = connectUrl || LIVEKIT_URL;
      ACTIVE_LIVEKIT_ROOM = connectRoom || ROOM;
      window.__LK_DEBUG.activeLivekitUrl = ACTIVE_LIVEKIT_URL;
      window.__LK_DEBUG.activeRoom = ACTIVE_LIVEKIT_ROOM;
      window.__LK_DEBUG.tokenEndpoint = t.endpoint || null;
      window.__LK_DEBUG.tokenRole = t.role || null;
      window.__LK_DEBUG.lastError = null;
      window.__LK_DEBUG.lastJoinTs = Date.now();

      // Prepare & connect
      room = new LivekitClient.Room({
        adaptiveStream: true,
        dynacast: true,
      });

      bindRoomEvents();

      console.info("[LiveKit] connect", {
        url: ACTIVE_LIVEKIT_URL,
        room: ACTIVE_LIVEKIT_ROOM,
        identity: connectIdentity,
        requestedIdentity: IDENTITY,
        name: NAME,
        tokenEndpoint: t.endpoint || null,
        tokenRole: t.role || null
      });

      await room.connect(ACTIVE_LIVEKIT_URL, token, { autoSubscribe: true });
      window.lkRoom = room;
      window.__LK_DEBUG.connectionState = String(room?.state || "connected");
      window.__LK_DEBUG.reconnecting = false;
      console.info("[LiveKit] connected", {
        room: ACTIVE_LIVEKIT_ROOM,
        localIdentity: room?.localParticipant?.identity || connectIdentity || IDENTITY,
        sid: room?.sid || null
      });

      // Publish studio return (ONAIR back to Guests)
      await ensureStudioReturnPublish();
      startStudioReturnWatchdog();

      if (hostLbl) hostLbl.textContent = HOST_FIXED ? String(HOST_UID) : "AUTO";

      // seed any existing participants/tracks (TrackSubscribed will also fire)
      await seedExistingRemoteUsers();

      setStatus(true);
      statusChip.textContent = "CONNECTED";
      statusChip.style.opacity = 1;
      renderUI();
      updateOnAirPanel();
      updatePreviewPanel();
      startMeters();
      return room;

    }catch(err){
      try{ window.lkRoom = null; }catch(_){ }
      window.__LK_DEBUG.connectionState = "error";
      window.__LK_DEBUG.reconnecting = false;
      window.__LK_DEBUG.lastError = String(err && err.message ? err.message : err);
      console.error("Join failed:", err, {
        room: ACTIVE_LIVEKIT_ROOM || ROOM,
        identity: IDENTITY,
        livekitUrl: ACTIVE_LIVEKIT_URL || LIVEKIT_URL
      });
      setStatus(false);
      statusChip.textContent = "OFFLINE";
      statusChip.style.opacity = 1;
      if (!pageIsUnloading && !intentionalLeave) scheduleReconnect(1500);
      throw err;
    }finally{
      joinInFlight = null;
    }
  })();
  joinInFlight = run;
  return run;
}


/* ---------------------------
   Slot helpers
--------------------------- */
function isHostUid(uid){
  const u = safeUid(uid);
  if (HOST_FIXED) return u === safeUid(HOST_UID);
  return (HOST_AUTO_UID !== null) && (u === safeUid(HOST_AUTO_UID));
}

function findSlotByUid(uid){
  return slots.find(s => safeUid(s.uid) === safeUid(uid)) || null;
}

function allocateSlotForUser(user){
  const existing = findSlotByUid(user.uid);
  if (existing) return existing;
  const empty = slots.find(s => !s.connected);
  if (!empty) return null;
  empty.uid = user.uid;
  empty.connected = true;
  empty.hasVideo = false;
  empty.hasAudio = false;
  empty.camPreview = true;
  empty.micMonitorOn = true;
  empty.cue = false;
  empty.videoTrack = null;
  empty.audioTrack = null;
  return empty;
}

function releaseSlot(uid){
  const s = findSlotByUid(uid);
  if (!s) return;

  // Detach/cleanup any media sources
  try{
    if (s.videoTrack && typeof s.videoTrack.detach === "function") s.videoTrack.detach();
  }catch(_){}
  try{
    if (s.audioTrack && typeof s.audioTrack.detach === "function") s.audioTrack.detach();
  }catch(_){}

  // Disconnect the WebAudio source node for this channel
  try{ disconnectChannel(s.idx); }catch(_){}

  s.uid = null;
  s.connected = false;
  s.hasVideo = false;
  s.hasAudio = false;
  s.camPreview = true;
  s.micMonitorOn = true;
  s.cue = false;
  s.videoTrack = null;
  s.audioTrack = null;
}

/* ---------------------------
   Mixer model + render
--------------------------- */
const mixer = {
  strips: Array.from({length:6}, (_,i)=>({
    idx:i,
    name: i<5 ? `G${i+1}` : "M",
    title: i<5 ? `G${i+1} CH ${i+1}` : "M MEDIA",
    on:false,
    vuL:null, vuR:null,
    fader:null,
    cap:null,
    volLabel:null,
    levelL:0, levelR:0,
    val: i<5 ? 80 : 85,
  }))
};

function renderUI(){
  // Guests list
  guestsList.innerHTML = "";
  for (const s of slots){
    const wrap = document.createElement("div");
    wrap.className = "guest" + (slotIsMuted(s) ? " muted" : "");

    const scr = document.createElement("div");
    scr.className = "screen" + (s.camPreview && s.hasVideo ? "" : " off");
    scr.innerHTML = `<div class="scrLabel">${slotConnState(s)}</div>`;
    scr.id = `scr-${s.idx}`;
    scr.dataset.slotIdx = String(s.idx);
    wrap.dataset.slotIdx = String(s.idx);
    s.scrEl = scr;

    const info = document.createElement("div");
    const name = `G${s.idx+1}`;
    const pills = [];
    if (s.cue) pills.push(`<span class="pill ready">CUE</span>`);
    const st = slotConnState(s);
    const stCls = (st === "LIVE") ? "live" : (st === "AUDIO") ? "audio" : "offline";
    pills.push(`<span class="pill ${stCls}">${st}</span>`);
    info.innerHTML = `<div class="gName">${name}</div><div class="gMeta">${pills.join("")}</div>`;

    const btns = document.createElement("div");
    btns.className = "btnRow";

    const camBtn = document.createElement("button");
    camBtn.className = "btn " + (s.camPreview ? "on" : "");
    camBtn.title = "Camera Preview (local)";
    camBtn.innerHTML = `<span class="glow"></span>${svgIcon("cam")}`;
    camBtn.disabled = !s.connected;
    camBtn.onclick = () => toggleCamPreview(s.idx);

    const micBtn = document.createElement("button");
    micBtn.className = "btn " + (s.micMonitorOn ? "on" : "");
    micBtn.title = "Monitor (mute/unmute playback)";
    micBtn.innerHTML = `<span class="glow"></span>${svgIcon("mic")}`;
    micBtn.disabled = !s.connected;
    micBtn.onclick = () => toggleMicMonitor(s.idx);

    const cueBtn = document.createElement("button");
    cueBtn.className = "btn cue " + (s.cue ? "on blink" : "");
    cueBtn.title = "Cue";
    cueBtn.innerHTML = `<span class="glow"></span>${svgIcon("bell")}`;
    cueBtn.disabled = !s.connected;
    cueBtn.onclick = () => toggleCue(s.idx);

    btns.appendChild(camBtn);
    btns.appendChild(micBtn);
    btns.appendChild(cueBtn);

    // Remote controls (requires guest page to honor data-channel commands)
    const rMicBtn = document.createElement("button");
    rMicBtn.className = "btn remote";
    rMicBtn.title = "Remote: toggle guest Mic (data message)";
    rMicBtn.innerHTML = `<span class="glow"></span>${svgIcon("mic")}`;
    rMicBtn.disabled = !s.connected;
    rMicBtn.onclick = ()=>{
      if (!s.connected || !s.uid) return;
      const wantOff = !(s.remoteMicOff === true);
      // toggle desired
      s.remoteMicOff = wantOff;
      sendGuestCmd(s.uid, wantOff ? "mic_off" : "mic_on", { slot: s.idx });
      renderUI();
    };

    const rCamBtn = document.createElement("button");
    rCamBtn.className = "btn remote";
    rCamBtn.title = "Remote: toggle guest Camera (data message)";
    rCamBtn.innerHTML = `<span class="glow"></span>${svgIcon("cam")}`;
    rCamBtn.disabled = !s.connected;
    rCamBtn.onclick = ()=>{
      if (!s.connected || !s.uid) return;
      const wantOff = !(s.remoteCamOff === true);
      s.remoteCamOff = wantOff;
      sendGuestCmd(s.uid, wantOff ? "cam_off" : "cam_on", { slot: s.idx });
      renderUI();
    };

    // Visual state
    if (s.remoteMicOff) rMicBtn.classList.add("off");
    if (s.remoteCamOff) rCamBtn.classList.add("off");

    btns.appendChild(rMicBtn);
    btns.appendChild(rCamBtn);


    // Double-click Guest tile or screen to open framing/crop editor
    wireClickAndDbl(wrap, null, ()=>openGuestFramingModal(s.idx));
    wireClickAndDbl(scr, null, ()=>openGuestFramingModal(s.idx));
    wrap.appendChild(scr);
    wrap.appendChild(info);
    wrap.appendChild(btns);
    guestsList.appendChild(wrap);

    if (s.connected && s.hasVideo && s.camPreview && s.videoTrack){
      mountVideoInto(scr, s.videoTrack, true);
    }
  }

  if (window.renderGuestStudio && typeof window.renderGuestStudio === 'function') {
    window.renderGuestStudio();
  } else if (window.GuestStudio && typeof window.GuestStudio.render === 'function') {
    window.GuestStudio.render();
  }

  // Switch cue buttons enable/active
  for (let i=0;i<SLOTS;i++){
    const b = document.getElementById("cueBtn"+i);
    if (!b) continue;
    b.disabled = !slots[i].connected;
    b.classList.toggle("active", previewSource?.type==="guest" && safeUid(previewSource.uid) === safeUid(slots[i].uid));
  }

  // Mixer strips
  stripRow.innerHTML = "";
  for (const st of mixer.strips){
    const div = document.createElement("div");
    div.className = "strip";
    div.id = `st-${st.idx}`;
    div.innerHTML = `
      <div class="stripTop"><b>${st.title}</b><span class="state" id="st-${st.idx}-state">OFFLINE</span></div>
      <div class="vuScale">
        <span>-40</span><span>-20</span><span>-12</span><span>-6</span><span>0</span><span>+3</span>
      </div>
      <div class="vuRow">
        <div class="vuCol"><div class="vuLbl">L</div><div class="vuFill" id="vu-${st.idx}-L"></div></div>
        <div class="vuCol"><div class="vuLbl">R</div><div class="vuFill" id="vu-${st.idx}-R"></div></div>
        <div class="faderStack">
          <div class="faderWell" aria-hidden="true"></div>
          <input class="vfader" type="range" min="0" max="100" value="${st.val}" id="f-${st.idx}" />
          <div class="faderCap" id="cap-${st.idx}" style="top:0px;"></div>
        </div>
      </div>
      <div class="volLbl">VOL</div>
    `;
    stripRow.appendChild(div);

    st.vuL = document.getElementById(`vu-${st.idx}-L`);
    st.vuR = document.getElementById(`vu-${st.idx}-R`);
    st.fader = document.getElementById(`f-${st.idx}`);
    st.cap   = document.getElementById(`cap-${st.idx}`);

    if (st.fader){
      st.fader.oninput = () => {
        resumeAudioContext();
        st.val = Number(st.fader.value);
        setChannelVolume(st.idx, st.val);
        updateFaderCap(st.idx);
      };
    }
    updateFaderCap(st.idx);
  }

  // Apply strip state based on connected
  updateMixerStates();
}

function updateMixerStates(){
  for (let i=0;i<5;i++){
    const s = slots[i];
    const el = document.getElementById(`st-${i}-state`);
    if (el) el.textContent = slotConnState(s);
    const stripEl = document.getElementById(`st-${i}`);
    if (stripEl) stripEl.classList.toggle("muted", slotIsMuted(s));
  }
  const mediaOn = !!getActiveMediaAudio();
  const elM = document.getElementById("st-5-state");
  if (elM) elM.textContent = mediaOn ? "AUDIO" : "OFFLINE";
}

function updateFaderCap(idx){
  const st = mixer.strips[idx];
  if (!st?.fader || !st?.cap) return;

  const v = Number(st.fader.value);
  const min = Number(st.fader.min || 0);
  const max = Number(st.fader.max || 100);
  const t = (v - min) / (max - min);

  const stack = st.fader.parentElement; // .faderStack
  const h = stack ? stack.clientHeight : 260;
  const capH = st.cap ? st.cap.offsetHeight : 76;

  const y = (1 - t) * Math.max(0, (h - capH));
  st.cap.style.top = `${y}px`;
}

function toggleCamPreview(idx){
  const s = slots[idx];
  if (!s.connected) return;
  s.camPreview = !s.camPreview;
  renderUI();
}

function toggleMicMonitor(idx){
  const s = slots[idx];
  if (!s.connected) return;
  s.micMonitorOn = !s.micMonitorOn;
  try{
    if (s.audioTrack && typeof s.audioTrack.setVolume === "function"){
      const vol = Number(mixer.strips[idx]?.fader?.value || 80);
      s.audioTrack.setVolume(s.micMonitorOn ? Math.max(0,Math.min(1, vol/100)) : 0);
    }
  }catch(_){}
  updateMixerStates();
  renderUI();
}
function toggleCue(idx){
  const s = slots[idx];
  if (!s.connected) return;
  s.cue = !s.cue;
  renderUI();
}

function selectPreviewBySlot(idx){
  const s = slots[idx];
  if (!s.connected) return;
  setPreviewSource({type:"guest", uid:s.uid});
}

/* ---------------------------
   Preview selection + On Air panel
--------------------------- */
function setPreviewSource(src){
  previewSource = src;
  const isMedia = src?.type === "media";
  pillPreviewSrc.textContent = isMedia ? "PREVIEW: MEDIA" : "PREVIEW: GUEST";
  if (isMedia){
    const item = mediaItems.find(m => m.id === src?.id);
    previewSrcLbl.textContent = item ? String(item.kind).toUpperCase() : "MEDIA";
  } else {
    const slot = src?.uid ? slots.find(s => safeUid(s.uid) === safeUid(src.uid)) : null;
    previewSrcLbl.textContent = slot ? slotConnState(slot) : "—";
  }
  updatePreviewPanel();
  renderUI(); // update active cue button

  updateMediaButtons();
  saveUiState();
}

function updatePreviewPanel(){
  // Clear previous preview nodes
  previewScreen.querySelectorAll("video,img,iframe,div._ph,div._pvholder,audio").forEach(n => n.remove());

  // Default labels
  if (!previewSource){
    previewSrcLbl.textContent = "—";
    return;
  }

  // Guest preview
  if (previewSource.type === "guest"){
    const slot = slots.find(s => safeUid(s.uid) === safeUid(previewSource.uid));
    const st = slot ? slotConnState(slot) : "OFFLINE";
    previewSrcLbl.textContent = st;

    if (slot && slot.connected && slot.hasVideo && slot.videoTrack){
      const holder = document.createElement("div");
      holder.className = "_pvholder";
      holder.style.position="absolute";
      holder.style.inset="0";
      previewScreen.appendChild(holder);
      holder.dataset.slotIdx = String(slot.idx);
      mountVideoInto(holder, slot.videoTrack, true);
      return;
    }

    const ph = document.createElement("div");
    ph.className="_ph";
    ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
    ph.textContent = (st === "AUDIO") ? "AUDIO" : "NO VIDEO";
    previewScreen.appendChild(ph);
    return;
  }

  // Media preview
  if (previewSource.type === "media"){
    const item = mediaItems.find(m => m.id === previewSource.id);
    previewSrcLbl.textContent = item ? String(item.kind).toUpperCase() : "MEDIA";
    if (!item){
      return;
    }

    if (item.kind === "video"){
      const v = document.createElement("video");
      v.src = item.url;
      v.controls = true;
      v.playsInline = true;
      v.preload = "metadata";
      v.style.objectFit = "cover";
      previewScreen.appendChild(v);
      attachMediaAudio(v, item);
      return;
    }

    if (item.kind === "audio"){
      const ph = document.createElement("div");
      ph.className="_ph";
      ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
      ph.textContent = "AUDIO";
      previewScreen.appendChild(ph);

      const a = document.createElement("audio");
      a.src = item.url;
      a.controls = true;
      a.preload = "metadata";
      a.style.position = "absolute";
      a.style.left = "10px";
      a.style.right = "10px";
      a.style.bottom = "10px";
      a.style.width = "calc(100% - 20px)";
      previewScreen.appendChild(a);
      attachMediaAudio(a, item);
      return;
    }

    if (item.kind === "image"){
      const img = document.createElement("img");
      img.src = item.url;
      img.alt = "";
      img.style.objectFit = "contain";
      previewScreen.appendChild(img);
      return;
    }

    if (item.kind === "pdf"){
      const iframe = document.createElement("iframe");
      iframe.src = item.url;
      previewScreen.appendChild(iframe);
      return;
    }

    const ph = document.createElement("div");
    ph.className="_ph";
    ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
    ph.textContent = "FILE";
    previewScreen.appendChild(ph);
    return;
  }
  // Keep studio return video in sync
  try{ refreshStudioReturnVideo(); }catch(_){ }
}

/* On-Air panel */
function updateOnAirPanel(){
  onAirScreen.querySelectorAll("div._ph").forEach(n=>n.remove());

  const state = hostConnState();
  if (onAirBadge){
    onAirBadge.textContent = state;
    onAirBadge.classList.toggle("live", state==="LIVE");
    onAirBadge.classList.toggle("audio", state==="AUDIO");
    onAirBadge.classList.toggle("offline", state==="OFFLINE");
  }

  const holderId = "__onair_holder";
  let holder = onAirScreen.querySelector("#"+holderId);
  if (!holder){
    holder = document.createElement("div");
    holder.id = holderId;
    holder.style.position="absolute";
    holder.style.inset="0";
    onAirScreen.appendChild(holder);
  }
  holder.innerHTML = "";

  if (hostVideoTrack){
    mountVideoInto(holder, hostVideoTrack, true);
    return;
  }

  const ph = document.createElement("div");
  ph.className="_ph";
  ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
  ph.textContent = (state === "AUDIO") ? "AUDIO" : "OFFLINE";
  onAirScreen.appendChild(ph);
  // Keep studio return video in sync
  try{ refreshStudioReturnVideo(); }catch(_){ }
}


/* ---------------------------
   Media: project-backed files
--------------------------- */
fileInput.addEventListener("change", async ()=>{
  const files = Array.from(fileInput.files || []);
  if (!files.length) return;
  for (const f of files){
    const name = f.name;
    const ext = (name.split(".").pop() || "").toLowerCase();
    let kind = "file";
    if (f.type.startsWith("video/") || ext==="mp4") kind = "video";
    else if (f.type.startsWith("audio/") || ext==="mp3" || ext==="mpeg" || ext==="wav") kind="audio";
    else if (f.type.startsWith("image/") || ["png","jpg","jpeg","webp","gif","svg"].includes(ext)) kind="image";
    else if (f.type==="application/pdf" || ext==="pdf") kind="pdf";

    // optimistic local preview while upload happens
    const localUrl = URL.createObjectURL(f);
    const tempId = "tmp_"+Date.now()+"_"+Math.random().toString(36).slice(2,8);
    const tempItem = {
      id: tempId,
      name,
      kind,
      url: localUrl,
      persistedDataUrl: "",
      thumb: kind==="image" ? localUrl : null,
      fileRelPath: "",
      thumbRelPath: "",
      meta: { uploading:true },
      audioNode: null,
      audioEl: null,
    };
    mediaItems.unshift(tempItem);
    renderMediaThumbs();
    updateMediaButtons();

    try{
      const fd = new FormData();
      fd.append("file", f, f.name);
      fd.append("name", name);
      fd.append("kind", kind);
      const r = await fetch(projectAssetApiUrl("media"), {
        method:"POST",
        body: fd,
        credentials:"include"
      });
      const j = await r.json().catch(()=>null);
      if (!r.ok || !j || !j.ok || !j.item) throw new Error((j && j.error) ? j.error : ("HTTP "+r.status));
      const saved = j.item;
      const idx = mediaItems.findIndex(m => m.id === tempId);
      const item = {
        id: String(saved.id || tempId),
        name: String(saved.name || name),
        kind: String(saved.kind || kind),
        url: String(saved.url || saved.file_url || localUrl),
        persistedDataUrl: "",
        thumb: String(saved.thumb || saved.thumb_url || (kind==="image" ? (saved.url || saved.file_url || localUrl) : "")) || null,
        fileRelPath: String(saved.fileRelPath || saved.file_rel_path || ""),
        thumbRelPath: String(saved.thumbRelPath || saved.thumb_rel_path || ""),
        meta: (saved.meta && typeof saved.meta === "object") ? saved.meta : {},
        audioNode: null,
        audioEl: null,
      };
      if (idx >= 0) mediaItems[idx] = item; else mediaItems.unshift(item);
      try{ URL.revokeObjectURL(localUrl); }catch(_){}
      saveMediaLibrary();
      renderMediaThumbs();
      renderUI();
      updateMediaButtons();
    }catch(err){
      console.warn("Project media upload failed", err);
      const idx = mediaItems.findIndex(m => m.id === tempId);
      if (idx >= 0) mediaItems.splice(idx, 1);
      try{ URL.revokeObjectURL(localUrl); }catch(_){}
      alert("Media upload failed: " + (err && err.message ? err.message : err));
      renderMediaThumbs();
      renderUI();
      updateMediaButtons();
    }
  }
  fileInput.value = "";
});

function updateMediaButtons(){
  if (btnRemoveMedia){
    btnRemoveMedia.disabled = !(previewSource?.type==="media" && mediaItems.length);
  }
  if (btnClearMedia){
    btnClearMedia.disabled = !mediaItems.length;
  }
}


async function removeMediaById(id){
  const idx = mediaItems.findIndex(m => m.id === id);
  if (idx < 0) return;
  const item = mediaItems[idx];

  try{ if (item.audioEl && item.audioEl.pause) item.audioEl.pause(); }catch(_){ }
  try{ if (item.audioEl){ item.audioEl.removeAttribute("src"); item.audioEl.src = ""; if (item.audioEl.load) item.audioEl.load(); } }catch(_){ }

  try{
    if (PROJECT && !String(id).startsWith("tmp_")){
      const r = await fetch(projectAssetApiUrl("media") + "&asset_id=" + encodeURIComponent(id), {
        method:"DELETE",
        credentials:"include"
      });
      const j = await r.json().catch(()=>null);
      if (!r.ok || !j || !j.ok) throw new Error((j && j.error) ? j.error : ("HTTP " + r.status));
    }
  }catch(err){
    console.warn("removeMediaById DB delete failed", err);
  }

  try{ if (item.url && String(item.url).startsWith("blob:")) URL.revokeObjectURL(item.url); }catch(_){}
  try{
    if (item.thumb && String(item.thumb).startsWith("blob:") && item.thumb !== item.url){
      URL.revokeObjectURL(item.thumb);
    }
  }catch(_){}

  mediaItems.splice(idx, 1);
  saveMediaLibrary();

  if (previewSource?.type==="media" && previewSource.id === id){
    previewSource = null;
    detachMediaAudio();
    updatePreviewPanel();
  }

  renderMediaThumbs();
  renderUI();
  updateMediaButtons();
}

async function clearAllMedia(){
  const ids = mediaItems.map(m => m.id);
  for (const id of ids) await removeMediaById(id);
  saveMediaLibrary();
}

if (btnRemoveMedia){
  btnRemoveMedia.onclick = ()=> {
    if (previewSource?.type==="media") removeMediaById(previewSource.id);
  };
}
if (btnClearMedia){
  btnClearMedia.onclick = ()=> clearAllMedia();
}



function createStudioLaunchThumb(kind){
  const map = {
    media: {
      id: "btnOpenMediaStudio",
      badge: "MEDIA",
      title: "Media Studio",
      sub: "Open editor",
      accent: "#39d7ff",
      open: ()=> (window.MediaStudio && typeof window.MediaStudio.open === "function") ? window.MediaStudio.open({ projectId: (window.projectId || window.CURRENT_PROJECT_ID || "") }) : null
    },
    overlay: {
      id: "btnOpenOverlayComposerStudio",
      badge: "GRAPHICS",
      title: "Overlay Composer",
      sub: "Open editor",
      accent: "#ffd24d",
      open: ()=> (typeof window.openOverlayComposer === "function") ? window.openOverlayComposer() : null
    },
    virtual: {
      id: "btnOpenVirtualStudio",
      badge: "VIRTUAL",
      title: "Virtual Studio",
      sub: "Open editor",
      accent: "#b26cff",
      open: ()=> (window.VirtualStudio && typeof window.VirtualStudio.open === "function") ? window.VirtualStudio.open({ projectId: (window.projectId || window.CURRENT_PROJECT_ID || "") }) : null
    },
    transition: {
      id: "btnOpenTransitionStudio",
      badge: "TRANSITION",
      title: "Transition Studio",
      sub: "Open editor",
      accent: "#ffbf47",
      open: ()=> (window.TransitionStudio && typeof window.TransitionStudio.open === "function") ? window.TransitionStudio.open({ projectId: (window.projectId || window.CURRENT_PROJECT_ID || "") }) : null
    }
  };
  const cfg = map[kind];
  if(!cfg) return null;

  const el = document.createElement("div");
  el.id = cfg.id;
  el.className = "thumb";
  el.setAttribute("role","button");
  el.setAttribute("tabindex","0");
  el.setAttribute("data-studio-launch", kind);
  el.setAttribute("title", cfg.title);
  el.style.cursor = "pointer";
  el.style.userSelect = "none";
  el.style.boxShadow = "inset 0 0 0 1px rgba(255,255,255,.08), inset 0 0 0 2px "+cfg.accent+"55";

  const pv = document.createElement("div");
  pv.className = "pv";
  pv.style.position = "relative";
  pv.style.display = "flex";
  pv.style.alignItems = "center";
  pv.style.justifyContent = "center";
  pv.textContent = cfg.title.toUpperCase();

  const badge = document.createElement("span");
  badge.textContent = cfg.badge;
  badge.style.position = "absolute";
  badge.style.left = "8px";
  badge.style.top = "8px";
  badge.style.fontSize = "10px";
  badge.style.letterSpacing = ".08em";
  badge.style.padding = "2px 6px";
  badge.style.borderRadius = "999px";
  badge.style.background = cfg.accent + "22";
  badge.style.border = "1px solid " + cfg.accent + "88";
  badge.style.color = "#fff";
  pv.appendChild(badge);

  const meta = document.createElement("div");
  meta.className = "meta";
  meta.innerHTML = `<div class="name">${cfg.title}</div><div class="kind">${cfg.sub}</div>`;

  el.appendChild(pv);
  el.appendChild(meta);

  const openNow = (ev)=>{
    try{ ev && ev.preventDefault && ev.preventDefault(); }catch(_){}
    try{ ev && ev.stopPropagation && ev.stopPropagation(); }catch(_){}
    cfg.open();
    return false;
  };
  el.addEventListener("click", openNow, true);
  el.addEventListener("dblclick", openNow, true);
  el.addEventListener("keydown", (ev)=>{
    if(ev.key === "Enter" || ev.key === " ") openNow(ev);
  }, true);
  return el;
}


function renderMediaThumbs(){
  thumbs.innerHTML = "";
  const mediaLaunch = createStudioLaunchThumb("media");
  if (mediaLaunch) thumbs.appendChild(mediaLaunch);
  for (const item of mediaItems){
    const el = document.createElement("div");
    el.className = "thumb" + (previewSource?.type==="media" && previewSource.id===item.id ? " active" : "");
    wireClickAndDbl(el,
      ()=>{ setPreviewSource({type:"media", id:item.id}); },
      ()=>{ openMediaEditor(item.id); }
    );

    const pv = document.createElement("div");
    pv.className="pv";
    if (item.thumb){
      const img = document.createElement("img");
      img.src = item.thumb;
      pv.innerHTML = "";
      pv.appendChild(img);
    }else{
      pv.textContent = item.kind.toUpperCase();
    }

    // remove button
    const rm = document.createElement("button");
    rm.className = "rm";
    rm.type = "button";
    rm.title = "Remove media";
    rm.textContent = "×";
    rm.onclick = (ev)=>{ ev.stopPropagation(); removeMediaById(item.id); };
    pv.appendChild(rm);

    const meta = document.createElement("div");
    meta.className="meta";
    meta.innerHTML = `<div class="name">${item.name}</div><div class="kind">${item.kind}</div>`;

    el.appendChild(pv);
    el.appendChild(meta);
    thumbs.appendChild(el);
  }
  updateMediaButtons();
}

function getActiveMediaAudio(){
  // prefer stored audio element (audio or video) for the selected preview media
  const cur = previewSource?.type==="media" ? mediaItems.find(m=>m.id===previewSource.id) : null;
  if (cur && cur.audioEl) return cur.audioEl;
  return null;
}

function attachMediaAudio(mediaEl, item){
  // route audio from media element into channel 6 metering/volume
  item.audioEl = mediaEl;
  resumeAudioContext();
  ensureAudioNodes();
  connectMediaElementToChannel6(mediaEl);
  updateMixerStates();
}


function detachMediaAudio(){
  // disconnect channel 6 input if present
  const st = meterNodes[5];
  if (st){
    const node = st.src || st.source;
    if (node){
      try{ node.disconnect(); }catch(_){}
    }
    st.src = null;
    st.source = null;
  }
  for (const item of mediaItems){
    item.audioEl = null;
  }
}

/* ---------------------------
   Audio graph for metering + volumes
--------------------------- */
function resumeAudioContext(){
  if (!audioCtx){
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
  }
  if (audioCtx.state === "suspended"){
    audioCtx.resume().catch(()=>{});
  }
}

function ensureAudioNodes(){
  if (!audioCtx) return;
  if (masterGain) return;

  masterGain = audioCtx.createGain();
  masterGain.gain.value = 1;
  masterGain.connect(audioCtx.destination);

  // create per channel nodes
  meterNodes = Array.from({length:6}, (_,i)=>{
    const gain = audioCtx.createGain();
    gain.gain.value = (i<5 ? 0.8 : 0.85);

    const splitter = audioCtx.createChannelSplitter(2);
    const analyserL = audioCtx.createAnalyser();
    const analyserR = audioCtx.createAnalyser();
    analyserL.fftSize = 512;
    analyserR.fftSize = 512;

    splitter.connect(analyserL, 0);
    splitter.connect(analyserR, 1);

    // for output monitoring (guests + media) we connect gain to master
    gain.connect(masterGain);

    return {gain, splitter, analyserL, analyserR, src:null};
  });
}


/* ---------------------------
   Studio return (ONAIR back to Guests)
   Publishes program audio/video into the same room so any Guest can listen without extra services.
   Audio: from masterGain -> MediaStreamDestination -> LocalAudioTrack
   Video: captureStream() from #onAirScreen (fallback #previewScreen) -> LocalVideoTrack
--------------------------- */
let studioReturnDest = null;
let studioReturnAudio = null; // LocalAudioTrack
let studioReturnVideo = null; // LocalVideoTrack
let studioReturnVideoTrack = null; // MediaStreamTrack
let studioReturnVideoSourceEl = null;
let studioReturnVideoSourceKey = "";

function supportsCaptureStream(videoEl){
  return !!(videoEl && (videoEl.captureStream || videoEl.mozCaptureStream));
}

function getCaptureStream(videoEl){
  try{
    if (!videoEl) return null;
    if (videoEl.captureStream) return videoEl.captureStream();
    if (videoEl.mozCaptureStream) return videoEl.mozCaptureStream();
  }catch(_){}
  return null;
}

function getVideoTrackFromVideoEl(videoEl){
  try{
    if (!videoEl) return null;
    const so = videoEl.srcObject;
    if (so && typeof so.getVideoTracks === "function"){
      const t = so.getVideoTracks()[0];
      if (t) return t;
    }
    const cs = getCaptureStream(videoEl);
    if (cs && typeof cs.getVideoTracks === "function"){
      const t = cs.getVideoTracks()[0];
      if (t) return t;
    }
  }catch(_){ }
  return null;
}

function getCurrentProgramVideoSource(){
  try{
    if (programLocked && programSnapshot && programSnapshot.source){
      const src = programSnapshot.source;
      if (src.type === "return" && hostVideoTrack){
        return { key: "return:host", track: getMediaStreamTrack(hostVideoTrack), el: null };
      }
      if (src.type === "guest" && src.uid){
        const slot = slots.find(s => safeUid(s.uid) === safeUid(src.uid));
        if (slot && slot.videoTrack){
          return { key: "guest:" + safeUid(src.uid), track: getMediaStreamTrack(slot.videoTrack), el: null };
        }
      }
      if (src.type === "media" && src.id){
        const mediaEl = document.querySelector('#onAirScreen video, #__programRoot video, #previewScreen video');
        const mt = getVideoTrackFromVideoEl(mediaEl);
        if (mt) return { key: "media:" + String(src.id), track: mt, el: mediaEl || null };
      }
    }

    const onAirVid = document.querySelector("#onAirScreen video") || document.querySelector("#__programRoot video");
    const onAirTrack = getVideoTrackFromVideoEl(onAirVid);
    if (onAirTrack) return { key: "dom:onair", track: onAirTrack, el: onAirVid || null };

    const prevVid = document.querySelector("#previewScreen video");
    const prevTrack = getVideoTrackFromVideoEl(prevVid);
    if (prevTrack) return { key: "dom:preview", track: prevTrack, el: prevVid || null };

    const captureVid = supportsCaptureStream(onAirVid) ? onAirVid : (supportsCaptureStream(prevVid) ? prevVid : null);
    if (captureVid){
      const stream = getCaptureStream(captureVid);
      const vt = stream && typeof stream.getVideoTracks === "function" ? stream.getVideoTracks()[0] : null;
      if (vt) return { key: "capture:" + (captureVid === onAirVid ? "onair" : "preview"), track: vt, el: captureVid };
    }
  }catch(_){ }
  return { key: "", track: null, el: null };
}

async function ensureStudioReturnAudio(){
  try{
    if (!room || !room.localParticipant) return;
    resumeAudioContext();
    ensureAudioNodes();
    if (!audioCtx || !masterGain) return;

    if (!studioReturnDest){
      studioReturnDest = audioCtx.createMediaStreamDestination();
      // send full master bus back to LiveKit
      masterGain.connect(studioReturnDest);
    }

    const pubs = Array.from(room.localParticipant?.audioTrackPublications?.values?.() || []);
    const existingPub = pubs.find(p => p?.trackName === "studio_return_audio") || null;

    if (!studioReturnAudio || !existingPub){
      const t = studioReturnDest.stream.getAudioTracks()[0];
      if (!t) return;

      const LocalAudioTrack = (window.LivekitClient && LivekitClient.LocalAudioTrack) ? LivekitClient.LocalAudioTrack : null;
      if (!LocalAudioTrack) return;

      if (studioReturnAudio && !existingPub){
        try{ await room.localParticipant.unpublishTrack(studioReturnAudio); }catch(_){ }
        try{ studioReturnAudio.stop && studioReturnAudio.stop(); }catch(_){ }
      }
      studioReturnAudio = new LocalAudioTrack(t);

      await room.localParticipant.publishTrack(studioReturnAudio, {
        name: "studio_return_audio",
        // source is optional; LiveKit will treat as microphone-like
      });
      console.info("[StudioReturn] published audio");
    }
  }catch(e){
    console.warn("[StudioReturn] audio publish failed", e);
  }
}

async function refreshStudioReturnVideo(){
  try{
    if (!room || !room.localParticipant) return;
    const LocalVideoTrack = (window.LivekitClient && LivekitClient.LocalVideoTrack) ? LivekitClient.LocalVideoTrack : null;
    if (!LocalVideoTrack) return;

    const currentPub = Array.from(room.localParticipant?.videoTrackPublications?.values?.() || []).find(p => p?.trackName === "studio_return_video") || null;
    const src = getCurrentProgramVideoSource();
    if (!src || !src.track) return;

    const srcKey = String(src.key || "dom:unknown");
    if (studioReturnVideo && currentPub && studioReturnVideoSourceKey === srcKey) return;

    if (studioReturnVideo){
      try{ await room.localParticipant.unpublishTrack(studioReturnVideo); }catch(_){}
      try{ studioReturnVideo.stop && studioReturnVideo.stop(); }catch(_){}
      studioReturnVideo = null;
      studioReturnVideoTrack = null;
      studioReturnVideoSourceEl = null;
      studioReturnVideoSourceKey = "";
      studioReturnVideoSourceKey = "";
    }

    const rawTrack = src.track;
    const vt = (rawTrack && typeof rawTrack.clone === "function") ? rawTrack.clone() : rawTrack;
    if (!vt) return;

    studioReturnVideoTrack = vt;
    studioReturnVideoSourceEl = src.el || null;
    studioReturnVideoSourceKey = srcKey;
    studioReturnVideo = new LocalVideoTrack(vt);

    await room.localParticipant.publishTrack(studioReturnVideo, { name:"studio_return_video" });
    console.info("[StudioReturn] published video", srcKey);
  }catch(e){
    console.warn("[StudioReturn] video publish failed", e);
  }
}

async function ensureStudioReturnPublish(){
  await ensureStudioReturnAudio();
  await refreshStudioReturnVideo();
}


function getMediaStreamTrack(track){
  try{
    if (!track) return null;
    if (typeof track.getMediaStreamTrack === "function") return track.getMediaStreamTrack();
    if (track.mediaStreamTrack) return track.mediaStreamTrack;
  }catch(_){}
  return null;
}

// Host (program return) audio element
let hostAudioEl = null;
function ensureHostAudioEl(){
  if (hostAudioEl) return hostAudioEl;
  hostAudioEl = document.createElement("audio");
  hostAudioEl.autoplay = true;
  hostAudioEl.playsInline = true;
  hostAudioEl.controls = false;
  hostAudioEl.style.display = "none";
  document.body.appendChild(hostAudioEl);
  return hostAudioEl;
}
function mountHostAudio(){
  try{
    if (!hostAudioTrack) return;
    const t = getMediaStreamTrack(hostAudioTrack);
    if (!t) return;
    const el = ensureHostAudioEl();
    el.srcObject = new MediaStream([t]);
    el.muted = false;
    el.volume = 1;
    const p = el.play();
    if (p && typeof p.catch === "function") p.catch(()=>{});
  }catch(_){}
}
function unmountHostAudio(){
  try{
    if (!hostAudioEl) return;
    hostAudioEl.pause();
    hostAudioEl.srcObject = null;
  }catch(_){}
}

function mountVideoInto(container, track, muted=true){
  // Preserve any overlay label (used in guest screens)
  const lbl = container.querySelector(".scrLabel");
  container.innerHTML = "";

  const v = document.createElement("video");
  v.autoplay = true;
  v.playsInline = true;
  v.muted = !!muted;
  v.style.width = "100%";
  v.style.height = "100%";
  v.style.objectFit = "cover";
  container.appendChild(v);

  const t = getMediaStreamTrack(track);
  if (t){
    v.srcObject = new MediaStream([t]);
  }else{
    container.innerHTML = '<div class="empty">NO VIDEO</div>';
  }


  // Apply per-guest framing if this container is bound to a guest slot
  try{
    const si = container && container.dataset ? container.dataset.slotIdx : null;
    if (si !== null && si !== undefined && si !== ""){
      const cfg = getGuestVidCfg(Number(si));
      applyVideoFramingToContainer(container, cfg);
    }
  }catch(_){}
  // Re-attach label on top
  if (lbl){
    try{
      lbl.style.position = "absolute";
      lbl.style.left = "10px";
      lbl.style.top = "10px";
      container.appendChild(lbl);
    }catch(_){}
  }

  return v;
}

function connectTrackToChannel(idx, track){
  if (!audioCtx || !meterNodes[idx]) return;
  ensureAudioNodes();
  try{
    // LiveKit track: MediaStreamTrack -> MediaStream
    const msTrack = track.getMediaStreamTrack ? track.getMediaStreamTrack() : null;
    if (!msTrack) return;
    const ms = new MediaStream([msTrack]);
    const src = audioCtx.createMediaStreamSource(ms);
    // connect to gain and splitter
    src.connect(meterNodes[idx].gain);
    src.connect(meterNodes[idx].splitter);
    meterNodes[idx].src = src;
  }catch(err){
    console.warn("connectTrackToChannel failed", err);
  }
}

function disconnectChannel(idx){
  try{
    const n = meterNodes[idx];
    if (!n) return;
    if (n.src){
      try{ n.src.disconnect(); }catch(_){}
      n.src = null;
    }
  }catch(_){}
}

function connectMediaElementToChannel6(el){
  const idx = 5;
  if (!audioCtx || !meterNodes[idx]) return;
  ensureAudioNodes();
  try{
    // disconnect prior MediaElementSource to avoid stacking + leaks
    if (meterNodes[idx].src){
      try{ meterNodes[idx].src.disconnect(); }catch(_){}
      meterNodes[idx].src = null;
    }
  }catch(_){}
  try{
    const src = audioCtx.createMediaElementSource(el);
    src.connect(meterNodes[idx].gain);
    src.connect(meterNodes[idx].splitter);
    meterNodes[idx].src = src;
  }catch(err){
    // can fail if element was already used to create a MediaElementSource in this context
    console.warn("connectMediaElementToChannel6 failed", err);
  }
}

function setChannelVolume(idx, vol){
  const v = Math.max(0, Math.min(100, Number(vol)));
  const gain = v/100;
  if (!audioCtx || !meterNodes[idx]) return;

  meterNodes[idx].gain.gain.value = gain;

  // also set LiveKit playback volume for guests (local monitor)
  if (idx < 5){
    const s = slots[idx];
    if (s.connected && s.audioTrack && typeof s.audioTrack.setVolume === "function"){
      // if muted (micMonitorOn false) force 0, else use gain scaled to 100
      const agVol = s.micMonitorOn ? Math.max(0,Math.min(1, v/100)) : 0;
      try{ s.audioTrack.setVolume(agVol); }catch(_){}
    }
  }
}

function startMeters(){
  resumeAudioContext();
  ensureAudioNodes();
  if (rafMeters) cancelAnimationFrame(rafMeters);

  const bufL = new Float32Array(256);
  const bufR = new Float32Array(256);

  function tick(){
    for (let i=0;i<6;i++){
      const n = meterNodes[i];
      const st = mixer.strips[i];
      if (!n || !st?.vuL || !st?.vuR) continue;

      // compute RMS
      let rmsL=0, rmsR=0;
      n.analyserL.getFloatTimeDomainData(bufL);
      n.analyserR.getFloatTimeDomainData(bufR);
      for (let j=0;j<bufL.length;j++) rmsL += bufL[j]*bufL[j];
      for (let j=0;j<bufR.length;j++) rmsR += bufR[j]*bufR[j];
      rmsL = Math.sqrt(rmsL/bufL.length);
      rmsR = Math.sqrt(rmsR/bufR.length);

      // map to 0..100% with log-ish scaling
      const pctL = Math.min(100, Math.max(0, (20*Math.log10(rmsL+1e-4) + 40) / 43 * 100));
      const pctR = Math.min(100, Math.max(0, (20*Math.log10(rmsR+1e-4) + 40) / 43 * 100));

      // Apply volume/mute to meter display (setVolume does not affect the raw MediaStreamTrack)
      let mul = (Number(st.val) || 0) / 100;
      if (i < 5){
        const s = slots[i];
        if (!s || !s.connected || !s.hasAudio) mul = 0;
        if (s && slotIsMuted(s)) mul = 0;
      } else {
        if (!getActiveMediaAudio()) mul = 0;
      }
      const hL = Math.max(0, Math.min(100, pctL * mul));
      const hR = Math.max(0, Math.min(100, pctR * mul));
      st.vuL.style.height = hL.toFixed(1) + "%";
      st.vuR.style.height = hR.toFixed(1) + "%";
    }
    rafMeters = requestAnimationFrame(tick);
  }
  tick();
}

/* ---------------------------
   LiveKit room events
--------------------------- */
function bindRoomEvents(){
  if (!room || !window.LivekitClient) return;

  const { RoomEvent } = LivekitClient;

  room.on(RoomEvent.ParticipantConnected, (p)=>{
    try{
      const uid = p?.identity;
      if (!uid) return;

      // AUTO host selection (first participant with video pubs OR first video track later)
      if (!HOST_FIXED && HOST_AUTO_UID === null && p.videoTrackPublications && p.videoTrackPublications.size > 0){
        HOST_AUTO_UID = uid;
        if (hostLbl) hostLbl.textContent = String(uid);
      }

      if (isHostUid(uid)){
        hostUser = p;
      }else{
        allocateSlotForUser({ uid });
      }

      renderUI();
      updateOnAirPanel();
      if (previewSource?.type==="guest") updatePreviewPanel();
    }catch(_){} 
  });

  room.on(RoomEvent.ParticipantDisconnected, (p)=>{
    try{
      const uid = p?.identity;
      if (!uid) return;

      if (isHostUid(uid)){
        hostUser = null;
        hostVideoTrack = null;
        hostAudioTrack = null;
        unmountHostAudio();
        updateOnAirPanel();
      }else{
        releaseSlot(uid);
        renderUI();
        if (previewSource?.type==="guest" && safeUid(previewSource.uid)===safeUid(uid)){
          previewSource = null;
          updatePreviewPanel();
        }
      }
    }catch(_){} 
  });

  room.on(RoomEvent.TrackSubscribed, (track, pub, participant)=>{
    try{
      const uid = participant?.identity;
      if (!uid || !track) return;

      // AUTO host selection on first subscribed video
      if (!HOST_FIXED && HOST_AUTO_UID === null && track.kind === "video"){
        HOST_AUTO_UID = uid;
        if (hostLbl) hostLbl.textContent = String(uid);
      }

      if (isHostUid(uid)){
        hostUser = participant;

        if (track.kind === "video"){
          hostVideoTrack = track;
          updateOnAirPanel();
        }else if (track.kind === "audio"){
          hostAudioTrack = track;
          mountHostAudio();
          updateOnAirPanel();
        }
        return;
      }

      const s = findSlotByUid(uid) || allocateSlotForUser({ uid });
      if (!s) return;

      if (track.kind === "video"){
        s.videoTrack = track;
        s.hasVideo = true;
      }else if (track.kind === "audio"){
        s.audioTrack = track;
        s.hasAudio = true;

        resumeAudioContext();
        ensureAudioNodes();
        connectTrackToChannel(s.idx, track);

        const vol = Number(mixer.strips[s.idx]?.fader?.value || mixer.strips[s.idx]?.val || 80);
        try{
          if (typeof track.setVolume === "function"){
            track.setVolume(s.micMonitorOn ? Math.max(0,Math.min(1, vol/100)) : 0);
          }
        }catch(_){}
      }

      renderUI();
      updateOnAirPanel();
      if (previewSource?.type==="guest" && safeUid(previewSource.uid)===safeUid(uid)){
        updatePreviewPanel();
      }
    }catch(err){
      console.warn("TrackSubscribed handler error:", err);
    }
  });

  room.on(RoomEvent.TrackUnsubscribed, (track, pub, participant)=>{
    try{
      const uid = participant?.identity;
      if (!uid || !track) return;

      if (isHostUid(uid)){
        if (track.kind === "video" && hostVideoTrack === track) hostVideoTrack = null;
        if (track.kind === "audio" && hostAudioTrack === track){
          hostAudioTrack = null;
          unmountHostAudio();
        }
        updateOnAirPanel();
        return;
      }

      const s = findSlotByUid(uid);
      if (!s) return;

      if (track.kind === "video" && s.videoTrack === track){
        s.videoTrack = null;
        s.hasVideo = false;
      }
      if (track.kind === "audio" && s.audioTrack === track){
        s.audioTrack = null;
        s.hasAudio = false;
        try{ disconnectChannel(s.idx); }catch(_){}
      }

      renderUI();
      if (previewSource?.type==="guest" && safeUid(previewSource.uid)===safeUid(uid)){
        updatePreviewPanel();
      }
      updateOnAirPanel();
    }catch(err){
      console.warn("TrackUnsubscribed handler error:", err);
    }
  });

  room.on(RoomEvent.Disconnected, (reason)=>{
    console.warn("[LiveKit] disconnected:", reason);
    try{
      window.lkRoom = null;
      window.__LK_DEBUG.lastError = reason ? String(reason) : "disconnected";
      for (const s of slots){
        if (s.connected) releaseSlot(s.uid);
      }
      hostUser = null;
      hostVideoTrack = null;
      hostAudioTrack = null;
      unmountHostAudio();

      setStatus(false);
      renderUI();
      updateOnAirPanel();
      updatePreviewPanel();
    }catch(_){}
  });


  // Track mute/unmute + unpublish updates (so UI reflects guest device state)
  room.on(RoomEvent.TrackMuted, (pub, participant)=>{
    try{
      const uid = participant?.identity;
      if (!uid) return;
      if (isHostUid(uid)){
        updateOnAirPanel();
        return;
      }
      const s = findSlotByUid(uid);
      if (!s) return;
      if (pub?.kind === "audio") s.hasAudio = false;
      if (pub?.kind === "video") s.hasVideo = false;
      renderUI();
      if (previewSource?.type==="guest") updatePreviewPanel();
    }catch(_){}
  });

  room.on(RoomEvent.TrackUnmuted, (pub, participant)=>{
    try{
      const uid = participant?.identity;
      if (!uid) return;
      if (isHostUid(uid)){
        updateOnAirPanel();
        return;
      }
      const s = findSlotByUid(uid);
      if (!s) return;
      if (pub?.kind === "audio") s.hasAudio = true;
      if (pub?.kind === "video") s.hasVideo = true;
      renderUI();
      if (previewSource?.type==="guest") updatePreviewPanel();
    }catch(_){}
  });

  room.on(RoomEvent.TrackUnsubscribed, (track, pub, participant)=>{
    try{
      const uid = participant?.identity;
      if (!uid) return;
      if (isHostUid(uid)){
        if (track?.kind==="audio"){ hostAudioTrack = null; unmountHostAudio(); }
        if (track?.kind==="video"){ hostVideoTrack = null; }
        updateOnAirPanel();
        return;
      }
      const s = findSlotByUid(uid);
      if (!s) return;
      if (track?.kind === "audio"){ s.audioTrack = null; s.hasAudio = false; }
      if (track?.kind === "video"){ s.videoTrack = null; s.hasVideo = false; }
      renderUI();
      if (previewSource?.type==="guest") updatePreviewPanel();
    }catch(_){}
  });

  room.on(RoomEvent.AudioPlaybackStatusChanged, ()=>{
    try{
      if (room && room.canPlaybackAudio === false){
        statusChip.textContent = "CLICK TO ENABLE AUDIO";
        statusChip.style.opacity = 1;
      }
    }catch(_){}
  });
}


/* ---------------------------
   FX (Transitions / Overlays / Virtuals)
   - Click: select (Transition / Virtual) or toggle (Overlay)
   - Double-click: settings popup (saved)
   - Selected FX apply to PREVIEW mix; TAKE commits PREVIEW to PROGRAM.
--------------------------- */
let programLocked = false;
let programSnapshot = null;
const SWAP_PREVIEW_SHOWS_OVERLAYS = false;
let guidesOn = false;

const FX_STORE_KEY = "coproducer_fx_v1";

const MONITOR_STORE_KEY = "coproducer_monitor_v1";
let monitorMode = "onair"; // "onair" | "preview"

const GUIDES_STORE_KEY = "coproducer_guides_v1";
function getDefaultGuidesState(){
  return {
    enabled: false,
    standard: "16:9",
    safe: 0.08,
    grid: true,
    ruler: true,
    pixels: true,
    pxw: 1920,
    pxh: 1080,
    color: "#00ff88",
    opacity: 0.75,
    thickness: 1
  };
}
let guidesState = getDefaultGuidesState();

function loadGuides(){
  try{
    const raw = localStorage.getItem(GUIDES_STORE_KEY);
    if (!raw) return;
    const obj = JSON.parse(raw);
    if (!obj || typeof obj !== "object") return;
    Object.assign(guidesState, obj);
  }catch(_){}
}
function saveGuides(){
  try{ localStorage.setItem(GUIDES_STORE_KEY, JSON.stringify(guidesState)); }catch(_){}
}


function loadMonitor(){
  try{
    const v = localStorage.getItem(MONITOR_STORE_KEY);
    if (v === "preview" || v === "onair") monitorMode = v;
  }catch(_){}
}
function saveMonitor(){
  try{ localStorage.setItem(MONITOR_STORE_KEY, monitorMode); }catch(_){}
}
function applyMonitorUi(){
  if (!btnMonitor) return;
  btnMonitor.textContent = (monitorMode === "preview") ? "MONITOR: PREVIEW" : "MONITOR: ON AIR";
  btnMonitor.setAttribute("aria-pressed", monitorMode === "preview" ? "true" : "false");
}

const transitionDefs = [
  {id:"cut",        name:"Cut",          kind:"transition"},
  {id:"fade",       name:"Fade",         kind:"transition"},
  {id:"wipe",       name:"Wipe",         kind:"transition"},
  {id:"dip",        name:"Dip",          kind:"transition"},
  {id:"slideLeft",  name:"Slide Left",   kind:"transition"},
  {id:"slideRight", name:"Slide Right",  kind:"transition"},
  {id:"slideUp",    name:"Slide Up",     kind:"transition"},
  {id:"slideDown",  name:"Slide Down",   kind:"transition"},
  {id:"zoom",       name:"Zoom",         kind:"transition"},
];

const overlayDefs = [
  {id:"lower", name:"Lower Third", kind:"overlay"},
  {id:"bug",   name:"Bug / Logo",  kind:"overlay"},
  {id:"ticker",name:"Ticker",      kind:"overlay"},
  {id:"full",  name:"Full Screen", kind:"overlay"},
];

const virtualDefs = [
  {id:"single", name:"Single", kind:"virtual"},
  {id:"2up",    name:"2-Up",   kind:"virtual"},
  {id:"3up",    name:"3-Up",   kind:"virtual"},
  {id:"4up",    name:"4-Up",   kind:"virtual"},
  {id:"1p3",    name:"1 + 3",  kind:"virtual"},
  {id:"5up",    name:"5-Up",   kind:"virtual"},
  {id:"pip",    name:"PiP",    kind:"virtual"},
];

const fxState = {
  transition: {
    selected: "cut",
    settings: {
      cut:{},
      fade:{dur:450, ease:"ease"},
      wipe:{dur:520, ease:"ease"},
      dip:{dur:640, ease:"ease", color:"#000000"},
      slideLeft:{dur:520, ease:"ease"},
      slideRight:{dur:520, ease:"ease"},
      slideUp:{dur:520, ease:"ease"},
      slideDown:{dur:520, ease:"ease"},
      zoom:{dur:520, ease:"ease"}
    }
  },
  overlays: {
    enabled: { lower:false, bug:false, ticker:false, full:false },
    settings:{
      lower:{ title:"TITLE", subtitle:"Subtitle", opacity:1, blend:"normal", anchor:"BL", offX:0, offY:0, scale:1, rot:0 },
      bug:{ text:"LOGO", opacity:1, blend:"normal", anchor:"TR", offX:0, offY:0, scale:1, rot:0 },
      ticker:{ text:"Breaking news ticker…", speed:40, opacity:1, blend:"normal", anchor:"BL", offX:0, offY:0, scale:1, rot:0 },
      full:{ text:"FULL SCREEN", opacity:1, blend:"normal", anchor:"C", offX:0, offY:0, scale:1, rot:0 }
    }
  },
  virtuals: {
    selected:"single",
    settings:{
      single:{},
      "2up":{sources:["auto","auto"]},
      "3up":{sources:["auto","auto","auto"]},
      "4up":{sources:["auto","auto","auto","auto"]},
      "1p3":{sources:["main","auto","auto","auto"]},
      "5up":{sources:["auto","auto","auto","auto","auto"]},
      pip:{pipSize:0.30, sources:["main","auto"]}
    }
  }
};

function loadFx(){
  try{
    const raw = localStorage.getItem(FX_STORE_KEY);
    if (!raw) return;
    const obj = JSON.parse(raw);
    if (obj && typeof obj === "object"){
      if (obj.transition) Object.assign(fxState.transition, obj.transition);
      if (obj.overlays){
        if (obj.overlays.enabled) Object.assign(fxState.overlays.enabled, obj.overlays.enabled);
        if (obj.overlays.settings) Object.assign(fxState.overlays.settings, obj.overlays.settings);
      }
      if (obj.virtuals) Object.assign(fxState.virtuals, obj.virtuals);
    }
  }catch(_){}
}
function saveFx(){
  try{ localStorage.setItem(FX_STORE_KEY, JSON.stringify(fxState)); }catch(_){}
}

/* FX thumbs renderer */
function makeThumb(def, active=false, pvText=""){
  const el = document.createElement("div");
  el.className = "thumb" + (active ? " active" : "");
  el.dataset.kind = def.kind;
  el.dataset.id = def.id;

  const pv = document.createElement("div");
  pv.className = "pv";
  pv.textContent = pvText || def.kind.toUpperCase();
  el.appendChild(pv);

  const meta = document.createElement("div");
  meta.className = "meta";
  meta.innerHTML = `<div class="name">${def.name}</div><div class="kind">${def.kind}</div>`;
  el.appendChild(meta);
  return el;
}

function wireClickAndDbl(el, onClick, onDbl){
  // Robust click + double-click wiring.
  // Also supports Shift+Click and Right-Click for settings paths so modal openers
  // do not depend entirely on dblclick timing.
  let t = null;

  const clearT = ()=>{ if (t){ clearTimeout(t); t = null; } };

  if (onDbl){
    el.addEventListener("dblclick", (ev)=>{
      clearT();
      try { ev.preventDefault(); } catch(_){}
      try { ev.stopPropagation(); } catch(_){}
      onDbl(ev);
    }, true);

    el.addEventListener("contextmenu", (ev)=>{
      try { ev.preventDefault(); } catch(_){}
      try { ev.stopPropagation(); } catch(_){}
      clearT();
      onDbl(ev);
    }, true);
  }

  el.addEventListener("click", (ev)=>{
    if (!onClick && !onDbl) return;

    if (onDbl && ev && ev.shiftKey){
      clearT();
      try { ev.preventDefault(); } catch(_){}
      try { ev.stopPropagation(); } catch(_){}
      onDbl(ev);
      return;
    }

    if (t){
      clearT();
      try { ev.preventDefault(); } catch(_){}
      try { ev.stopPropagation(); } catch(_){}
      onDbl && onDbl(ev);
      return;
    }

    if (!onClick){
      return;
    }

    t = setTimeout(()=>{
      t = null;
      onClick && onClick(ev);
    }, 260);
  }, true);

  try { el.style.userSelect = "none"; } catch(_){}
}


function buildFxThumbs(){
  if (fxTransitions){
    fxTransitions.innerHTML = "";
    const transitionLaunch = createStudioLaunchThumb("transition");
    if (transitionLaunch) fxTransitions.appendChild(transitionLaunch);
    for (const d of transitionDefs){
      const el = makeThumb(d, fxState.transition.selected === d.id, d.name.toUpperCase()); el.title = "Click to select • Shift+Click / Right-click / Double-click for settings";
      wireClickAndDbl(el,
        ()=>{
          fxState.transition.selected = d.id;
          saveFx();
          buildFxThumbs();
        },
        ()=> openFxSettings("transition", d.id, d.name)
      );
      fxTransitions.appendChild(el);
    }
  }

  if (fxOverlays){
    fxOverlays.innerHTML = "";
    const overlayLaunch = createStudioLaunchThumb("overlay");
    if (overlayLaunch) fxOverlays.appendChild(overlayLaunch);
    for (const d of overlayDefs){
      const on = !!fxState.overlays.enabled[d.id];
      const el = makeThumb(d, on, d.name.toUpperCase()); el.title = "Click to toggle • Shift+Click / Right-click / Double-click for settings";
      wireClickAndDbl(el,
        ()=>{
          fxState.overlays.enabled[d.id] = !fxState.overlays.enabled[d.id];
          saveFx();
          buildFxThumbs();
          updateOverlaysAfterToggle();
        },
        ()=> openFxSettings("overlay", d.id, d.name)
      );
      fxOverlays.appendChild(el);
    }
  }

  if (fxVirtuals){
    fxVirtuals.innerHTML = "";
    const virtualLaunch = createStudioLaunchThumb("virtual");
    if (virtualLaunch) fxVirtuals.appendChild(virtualLaunch);
    for (const d of virtualDefs){
      const el = makeThumb(d, fxState.virtuals.selected === d.id, d.name.toUpperCase()); el.title = "Click to select • Shift+Click / Right-click / Double-click for settings";
      wireClickAndDbl(el,
        ()=>{
          fxState.virtuals.selected = d.id;
          saveFx();
          buildFxThumbs();
          updatePreviewPanel(); // virtual layout affects composition
        },
        ()=> openFxSettings("virtual", d.id, d.name)
      );
      fxVirtuals.appendChild(el);
    }
  }
}

/* Settings modal (single reusable) */
const fxModal = document.createElement("div");
fxModal.className = "overlay";
fxModal.id = "fxModal";
fxModal.setAttribute("aria-hidden","true");
fxModal.innerHTML = `
  <div class="tuner" style="width:min(720px,94vw);">
    <div class="tHdr">
      <b id="fxTitle">Settings</b>
      <button id="fxClose">Close</button>
    </div>
    <div class="tBody" id="fxBody" style="gap:12px;"></div>
    <div style="display:flex;gap:10px;justify-content:flex-end;padding: 0 16px 16px;">
      <button class="miniBtn" id="fxSave">SAVE</button>
    </div>
  </div>
`;
document.body.appendChild(fxModal);

/* Media editor modal (double click on media item) */
const mediaModal = document.createElement("div");
mediaModal.className = "overlay";
mediaModal.id = "mediaModal";
mediaModal.setAttribute("aria-hidden","true");
mediaModal.innerHTML = `
  <div class="tuner" style="width:min(820px,94vw);">
    <div class="tHdr">
      <b id="mediaTitle">Media Editor</b>
      <button id="mediaClose">Close</button>
    </div>
    <div class="tBody" id="mediaBody" style="gap:12px;"></div>
    <div style="display:flex;gap:10px;justify-content:flex-end;padding: 0 16px 16px;">
      <button class="miniBtn" id="mediaSave">SAVE</button>
    </div>
  </div>
`;
document.body.appendChild(mediaModal);

const mediaTitle = mediaModal.querySelector("#mediaTitle");
const mediaBody  = mediaModal.querySelector("#mediaBody");
const mediaClose = mediaModal.querySelector("#mediaClose");
const mediaSave  = mediaModal.querySelector("#mediaSave");
let mediaModalId = null;

mediaClose.onclick = ()=> closeMediaModal();
mediaModal.addEventListener("click",(e)=>{ if (e.target === mediaModal) closeMediaModal(); });

function addRowTo(targetEl, label, inputEl){
  const row = document.createElement("div");
  row.className = "row";
  const lab = document.createElement("label");
  lab.textContent = label;
  row.appendChild(lab);
  row.appendChild(inputEl);
  targetEl.appendChild(row);
}
function mkSelectSafe(name, options, value){
  if (typeof mkSelect === "function") return mkSelect(name, options, value);
  const sel = document.createElement("select");
  sel.name = name;
  for (const o of (options||[])){
    const opt = document.createElement("option");
    opt.value = o.value;
    opt.textContent = o.label;
    if (String(value) === String(o.value)) opt.selected = true;
    sel.appendChild(opt);
  }
  return sel;
}
function mkInputSafe(name, type, value){
  if (typeof mkInput === "function") return mkInput(name, type, value);
  const i = document.createElement("input");
  i.name = name; i.type = type;
  if (type === "checkbox") i.checked = !!value; else i.value = (value ?? "");
  return i;
}
function mkRangeSafe(name, min, max, step, value, suffix){
  if (typeof mkRange === "function") return mkRange(name, min, max, step, value, suffix);
  const rng = document.createElement("input");
  rng.type="range"; rng.name=name; rng.min=String(min); rng.max=String(max); rng.step=String(step); rng.value=String(value);
  const out = document.createElement("output");
  const fmt = ()=> out.value = rng.value + (suffix || "");
  rng.oninput = fmt; fmt();
  const wrap = document.createElement("div");
  wrap.style.display="grid"; wrap.style.gridTemplateColumns="1fr auto"; wrap.style.gap="10px";
  wrap.appendChild(rng); wrap.appendChild(out);
  return {wrap, rng, out};
}
function openMediaEditor(id) {

  if (arguments.length >= 1) window.__lastMediaEditorId = arguments[0];
  return __cpForceModalOpen('mediaModal');

}

function closeMediaModal() {

  return __cpForceModalClose('mediaModal');

}

mediaSave.onclick = ()=>{
  if (!mediaModalId){ closeMediaModal(); return; }
  const vals = {};
  const els = mediaBody.querySelectorAll("input, select, textarea");
  for (const el of els){
    if (!el.name) continue;
    if (el.type === "checkbox") vals[el.name] = !!el.checked;
    else vals[el.name] = el.value;
  }

  setMediaSettings(mediaModalId, {
    rotate: parseInt(vals.rotate||"0",10) || 0,
    scale: Math.max(0.25, Math.min(3, parseFloat(vals.scale)||1)),
    offX: Math.max(-2000, Math.min(2000, parseFloat(vals.offX)||0)),
    offY: Math.max(-2000, Math.min(2000, parseFloat(vals.offY)||0)),
    fit: String(vals.fit || "cover"),

    cropT: Math.max(0, Math.min(49, parseFloat(vals.cropT)||0)),
    cropR: Math.max(0, Math.min(49, parseFloat(vals.cropR)||0)),
    cropB: Math.max(0, Math.min(49, parseFloat(vals.cropB)||0)),
    cropL: Math.max(0, Math.min(49, parseFloat(vals.cropL)||0)),
    flipH: !!vals.flipH,
    flipV: !!vals.flipV,

    playback: String(vals.playback || "loop"),
    trimStart: Math.max(0, parseFloat(vals.trimStart)||0),
    trimEnd: Math.max(0, parseFloat(vals.trimEnd)||0),
    rate: Math.max(0.25, Math.min(4, parseFloat(vals.rate)||1)),
    vol: Math.max(0, Math.min(1, parseFloat(vals.vol)||1)),
  });

  // Re-render preview/program so settings apply immediately.
  updatePreviewPanel();
  refreshProgramAudioRouting();
  closeMediaModal();
};


/* STREAM modal — Professional wiring (isolated) */
const STREAM_STORE_KEY = "coproducer_stream_channels_v2";
const STREAM_CFG_KEY   = "coproducer_stream_cfg_v2";
const STREAM_LOG_KEY   = "coproducer_stream_log_v1";

/**
 * Backend contract (wired now):
 *  POST {endpoint}/start  { room, monitor, program, channel, opts }
 *  POST {endpoint}/stop   { room, channel }
 *  POST {endpoint}/status { room, channels:[{id}] }  -> { channels:[{id,state,message,stats}] }
 *  POST {endpoint}/test   { room, channel }          -> { ok, message }
 *
 * Notes:
 * - endpoint is a base path, e.g. "https://stream.itahukamedia.com"
 * - server decides how to egress: FFmpeg/LiveKit egress/etc.
 */
let streamCfg = {
  endpoint:"https://studio.itahukamedia.com/stream_api.php",
  authToken: "",
  inputUrl: "rtmp://mtx.itahukamedia.com:1935",
  timeoutMs: 12000,
  pollMs: 2000
};
let streamChannels = []; // {id,name,rtmp_url,key,enabled,opts:{...}}

let streamPollTimer = null;
let streamBusy = new Set(); // channel.id currently in flight
let streamLog = []; // {t, lvl, msg}

function streamNow(){
  const d = new Date();
  return d.toISOString().replace("T"," ").slice(0,19);
}
function streamLogAdd(lvl, msg){
  const e = { t: streamNow(), lvl, msg: String(msg||"") };
  streamLog.push(e);
  if (streamLog.length > 300) streamLog = streamLog.slice(-300);
  try{ localStorage.setItem(STREAM_LOG_KEY, JSON.stringify(streamLog)); }catch(_){}
  renderStreamLog();
}
function summarizeStreamState(map){
  const vals = Object.values(map||{}).map(v => String(v?.state||"IDLE").toUpperCase());
  if (vals.some(v => v === "RUNNING")) return "RUNNING";
  if (vals.some(v => v === "STARTING" || v === "STOPPING")) return "BUSY";
  if (vals.some(v => v === "ERROR")) return "ERROR";
  return "IDLE";
}
function syncStreamGlobals(map, err){
  try{
    const channels = Array.isArray(streamChannels) ? streamChannels.map(ch => ({
      id: ch.id,
      name: ch.name,
      enabled: ch.enabled !== false,
      state: map && map[ch.id] ? (map[ch.id].state || "IDLE") : "IDLE",
      message: map && map[ch.id] ? (map[ch.id].message || "") : ""
    })) : [];
    window.streamState = {
      channels,
      summary: summarizeStreamState(map||{}),
      lastError: err ? String(err?.message || err) : null,
      lastStatusAt: Date.now()
    };
    syncDebugState();
  }catch(_){ }
}
function loadStream(){
  try{
    const rawCfg = localStorage.getItem(STREAM_CFG_KEY);
    if (rawCfg){
      const obj = JSON.parse(rawCfg);
      if (obj && typeof obj === "object") streamCfg = Object.assign(streamCfg, obj);
    // Normalize endpoint (allow full stream_api.php URL)
    if(!streamCfg.endpoint || typeof streamCfg.endpoint!=="string") streamCfg.endpoint = "https://studio.itahukamedia.com/stream_api.php";
    streamCfg.endpoint = streamCfg.endpoint.trim().replace(/\/+$/,'');
    if(!streamCfg.inputUrl || typeof streamCfg.inputUrl!=="string") streamCfg.inputUrl="rtmp://mtx.itahukamedia.com:1935";
    streamCfg.inputUrl = (streamCfg.inputUrl||"").trim() || "rtmp://mtx.itahukamedia.com:1935";
    }
  }catch(_){}
  try{
    const raw = localStorage.getItem(STREAM_STORE_KEY);
    if (raw){
      const arr = JSON.parse(raw);
      if (Array.isArray(arr)) streamChannels = arr;
    }
  }catch(_){}
  try{
    const rawL = localStorage.getItem(STREAM_LOG_KEY);
    if (rawL){
      const arr = JSON.parse(rawL);
      if (Array.isArray(arr)) streamLog = arr;
    }
  }catch(_){}
  if (!Array.isArray(streamChannels)) streamChannels = [];
  // sanitize
  streamChannels = streamChannels.map(ch => ({
    id: ch.id || ("ch_"+Math.random().toString(16).slice(2)),
    name: ch.name || "Channel",
    rtmp_url: ch.rtmp_url || "",
    key: ch.key || "",
    enabled: (ch.enabled !== false),
    opts: Object.assign({
      // future: video codec, audio codec, bitrate, fps, scale, etc.
      profile: ch?.opts?.profile || "rtmp",
      reconnect: (ch?.opts?.reconnect !== false),
      autoStart: !!ch?.opts?.autoStart
    }, ch.opts || {})
  }));
}
function saveStream(){
  try{ localStorage.setItem(STREAM_CFG_KEY, JSON.stringify(streamCfg)); }catch(_){}
  try{ localStorage.setItem(STREAM_STORE_KEY, JSON.stringify(streamChannels)); }catch(_){}
}

function streamBase(){
  let base=(streamCfg.endpoint||"").trim().replace(/\/+$/,"");
  // Do not fall back to legacy PHP proxies (shell_exec is commonly disabled).
  if(!base || /\.php(\?|$)/i.test(base) || /stream_api\.php/i.test(base) || base.startsWith("./") || base.startsWith("/")) base="https://stream.itahukamedia.com";
  return base;
}
async function streamCall(path, payload){
  const base = streamBase();
  const url = base + path;

  const headers = { "Content-Type":"application/json" };
  if (streamCfg.authToken && String(streamCfg.authToken).trim()){
    headers["Authorization"] = "Bearer " + String(streamCfg.authToken).trim();
  }

  const ac = new AbortController();
  const t = setTimeout(()=>ac.abort(), Math.max(1000, Number(streamCfg.timeoutMs)||12000));

  let res, txt;
  try{
    res = await fetch(url, { mode:"cors", cache:"no-store",
      method:"POST",
      headers,
      body: JSON.stringify(payload||{}),
      signal: ac.signal,
      credentials: "omit"
    });
    txt = await res.text().catch(()=> "");
  }finally{
    clearTimeout(t);
  }
  if (!res || !res.ok){
    throw new Error(txt || ("HTTP " + (res ? res.status : "ERR")));
  }
  try{ return JSON.parse(txt); }catch(_){ return { ok:true, text: txt }; }
}

/* UI (LEGACY STREAM MODAL DISABLED — use Clean V2 below) */
if(false){
const streamModal = document.createElement("div");
streamModal.className="overlay";
streamModal.id="streamModal";
streamModal.setAttribute("aria-hidden","true");
streamModal.innerHTML = `
  <div class="tuner" style="width:min(1080px,96vw);">
    <div class="tHdr">
      <b>STREAM</b>
      <div style="display:flex;gap:10px;align-items:center;">
        <span class="mono" id="streamRoomBadge" style="opacity:.75;"></span>
        <button class="miniBtn" id="streamClose">Close</button>
      </div>
    </div>
    <div class="tBody" style="gap:12px;">
      <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
        <span class="mono" style="min-width:92px;opacity:.75;">Endpoint</span>
    <input id="streamEndpoint" class="tInput" style="flex:1;min-width:240px;" placeholder="https://stream.itahukamedia.com" />
        <span class="mono" style="min-width:82px;opacity:.75;">Token</span>
        <input id="streamToken" class="tInput" style="flex:1;min-width:200px;" placeholder="(optional bearer token)" />

      <label class="lb">Input URL (PROGRAM)</label>
      <input id="streamInputUrl" class="in" placeholder="rtmp://mtx.itahukamedia.com:1935">
        <button class="miniBtn" id="streamSaveCfg">Save</button>
      </div>

      <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
        <span class="mono" style="min-width:92px;opacity:.75;">Timeout</span>
        <input id="streamTimeout" class="tInput" type="number" min="1000" step="500" style="width:140px;" />
        <span class="mono" style="min-width:92px;opacity:.75;">Poll</span>
        <input id="streamPoll" class="tInput" type="number" min="0" step="250" style="width:140px;" />
        <span style="opacity:.72;font-size:12px;">ms (0 disables status polling)</span>
      </div>

      <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
        <b>Channels</b>
        <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
          <button class="miniBtn" id="streamStartEnabled">Start Enabled</button>
          <button class="miniBtn" id="streamStopAll">Stop All</button>
          <button class="miniBtn" id="streamAdd">+ Add</button>
        </div>
      </div>

      <div id="streamList" style="display:flex;flex-direction:column;gap:10px;"></div>

      <div style="display:grid;grid-template-columns: 1fr; gap:10px;">
        <div style="display:flex;justify-content:space-between;align-items:center;">
          <b>Activity</b>
          <div style="display:flex;gap:10px;">
            <button class="miniBtn" id="streamCopyLog">Copy</button>
            <button class="miniBtn" id="streamClearLog">Clear</button>
          </div>
        </div>
        <pre id="streamLog" style="margin:0;max-height:180px;overflow:auto;padding:10px;border-radius:12px;border:1px solid rgba(255,255,255,.10);background:rgba(0,0,0,.20);color:rgba(255,255,255,.82);font-size:12px;line-height:1.35;"></pre>
      </div>

      <div style="opacity:.72;font-size:12px;line-height:1.35;">
              </div>
    </div>
  </div>
`;
document.body.appendChild(streamModal);

const streamClose = streamModal.querySelector("#streamClose");
const streamEndpoint = streamModal.querySelector("#streamEndpoint");
const streamToken = streamModal.querySelector("#streamToken");
const streamSaveCfg = streamModal.querySelector("#streamSaveCfg");
const streamTimeout = streamModal.querySelector("#streamTimeout");
const streamPoll = streamModal.querySelector("#streamPoll");
const streamAdd = streamModal.querySelector("#streamAdd");
const streamStartEnabled = streamModal.querySelector("#streamStartEnabled");
const streamStopAll = streamModal.querySelector("#streamStopAll");
const streamList = streamModal.querySelector("#streamList");
const streamRoomBadge = streamModal.querySelector("#streamRoomBadge");
const streamLogEl = streamModal.querySelector("#streamLog");
const streamCopyLog = streamModal.querySelector("#streamCopyLog");
const streamClearLog = streamModal.querySelector("#streamClearLog");

streamClose.onclick = ()=> closeStreamModal();
streamModal.addEventListener("click",(e)=>{ if (e.target === streamModal) closeStreamModal(); });
document.addEventListener("keydown",(e)=>{ if (e.key === "Escape" && streamModal.classList.contains("show")) closeStreamModal(); });

function openStreamModal() {

  return __cpForceModalOpen(document.getElementById('streamModalV3') ? 'streamModalV3' : 'streamModal');

}
function closeStreamModal() {

  __cpForceModalClose('streamModalV3');
  return __cpForceModalClose('streamModal');

}

streamSaveCfg.onclick = ()=>{
  streamCfg.endpoint = (streamEndpoint.value || "").trim() || "https://stream.itahukamedia.com";
  if(/\.php(\?|$)/i.test(streamCfg.endpoint) || /stream_api\.php/i.test(streamCfg.endpoint) || streamCfg.endpoint.startsWith("./") || streamCfg.endpoint.startsWith("/")) streamCfg.endpoint="https://stream.itahukamedia.com";
  streamCfg.endpoint = streamCfg.endpoint.replace(/\/+$/,"");
  streamCfg.authToken = (streamToken.value || "").trim();
  streamCfg.timeoutMs = Math.max(1000, Number(streamTimeout.value)||12000);
  streamCfg.pollMs = Math.max(0, Number(streamPoll.value)||0);
  saveStream();
  streamLogAdd("INFO", "Saved stream config: endpoint=" + streamCfg.endpoint);
  stopStreamPolling();
  startStreamPolling();
};

streamAdd.onclick = ()=>{
  streamChannels.push({
    id: "ch_"+Math.random().toString(16).slice(2),
    name:"Channel",
    rtmp_url:"",
    key:"",
    enabled:true,
    opts:{ profile:"rtmp", reconnect:true, autoStart:false }
  });
  saveStream();
  renderStreamList();
};

streamCopyLog.onclick = async ()=>{
  try{
    const txt = streamLog.map(e=>`[${e.t}] ${e.lvl} ${e.msg}`).join("\n");
    await navigator.clipboard.writeText(txt);
    streamLogAdd("INFO","Copied activity log to clipboard.");
  }catch(err){
    streamLogAdd("ERR","Copy failed: " + (err?.message||err));
  }
};
streamClearLog.onclick = ()=>{
  streamLog = [];
  try{ localStorage.setItem(STREAM_LOG_KEY, JSON.stringify(streamLog)); }catch(_){}
  renderStreamLog();
};

function renderStreamLog(){
  if (!streamLogEl) return;
  const lines = streamLog.map(e=>`[${e.t}] ${e.lvl} ${e.msg}`);
  streamLogEl.textContent = lines.join("\n");
  streamLogEl.scrollTop = streamLogEl.scrollHeight;
}

function channelIsValid(ch){
  const url = String(ch.rtmp_url||"").trim();
  return url.startsWith("rtmp://") || url.startsWith("rtmps://");
}
function statusBadge(state){
  const s = String(state||"IDLE").toUpperCase();
  const map = {
    IDLE: "rgba(255,255,255,.55)",
    STARTING: "rgba(245,158,11,.85)",
    RUNNING: "rgba(34,197,94,.85)",
    STOPPING: "rgba(245,158,11,.85)",
    ERROR: "rgba(239,68,68,.85)"
  };
  const c = map[s] || "rgba(255,255,255,.55)";
  return `<span class="pill" style="border-color:${c}44;background:${c}1c;color:rgba(255,255,255,.92);">${s}</span>`;
}

async function withBusy(chId, fn){
  if (streamBusy.has(chId)) return;
  streamBusy.add(chId);
  try{ await fn(); } finally { streamBusy.delete(chId); }
}

async function doStreamStart(ch){
  const opts = Object.assign({}, deepClone(ch.opts || {}));
  // Always provide input_url (server requires it unless STREAM_INPUT_URL env var is set)
  if(!opts.input_url) opts.input_url = (streamCfg.streamInputUrl || "").trim();

  const payload = {
    room: ROOM || "",
    user_id: (window.ITAHUKA_PROJECT && ITAHUKA_PROJECT.user_id) ? ITAHUKA_PROJECT.user_id : null,
    project_id: (window.ITAHUKA_PROJECT && ITAHUKA_PROJECT.id) ? ITAHUKA_PROJECT.id : null,
    monitor: (typeof monitorMode !== "undefined" ? monitorMode : "onair"),
    program: deepClone(programSnapshot || {}),
    channel: deepClone(ch),
    opts
  };

  streamLogAdd("INFO", `START ${ch.name}`);
  await streamCall("/start", payload);
  streamLogAdd("INFO", `STARTED ${ch.name}`);
}
async function doStreamStop(ch){
  const payload = { room: ROOM || "", user_id: (window.ITAHUKA_PROJECT && ITAHUKA_PROJECT.user_id) ? ITAHUKA_PROJECT.user_id : null, project_id: (window.ITAHUKA_PROJECT && ITAHUKA_PROJECT.id) ? ITAHUKA_PROJECT.id : null, channel: deepClone(ch) };
  streamLogAdd("INFO", `STOP ${ch.name}`);
  await streamCall("/stop", payload);
  streamLogAdd("INFO", `STOPPED ${ch.name}`);
}
async function doStreamTest(ch){
  const payload = { room: ROOM || "", user_id: (window.ITAHUKA_PROJECT && ITAHUKA_PROJECT.user_id) ? ITAHUKA_PROJECT.user_id : null, project_id: (window.ITAHUKA_PROJECT && ITAHUKA_PROJECT.id) ? ITAHUKA_PROJECT.id : null, channel: deepClone(ch) };
  streamLogAdd("INFO", `TEST ${ch.name}`);
  const r = await streamCall("/test", payload);
  streamLogAdd(r?.ok ? "INFO":"ERR", `TEST ${ch.name}: ${r?.message || (r?.ok ? "OK":"FAIL")}`);
}

function renderStreamList(statusMap){
  streamList.innerHTML = "";
  if (!streamChannels.length){
    const empty = document.createElement("div");
    empty.style.opacity = ".6";
    empty.textContent = "No channels yet. Click + Add Channel.";
    streamList.appendChild(empty);
    return;
  }

  const mk = (tag, cls, html) => {
    const el = document.createElement(tag);
    if (cls) el.className = cls;
    if (html != null) el.innerHTML = html;
    return el;
  };

  for (const ch of streamChannels){
    const st = statusMap && statusMap[ch.id] ? statusMap[ch.id] : { state:"IDLE", message:"" };
    const safeMsg = (st && st.message) ? String(st.message) : "";
    const showMsg = (!/rtmps?:\/\//i.test(safeMsg) && safeMsg.length < 180) ? safeMsg : "";

    const row = mk("div","scv2-item");
    row.style.gridTemplateColumns = (window.innerWidth >= 860 ? "1.25fr 1fr 220px" : "1fr");
    row.style.alignItems = "center";

    // Left: name + status (NO URL/KEY shown)
    const left = mk("div","");
    left.innerHTML = `
      <div style="display:flex;align-items:center;gap:10px;">
        <div class="name">${escapeHtml(ch.name || "Channel")}</div>
        <div>${statusBadge(st.state)}</div>
      </div>
      <div class="mono" style="opacity:.55;font-size:11px;margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:520px;" title="${escapeHtml(showMsg)}">${escapeHtml(showMsg)}</div>
    `;

    // Middle: enable toggle + optional quick info
    const mid = mk("div","");
    mid.innerHTML = `
      <div style="display:flex;align-items:center;gap:12px;justify-content:space-between;">
        <label style="display:flex;align-items:center;gap:8px;font-size:12px;opacity:.9;">
          <input type="checkbox" data-k="enabled" ${ch.enabled ? "checked":""}>
          enable
        </label>
        <button class="miniBtn" data-act="edit" title="Edit (hidden fields)">Edit</button>
      </div>
    `;

    // Right: actions (Go Live / Stop / Test)
    const btns = mk("div","btns");
    btns.innerHTML = `
      <button class="miniBtn" data-act="test">Test</button>
      <button class="miniBtn" data-act="start">Go Live</button>
      <button class="miniBtn" data-act="stop">Stop</button>
      <button class="miniBtn" data-act="del">Remove</button>
    `;

    row.appendChild(left);
    row.appendChild(mid);
    row.appendChild(btns);

    // enable toggle (and only name inline edit via edit dialog)
    row.addEventListener("change",(e)=>{
      const el = e.target;
      if (!(el instanceof HTMLElement)) return;
      const k = el.getAttribute("data-k");
      if (k === "enabled"){
        ch.enabled = !!el.checked;
        saveStream();
      }
    });

    row.addEventListener("click", async (e)=>{
      const btn = e.target.closest("button");
      if (!btn) return;
      const act = btn.getAttribute("data-act");
      if (!act) return;

      if (act === "edit"){
        // Hidden edit dialog: only shown on explicit edit
        const newName = prompt("Channel name:", ch.name || "Channel");
        if (newName != null) ch.name = String(newName).trim() || "Channel";

        const newUrl = prompt("RTMP URL (rtmp:// or rtmps://):", ch.rtmp_url || "");
        if (newUrl != null) ch.rtmp_url = String(newUrl).trim();

        const newKey = prompt("Stream key / query (optional):", ch.key || "");
        if (newKey != null) ch.key = String(newKey).trim();

        saveStream();
        renderStreamList(statusMap);
        return;
      }

      if (act === "del"){
        if (!confirm(`Remove channel "${ch.name || "Channel"}"?`)) return;
        streamChannels = streamChannels.filter(x => x.id !== ch.id);
        saveStream();
        renderStreamList(statusMap);
        return;
      }

      await withBusy(ch.id, async ()=>{
        try{
          if (act === "test") await doStreamTest(ch);
          if (act === "start") await doStreamStart(ch);
          if (act === "stop") await doStreamStop(ch);
          await pollStreamStatusOnce().catch(()=>{});
        }catch(err){
          streamLogAdd("ERR", (act.toUpperCase()) + " failed: " + (err?.message || err));
        }
      });
    });

    streamList.appendChild(row);
  }
}

streamStartEnabled.onclick = async ()=>{
  const enabled = streamChannels.filter(c=>c.enabled);
  if (!enabled.length){
    streamLogAdd("INFO","No enabled channels to start.");
    return;
  }
  for (const ch of enabled){
    if (streamBusy.has(ch.id)) continue;
    await withBusy(ch.id, async ()=>{
      try{
        if (!channelIsValid(ch)){
          streamLogAdd("ERR", `Invalid RTMP URL for ${ch.name}`);
          return;
        }
        await doStreamStart(ch);
      }catch(err){
        streamLogAdd("ERR", `START ${ch.name}: ${err?.message || err}`);
      }
    });
  }
  await pollStreamStatusOnce().catch(()=>{});
};
streamStopAll.onclick = async ()=>{
  const list = streamChannels.slice();
  for (const ch of list){
    if (streamBusy.has(ch.id)) continue;
    await withBusy(ch.id, async ()=>{
      try{ await doStreamStop(ch); }catch(err){
        streamLogAdd("ERR", `STOP ${ch.name}: ${err?.message || err}`);
      }
    });
  }
  await pollStreamStatusOnce().catch(()=>{});
};

async function pollStreamStatusOnce(){
  if (!streamModal.classList.contains("show")) return;
  const ids = streamChannels.map(c=>({id:c.id}));
  if (!ids.length){
    renderStreamList({});
    return;
  }
  const payload = { room: ROOM || "", channels: ids };
  try{
    const r = await streamCall("/status", payload);
    const map = {};
    const arr = Array.isArray(r?.channels) ? r.channels : [];
    for (const it of arr){
      if (!it || !it.id) continue;
      map[it.id] = { state: it.state || "IDLE", message: it.message || "" };
    }
    renderStreamList(map);
    syncStreamGlobals(map, null);
  }catch(err){
    streamLogAdd("ERR","STATUS: " + (err?.message || err));
    syncStreamGlobals({}, err);
  }
}

function startStreamPolling(){
  stopStreamPolling();
  const ms = Math.max(0, Number(streamCfg.pollMs)||0);
  if (!ms) return;
  pollStreamStatusOnce().catch(()=>{});
  streamPollTimer = setInterval(()=>{ pollStreamStatusOnce().catch(()=>{}); }, ms);
}
function stopStreamPolling(){
  if (streamPollTimer){
    clearInterval(streamPollTimer);
    streamPollTimer = null;
  }
}

if (btnStream){
  // STREAM UI uses Clean V2 modal (see "STREAM UI CLEAN v2" marker).
  // Binding is handled by the Clean V2 module at the bottom of this file.
}



const fxTitle = fxModal.querySelector("#fxTitle");
const fxBody  = fxModal.querySelector("#fxBody");
const fxClose = fxModal.querySelector("#fxClose");
const fxSave  = fxModal.querySelector("#fxSave");

let fxModalCtx = null;

fxClose.onclick = ()=>closeFxModal();
fxModal.addEventListener("click",(e)=>{ if (e.target === fxModal) closeFxModal(); });

function mkSelect(name, options, value){
  const sel = document.createElement("select");
  sel.name = name;
  for (const o of options){
    const opt = document.createElement("option");
    opt.value = o.value;
    opt.textContent = o.label;
    if (String(value) === String(o.value)) opt.selected = true;
    sel.appendChild(opt);
  }
  return sel;
}
function mkInput(name, type, value){
  const i = document.createElement("input");
  i.name = name;
  i.type = type;
  if (type === "checkbox"){
    i.checked = !!value;
  }else{
    i.value = (value ?? "");
  }
  return i;
}
function mkRange(name, min, max, step, value, suffix){
  const rng = document.createElement("input");
  rng.type="range";
  rng.name = name;
  rng.min = String(min);
  rng.max = String(max);
  rng.step = String(step);
  rng.value = String(value);
  const out = document.createElement("output");
  const fmt = ()=> out.value = rng.value + (suffix || "");
  rng.oninput = fmt; fmt();
  const wrap = document.createElement("div");
  wrap.style.display="grid";
  wrap.style.gridTemplateColumns="1fr auto";
  wrap.style.gap="10px";
  wrap.appendChild(rng);
  wrap.appendChild(out);
  return {wrap, rng, out};
}
window.mkSelect = mkSelect;
window.mkInput = mkInput;
window.mkRange = mkRange;
window.mkSelectSafe = mkSelectSafe;
window.mkInputSafe = mkInputSafe;
window.mkRangeSafe = mkRangeSafe;
function addRow(label, inputEl){
  const row = document.createElement("div");
  row.className = "row";
  const lab = document.createElement("label");
  lab.textContent = label;
  row.appendChild(lab);
  row.appendChild(inputEl);
  fxBody.appendChild(row);
}

function buildSourceOptions(){
  const opts = [];
  opts.push({value:"auto", label:"Auto"});
  opts.push({value:"main", label:"Main (Preview source)"});
  opts.push({value:"return", label:"Return (Host)"});
  for (let i=0;i<slots.length;i++){
    opts.push({value:`g${i+1}`, label:`Guest G${i+1}`});
  }
  if (mediaItems.length){
    opts.push({value:"", label:"— Media —"});
    for (const m of mediaItems){
      opts.push({value:`media:${m.id}`, label:`Media: ${m.name || m.kind || m.id}`});
    }
  }
  return opts;
}

function openFxSettings(kind, id, name) {

  if (arguments.length >= 1) window.__lastFxKind = arguments[0];
  if (arguments.length >= 2) window.__lastFxId = arguments[1];
  if (arguments.length >= 3) window.__lastFxName = arguments[2];
  return __cpForceModalOpen('fxModal');

}

function closeFxModal() {

  return __cpForceModalClose('fxModal');

}

function readModalValues(){
  const out = {};
  const els = fxBody.querySelectorAll("input, select, textarea");
  for (const el of els){
    const name = el.name;
    if (!name) continue;
    if (el.type === "checkbox") out[name] = !!el.checked;
    else if (el.type === "number" || el.type === "range") out[name] = el.value;
    else out[name] = el.value;
  }
  return out;
}

fxSave.onclick = ()=>{
  if (!fxModalCtx){ closeFxModal(); return; }
  const {kind, id} = fxModalCtx;
  const v = readModalValues();

  if (kind === "transition"){
    fxState.transition.settings[id] = fxState.transition.settings[id] || {};
    if (id !== "cut"){
      fxState.transition.settings[id].dur = Math.max(80, Math.min(1800, parseInt(v.dur,10) || 520));
      fxState.transition.settings[id].ease = String(v.ease || "ease");
      if (id === "dip"){
        fxState.transition.settings[id].color = String(v.color || "#000000");
      }
    }
  }

  if (kind === "overlay"){
    fxState.overlays.settings[id] = fxState.overlays.settings[id] || {};
    const s = fxState.overlays.settings[id];
    // content
    if (id === "lower"){
      s.title = String(v.title || "TITLE");
      s.subtitle = String(v.subtitle || "");
    }else{
      s.text = String(v.text || "");
    }
    if (id === "ticker"){
      s.speed = Math.max(5, Math.min(200, parseFloat(v.speed) || 40));
    }
    // common
    s.opacity = Math.max(0, Math.min(1, parseFloat(v.opacity) || 1));
    s.blend = String(v.blend || "normal");
    s.anchor = String(v.anchor || s.anchor || "C");
    s.offX = Math.max(-2000, Math.min(2000, parseFloat(v.offX) || 0));
    s.offY = Math.max(-2000, Math.min(2000, parseFloat(v.offY) || 0));
    s.scale = Math.max(0.2, Math.min(3, parseFloat(v.scale) || 1));
    s.rot = Math.max(-360, Math.min(360, parseFloat(v.rot) || 0));

    saveFx();
    buildFxThumbs();
    updatePreviewOverlays();
    updateProgramOverlays();
    closeFxModal();
    return;
  }

  if (kind === "virtual"){
    fxState.virtuals.settings[id] = fxState.virtuals.settings[id] || {};
    const s = fxState.virtuals.settings[id];

    if (id === "pip"){
      s.pipSize = Math.max(0.18, Math.min(0.45, parseFloat(v.pipSize) || 0.30));
    }

    // sources array
    const keys = Object.keys(v).filter(k=>/^src\d+$/.test(k)).sort((a,b)=>parseInt(a.slice(3),10)-parseInt(b.slice(3),10));
    if (keys.length){
      s.sources = keys.map(k=>String(v[k] || "auto"));
    }
  }

  if (kind === "guides"){
    closeFxModal();
    openGuidesSettings();
    return;
  }

  saveFx();
  buildFxThumbs();
  updatePreviewPanel();
  closeFxModal();
};

function updateOverlaysAfterToggle(){
  updatePreviewOverlays();
  updateProgramOverlays();
}
}

/* Guides icon: single click toggle, double click settings */
function applyGuidesPreset(preset){
  const map = {
    "16:9": {w:1920, h:1080},
    "9:16": {w:1080, h:1920},
    "1:1": {w:1080, h:1080},
    "4:3": {w:1440, h:1080}
  };
  const p = map[preset] || map["16:9"];
  const els = getGuidesModalEls();
  if (els.pxw && (!els.pxw.value || Number(els.pxw.value) < 1)) els.pxw.value = String(p.w);
  if (els.pxh && (!els.pxh.value || Number(els.pxh.value) < 1)) els.pxh.value = String(p.h);
}
function loadGuidesIntoPanel(){
  const els = getGuidesModalEls();
  if (!els.modal) return;
  if (els.standard) els.standard.value = guidesState.standard || "16:9";
  if (els.safe) els.safe.value = String(guidesState.safe ?? 0.08);
  if (els.grid) els.grid.checked = !!guidesState.grid;
  if (els.ruler) els.ruler.checked = !!guidesState.ruler;
  if (els.pixels) els.pixels.checked = !!guidesState.pixels;
  if (els.pxw) els.pxw.value = String(guidesState.pxw ?? 1920);
  if (els.pxh) els.pxh.value = String(guidesState.pxh ?? 1080);
  if (els.color) els.color.value = guidesState.color || "#00ff88";
  if (els.opacity) els.opacity.value = String(guidesState.opacity ?? 0.75);
  if (els.thickness) els.thickness.value = String(guidesState.thickness ?? 1);
}
function openGuidesSettings() {

  return __cpForceModalOpen('guidesModal');

}
function closeGuidesSettings() {

  return __cpForceModalClose('guidesModal');

}
window.loadGuidesIntoPanel = loadGuidesIntoPanel;
window.openGuidesSettings = openGuidesSettings;
window.closeGuidesSettings = closeGuidesSettings;
function saveGuidesFromPanel(){
  const els = getGuidesModalEls();
  const preset = (els.standard && els.standard.value) || "16:9";
  const presets = {
    "16:9": {w:1920, h:1080},
    "9:16": {w:1080, h:1920},
    "1:1": {w:1080, h:1080},
    "4:3": {w:1440, h:1080}
  };
  const fallback = presets[preset] || presets["16:9"];
  guidesState.standard = preset;
  guidesState.safe = Math.max(0, Math.min(0.20, parseFloat((els.safe && els.safe.value) || "0.08") || 0.08));
  guidesState.grid = !!(els.grid && els.grid.checked);
  guidesState.ruler = !!(els.ruler && els.ruler.checked);
  guidesState.pixels = !!(els.pixels && els.pixels.checked);
  guidesState.pxw = Math.max(1, parseInt((els.pxw && els.pxw.value) || String(fallback.w), 10) || fallback.w);
  guidesState.pxh = Math.max(1, parseInt((els.pxh && els.pxh.value) || String(fallback.h), 10) || fallback.h);
  guidesState.color = String((els.color && els.color.value) || "#00ff88");
  guidesState.opacity = Math.max(0.10, Math.min(1.00, parseFloat((els.opacity && els.opacity.value) || "0.75") || 0.75));
  guidesState.thickness = Math.max(1, Math.min(4, parseInt((els.thickness && els.thickness.value) || "1", 10) || 1));
  saveGuides();
  if (guidesOn) updatePreviewGuides();
  else updatePreviewPanel();
  closeGuidesSettings();
}
function resetGuidesPanel(){
  guidesState = getDefaultGuidesState();
  loadGuidesIntoPanel();
}
window.saveGuidesFromPanel = saveGuidesFromPanel;
window.resetGuidesPanel = resetGuidesPanel;
function toggleGuides(){
  guidesOn = !guidesOn;
  guidesState.enabled = guidesOn;
  saveGuides();
  if (btnGuides) btnGuides.setAttribute("aria-pressed", guidesOn ? "true":"false");
  updatePreviewPanel();
}
if (btnGuides){
  btnGuides.setAttribute("title", "Guides — click to toggle");
  btnGuides.addEventListener("click", toggleGuides);
}
if (btnGuidesSettings) btnGuidesSettings.addEventListener("click", openGuidesSettings);
document.addEventListener("click", (ev)=>{
  if (ev.target && ev.target.id === "btnCloseGuidesModal") closeGuidesSettings();
  if (ev.target && ev.target.id === "btnGuidesSave") saveGuidesFromPanel();
  if (ev.target && ev.target.id === "btnGuidesReset") resetGuidesPanel();

  const gm = document.getElementById("guidesModal");
  if (gm && ev.target === gm) closeGuidesSettings();
}, true);
document.addEventListener("change", (ev)=>{
  if (!(ev.target && ev.target.id === "guidesStandard")) return;
  const preset = ev.target.value || "16:9";
  const map = {"16:9":[1920,1080],"9:16":[1080,1920],"1:1":[1080,1080],"4:3":[1440,1080]};
  const pair = map[preset] || map["16:9"];
  const els = getGuidesModalEls();
  if (els.pxw) els.pxw.value = String(pair[0]);
  if (els.pxh) els.pxh.value = String(pair[1]);
}, true);
document.addEventListener("keydown", (ev)=>{
  const gm = document.getElementById("guidesModal");
  if (ev.key === "Escape" && gm && !gm.hidden) closeGuidesSettings();
});

if (btnMonitor){
  btnMonitor.addEventListener("click", ()=>{
    monitorMode = (monitorMode === "onair") ? "preview" : "onair";
    saveMonitor();
    applyMonitorUi();

    // Re-render both sides so media elements connect to the correct monitor target.
    // Program rebuild is safe here because we only re-render the current program snapshot into the existing stage.
    updatePreviewPanel();
    refreshProgramAudioRouting();
  });
}
/* Program output lock + TAKE */
function ensureProgramRoot(){
  let root = onAirScreen.querySelector("#__programRoot");
  if (!root){
    root = document.createElement("div");
    root.id = "__programRoot";
    root.style.position = "absolute";
    root.style.inset = "0";
    onAirScreen.appendChild(root);
  }
  return root;
}

function applyTransition(fromEl, toEl){
  const sel = fxState.transition.selected || "cut";
  const set = fxState.transition.settings[sel] || {};
  const dur = Math.max(80, Math.min(1800, Number(set.dur || 0) || 0));
  const ease = String(set.ease || "ease");

  // Default: cut
  if (!fromEl || sel === "cut" || !dur){
    if (fromEl) fromEl.remove();
    toEl.style.opacity = "1";
    toEl.style.transform = "";
    return;
  }

  // Helpers
  const done = ()=>{
    try{ fromEl.remove(); }catch(_){}
    toEl.style.opacity = "1";
    toEl.style.transform = "";
    toEl.style.transition = "";
  };

  // Dip to color
  if (sel === "dip"){
    const color = String(set.color || "#000000");
    const dip = document.createElement("div");
    dip.style.position="absolute";
    dip.style.inset="0";
    dip.style.background = color;
    dip.style.opacity = "0";
    dip.style.pointerEvents="none";
    dip.style.transition = `opacity ${Math.floor(dur/2)}ms ${ease}`;
    toEl.parentElement.appendChild(dip);

    toEl.style.opacity = "0";
    toEl.style.transition = `opacity ${Math.floor(dur/2)}ms ${ease}`;
    fromEl.style.opacity = "1";
    fromEl.style.transition = `opacity ${Math.floor(dur/2)}ms ${ease}`;

    requestAnimationFrame(()=>{
      dip.style.opacity = "1";
      fromEl.style.opacity = "0";
      setTimeout(()=>{
        toEl.style.opacity = "1";
        dip.style.opacity = "0";
        setTimeout(()=>{
          try{ dip.remove(); }catch(_){}
          done();
        }, Math.floor(dur/2) + 20);
      }, Math.floor(dur/2) + 20);
    });
    return;
  }

  // Fade
  if (sel === "fade"){
    toEl.style.opacity = "0";
    toEl.style.transition = `opacity ${dur}ms ${ease}`;
    fromEl.style.opacity = "1";
    fromEl.style.transition = `opacity ${dur}ms ${ease}`;
    requestAnimationFrame(()=>{
      toEl.style.opacity = "1";
      fromEl.style.opacity = "0";
      setTimeout(done, dur + 30);
    });
    return;
  }

  // Simple wipe (crossfade for now, preserves existing behavior)
  if (sel === "wipe"){
    toEl.style.opacity = "0";
    toEl.style.transition = `opacity ${dur}ms ${ease}`;
    fromEl.style.opacity = "1";
    fromEl.style.transition = `opacity ${dur}ms ${ease}`;
    requestAnimationFrame(()=>{
      toEl.style.opacity = "1";
      fromEl.style.opacity = "0";
      setTimeout(done, dur + 30);
    });
    return;
  }

  // Slides
  if (sel.startsWith("slide")){
    const dir = sel.replace("slide","");
    const enterFrom = {
      "Left":"translateX(100%)",
      "Right":"translateX(-100%)",
      "Up":"translateY(100%)",
      "Down":"translateY(-100%)"
    }[dir] || "translateX(100%)";
    const exitTo = {
      "Left":"translateX(-100%)",
      "Right":"translateX(100%)",
      "Up":"translateY(-100%)",
      "Down":"translateY(100%)"
    }[dir] || "translateX(-100%)";

    toEl.style.opacity = "1";
    toEl.style.transform = enterFrom;
    toEl.style.transition = `transform ${dur}ms ${ease}`;
    fromEl.style.opacity = "1";
    fromEl.style.transform = "translate(0,0)";
    fromEl.style.transition = `transform ${dur}ms ${ease}`;
    requestAnimationFrame(()=>{
      toEl.style.transform = "translate(0,0)";
      fromEl.style.transform = exitTo;
      setTimeout(done, dur + 30);
    });
    return;
  }

  // Zoom
  if (sel === "zoom"){
    toEl.style.opacity = "0";
    toEl.style.transform = "scale(1.08)";
    toEl.style.transition = `transform ${dur}ms ${ease}, opacity ${dur}ms ${ease}`;
    fromEl.style.opacity = "1";
    fromEl.style.transform = "scale(1)";
    fromEl.style.transition = `transform ${dur}ms ${ease}, opacity ${dur}ms ${ease}`;
    requestAnimationFrame(()=>{
      toEl.style.opacity = "1";
      toEl.style.transform = "scale(1)";
      fromEl.style.opacity = "0";
      fromEl.style.transform = "scale(0.96)";
      setTimeout(done, dur + 30);
    });
    return;
  }

  // fallback
  done();
}


function takeToProgram(){
  // Snapshot old ON AIR state for swap-back into Preview
  const oldOnAir = (programLocked && programSnapshot)
    ? deepClone(programSnapshot)
    : { source:{type:"return"}, virtualId:"single", virtualSettings:{} };

  // Commit Preview -> PROGRAM snapshot
  const nextProgram = getPreviewSnapshot(true);

  // Ensure program root exists and has a previous stage node for transition
  const root = ensureProgramRoot();
  const prevStage = root.querySelector(".programStage");

  const newStage = document.createElement("div");
  newStage.className = "programStage";
  newStage.style.position = "absolute";
  newStage.style.inset = "0";
  newStage.style.opacity = "1";
  newStage.style.transform = "";
  root.appendChild(newStage);

  programLocked = true;
  programSnapshot = {
    source: nextProgram.source,
    virtualId: nextProgram.virtualId,
    virtualSettings: nextProgram.virtualSettings
  };
  if (onAirStatusLbl) onAirStatusLbl.textContent = "PROGRAM";

  // Render program composition (no guides)
  renderSnapshotInto(newStage, {
    source: programSnapshot.source,
    virtualId: programSnapshot.virtualId,
    virtualSettings: programSnapshot.virtualSettings,
    overlaysEnabled: deepClone(fxState.overlays.enabled),
    overlaysSettings: deepClone(fxState.overlays.settings)
  }, false, false);

  // Transition from previous stage (if any)
  if (prevStage){
    applyTransition(prevStage, newStage);
  }

  // Swap back: ON AIR -> PREVIEW (source + virtual layout)
  if (oldOnAir && oldOnAir.source){
    previewSource = oldOnAir.source;
  }else{
    previewSource = {type:"return"};
  }

  if (oldOnAir && oldOnAir.virtualId){
    fxState.virtuals.selected = oldOnAir.virtualId;
    // restore per-layout settings if present
    if (oldOnAir.virtualSettings){
      fxState.virtuals.settings[oldOnAir.virtualId] = oldOnAir.virtualSettings;
    }
    saveFx();
    buildFxThumbs();
  }

  // Refresh preview (Preview always shows current overlay tab state)
  updatePreviewPanel();
  saveUiState();
}

/* Bind TAKE */
if (btnTake){
  btnTake.onclick = ()=> takeToProgram();
}

/* Scene rendering: PREVIEW and PROGRAM */
function clearContainerHard(el){
  // remove children and stop any media elements inside
  try{
    el.querySelectorAll("video,audio").forEach(m=>{
      try{ m.pause && m.pause(); }catch(_){}
      try{ m.srcObject = null; }catch(_){}
      try{ m.removeAttribute("src"); m.src = ""; if (m.load) m.load(); }catch(_){}
    });
  }catch(_){}
  el.innerHTML = "";
}

function resolveSourceEl(parent, src, isPreview){
  // src: {type:"guest", uid} or {type:"media", id} or {type:"return"}
  if (!src){
    const ph = document.createElement("div");
    ph.className="_ph";
    ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
    ph.textContent = "EMPTY";
    parent.appendChild(ph);
    return;
  }

  if (src.type === "return"){
    // Host return feed
    const state = hostConnState();
    if (hostVideoTrack){
      mountVideoInto(parent, hostVideoTrack, true);
      return;
    }
    const ph = document.createElement("div");
    ph.className="_ph";
    ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
    ph.textContent = (state === "AUDIO") ? "AUDIO" : "RETURN";
    parent.appendChild(ph);
    return;
  }

  if (src.type === "guest"){
    const slot = slots.find(s => safeUid(s.uid) === safeUid(src.uid));
    const st = slot ? slotConnState(slot) : "OFFLINE";
    if (slot && slot.connected && slot.hasVideo && slot.videoTrack){
      mountVideoInto(parent, slot.videoTrack, true);
      return;
    }
    const ph = document.createElement("div");
    ph.className="_ph";
    ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
    ph.textContent = (st === "AUDIO") ? "AUDIO" : "NO VIDEO";
    parent.appendChild(ph);
    return;
  }

  if (src.type === "media"){
    const item = mediaItems.find(m => m.id === src.id);
    if (!item){
      const ph = document.createElement("div");
      ph.className="_ph";
      ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
      ph.textContent = "MEDIA";
      parent.appendChild(ph);
      return;
    }

    if (item.kind === "video"){
      const v = document.createElement("video");
      v.src = item.url;
      v.autoplay = true;
      v.playsInline = true;
      const ms = getMediaSettings(item.id);
      v.loop = (ms.playback === "loop");
      v.preload = "metadata";
      // In this control panel, what you HEAR should be ON AIR (program).
      // Preview is visual-only by default.
      v.controls = !!isPreview;
      v.muted = false;
      const hear = (src.type === "media") ? true : ((monitorMode === "preview") ? !!isPreview : !isPreview);
      v.playbackRate = Number(ms.rate || 1) || 1;
      v.volume = hear ? Math.max(0, Math.min(1, Number(ms.vol ?? 1))) : 0;
      v.style.width="100%"; v.style.height="100%"; v.style.objectFit = ms.fit || "cover";
      const sx = (ms.flipH ? -1 : 1) * (ms.scale||1);
      const sy = (ms.flipV ? -1 : 1) * (ms.scale||1);
      v.style.transform = `translate(${ms.offX||0}px, ${ms.offY||0}px) rotate(${ms.rotate||0}deg) scale(${sx}, ${sy})`;
      v.style.clipPath = `inset(${Math.max(0,ms.cropT||0)}% ${Math.max(0,ms.cropR||0)}% ${Math.max(0,ms.cropB||0)}% ${Math.max(0,ms.cropL||0)}%)`;
      v.style.transformOrigin = "50% 50%";
      parent.appendChild(v);

      // Apply trim + continuous modes
      v.addEventListener("loadedmetadata", ()=>{
        const s = getMediaSettings(item.id);
        const start = Math.max(0, s.trimStart||0);
        const end = Math.max(0, s.trimEnd||0);
        if (s.playback === "resume" && (s.lastPos||0) > 0){
          try{ v.currentTime = Math.min(v.duration||1e9, s.lastPos); }catch(_){}
        }else if (s.playback === "restart" || start > 0){
          try{ v.currentTime = start; }catch(_){}
        }
        if (end > 0 && end > start){
          v.addEventListener("timeupdate", ()=>{
            const ss = getMediaSettings(item.id);
            const st = Math.max(0, ss.trimStart||0);
            const en = Math.max(0, ss.trimEnd||0);
            if (en > 0 && v.currentTime >= en - 0.03){
              if (ss.playback === "once"){
                v.pause();
              }else{
                try{ v.currentTime = st; }catch(_){}
                if (ss.playback !== "once") v.play().catch(()=>{});
              }
            }
            // store last pos for resume
            if (ss.playback === "resume"){
              setMediaSettings(item.id, { lastPos: v.currentTime });
            }
          }, {passive:true});
        }else{
          v.addEventListener("timeupdate", ()=>{
            const ss = getMediaSettings(item.id);
            if (ss.playback === "resume"){
              setMediaSettings(item.id, { lastPos: v.currentTime });
            }
          }, {passive:true});
        }
      }, {once:true});

      // Route media audio only when this element is part of PROGRAM (not Preview).
      if (hear){
        attachMediaAudio(v, item);
      }

      // best-effort autoplay (usually allowed after user click that selected media / TAKE gesture)
      Promise.resolve().then(()=>v.play()).catch(()=>{});
      return;
    }
    if (item.kind === "audio"){
      const ph = document.createElement("div");
      ph.className="_ph";
      ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
      ph.textContent = "AUDIO";
      parent.appendChild(ph);

      const a = document.createElement("audio");
      a.src = item.url;
      a.controls = !!isPreview;
      a.preload = "metadata";
      a.muted = false;
      const hear = (src.type === "media") ? true : ((monitorMode === "preview") ? !!isPreview : !isPreview);
      const ms = getMediaSettings(item.id);
      a.playbackRate = Number(ms.rate || 1) || 1;
      a.volume = hear ? Math.max(0, Math.min(1, Number(ms.vol ?? 1))) : 0;
      a.loop = (ms.playback === "loop");
      a.style.position="absolute";
      a.style.left="10px"; a.style.right="10px"; a.style.bottom="10px";
      a.style.width="calc(100% - 20px)";
      parent.appendChild(a);

      a.addEventListener("loadedmetadata", ()=>{
        const s = getMediaSettings(item.id);
        const start = Math.max(0, s.trimStart||0);
        const end = Math.max(0, s.trimEnd||0);
        if (s.playback === "resume" && (s.lastPos||0) > 0){
          try{ a.currentTime = Math.min(a.duration||1e9, s.lastPos); }catch(_){}
        }else if (s.playback === "restart" || start > 0){
          try{ a.currentTime = start; }catch(_){}
        }
        a.addEventListener("timeupdate", ()=>{
          const ss = getMediaSettings(item.id);
          const st = Math.max(0, ss.trimStart||0);
          const en = Math.max(0, ss.trimEnd||0);
          if (en > 0 && a.currentTime >= en - 0.03){
            if (ss.playback === "once"){
              a.pause();
            }else{
              try{ a.currentTime = st; }catch(_){}
              a.play().catch(()=>{});
            }
          }
          if (ss.playback === "resume"){
            setMediaSettings(item.id, { lastPos: a.currentTime });
          }
        }, {passive:true});
      }, {once:true});


      // Route media audio only when this element is part of PROGRAM (not Preview).
      if (hear){
        attachMediaAudio(a, item);
      }

      Promise.resolve().then(()=>a.play()).catch(()=>{});
      return;
    }
    if (item.kind === "image"){
      const img = document.createElement("img");
      img.src = item.url;
      img.alt = "";
      const ms = getMediaSettings(item.id);
      img.style.width="100%"; img.style.height="100%";
      img.style.objectFit = ms.fit || "contain";
      const sx = (ms.flipH ? -1 : 1) * (ms.scale||1);
      const sy = (ms.flipV ? -1 : 1) * (ms.scale||1);
      img.style.transform = `translate(${ms.offX||0}px, ${ms.offY||0}px) rotate(${ms.rotate||0}deg) scale(${sx}, ${sy})`;
      img.style.transformOrigin = "50% 50%";
      img.style.clipPath = `inset(${Math.max(0,ms.cropT||0)}% ${Math.max(0,ms.cropR||0)}% ${Math.max(0,ms.cropB||0)}% ${Math.max(0,ms.cropL||0)}%)`;
      parent.appendChild(img);
      return;
    }
    if (item.kind === "pdf"){
      const iframe = document.createElement("iframe");
      iframe.src = item.url;
      iframe.style.width="100%"; iframe.style.height="100%";
      parent.appendChild(iframe);
      return;
    }

    const ph = document.createElement("div");
    ph.className="_ph";
    ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
    ph.textContent = "FILE";
    parent.appendChild(ph);
    return;
  }
}


function getAuxGuestSources(excludeUid, limit){
  const out = [];
  for (const s of slots){
    if (!s.connected) continue;
    if (excludeUid && safeUid(s.uid) === safeUid(excludeUid)) continue;
    out.push({type:"guest", uid:s.uid});
    if (out.length >= limit) break;
  }
  return out;
}

function renderVirtual(stage, baseSrc, isPreview, layoutId, layoutSettings){
  const vid = layoutId || "single";
  const set = layoutSettings || {};

  // Auto pool (base + other guests)
  let main = baseSrc;
  if (!main){
    const first = slots.find(s=>s.connected);
    if (first) main = {type:"guest", uid:first.uid};
  }
  const mainUid = (main && main.type==="guest") ? main.uid : null;
  const extra = getAuxGuestSources(mainUid, 6);
  const autoPool = [main, ...extra];

  const takeAuto = ()=> autoPool.find(s=>!!s) || null;

  const tokenToSource = (tok, winIdx)=>{
    const t = String(tok || "auto");
    if (t === "auto") return takeAuto();
    if (t === "main") return main;
    if (t === "return") return {type:"return"};
    if (/^g\d+$/.test(t)){
      const gi = Math.max(0, Math.min(slots.length-1, parseInt(t.slice(1),10)-1));
      const s = slots[gi];
      return (s && s.uid) ? {type:"guest", uid:s.uid} : null;
    }
    if (t.startsWith("media:")){
      return {type:"media", id:t.slice(6)};
    }
    // fallback
    return takeAuto();
  };

  const mkWin = (x,y,w,h, srcTok)=>{
    const win = document.createElement("div");
    win.className = "mixWin";
    win.style.left = x+"%";
    win.style.top = y+"%";
    win.style.width = w+"%";
    win.style.height = h+"%";
    stage.appendChild(win);
    resolveSourceEl(win, tokenToSource(srcTok), isPreview);
  };

  const srcs = Array.isArray(set.sources) ? set.sources : [];

  if (vid === "single"){
    mkWin(0,0,100,100, srcs[0] || "main");
    return;
  }

  if (vid === "2up"){
    mkWin(0,0,50,100, srcs[0] || "main");
    mkWin(50,0,50,100, srcs[1] || "auto");
    return;
  }

  if (vid === "3up"){
    mkWin(0,0,50,100, srcs[0] || "main");
    mkWin(50,0,50,50, srcs[1] || "auto");
    mkWin(50,50,50,50, srcs[2] || "auto");
    return;
  }

  if (vid === "4up"){
    mkWin(0,0,50,50, srcs[0] || "main");
    mkWin(50,0,50,50, srcs[1] || "auto");
    mkWin(0,50,50,50, srcs[2] || "auto");
    mkWin(50,50,50,50, srcs[3] || "auto");
    return;
  }

  if (vid === "1p3"){
    mkWin(0,0,66.666,100, srcs[0] || "main");
    mkWin(66.666,0,33.334,33.333, srcs[1] || "auto");
    mkWin(66.666,33.333,33.334,33.333, srcs[2] || "auto");
    mkWin(66.666,66.666,33.334,33.334, srcs[3] || "auto");
    return;
  }

  if (vid === "5up"){
    mkWin(0,0,33.333,50, srcs[0] || "main");
    mkWin(33.333,0,33.333,50, srcs[1] || "auto");
    mkWin(66.666,0,33.334,50, srcs[2] || "auto");
    mkWin(0,50,50,50, srcs[3] || "auto");
    mkWin(50,50,50,50, srcs[4] || "auto");
    return;
  }

  if (vid === "pip"){
    const pipSize = Number(set.pipSize || 0.30);

    // main full
    const mainWin = document.createElement("div");
    mainWin.className="mixWin";
    mainWin.style.left="0"; mainWin.style.top="0"; mainWin.style.width="100%"; mainWin.style.height="100%";
    stage.appendChild(mainWin);
    resolveSourceEl(mainWin, tokenToSource(srcs[0] || "main"), isPreview);

    // pip top-right
    const w = Math.max(0.18, Math.min(0.45, pipSize)) * 100;
    const h = w * 0.56;
    const pip = document.createElement("div");
    pip.className="mixWin";
    pip.style.right="12px";
    pip.style.top="12px";
    pip.style.width = `calc(${w}% - 12px)`;
    pip.style.height = `calc(${h}% - 12px)`;
    pip.style.left = "auto";
    pip.style.bottom = "auto";
    stage.appendChild(pip);
    resolveSourceEl(pip, tokenToSource(srcs[1] || "auto"), isPreview);
    return;
  }

  // fallback
  mkWin(0,0,100,100, srcs[0] || "main");
}


function renderOverlays(layer, overlaysEnabled, overlaysSettings){
  // clean
  layer.innerHTML = "";

  const en = overlaysEnabled || fxState.overlays.enabled;
  const st = overlaysSettings || fxState.overlays.settings;

  const applyCommon = (el, s, defaultPos)=>{
    const opacity = (s && s.opacity != null) ? s.opacity : 1;
    const blend = (s && s.blend) ? s.blend : "normal";
    const anchor = (s && s.anchor) ? s.anchor : (defaultPos || "C");
    const offX = (s && s.offX != null) ? Number(s.offX) : 0;
    const offY = (s && s.offY != null) ? Number(s.offY) : 0;
    const scale = (s && s.scale != null) ? Number(s.scale) : 1;
    const rot = (s && s.rot != null) ? Number(s.rot) : 0;

    el.style.opacity = String(opacity);
    el.style.mixBlendMode = (blend === "normal") ? "normal" : blend;

    // base anchoring
    el.style.left = "auto";
    el.style.right = "auto";
    el.style.top = "auto";
    el.style.bottom = "auto";
    el.style.transformOrigin = "center center";

    if (anchor === "TL"){ el.style.left="18px"; el.style.top="18px"; el.style.transformOrigin="0% 0%"; }
    if (anchor === "TR"){ el.style.right="18px"; el.style.top="18px"; el.style.transformOrigin="100% 0%"; }
    if (anchor === "BL"){ el.style.left="18px"; el.style.bottom="18px"; el.style.transformOrigin="0% 100%"; }
    if (anchor === "BR"){ el.style.right="18px"; el.style.bottom="18px"; el.style.transformOrigin="100% 100%"; }
    if (anchor === "C"){ el.style.left="50%"; el.style.top="50%"; el.style.transformOrigin="50% 50%"; }

    // translate for offsets (C anchor needs -50% shift)
    let t = "";
    if (anchor === "C") t += "translate(-50%,-50%) ";
    t += `translate(${offX}px,${offY}px) scale(${scale}) rotate(${rot}deg)`;
    el.style.transform = t;
  };

  if (en.bug){
    const s = st.bug || {};
    const d = document.createElement("div");
    d.className="ovBug";
    d.textContent = (s.text != null && String(s.text).trim() !== "") ? String(s.text) : "LOGO";
    applyCommon(d, s, "TR");
    layer.appendChild(d);
  }
  if (en.lower){
    const s = st.lower || {};
    const d = document.createElement("div");
    d.className="ovLower";
    const t = (s.title != null && String(s.title).trim() !== "") ? String(s.title) : "TITLE";
    const sub = (s.subtitle != null) ? String(s.subtitle) : "";
    d.innerHTML = `<b>${escapeHtml(t)}</b><span>${escapeHtml(sub)}</span>`;
    applyCommon(d, s, "BL");
    layer.appendChild(d);
  }
  if (en.ticker){
    const s = st.ticker || {};
    const d = document.createElement("div");
    d.className="ovTicker";
    const txt = (s.text != null && String(s.text).trim() !== "") ? String(s.text) : "Ticker…";
    d.innerHTML = `<div class="ovTickerInner">${escapeHtml(txt)}</div>`;
    // speed control (px/s)
    const speed = Math.max(5, Math.min(200, Number(s.speed || 40)));
    d.style.setProperty("--ticker-speed", String(speed));
    applyCommon(d, s, "BL");
    layer.appendChild(d);
  }
  if (en.full){
    const s = st.full || {};
    const d = document.createElement("div");
    d.className="ovFull";
    d.textContent = (s.text != null && String(s.text).trim() !== "") ? String(s.text) : "FULL SCREEN";
    applyCommon(d, s, "C");
    layer.appendChild(d);
  }
}


function escapeHtml(str){
  return String(str || "")
    .replaceAll("&","&amp;")
    .replaceAll("<","&lt;")
    .replaceAll(">","&gt;")
    .replaceAll('"',"&quot;")
    .replaceAll("'","&#39;");
}

function hexToRgba(hex, alpha){
  const s = String(hex || "#00ff88").replace("#", "").trim();
  const n = (s.length === 3)
    ? s.split("").map(ch => ch + ch).join("")
    : s.padEnd(6, "0").slice(0, 6);
  const r = parseInt(n.slice(0,2), 16) || 0;
  const g = parseInt(n.slice(2,4), 16) || 0;
  const b = parseInt(n.slice(4,6), 16) || 0;
  const a = Math.max(0, Math.min(1, Number(alpha || 0)));
  return `rgba(${r},${g},${b},${a})`;
}

function getAspectBox(w, h, standard){
  const ratios = {"16:9":16/9, "9:16":9/16, "1:1":1, "4:3":4/3};
  const target = ratios[standard] || (16/9);
  const cur = w / h;
  if (cur > target){
    // too wide -> pillarbox
    const bw = h * target;
    const x = (w - bw) / 2;
    return {x, y:0, w:bw, h};
  }else{
    // too tall -> letterbox
    const bh = w / target;
    const y = (h - bh) / 2;
    return {x:0, y, w, h:bh};
  }
}

function drawGuides(canvas, stageEl){
  if (!canvas || !stageEl) return;
  const rect = stageEl.getBoundingClientRect();
  const w = Math.max(1, Math.floor(rect.width));
  const h = Math.max(1, Math.floor(rect.height));
  const dpr = Math.max(1, window.devicePixelRatio || 1);

  canvas.width = Math.floor(w * dpr);
  canvas.height = Math.floor(h * dpr);
  const ctx = canvas.getContext("2d");
  ctx.setTransform(dpr,0,0,dpr,0,0);
  ctx.clearRect(0,0,w,h);

  const box = getAspectBox(w, h, guidesState.standard);

  const guideColor = String(guidesState.color || "#00ff88");
  const guideOpacity = Math.max(0.10, Math.min(1.00, Number(guidesState.opacity || 0.75)));
  const guideThickness = Math.max(1, Math.min(4, Number(guidesState.thickness || 1)));

  // outer box
  ctx.strokeStyle = `rgba(255,255,255,${Math.max(.12, guideOpacity * .4)})`;
  ctx.lineWidth = guideThickness;
  ctx.strokeRect(box.x+0.5, box.y+0.5, box.w-1, box.h-1);

  // safe area
  const safe = Math.max(0, Math.min(0.20, Number(guidesState.safe || 0)));
  if (safe > 0){
    const m = Math.min(box.w, box.h) * safe;
    ctx.strokeStyle = hexToRgba(guideColor, Math.max(.18, guideOpacity * .55));
    ctx.lineWidth = guideThickness;
    ctx.strokeRect(box.x+m+0.5, box.y+m+0.5, box.w-2*m-1, box.h-2*m-1);
  }

  // center cross
  ctx.strokeStyle = hexToRgba(guideColor, Math.max(.10, guideOpacity * .32));
  ctx.lineWidth = Math.max(1, guideThickness * 0.8);
  ctx.beginPath();
  ctx.moveTo(box.x + box.w/2, box.y);
  ctx.lineTo(box.x + box.w/2, box.y + box.h);
  ctx.moveTo(box.x, box.y + box.h/2);
  ctx.lineTo(box.x + box.w, box.y + box.h/2);
  ctx.stroke();

  // 10x10 grid
  if (guidesState.grid){
    ctx.strokeStyle = hexToRgba(guideColor, Math.max(.08, guideOpacity * .18));
    ctx.lineWidth = Math.max(1, guideThickness * 0.7);
    ctx.beginPath();
    for (let i=1;i<10;i++){
      const gx = box.x + (box.w * i/10);
      const gy = box.y + (box.h * i/10);
      ctx.moveTo(gx, box.y);
      ctx.lineTo(gx, box.y + box.h);
      ctx.moveTo(box.x, gy);
      ctx.lineTo(box.x + box.w, gy);
    }
    ctx.stroke();
  }

  // rulers
  if (guidesState.ruler){
    ctx.strokeStyle = "rgba(255,255,255,.10)";
    ctx.beginPath();
    const step = 50;
    for (let x=0; x<=box.w; x+=step){
      const xx = box.x + x;
      ctx.moveTo(xx+0.5, box.y);
      ctx.lineTo(xx+0.5, box.y + 10);
    }
    for (let y=0; y<=box.h; y+=step){
      const yy = box.y + y;
      ctx.moveTo(box.x, yy+0.5);
      ctx.lineTo(box.x + 10, yy+0.5);
    }
    ctx.stroke();
  }

  // pixel overlay
  if (guidesState.pixels){
    ctx.fillStyle = "rgba(0,0,0,.35)";
    ctx.fillRect(box.x + 10, box.y + 10, 120, 26);
    ctx.fillStyle = "rgba(255,255,255,.85)";
    ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace";
    ctx.fillText(`${Math.round(box.w)}×${Math.round(box.h)}`, box.x + 18, box.y + 28);
  }
}

function updatePreviewGuides(){
  const stage = previewScreen ? previewScreen.querySelector(".mixStage") : null;
  const cvs = stage ? stage.querySelector("canvas.guidesCanvas") : null;
  if (!cvs || !stage) return;
  drawGuides(cvs, stage);
}

const previewResizeObs = new ResizeObserver(()=>{ if (guidesOn) updatePreviewGuides(); });
try{ previewResizeObs.observe(previewScreen); }catch(_){}

function deepClone(obj){
  try{ return JSON.parse(JSON.stringify(obj)); }catch(_){ return null; }
}

function getPreviewSnapshot(includeOverlays=true){
  return {
    source: previewSource ? deepClone(previewSource) : null,
    virtualId: fxState.virtuals.selected || "single",
    virtualSettings: deepClone(fxState.virtuals.settings[fxState.virtuals.selected || "single"] || {}),
    overlaysEnabled: includeOverlays ? deepClone(fxState.overlays.enabled) : {lower:false, bug:false, ticker:false, full:false},
    overlaysSettings: includeOverlays ? deepClone(fxState.overlays.settings) : deepClone(fxState.overlays.settings)
  };
}

function renderSnapshotInto(container, snap, isPreview, withGuides){
  clearContainerHard(container);

  const stage = document.createElement("div");
  stage.className = "mixStage";
  container.appendChild(stage);

  const src = snap ? snap.source : null;
  const virtualId = (snap && snap.virtualId) ? snap.virtualId : "single";
  const vset = (snap && snap.virtualSettings) ? snap.virtualSettings : {};

  if (virtualId !== "single"){
    renderVirtual(stage, src, isPreview, virtualId, vset);
  }else{
    const win = document.createElement("div");
    win.className="mixWin";
    win.style.left="0"; win.style.top="0"; win.style.width="100%"; win.style.height="100%";
    stage.appendChild(win);
    resolveSourceEl(win, src, isPreview);
  }

  const ovs = document.createElement("div");
  ovs.className="mixOverlays";
  stage.appendChild(ovs);
  renderOverlays(ovs, snap ? snap.overlaysEnabled : null, snap ? snap.overlaysSettings : null);

  if (isPreview && withGuides){
    const g = document.createElement("div");
    g.className="guidesLayer";
    const cvs = document.createElement("canvas");
    cvs.className = "guidesCanvas";
    g.appendChild(cvs);
    stage.appendChild(g);
    // defer draw until layout paints
    requestAnimationFrame(()=>updatePreviewGuides());
  }
}

function updatePreviewOverlays(){
  const stage = previewScreen ? previewScreen.querySelector(".mixStage") : null;
  if (!stage) return;
  const ovs = stage.querySelector(".mixOverlays");
  if (!ovs) return;
  const snap = getPreviewSnapshot(true);
  renderOverlays(ovs, snap.overlaysEnabled, snap.overlaysSettings);
}

function updateProgramOverlays(){
  if (!programLocked) return;
  const root = ensureProgramRoot();
  const stage = root ? root.querySelector(".mixStage") : null;
  if (!stage) return;
  const ovs = stage.querySelector(".mixOverlays");
  if (!ovs) return;
  renderOverlays(ovs, fxState.overlays.enabled, fxState.overlays.settings);
}

function refreshProgramAudioRouting(){
  if (!programLocked || !programSnapshot) return;
  const root = ensureProgramRoot();
  const stage = root ? root.querySelector(".programStage") : null;
  if (!stage) return;

  renderSnapshotInto(stage, {
    source: deepClone(programSnapshot.source),
    virtualId: programSnapshot.virtualId,
    virtualSettings: deepClone(programSnapshot.virtualSettings || {}),
    overlaysEnabled: deepClone(fxState.overlays.enabled),
    overlaysSettings: deepClone(fxState.overlays.settings)
  }, false, false);
}

function updatePreviewPanel(){
  if (!previewSource){
    previewSrcLbl.textContent = "—";
  }else if (previewSource.type === "media"){
    const item = mediaItems.find(m=>m.id===previewSource.id);
    previewSrcLbl.textContent = item ? String(item.kind).toUpperCase() : "MEDIA";
  }else if (previewSource.type === "return"){
    previewSrcLbl.textContent = "RETURN";
  }else{
    const slot = previewSource.uid ? slots.find(s=>safeUid(s.uid)===safeUid(previewSource.uid)) : null;
    previewSrcLbl.textContent = slot ? slotConnState(slot) : "—";
  }

  renderSnapshotInto(previewScreen, getPreviewSnapshot(true), true, guidesOn);
  syncDebugState();
}

/* Override on-air panel: if programLocked keep program stage intact */

/* Override on-air panel: if programLocked keep program stage intact */
function updateOnAirPanel(){
  // Update badge only; do not stomp PROGRAM output once TAKEN
  const state = programLocked ? "PROGRAM" : deriveOnAirBadgeState();
  if (onAirBadge){
    onAirBadge.textContent = state;
    onAirBadge.classList.toggle("live", state==="LIVE" || state==="PROGRAM");
    onAirBadge.classList.toggle("audio", state==="AUDIO");
    onAirBadge.classList.toggle("offline", state==="OFFLINE");
  }

  if (programLocked){
    if (onAirStatusLbl) onAirStatusLbl.textContent = "PROGRAM";
    syncDebugState();
    return;
  }

  if (onAirStatusLbl) onAirStatusLbl.textContent = "RETURN";

  // Default: show host return feed (original behavior)
  onAirScreen.querySelectorAll("div._ph").forEach(n=>n.remove());

  const holderId = "__onair_holder";
  let holder = onAirScreen.querySelector("#"+holderId);
  if (!holder){
    holder = document.createElement("div");
    holder.id = holderId;
    holder.style.position="absolute";
    holder.style.inset="0";
    onAirScreen.appendChild(holder);
  }
  holder.innerHTML = "";

  if (hostVideoTrack){
    mountVideoInto(holder, hostVideoTrack, true);
    return;
  }

  const ph = document.createElement("div");
  ph.className="_ph";
  ph.style.cssText="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.28);font-weight:900;letter-spacing:.14em;text-transform:uppercase;";
  ph.textContent = (state === "AUDIO") ? "AUDIO" : "OFFLINE";
  onAirScreen.appendChild(ph);
  syncDebugState();
}

/* Load + build */
loadGuides();
guidesOn = !!guidesState.enabled;
if (btnGuides) btnGuides.setAttribute('aria-pressed', guidesOn ? 'true':'false');

loadMonitor();
applyMonitorUi();
loadMediaSettings();
loadMediaLibrary();
loadUiState();
renderMediaThumbs();
updateMediaButtons();

loadFx();
buildFxThumbs();
applyUiState();
loadStream();
syncDebugState();
syncStreamGlobals({}, null);
updateOnAirPanel();
updatePreviewPanel();
if (programLocked && programSnapshot){
  try{
    const root = ensureProgramRoot();
    let stage = root.querySelector('.programStage');
    if (!stage){
      stage = document.createElement('div');
      stage.className = 'programStage';
      stage.style.position='absolute';
      stage.style.inset='0';
      root.appendChild(stage);
    }
    renderSnapshotInto(stage, {
      source: deepClone(programSnapshot.source),
      virtualId: programSnapshot.virtualId,
      virtualSettings: deepClone(programSnapshot.virtualSettings || {}),
      overlaysEnabled: deepClone(fxState.overlays.enabled),
      overlaysSettings: deepClone(fxState.overlays.settings)
    }, false, false);
  }catch(err){ console.warn('restore program snapshot failed', err); }
}

/* ---------------------------
   Start
--------------------------- */
join();


/* V10 BUILD MARKER — 2026-03-07 | Overlay Composer addon moved to external module */
</script>
<?php include __DIR__ . '/assets/templates/media-studio-shell.php'; ?>
<?php include __DIR__ . '/assets/templates/virtual-studio-shell.php'; ?>
<?php include __DIR__ . '/assets/templates/transition-studio-shell.php'; ?>
<script src="assets/js/feature-modal-core.js"></script>
<script src="assets/js/modules/media-studio.js"></script>
<script src="assets/js/modules/virtual-studio.js"></script>
<script src="assets/js/modules/transition-studio.js"></script>
<script src="assets/js/modules/overlay-composer.js"></script>

<?php page_footer(); ?>
<!-- =========================================================
     STREAM UI CLEAN v2 (override layer)
     - Clean modal layout, collapsible Advanced
     - Fix streamList null
     - Use ?action=... calls for stream_api.php (avoids /status 404 on shared hosting)
     - Normalize media buttons & meter spacing tweaks
========================================================= -->


<!-- STREAM UI V3 (user-friendly, hides URL/KEY) -->
<div id="streamModalV3" aria-hidden="true">
  <div class="box" role="dialog" aria-modal="true">
    <div class="hdr">
      <div>
        <div class="title">STREAM</div>
        <div class="meta" id="stv3Meta">ROOM —</div>
      </div>
      <div style="display:flex;gap:10px;align-items:center;">
        <button class="close" id="stv3Close">Close</button>
      </div>
    </div>

    <div class="body">
      <div class="stv3Row">
        <div class="stv3Card">
          <div class="stv3Head">
            <h3>Destinations</h3>
            <div class="stv3HeadRight">
              <button id="stv3Refresh" class="miniBtn">Refresh</button>
              <button id="stv3Add" class="miniBtn">+ Add</button>
            </div>
          </div>

          <div class="stv3List" id="stv3List"></div>

          <div class="stv3Foot">
            <button id="stv3StopAll" class="miniBtn">Stop All</button>
            <div style="flex:1"></div>
            <div style="font-size:12px; opacity:.65;">STREAM UI V3</div>
          </div>
        </div>

        <div class="stv3Card">
          <div class="stv3Head">
            <h3>Connection</h3>
          </div>

          <div class="stv3Form">
            <div class="stv3Field">
              <label>Endpoint</label>
              <input id="stv3Endpoint" placeholder="https://studio.itahukamedia.com/stream_api.php" />
            </div>
            <div class="stv3Field">
              <label>Bearer Token (optional)</label>
              <input id="stv3Token" placeholder="(optional)" />
            </div>
            <div class="stv3Field" style="grid-column:1 / -1">
              <label>Input URL (PROGRAM)</label>
              <input id="stv3Input" placeholder="rtmp://mtx.itahukamedia.com:1935" />
            </div>

            <details class="stv3Adv">
              <summary>Advanced</summary>
              <div class="stv3Form" style="margin-top:10px;">
                <div class="stv3Field">
                  <label>Status poll (ms)</label>
                  <input id="stv3Poll" type="number" min="0" step="100" />
                </div>
                <div class="stv3Field">
                  <label>Timeout (ms)</label>
                  <input id="stv3Timeout" type="number" min="1000" step="500" />
                </div>
              </div>
            </details>

            <div class="stv3Actions">
              <button id="stv3Save" class="miniBtn">Save</button>
              <button id="stv3TestApi" class="miniBtn">Test API</button>
            </div>
          </div>

          <div class="stv3LogWrap">
            <div class="stv3Head" style="margin:0 0 8px 0;">
              <h3>Activity</h3>
              <div class="stv3HeadRight">
                <button id="stv3Copy" class="miniBtn">Copy</button>
                <button id="stv3Clear" class="miniBtn">Clear</button>
              </div>
            </div>
            <pre class="stv3Log" id="stv3Log"></pre>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

<div id="guidesModal" class="cpModal" hidden aria-hidden="true">
  <div class="cpModalCard guidesCard" role="dialog" aria-modal="true" aria-labelledby="guidesModalTitle">
    <div class="cpModalHead">
      <div class="cpModalTitle" id="guidesModalTitle">Preview Guides</div>
      <button id="btnCloseGuidesModal" class="iconBtn" type="button" title="Close Guides Settings">
        <svg viewBox="0 0 24 24"><path d="M18.3 5.71 12 12l6.3 6.29-1.41 1.42L10.59 13.4 4.29 19.71 2.88 18.3 9.17 12 2.88 5.71 4.29 4.29l6.3 6.3 6.3-6.3z"/></svg>
      </button>
    </div>
    <div class="cpModalBody">
      <div class="fxRow"><label for="guidesStandard">Aspect preset</label><select id="guidesStandard"><option value="16:9">16:9 Broadcast</option><option value="9:16">9:16 Vertical</option><option value="1:1">1:1 Square</option><option value="4:3">4:3 Classic</option></select></div>
      <div class="fxRow"><label for="guidesSafe">Safe area</label><input id="guidesSafe" type="range" min="0" max="0.20" step="0.005"></div>
      <div class="fxRow"><label for="guidesGrid">Grid (10×10)</label><input id="guidesGrid" type="checkbox"></div>
      <div class="fxRow"><label for="guidesRuler">Ruler ticks</label><input id="guidesRuler" type="checkbox"></div>
      <div class="fxRow"><label for="guidesPixels">Pixel overlay</label><input id="guidesPixels" type="checkbox"></div>
      <div class="fxRow"><label for="guidesPxw">Resolution width</label><input id="guidesPxw" type="number" min="1"></div>
      <div class="fxRow"><label for="guidesPxh">Resolution height</label><input id="guidesPxh" type="number" min="1"></div>
      <div class="fxRow"><label for="guidesColor">Guide color</label><input id="guidesColor" type="color"></div>
      <div class="fxRow"><label for="guidesOpacity">Guide opacity</label><input id="guidesOpacity" type="range" min="0.10" max="1.00" step="0.05"></div>
      <div class="fxRow"><label for="guidesThickness">Line thickness</label><input id="guidesThickness" type="range" min="1" max="4" step="1"></div>
    </div>
    <div class="cpModalFoot">
      <button id="btnGuidesReset" class="miniBtn" type="button">Reset</button>
      <button id="btnGuidesSave" class="miniBtn" type="button">Save</button>
    </div>
  </div>
</div>

<script>
(function(){
  function q(id){ return document.getElementById(id); }
  function show(id){ var el = typeof id === 'string' ? q(id) : id; if(!el) return false; if(window.__modalShow) return window.__modalShow(el); el.hidden=false; el.setAttribute('aria-hidden','false'); el.classList.add('show'); return true; }
  function hide(id){ var el = typeof id === 'string' ? q(id) : id; if(!el) return false; if(window.__modalHide) return window.__modalHide(el); el.classList.remove('show'); el.setAttribute('aria-hidden','true'); el.hidden=true; return true; }
  function bindBackdrop(id){ var el=q(id); if(!el || el.__lateBound) return; el.__lateBound=true; el.addEventListener('click', function(ev){ if(ev.target===el) hide(el); }); }
  function boot(){
    ['guidesModal','fxModal','overlayComposerModal','skinOverlay','mediaModal','streamModal','streamModalV3','featureStudioModal'].forEach(bindBackdrop);

    var btnGs = q('btnGuidesSettings');
    var btnOpenMediaStudio = q('btnOpenMediaStudio');
    if(btnOpenMediaStudio && !btnOpenMediaStudio.__bound && typeof window.openMediaStudioLauncher === 'function'){ btnOpenMediaStudio.__bound=true; btnOpenMediaStudio.addEventListener('click', window.openMediaStudioLauncher, true); }
    var btnOpenVirtualStudio = q('btnOpenVirtualStudio');
    if(btnOpenVirtualStudio && !btnOpenVirtualStudio.__bound && typeof window.openVirtualStudioLauncher === 'function'){ btnOpenVirtualStudio.__bound=true; btnOpenVirtualStudio.addEventListener('click', window.openVirtualStudioLauncher, true); }
    var btnOpenTransitionStudio = q('btnOpenTransitionStudio');
    if(btnOpenTransitionStudio && !btnOpenTransitionStudio.__bound && typeof window.openTransitionStudioLauncher === 'function'){ btnOpenTransitionStudio.__bound=true; btnOpenTransitionStudio.addEventListener('click', window.openTransitionStudioLauncher, true); }

    // Guides settings uses the primary openGuidesSettings() binding above.
    // Keep late-boot cleanup only; do not add a second capture-phase opener here.
    var btnGc = q('btnCloseGuidesModal');
    if(btnGc && !btnGc.__lateCloseBound){ btnGc.__lateCloseBound=true; btnGc.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); hide('guidesModal'); }, true); }
    var btnGsav = q('btnGuidesSave');
    if(btnGsav && !btnGsav.__lateSaveBound){ btnGsav.__lateSaveBound=true; btnGsav.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); if (typeof window.saveGuidesFromPanel === 'function') window.saveGuidesFromPanel(); else hide('guidesModal'); }, true); }
    var btnGres = q('btnGuidesReset');
    if(btnGres && !btnGres.__lateResetBound){ btnGres.__lateResetBound=true; btnGres.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); if (typeof window.resetGuidesPanel === 'function') window.resetGuidesPanel(); }, true); }

    var fx=q('fxModal');
    var fxClose=q('fxClose');
    if(fxClose && !fxClose.__lateBound){ fxClose.__lateBound=true; fxClose.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); hide(fx); }, true); }

    // Reliable delegated FX settings open path: shift+click, right click, double click on any thumb
    var root=document;
    if(!root.__fxDelegatedBound){
      root.__fxDelegatedBound=true;
      function resolveFxThumb(target){
        var el = target && target.closest ? target.closest('.fxThumb,.thumb.fxThumb,[data-composer-scene],[data-composer-launcher]') : null;
        if(!el) return null;
        if(el.closest && el.closest('#fxTransitions')) return {kind:'transition', el:el};
        if(el.closest && (el.closest('#fxOverlays') || el.closest('#fxOverlayComposerThumbs'))) return {kind:'overlay', el:el};
        if(el.closest && el.closest('#fxVirtuals')) return {kind:'virtual', el:el};
        return null;
      }
      function inferInfo(info){
        var el=info.el;
        var id = el.getAttribute('data-id') || el.dataset.id || el.dataset.fxId || el.dataset.sceneId || '';
        var nameEl = el.querySelector('.name,b,.thumbName,.thumbMeta b');
        var name = nameEl ? nameEl.textContent.trim() : (el.getAttribute('title') || id || info.kind);
        if(!id && el.getAttribute('data-composer-scene')==='1') id = (window.fxState&&window.fxState.overlays&&window.fxState.overlays.settings&&window.fxState.overlays.settings.composer&&window.fxState.overlays.settings.composer.sceneId) || 'composer';
        return {kind: info.kind, id: id || name || info.kind, name: name || id || info.kind};
      }
      root.addEventListener('contextmenu', function(ev){
        var info=resolveFxThumb(ev.target); if(!info) return; ev.preventDefault(); ev.stopPropagation(); var x=inferInfo(info); if(typeof window.openFxSettings==='function') window.openFxSettings(x.kind,x.id,x.name);
      }, true);
      root.addEventListener('dblclick', function(ev){
        var info=resolveFxThumb(ev.target); if(!info) return; ev.preventDefault(); ev.stopPropagation(); var x=inferInfo(info); if(typeof window.openFxSettings==='function') window.openFxSettings(x.kind,x.id,x.name);
      }, true);
      root.addEventListener('click', function(ev){
        if(!ev.shiftKey) return; var info=resolveFxThumb(ev.target); if(!info) return; ev.preventDefault(); ev.stopPropagation(); var x=inferInfo(info); if(typeof window.openFxSettings==='function') window.openFxSettings(x.kind,x.id,x.name);
      }, true);
    }

    // Overlay composer direct open path through same engine
    var oc=q('overlayComposerModal');
    if(oc){
      window.openOverlayComposer = function(sceneId){
        try {
          if (typeof window.__ocOpenComposerInner === 'function') window.__ocOpenComposerInner(sceneId);
        } catch(_){}
        show(oc);
        return false;
      };
      var close = oc.querySelector('.ocClose,[data-close="overlay-composer"],.close,button[aria-label="Close"]');
      if(close && !close.__lateBound){ close.__lateBound=true; close.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); hide(oc); }, true); }
    }

    document.addEventListener('keydown', function(ev){
      if(ev.key!=='Escape') return;
      ['overlayComposerModal','guidesModal','fxModal','mediaModal','streamModal','streamModalV3','skinOverlay','featureStudioModal'].forEach(hide);
    }, true);
  }
  if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, {once:true}); else boot();
})();

(function(){
  function dedupeGuidesSettingsButton(){
    var nodes = document.querySelectorAll('#btnGuidesSettings');
    if (!nodes || nodes.length <= 1) return;
    for (var i = 1; i < nodes.length; i++) {
      try { nodes[i].remove(); } catch(_){}
    }
  }
  if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', dedupeGuidesSettingsButton, {once:true});
  else dedupeGuidesSettingsButton();
})();
</script>


<div id="featureStudioModal" class="featureStudioModal" hidden aria-hidden="true">
  <div class="featureStudioModal__backdrop" data-close-feature-studio="1"></div>
  <div class="featureStudioModal__panel" role="dialog" aria-modal="true" aria-labelledby="featureStudioTitle">
    <div class="featureStudioModal__head">
      <div>
        <div class="featureStudioModal__eyebrow">Studio</div>
        <h3 id="featureStudioTitle" class="featureStudioModal__title">Studio</h3>
      </div>
      <div class="featureStudioModal__actions">
        <button type="button" class="miniBtn" id="btnFeatureStudioReturn">Return to Tab</button>
        <button type="button" class="iconBtn" id="btnFeatureStudioClose" aria-label="Close Studio">
          <svg viewBox="0 0 24 24"><path d="M18.3 5.71 12 12l6.3 6.29-1.41 1.42L10.59 13.4 4.29 19.71 2.88 18.3 9.17 12 2.88 5.71 4.29 4.29l6.3 6.3 6.3-6.3z"/></svg>
        </button>
      </div>
    </div>
    <div class="featureStudioModal__body" id="featureStudioMount"></div>
  </div>
</div>

<?php
$__cp_js_path = __DIR__ . '/assets/js/modules/stream.module.js';
$__cp_js = '';
if (is_file($__cp_js_path) && is_readable($__cp_js_path)) {
  $__cp_js = file_get_contents($__cp_js_path);
}
?>
<script>

function __cpForceModalOpen(id){
  var el = document.getElementById(id);
  if(!el) return false;
  el.hidden = false;
  el.removeAttribute('hidden');
  el.setAttribute('aria-hidden','false');
  el.classList.add('show','is-open');
  el.style.display = 'flex';
  el.style.visibility = 'visible';
  el.style.opacity = '1';
  el.style.pointerEvents = 'auto';
  document.documentElement.classList.add('appModalNoScroll');
  document.body.classList.add('appModalNoScroll');
  return true;
}

function __cpForceModalClose(id){
  var el = document.getElementById(id);
  if(!el) return false;
  el.classList.remove('show','is-open');
  el.setAttribute('aria-hidden','true');
  el.hidden = true;
  el.style.display = 'none';
  el.style.visibility = 'hidden';
  el.style.opacity = '0';
  el.style.pointerEvents = 'none';

  var open = document.querySelectorAll(
    '.cpModal.show,.overlay.show,.lkModal.show,#streamModalV3.show,.featureStudioModal.show,#cpOverlayHardModal.show,#overlayComposerModal.show'
  );
  if(!open.length){
    document.documentElement.classList.remove('appModalNoScroll');
    document.body.classList.remove('appModalNoScroll');
  }
  return true;
}

</script>
<script id="__COPRODUCER_JS__">
<?php echo $__cp_js; ?>
</script>
