Your IP : 216.73.216.79


Current Path : /var/www/html/unity/development/interactive-videoplayer/
Upload File :
Current File : /var/www/html/unity/development/interactive-videoplayer/admin.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Video Studio — Author</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
  --bg-deep: #0a0a0f;
  --bg-surface: #12121a;
  --bg-card: #1a1a26;
  --bg-elevated: #22222e;
  --bg-hover: #2a2a38;
  --border-dim: #2a2a3a;
  --border-active: #4a4a6a;
  --text-primary: #e8e8f0;
  --text-secondary: #8888a0;
  --text-dim: #5a5a72;
  --accent-blue: #4a9eff;
  --accent-blue-dim: rgba(74,158,255,0.15);
  --accent-green: #3ddc84;
  --accent-green-dim: rgba(61,220,132,0.15);
  --accent-orange: #ff8a50;
  --accent-orange-dim: rgba(255,138,80,0.15);
  --accent-purple: #b47aff;
  --accent-purple-dim: rgba(180,122,255,0.15);
  --accent-red: #ff5a5a;
  --accent-red-dim: rgba(255,90,90,0.15);
  --accent-yellow: #ffd75a;
  --font-body: 'DM Sans', sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --shadow-float: 0 8px 32px rgba(0,0,0,0.5);
  --transition-fast: 0.15s ease;
  --transition-med: 0.25s ease;
}
* { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; overflow:hidden; background:var(--bg-deep); color:var(--text-primary); font-family:var(--font-body); font-size:14px; user-select:none; }

.app-layout { display:grid; grid-template-columns:260px 1fr 300px; grid-template-rows:48px 1fr 200px; height:100vh; }
.app-topbar { grid-column:1/-1; background:var(--bg-surface); border-bottom:1px solid var(--border-dim); display:flex; align-items:center; padding:0 16px; gap:12px; z-index:100; }
.app-sidebar-left { grid-row:2/4; background:var(--bg-surface); border-right:1px solid var(--border-dim); display:flex; flex-direction:column; overflow:hidden; }
.app-canvas { grid-row:2/3; background:var(--bg-deep); position:relative; overflow:hidden; }
.app-sidebar-right { grid-row:2/4; background:var(--bg-surface); border-left:1px solid var(--border-dim); overflow-y:auto; }
.app-timeline { grid-row:3/4; background:var(--bg-surface); border-top:1px solid var(--border-dim); display:flex; flex-direction:column; overflow:hidden; }

::-webkit-scrollbar { width:5px; height:5px; }
::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:var(--border-dim); border-radius:3px; }

.app-logo { font-family:var(--font-mono); font-weight:600; font-size:15px; color:var(--accent-blue); letter-spacing:-0.5px; white-space:nowrap; }
.app-logo span { color:var(--text-dim); font-weight:400; }
.topbar-divider { width:1px; height:24px; background:var(--border-dim); flex-shrink:0; }
.topbar-btn { background:none; border:1px solid var(--border-dim); color:var(--text-secondary); border-radius:var(--radius-sm); padding:6px 12px; font-size:12px; font-family:var(--font-body); cursor:pointer; transition:all var(--transition-fast); display:flex; align-items:center; gap:6px; white-space:nowrap; }
.topbar-btn:hover { background:var(--bg-hover); color:var(--text-primary); border-color:var(--border-active); }
.topbar-btn.primary { background:var(--accent-blue); border-color:var(--accent-blue); color:#fff; }
.topbar-btn.primary:hover { background:#3a8eef; }
.topbar-spacer { flex:1; }
.project-name { background:none; border:none; color:var(--text-primary); font-family:var(--font-body); font-size:14px; font-weight:500; padding:4px 8px; border-radius:var(--radius-sm); outline:none; width:180px; }
.project-name:focus { background:var(--bg-elevated); box-shadow:0 0 0 1px var(--accent-blue); }

.panel-header { padding:12px 14px 8px; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:1px; color:var(--text-dim); display:flex; align-items:center; justify-content:space-between; flex-shrink:0; }
.panel-header .badge { background:var(--accent-blue); color:#fff; font-size:9px; padding:1px 6px; border-radius:10px; font-weight:600; }
.panel-content { padding:0 12px 12px; }

.lib-item {
  display:flex; align-items:center; gap:10px; padding:8px 12px; cursor:grab;
  border-bottom:1px solid var(--border-dim); transition:background var(--transition-fast);
}
.lib-item:hover { background:var(--bg-hover); }
.lib-item:active { cursor:grabbing; }
.lib-item .thumb { width:44px; height:30px; border-radius:4px; background:var(--bg-elevated); display:flex; align-items:center; justify-content:center; color:var(--text-dim); font-size:12px; overflow:hidden; flex-shrink:0; }
.lib-item .thumb video { width:100%; height:100%; object-fit:cover; }
.lib-item .info { flex:1; min-width:0; }
.lib-item .info .name { font-size:12px; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.lib-item .info .meta { font-size:10px; color:var(--text-dim); }
.lib-item .actions button { background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:13px; padding:2px; }
.lib-item .actions button:hover { color:var(--accent-red); }

.add-zone { border:2px dashed var(--border-dim); border-radius:var(--radius-md); padding:16px; text-align:center; cursor:pointer; transition:all var(--transition-med); margin:8px 12px; flex-shrink:0; }
.add-zone:hover { border-color:var(--accent-blue); background:var(--accent-blue-dim); }
.add-zone i { font-size:20px; color:var(--text-dim); display:block; margin-bottom:4px; }
.add-zone span { font-size:11px; color:var(--text-secondary); }
.lib-list { flex:1; overflow-y:auto; }
.lib-hint { font-size:11px; color:var(--text-dim); padding:8px 14px; line-height:1.5; border-top:1px solid var(--border-dim); flex-shrink:0; }
.lib-hint i { color:var(--accent-blue); }

.drag-ghost {
  position:fixed; pointer-events:none; z-index:9999;
  background:var(--bg-card); border:2px solid var(--accent-blue); border-radius:var(--radius-md);
  padding:8px 14px; font-size:12px; font-weight:600; color:var(--accent-blue);
  box-shadow:var(--shadow-float); opacity:0.9; display:none;
}

.canvas-drop { position:absolute; inset:0; border:3px dashed var(--accent-blue); border-radius:12px; background:rgba(74,158,255,0.05); z-index:1; pointer-events:none; display:none; }
.canvas-grid { position:absolute; inset:0; pointer-events:none; opacity:0.03; background-image:radial-gradient(circle,var(--text-primary) 1px,transparent 1px); background-size:24px 24px; }
.canvas-controls { position:absolute; bottom:12px; left:50%; transform:translateX(-50%); display:flex; gap:4px; background:var(--bg-card); border:1px solid var(--border-dim); border-radius:var(--radius-md); padding:4px; z-index:20; }
.canvas-controls button { background:none; border:none; color:var(--text-secondary); cursor:pointer; width:30px; height:30px; border-radius:var(--radius-sm); display:flex; align-items:center; justify-content:center; font-size:15px; transition:all var(--transition-fast); }
.canvas-controls button:hover { background:var(--bg-hover); color:var(--text-primary); }
.canvas-zoom { font-size:10px; color:var(--text-dim); display:flex; align-items:center; padding:0 6px; font-family:var(--font-mono); }

.canvas-empty { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; color:var(--text-dim); gap:10px; pointer-events:none; z-index:0; }
.canvas-empty i { font-size:40px; opacity:0.2; }
.canvas-empty .title { font-size:15px; font-weight:600; color:var(--text-secondary); }
.canvas-empty .desc { font-size:12px; text-align:center; line-height:1.5; max-width:260px; }

.conn-svg { position:absolute; inset:0; pointer-events:none; z-index:15; overflow:visible; }
.conn-svg path { fill:none; stroke-width:2.5; pointer-events:stroke; cursor:pointer; }
.conn-svg path.hit-area { stroke-width:16; stroke:transparent; pointer-events:stroke; cursor:pointer; }

/* ====== VIDEO NODE ====== */
.vnode {
  position:absolute; width:220px; background:var(--bg-card); border:2px solid var(--border-dim);
  border-radius:var(--radius-lg); z-index:5; transition:box-shadow var(--transition-fast), border-color var(--transition-fast);
}
.vnode:hover { border-color:var(--border-active); }
.vnode.selected { border-color:var(--accent-blue); box-shadow:0 0 0 3px var(--accent-blue-dim), 0 4px 20px rgba(0,0,0,0.4); z-index:10; }
.vnode.start-node { border-color:var(--accent-green); }
.vnode.start-node .vnode-badge { background:var(--accent-green); }

.vnode-preview {
  width:100%; height:90px; background:var(--bg-elevated); border-radius:12px 12px 0 0;
  overflow:hidden; position:relative; display:flex; align-items:center; justify-content:center;
  cursor:grab;
}
.vnode-preview:active { cursor:grabbing; }
.vnode-preview video { width:100%; height:100%; object-fit:cover; pointer-events:none; }
.vnode-preview .preview-placeholder { font-size:24px; color:var(--text-dim); opacity:0.4; }

.vnode-header {
  display:flex; align-items:center; gap:8px; padding:8px 10px;
  border-bottom:1px solid var(--border-dim); cursor:grab; position:relative;
}
.vnode-header:active { cursor:grabbing; }
.vnode-badge { background:var(--accent-blue); color:#fff; font-size:8px; font-weight:700; padding:2px 6px; border-radius:8px; text-transform:uppercase; letter-spacing:0.5px; flex-shrink:0; }
.vnode-name { flex:1; font-size:12px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.vnode-dur { font-size:10px; color:var(--text-dim); font-family:var(--font-mono); flex-shrink:0; }

.vnode-ports-row {
  display:flex; align-items:center; justify-content:space-between; padding:6px 10px;
  border-bottom:1px solid var(--border-dim); min-height:30px;
}
.port {
  width:14px; height:14px; border-radius:50%; border:2px solid var(--border-active);
  background:var(--bg-card); cursor:crosshair; transition:all var(--transition-fast); flex-shrink:0;
}
.port:hover { transform:scale(1.3); }
.port.port-in { border-color:var(--accent-blue); }
.port.port-in:hover { background:var(--accent-blue); }
.port.port-out { border-color:var(--accent-green); }
.port.port-out:hover { background:var(--accent-green); }
.port.port-out.connected { background:var(--accent-green); }
.port-label { font-size:9px; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.5px; }
.port-row-center { flex:1; text-align:center; font-size:10px; color:var(--text-dim); }

.vnode-interactions { padding:0; }
.vnode-int-item {
  display:flex; align-items:center; gap:6px; padding:5px 10px;
  border-bottom:1px solid rgba(42,42,58,0.5); font-size:11px; transition:background var(--transition-fast);
  cursor:pointer; position:relative;
}
.vnode-int-item:hover { background:var(--bg-hover); }
.vnode-int-item.selected { background:var(--accent-blue-dim); }
.int-icon { width:18px; height:18px; border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:10px; flex-shrink:0; }
.int-icon.hotspot { background:var(--accent-orange-dim); color:var(--accent-orange); }
.int-icon.choice { background:var(--accent-green-dim); color:var(--accent-green); }
.int-icon.button { background:var(--accent-blue-dim); color:var(--accent-blue); }
.int-icon.overlay { background:var(--accent-purple-dim); color:var(--accent-purple); }
.int-icon.timer { background:rgba(255,215,90,0.15); color:var(--accent-yellow); }
.vnode-int-label { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.vnode-int-time { font-size:9px; color:var(--text-dim); font-family:var(--font-mono); white-space:nowrap; flex-shrink:0; }
.vnode-int-item .port { width:12px; height:12px; }
.del-int { background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:11px; padding:0 2px; opacity:0; transition:opacity var(--transition-fast); }
.vnode-int-item:hover .del-int { opacity:1; }
.del-int:hover { color:var(--accent-red); }

.vnode-add-int {
  padding:6px 10px; text-align:center; font-size:10px; color:var(--text-dim);
  cursor:pointer; transition:all var(--transition-fast); border-radius:0 0 12px 12px;
}
.vnode-add-int:hover { background:var(--bg-hover); color:var(--accent-blue); }

/* Context menu */
.ctx-menu { position:fixed; background:var(--bg-card); border:1px solid var(--border-dim); border-radius:var(--radius-md); padding:4px; z-index:500; min-width:160px; box-shadow:var(--shadow-float); display:none; }
.ctx-menu.show { display:block; }
.ctx-item { padding:7px 12px; font-size:12px; cursor:pointer; border-radius:var(--radius-sm); display:flex; align-items:center; gap:8px; transition:background var(--transition-fast); }
.ctx-item:hover { background:var(--bg-hover); }
.ctx-item i { width:16px; text-align:center; color:var(--text-dim); }
.ctx-sep { height:1px; background:var(--border-dim); margin:4px 0; }
.ctx-item.danger { color:var(--accent-red); }
.ctx-item.danger i { color:var(--accent-red); }

/* Properties */
.props-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--text-dim); gap:10px; padding:20px; }
.props-empty i { font-size:28px; opacity:0.3; }
.props-empty .title { font-size:13px; font-weight:600; color:var(--text-secondary); }
.prop-group { margin-bottom:14px; }
.prop-group-title { font-size:10px; font-weight:600; text-transform:uppercase; letter-spacing:0.8px; color:var(--text-dim); margin-bottom:6px; }
.prop-field { margin-bottom:8px; }
.prop-field label { display:block; font-size:11px; color:var(--text-secondary); margin-bottom:3px; font-weight:500; }
.prop-field input, .prop-field select, .prop-field textarea {
  width:100%; background:var(--bg-elevated); border:1px solid var(--border-dim);
  border-radius:var(--radius-sm); padding:6px 8px; color:var(--text-primary);
  font-family:var(--font-body); font-size:12px; outline:none; transition:border-color var(--transition-fast);
}
.prop-field input:focus, .prop-field select:focus { border-color:var(--accent-blue); }
.prop-check { display:flex; align-items:center; gap:6px; cursor:pointer; font-size:12px; }
.prop-check input { width:14px; height:14px; accent-color:var(--accent-blue); }
.prop-btn { width:100%; padding:7px; border-radius:var(--radius-sm); border:1px solid var(--border-dim); background:var(--bg-card); color:var(--text-secondary); font-family:var(--font-body); font-size:11px; cursor:pointer; transition:all var(--transition-fast); display:flex; align-items:center; justify-content:center; gap:4px; }
.prop-btn:hover { background:var(--bg-hover); color:var(--text-primary); border-color:var(--border-active); }
.prop-btn.danger { border-color:var(--accent-red); color:var(--accent-red); }
.prop-btn.danger:hover { background:var(--accent-red-dim); }

/* Timeline */
.tl-header { display:flex; align-items:center; gap:10px; padding:8px 14px; border-bottom:1px solid var(--border-dim); flex-shrink:0; }
.tl-title { font-size:12px; font-weight:600; color:var(--text-secondary); flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.tl-body { flex:1; overflow:auto; position:relative; }
.tl-empty { display:flex; align-items:center; justify-content:center; height:100%; font-size:12px; color:var(--text-dim); }
.tl-ruler { height:22px; background:var(--bg-card); border-bottom:1px solid var(--border-dim); position:sticky; top:0; z-index:3; }
.tl-tick { position:absolute; top:0; height:100%; border-left:1px solid var(--border-dim); }
.tl-tick-label { position:absolute; top:4px; left:4px; font-size:8px; color:var(--text-dim); font-family:var(--font-mono); white-space:nowrap; }
.tl-tracks { position:relative; padding:4px 0; }
.tl-track { height:28px; position:relative; margin-bottom:2px; }

.tl-bar {
  position:absolute; height:22px; top:3px; border-radius:4px; cursor:grab;
  display:flex; align-items:center; padding:0 8px; font-size:9px; font-weight:600;
  overflow:hidden; white-space:nowrap; min-width:20px;
  transition:box-shadow var(--transition-fast);
}
.tl-bar:hover { box-shadow:0 0 0 2px rgba(255,255,255,0.2); }
.tl-bar.selected { box-shadow:0 0 0 2px var(--accent-blue); }
.tl-bar.type-hotspot { background:var(--accent-orange-dim); border:1px solid rgba(255,138,80,0.5); color:var(--accent-orange); }
.tl-bar.type-button { background:var(--accent-blue-dim); border:1px solid rgba(74,158,255,0.5); color:var(--accent-blue); }
.tl-bar.type-choice { background:var(--accent-green-dim); border:1px solid rgba(61,220,132,0.5); color:var(--accent-green); }
.tl-bar.type-overlay { background:var(--accent-purple-dim); border:1px solid rgba(180,122,255,0.5); color:var(--accent-purple); }
.tl-bar.type-timer { background:rgba(255,215,90,0.15); border:1px solid rgba(255,215,90,0.5); color:var(--accent-yellow); }

.tl-handle { position:absolute; top:0; bottom:0; width:7px; cursor:col-resize; z-index:2; }
.tl-handle-l { left:0; border-radius:4px 0 0 4px; }
.tl-handle-r { right:0; border-radius:0 4px 4px 0; }
.tl-handle:hover { background:rgba(255,255,255,0.15); }

/* Toast */
.toast-box { position:fixed; top:56px; right:16px; z-index:2000; display:flex; flex-direction:column; gap:6px; }
.toast-msg { background:var(--bg-card); border:1px solid var(--border-dim); border-radius:var(--radius-md); padding:8px 14px; font-size:12px; display:flex; align-items:center; gap:6px; box-shadow:var(--shadow-float); animation:toastIn 0.3s ease; }
.toast-msg.ok { border-color:var(--accent-green); }
.toast-msg.ok i { color:var(--accent-green); }
.toast-msg.err { border-color:var(--accent-red); }
.toast-msg.err i { color:var(--accent-red); }
@keyframes toastIn { from{opacity:0;transform:translateX(20px)} to{opacity:1;transform:translateX(0)} }

/* Modal */
.modal-bg { position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:1000; display:none; align-items:center; justify-content:center; backdrop-filter:blur(4px); }
.modal-bg.show { display:flex; }
.modal-box { background:var(--bg-card); border:1px solid var(--border-dim); border-radius:var(--radius-lg); width:480px; max-width:92vw; max-height:80vh; overflow-y:auto; box-shadow:var(--shadow-float); }
.modal-head { padding:14px 18px; border-bottom:1px solid var(--border-dim); display:flex; align-items:center; justify-content:space-between; }
.modal-head h3 { font-size:14px; font-weight:600; }
.modal-head button { background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:16px; }
.modal-body { padding:16px 18px; }
.modal-foot { padding:10px 18px; border-top:1px solid var(--border-dim); display:flex; justify-content:flex-end; gap:8px; }

.tab-bar { display:flex; border-bottom:1px solid var(--border-dim); flex-shrink:0; }
.tab-bar .tab { padding:8px 14px; font-size:11px; font-weight:500; color:var(--text-dim); cursor:pointer; border-bottom:2px solid transparent; transition:all var(--transition-fast); }
.tab-bar .tab:hover { color:var(--text-secondary); }
.tab-bar .tab.active { color:var(--accent-blue); border-bottom-color:var(--accent-blue); }
.settings-scroll { padding:12px; overflow-y:auto; flex:1; }

/* LIGHT THEME */
body.light {
  --bg-deep: #f0f1f5;
  --bg-surface: #ffffff;
  --bg-card: #f8f9fb;
  --bg-elevated: #eef0f4;
  --bg-hover: #e4e6ec;
  --border-dim: #d4d6de;
  --border-active: #b0b4c0;
  --text-primary: #1a1a2e;
  --text-secondary: #5a5c72;
  --text-dim: #8a8ca0;
  --accent-blue: #2b7de9;
  --accent-blue-dim: rgba(43,125,233,0.12);
  --accent-green: #2db86a;
  --accent-green-dim: rgba(45,184,106,0.12);
  --accent-orange: #e06830;
  --accent-orange-dim: rgba(224,104,48,0.12);
  --accent-purple: #8a52d6;
  --accent-purple-dim: rgba(138,82,214,0.12);
  --accent-red: #e04040;
  --accent-red-dim: rgba(224,64,64,0.12);
  --accent-yellow: #c8a020;
  --shadow-float: 0 8px 32px rgba(0,0,0,0.12);
}
body.light .canvas-grid { opacity:0.06; }
body.light .vnode { box-shadow:0 2px 8px rgba(0,0,0,0.08); }
body.light .conn-svg path { opacity:0.8; }
</style>
</head>
<body>

<div class="app-layout">
  <div class="app-topbar">
    <div class="app-logo">IVS<span>.studio</span></div>
    <div class="topbar-divider"></div>
    <input type="text" class="project-name" id="projectName" value="Untitled Project" spellcheck="false">
    <div class="topbar-divider"></div>
    <button class="topbar-btn" onclick="doUndo()" title="Ctrl+Z"><i class="bi bi-arrow-counterclockwise"></i></button>
    <button class="topbar-btn" onclick="doRedo()" title="Ctrl+Y"><i class="bi bi-arrow-clockwise"></i></button>
    <div class="topbar-spacer"></div>
    <button class="topbar-btn" onclick="previewProject()"><i class="bi bi-play-circle"></i> Preview</button>
    <button class="topbar-btn" onclick="showImportModal()"><i class="bi bi-upload"></i> Import</button>
    <button class="topbar-btn primary" onclick="exportProject()"><i class="bi bi-download"></i> Export</button>
    <div class="topbar-divider"></div>
    <button class="topbar-btn" id="themeBtn" onclick="toggleTheme()" title="Toggle light/dark theme"><i class="bi bi-sun" id="themeIcon"></i></button>
  </div>

  <div class="app-sidebar-left">
    <div class="tab-bar">
      <div class="tab active" data-tab="library" onclick="switchTab(this)">Library</div>
      <div class="tab" data-tab="settings" onclick="switchTab(this)">Settings</div>
    </div>
    <div id="tab-library" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
      <div class="panel-header">Videos <span class="badge" id="libCount">0</span></div>
      <div class="add-zone" id="addZone" onclick="document.getElementById('fileInput').click()">
        <i class="bi bi-cloud-arrow-up"></i><span>Drop or click to add videos</span>
      </div>
      <button class="prop-btn" style="margin:0 12px 8px;background:var(--accent-blue-dim);border-color:rgba(74,158,255,0.3);color:var(--accent-blue)" onclick="openServerBrowser()"><i class="bi bi-folder2-open"></i> Browse Server Videos</button>
      <input type="file" id="fileInput" accept="video/*" multiple hidden onchange="addToLib(this.files)">
      <div class="lib-list" id="libList"></div>
      <div class="lib-hint"><i class="bi bi-info-circle"></i> Drag videos from here onto the canvas. Same video can be placed multiple times.</div>
    </div>
    <div id="tab-settings" style="display:none;flex:1;overflow:hidden;">
      <div class="settings-scroll">
        <div class="prop-group">
          <div class="prop-group-title">Player Controls</div>
          <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowCtrl" checked onchange="saveSettings()"> Show controls</label></div>
          <div class="prop-field"><label>Control Mode</label><select id="sCtrlMode" onchange="saveSettings();toggleCtrlUI()">
            <option value="standard">Standard (bar layout)</option>
            <option value="custom">Customized (free-position buttons)</option>
          </select></div>
          <div id="standardCtrlOpts">
            <div class="prop-field"><label>Bar Position</label><select id="sCtrlPos" onchange="saveSettings()">
              <option value="bottom">Bottom</option>
              <option value="top">Top</option>
              <option value="topLeft">Top Left (floating)</option>
              <option value="topRight">Top Right (floating)</option>
              <option value="bottomLeft">Bottom Left (floating)</option>
              <option value="bottomRight">Bottom Right (floating)</option>
            </select></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sAutoHide" checked onchange="saveSettings()"> Auto-hide controls</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sSeek" checked onchange="saveSettings()"> Allow seeking</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sProgress" checked onchange="saveSettings()"> Show progress bar</label></div>
          </div>
          <div id="customCtrlOpts" style="display:none">
            <div style="font-size:10px;color:var(--text-dim);margin-bottom:8px;line-height:1.4">Position each control button freely on screen using the visual editor.</div>
            <button class="prop-btn" style="margin-bottom:8px;background:var(--accent-blue-dim);border-color:rgba(74,158,255,0.3);color:var(--accent-blue)" onclick="openCtrlPosEditor()"><i class="bi bi-bounding-box"></i> Position Controls Visually</button>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowPlay" checked onchange="saveSettings()"> Show Play button</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowPause" checked onchange="saveSettings()"> Show Pause button (toggles with Play)</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowRewind" checked onchange="saveSettings()"> Show Rewind button</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowForward" checked onchange="saveSettings()"> Show Forward button</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowMute" checked onchange="saveSettings()"> Show Mute/Volume button</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowFs" checked onchange="saveSettings()"> Show Fullscreen button</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sShowReplay" checked onchange="saveSettings()"> Show Replay button</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sProgress2" checked onchange="saveSettings()"> Show progress bar</label></div>
            <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sAutoHide2" checked onchange="saveSettings()"> Auto-hide controls</label></div>
          </div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Control Button Icons</div>
          <div style="font-size:10px;color:var(--text-dim);margin-bottom:8px">Upload custom images for each control button. Leave empty to use default icons.</div>
          <div class="prop-field"><label>Play</label><div style="display:flex;gap:4px;align-items:center">
            <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('sPlayFile').click()"><i class="bi bi-upload"></i></button>
            <span id="sPlayPreview" style="font-size:10px;color:var(--text-dim)">Default</span>
            <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="clearIconUpload('play')"><i class="bi bi-x"></i></button>
          </div><input type="file" id="sPlayFile" accept="image/*" hidden onchange="handleIconUpload(this,'play')"><input type="hidden" id="sPlayIcon" value=""></div>
          <div class="prop-field"><label>Pause</label><div style="display:flex;gap:4px;align-items:center">
            <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('sPauseFile').click()"><i class="bi bi-upload"></i></button>
            <span id="sPausePreview" style="font-size:10px;color:var(--text-dim)">Default</span>
            <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="clearIconUpload('pause')"><i class="bi bi-x"></i></button>
          </div><input type="file" id="sPauseFile" accept="image/*" hidden onchange="handleIconUpload(this,'pause')"><input type="hidden" id="sPauseIcon" value=""></div>
          <div class="prop-field"><label>Seek (Rewind/Forward)</label><div style="display:flex;gap:4px;align-items:center">
            <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('sSeekFile').click()"><i class="bi bi-upload"></i></button>
            <span id="sSeekPreview" style="font-size:10px;color:var(--text-dim)">Default</span>
            <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="clearIconUpload('seek')"><i class="bi bi-x"></i></button>
          </div><input type="file" id="sSeekFile" accept="image/*" hidden onchange="handleIconUpload(this,'seek')"><input type="hidden" id="sSeekIcon" value=""></div>
          <div class="prop-field"><label>Mute/Volume</label><div style="display:flex;gap:4px;align-items:center">
            <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('sMuteFile').click()"><i class="bi bi-upload"></i></button>
            <span id="sMutePreview" style="font-size:10px;color:var(--text-dim)">Default</span>
            <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="clearIconUpload('mute')"><i class="bi bi-x"></i></button>
          </div><input type="file" id="sMuteFile" accept="image/*" hidden onchange="handleIconUpload(this,'mute')"><input type="hidden" id="sMuteIcon" value=""></div>
          <div class="prop-field"><label>Fullscreen</label><div style="display:flex;gap:4px;align-items:center">
            <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('sFsFile').click()"><i class="bi bi-upload"></i></button>
            <span id="sFsPreview" style="font-size:10px;color:var(--text-dim)">Default</span>
            <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="clearIconUpload('fs')"><i class="bi bi-x"></i></button>
          </div><input type="file" id="sFsFile" accept="image/*" hidden onchange="handleIconUpload(this,'fs')"><input type="hidden" id="sFsIcon" value=""></div>
          <div class="prop-field"><label>Replay</label><div style="display:flex;gap:4px;align-items:center">
            <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('sReplayFile').click()"><i class="bi bi-upload"></i></button>
            <span id="sReplayPreview" style="font-size:10px;color:var(--text-dim)">Default</span>
            <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="clearIconUpload('replay')"><i class="bi bi-x"></i></button>
          </div><input type="file" id="sReplayFile" accept="image/*" hidden onchange="handleIconUpload(this,'replay')"><input type="hidden" id="sReplayIcon" value=""></div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Overlay Image</div>
          <div style="font-size:10px;color:var(--text-dim);margin-bottom:8px">Image layer between video and controls (e.g. frame, border, vignette)</div>
          <div class="prop-field"><label>Image</label><div style="display:flex;gap:4px;align-items:center">
            <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('sOvImgFile').click()"><i class="bi bi-upload"></i></button>
            <span id="sOvImgPreview" style="font-size:10px;color:var(--text-dim)">None</span>
            <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="clearOverlayImg()"><i class="bi bi-x"></i></button>
          </div><input type="file" id="sOvImgFile" accept="image/*" hidden onchange="handleOverlayImg(this)"><input type="hidden" id="sOvImg" value=""></div>
          <div class="prop-field"><label>Size</label><select id="sOvImgSize" onchange="saveSettings()">
            <option value="cover">Cover (fill)</option>
            <option value="contain">Contain (fit)</option>
            <option value="100% 100%">Stretch</option>
            <option value="auto">Original size</option>
          </select></div>
          <div class="prop-field"><label>Position</label><select id="sOvImgPos" onchange="saveSettings()">
            <option value="center">Center</option>
            <option value="top">Top</option>
            <option value="bottom">Bottom</option>
            <option value="left">Left</option>
            <option value="right">Right</option>
          </select></div>
          <div class="prop-field"><label>Opacity</label><input type="range" id="sOvImgOp" min="0" max="1" step="0.05" value="1" style="width:100%" onchange="saveSettings()"></div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Accent Color</div>
          <div class="prop-field"><input type="color" id="sAccent" value="#4a9eff" onchange="saveSettings()"></div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Playback</div>
          <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sAutoPlay" checked onchange="saveSettings()"> Auto-play on load (uncheck to show play button first)</label></div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">End Behavior</div>
          <div class="prop-field"><select id="sEnd" onchange="saveSettings()">
            <option value="pause">Pause on last frame</option>
            <option value="loop">Loop video</option>
            <option value="restart">Restart from start</option>
          </select></div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Video Display</div>
          <div class="prop-field"><label>Video Fit Mode</label><select id="sVideoFit" onchange="saveSettings()">
            <option value="contain">Fit (contain, show bars)</option>
            <option value="cover">Cover (fill, crop edges)</option>
            <option value="fill">Stretch to fill</option>
          </select></div>
          <div class="prop-field"><label>Video Base Path</label><input type="text" id="sVideoBase" value="./videos/" placeholder="./videos/" onchange="saveSettings()">
            <div style="font-size:9px;color:var(--text-dim);margin-top:2px">Relative path prefix for all video files (e.g. ./videos/ or ./media/clips/)</div>
          </div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Choice Overlay</div>
          <div class="prop-field"><label>Title Text</label><input type="text" id="sChoiceTitle" value="What do you choose?" onchange="saveSettings()"></div>
          <div class="prop-field"><label>Subtitle Text</label><input type="text" id="sChoiceSub" value="Select an option to continue" onchange="saveSettings()"></div>
          <div class="prop-field"><label class="prop-check"><input type="checkbox" id="sChoiceShow" checked onchange="saveSettings()"> Show choice overlay (uncheck to auto-play if single connection)</label></div>
          <div class="prop-field"><label>Replay Icon (shown at end)</label>
            <div style="display:flex;gap:6px;align-items:center">
              <button class="prop-btn" style="width:auto;padding:4px 10px" onclick="document.getElementById('sReplayFile').click()"><i class="bi bi-upload"></i> Upload</button>
              <span id="sReplayPreview" style="font-size:10px;color:var(--text-dim)">Default icon</span>
              <button class="prop-btn" style="width:auto;padding:4px 8px" onclick="clearIconUpload('replay')" title="Reset"><i class="bi bi-x"></i></button>
            </div>
            <input type="file" id="sReplayFile" accept="image/*" hidden onchange="handleIconUpload(this,'replay')">
            <input type="hidden" id="sReplayIcon" value="">
          </div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Loading Screen</div>
          <div class="prop-field"><label>Loading Text</label><input type="text" id="sLoadText" value="Loading..." onchange="saveSettings()"></div>
          <div class="prop-field"><label>Loading Background</label><input type="color" id="sLoadBg" value="#000000" onchange="saveSettings()"></div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Audio</div>
          <div class="prop-field"><label>Audio Source</label><select id="sAudioSrc" onchange="saveSettings()">
            <option value="video">Play audio from videos</option>
            <option value="bgm">Play background music (loop)</option>
            <option value="both">Both video audio + background music</option>
            <option value="mute">Mute all</option>
          </select></div>
          <div class="prop-field"><label>Background Music Path</label><input type="text" id="sAudioFile" placeholder="./audio/bgm.mp3" onchange="saveSettings()">
            <div style="font-size:9px;color:var(--text-dim);margin-top:2px">Relative path to MP3 file (loops continuously)</div>
          </div>
          <div class="prop-field"><label>BGM Volume</label><input type="range" id="sAudioVol" min="0" max="1" step="0.1" value="0.5" style="width:100%" onchange="saveSettings()"></div>
        </div>
        <div class="prop-group">
          <div class="prop-group-title">Transitions</div>
          <div class="prop-field"><label>Default Transition</label><select id="sTransition" onchange="saveSettings()">
            <option value="cut">Cut (instant)</option>
            <option value="fade">Fade to black</option>
            <option value="fadeWhite">Fade to white</option>
            <option value="dissolve">Dissolve (crossfade)</option>
            <option value="slideLeft">Slide Left</option>
            <option value="slideRight">Slide Right</option>
            <option value="zoomIn">Zoom In</option>
            <option value="blur">Blur</option>
          </select></div>
          <div class="prop-field"><label>Duration (ms)</label><input type="number" id="sTransDur" value="600" min="0" max="3000" step="100" onchange="saveSettings()"></div>
        </div>
      </div>
    </div>
  </div>

  <div class="app-canvas" id="canvas">
    <div class="canvas-grid"></div>
    <div class="canvas-drop" id="dropInd"></div>
    <svg class="conn-svg" id="connSvg"></svg>
    <div id="nodeLayer"></div>
    <div class="canvas-empty" id="canvasEmpty">
      <i class="bi bi-film"></i>
      <div class="title">Drag videos here</div>
      <div class="desc">Drag from the library to build your interactive story. Connect OUT ports to IN ports.</div>
    </div>
    <div class="canvas-controls">
      <button onclick="doZoom(-0.1)"><i class="bi bi-dash"></i></button>
      <div class="canvas-zoom" id="zoomLbl">100%</div>
      <button onclick="doZoom(0.1)"><i class="bi bi-plus"></i></button>
      <button onclick="fitView()"><i class="bi bi-fullscreen"></i></button>
    </div>
  </div>

  <div class="app-sidebar-right" id="rightPanel">
    <div class="props-empty" id="propsEmpty">
      <i class="bi bi-sliders"></i>
      <div class="title">Properties</div>
      <div style="font-size:11px;text-align:center;color:var(--text-dim)">Select a node, interaction, or connection</div>
    </div>
    <div id="propsArea" style="display:none;">
      <div class="panel-header"><span id="propsTitle">Properties</span></div>
      <div class="panel-content" id="propsContent"></div>
    </div>
  </div>

  <div class="app-timeline">
    <div class="tl-header">
      <div class="tl-title" id="tlTitle">Timeline — select a node</div>
      <div class="topbar-spacer"></div>
      <button class="topbar-btn" style="padding:4px 6px;font-size:11px" onclick="tlZoom(-4)" title="Zoom out timeline"><i class="bi bi-dash"></i></button>
      <span style="font-size:10px;color:var(--text-dim);font-family:var(--font-mono)" id="tlZoomLbl">18px/s</span>
      <button class="topbar-btn" style="padding:4px 6px;font-size:11px" onclick="tlZoom(4)" title="Zoom in timeline"><i class="bi bi-plus"></i></button>
      <button class="topbar-btn" id="btnAddInt" style="display:none" onclick="openAddInt()"><i class="bi bi-plus"></i> Add</button>
    </div>
    <div class="tl-body" id="tlBody">
      <div class="tl-empty" id="tlEmpty">Select a video node to see its timeline</div>
      <div id="tlContent" style="display:none;height:100%;">
        <div class="tl-ruler" id="tlRuler"></div>
        <div class="tl-tracks" id="tlTracks"></div>
      </div>
    </div>
  </div>
</div>

<div class="drag-ghost" id="dragGhost"></div>

<div class="ctx-menu" id="ctxMenu">
  <div class="ctx-item" onclick="ctxAct('start')"><i class="bi bi-play-fill"></i> Set as Start</div>
  <div class="ctx-item" onclick="ctxAct('dup')"><i class="bi bi-copy"></i> Duplicate</div>
  <div class="ctx-sep"></div>
  <div class="ctx-item danger" onclick="ctxAct('del')"><i class="bi bi-trash3"></i> Delete Node</div>
</div>

<div class="modal-bg" id="modalInt">
  <div class="modal-box">
    <div class="modal-head"><h3 id="miTitle">Add Interaction</h3><button onclick="closeMod('modalInt')"><i class="bi bi-x-lg"></i></button></div>
    <div class="modal-body" id="miBody"></div>
    <div class="modal-foot">
      <button class="topbar-btn" onclick="closeMod('modalInt')">Cancel</button>
      <button class="topbar-btn primary" onclick="saveInt()">Save</button>
    </div>
  </div>
</div>

<div class="modal-bg" id="modalImport">
  <div class="modal-box">
    <div class="modal-head"><h3>Import Project</h3><button onclick="closeMod('modalImport')"><i class="bi bi-x-lg"></i></button></div>
    <div class="modal-body">
      <div class="prop-field"><label>Paste JSON or load file</label><textarea id="importJson" rows="8" style="font-family:var(--font-mono);font-size:11px;"></textarea></div>
      <div class="add-zone" onclick="document.getElementById('importFile').click()" style="margin:0"><i class="bi bi-file-code"></i><span>Load .json file</span></div>
      <input type="file" id="importFile" accept=".json" hidden onchange="loadImpFile(this.files[0])">
    </div>
    <div class="modal-foot">
      <button class="topbar-btn" onclick="closeMod('modalImport')">Cancel</button>
      <button class="topbar-btn primary" onclick="doImport()">Import</button>
    </div>
  </div>
</div>

<!-- POSITION EDITOR MODAL -->
<div class="modal-bg" id="modalPos">
  <div class="modal-box" style="width:860px;max-width:96vw;max-height:92vh;">
    <div class="modal-head">
      <h3>Position Editor — <span id="posTitle">Element</span></h3>
      <button onclick="closeMod('modalPos')"><i class="bi bi-x-lg"></i></button>
    </div>
    <div class="modal-body" style="padding:12px;">
      <div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap;">
        <button class="topbar-btn" id="vpDesk" onclick="switchVP('desktop')" style="font-size:11px"><i class="bi bi-display"></i> Desktop 1920×1080</button>
        <button class="topbar-btn" id="vpTab" onclick="switchVP('tablet')" style="font-size:11px"><i class="bi bi-tablet"></i> Tablet 1024×768</button>
        <button class="topbar-btn" id="vpMobP" onclick="switchVP('mobileP')" style="font-size:11px"><i class="bi bi-phone"></i> Mobile 390×844</button>
        <button class="topbar-btn" id="vpMobL" onclick="switchVP('mobileL')" style="font-size:11px"><i class="bi bi-phone-landscape"></i> Landscape 844×390</button>
      </div>
      <div style="position:relative;background:var(--bg-elevated);border-radius:8px;overflow:hidden;border:1px solid var(--border-dim);" id="posViewport">
        <div id="posFrame" style="position:relative;margin:0 auto;background:#111;overflow:hidden;">
          <div id="posDragEl" style="position:absolute;cursor:move;border:2px dashed var(--accent-blue);border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;z-index:5;">
            <div class="pos-handle pos-h-tl" style="position:absolute;top:-6px;left:-6px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:nw-resize;z-index:6"></div>
            <div class="pos-handle pos-h-tr" style="position:absolute;top:-6px;right:-6px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:ne-resize;z-index:6"></div>
            <div class="pos-handle pos-h-bl" style="position:absolute;bottom:-6px;left:-6px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:sw-resize;z-index:6"></div>
            <div class="pos-handle pos-h-br" style="position:absolute;bottom:-6px;right:-6px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:se-resize;z-index:6"></div>
            <div class="pos-handle pos-h-tm" style="position:absolute;top:-6px;left:50%;margin-left:-7px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:n-resize;z-index:6"></div>
            <div class="pos-handle pos-h-bm" style="position:absolute;bottom:-6px;left:50%;margin-left:-7px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:s-resize;z-index:6"></div>
            <div class="pos-handle pos-h-ml" style="position:absolute;top:50%;margin-top:-7px;left:-6px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:w-resize;z-index:6"></div>
            <div class="pos-handle pos-h-mr" style="position:absolute;top:50%;margin-top:-7px;right:-6px;width:14px;height:14px;background:#fff;border:2px solid var(--accent-blue);border-radius:3px;cursor:e-resize;z-index:6"></div>
          </div>
          <div style="position:absolute;bottom:4px;left:4px;font-size:9px;color:rgba(255,255,255,0.4);z-index:6;" id="posCoords">x:50% y:50%</div>
        </div>
      </div>
      <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;align-items:end;">
        <div class="prop-field" style="flex:1;min-width:60px;margin:0"><label>X</label><input type="number" id="posX" min="0" step="1" onchange="updatePosFromInput()"></div>
        <div class="prop-field" style="flex:1;min-width:60px;margin:0"><label>Y</label><input type="number" id="posY" min="0" step="1" onchange="updatePosFromInput()"></div>
        <div class="prop-field" style="flex:1;min-width:60px;margin:0"><label>W</label><input type="number" id="posW" min="1" step="1" onchange="updatePosFromInput()"></div>
        <div class="prop-field" style="flex:1;min-width:60px;margin:0"><label>H</label><input type="number" id="posH" min="1" step="1" onchange="updatePosFromInput()"></div>
        <button class="topbar-btn" id="posUnitBtn" onclick="togglePosUnit()" style="margin-bottom:0;height:32px;font-weight:700;min-width:40px">%</button>
      </div>
    </div>
    <div class="modal-foot">
      <button class="topbar-btn" onclick="closeMod('modalPos')">Cancel</button>
      <button class="topbar-btn primary" onclick="savePosEditor()">Apply</button>
    </div>
  </div>
</div>

<!-- SERVER BROWSER MODAL -->
<div class="modal-bg" id="modalServer">
  <div class="modal-box" style="width:600px;">
    <div class="modal-head"><h3>Browse Server Videos</h3><button onclick="closeMod('modalServer')"><i class="bi bi-x-lg"></i></button></div>
    <div class="modal-body">
      <div class="prop-field">
        <label>Server Directory URL</label>
        <div style="display:flex;gap:6px">
          <input type="text" id="srvPath" value="" placeholder="./videos/" style="flex:1">
          <button class="topbar-btn primary" onclick="scanServerDir()"><i class="bi bi-search"></i> Scan</button>
        </div>
        <div style="font-size:9px;color:var(--text-dim);margin-top:2px">Enter the directory path. Server must have directory listing enabled or provide a JSON index.</div>
      </div>
      <div id="srvBreadcrumb" style="font-size:11px;color:var(--text-dim);margin-bottom:8px;display:flex;gap:4px;flex-wrap:wrap"></div>
      <div id="srvList" style="max-height:350px;overflow-y:auto;border:1px solid var(--border-dim);border-radius:var(--radius-md);background:var(--bg-elevated)">
        <div style="padding:20px;text-align:center;color:var(--text-dim);font-size:12px">Enter a path and click Scan to browse videos</div>
      </div>
    </div>
    <div class="modal-foot">
      <button class="topbar-btn" onclick="closeMod('modalServer')">Close</button>
    </div>
  </div>
</div>

<!-- CONTROL POSITION EDITOR MODAL -->
<div class="modal-bg" id="modalCtrlPos">
  <div class="modal-box" style="width:900px;max-width:96vw;max-height:94vh;">
    <div class="modal-head"><h3>Position Control Buttons</h3><button onclick="closeMod('modalCtrlPos')"><i class="bi bi-x-lg"></i></button></div>
    <div class="modal-body" style="padding:12px;">
      <div style="display:flex;gap:8px;margin-bottom:10px;flex-wrap:wrap;">
        <button class="topbar-btn" id="cvpDesk" onclick="switchCtrlVP('desktop')" style="font-size:11px"><i class="bi bi-display"></i> Desktop</button>
        <button class="topbar-btn" id="cvpTab" onclick="switchCtrlVP('tablet')" style="font-size:11px"><i class="bi bi-tablet"></i> Tablet</button>
        <button class="topbar-btn" id="cvpMobP" onclick="switchCtrlVP('mobileP')" style="font-size:11px"><i class="bi bi-phone"></i> Mobile</button>
        <button class="topbar-btn" id="cvpMobL" onclick="switchCtrlVP('mobileL')" style="font-size:11px"><i class="bi bi-phone-landscape"></i> Landscape</button>
        <div style="flex:1"></div>
        <span style="font-size:10px;color:var(--text-dim);display:flex;align-items:center" id="ctrlPosInfo">Drag buttons to position them</span>
      </div>
      <div style="position:relative;background:var(--bg-elevated);border-radius:8px;overflow:hidden;border:1px solid var(--border-dim);" id="ctrlPosViewport">
        <div id="ctrlPosFrame" style="position:relative;margin:0 auto;background:#111;overflow:hidden;min-height:200px">
          <!-- Control buttons will be rendered here -->
        </div>
      </div>
    </div>
    <div class="modal-foot">
      <button class="topbar-btn" onclick="closeMod('modalCtrlPos')">Cancel</button>
      <button class="topbar-btn primary" onclick="saveCtrlPositions()">Apply</button>
    </div>
  </div>
</div>

<div class="toast-box" id="toastBox"></div>

<script>
// ===== DATA =====
let project = { name:'Untitled Project', startNodeId:null, settings:{ showControls:true, autoHide:true, allowSeeking:true, showProgress:true, accentColor:'#4a9eff', playIcon:'', pauseIcon:'', endBehavior:'pause', videoFit:'contain', videoBasePath:'./videos/', transition:'fade', transitionDuration:600, choiceTitle:'What do you choose?', choiceSubtitle:'Select an option to continue', showChoiceOverlay:true, loadingText:'Loading...', loadingBg:'#000000', autoPlay:true } };
let library = [];      // {libId, name, src, blobUrl, duration}
let nodes = [];        // {nodeId, libId, name, x, y, duration, blobUrl, src}
let interactions = [];  // {intId, nodeId, type, label, startTime, endTime, x, y, w, h, action:{type,value,pauseVideo}, style:{bg,color,icon,image}}
let connections = [];   // {connId, fromPortId, toNodeId, label, color}  — fromPortId = nodeId (video out) or intId (interaction out)

let selNode=null, selInt=null, selConn=null, editIntId=null, addIntNode=null;
let cOff={x:60,y:60}, cZoom=1;
let undoSt=[], redoSt=[];
let _id=1; const gid=()=>'_'+(_id++);

// ===== TOAST =====
function toast(m,t='ok'){const b=document.getElementById('toastBox'),d=document.createElement('div');d.className='toast-msg '+t;d.innerHTML=`<i class="bi bi-${t==='ok'?'check-circle':'exclamation-circle'}"></i> ${m}`;b.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.remove(),300)},2200);}

// ===== UNDO =====
function snap(){undoSt.push(JSON.stringify({project,library,nodes,interactions,connections}));if(undoSt.length>60)undoSt.shift();redoSt=[];}
function restore(s){const d=JSON.parse(s);project=d.project;library=d.library;nodes=d.nodes;interactions=d.interactions;connections=d.connections;}
function doUndo(){if(!undoSt.length)return;redoSt.push(JSON.stringify({project,library,nodes,interactions,connections}));restore(undoSt.pop());fullRender();toast('Undo');}
function doRedo(){if(!redoSt.length)return;undoSt.push(JSON.stringify({project,library,nodes,interactions,connections}));restore(redoSt.pop());fullRender();toast('Redo');}

// ===== TABS =====
function switchTab(el){el.parentElement.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));el.classList.add('active');document.querySelectorAll('[id^="tab-"]').forEach(c=>c.style.display='none');document.getElementById('tab-'+el.dataset.tab).style.display='flex';}

// ===== LIBRARY =====
function addToLib(files){
  Array.from(files).forEach(f=>{
    const libId=gid(), blobUrl=URL.createObjectURL(f);
    const item={libId, name:f.name.replace(/\.[^.]+$/,''), filename:f.name, blobUrl, duration:0};
    const v=document.createElement('video');v.src=blobUrl;
    v.onloadedmetadata=()=>{item.duration=v.duration;renderLib();};
    library.push(item);
  });
  renderLib(); toast(files.length+' video(s) added');
}
function getVideoPath(item){ return item.src || (project.settings.videoBasePath||'./videos/')+(item.filename||item.name+'.mp4'); }
function removeLib(id){library=library.filter(l=>l.libId!==id);renderLib();}
function renderLib(){
  document.getElementById('libCount').textContent=library.length;
  document.getElementById('libList').innerHTML=library.map(v=>{
    const vidSrc=v.blobUrl||getVideoPath(v);
    return `
    <div class="lib-item" data-libid="${v.libId}" onmousedown="startLibDrag(event,'${v.libId}')">
      <div class="thumb"><video src="${vidSrc}" muted preload="metadata"></video></div>
      <div class="info"><div class="name">${v.name}</div><div class="meta">${fmt(v.duration)} · ${getVideoPath(v)}</div></div>
      <div class="actions"><button onclick="event.stopPropagation();removeLib('${v.libId}')"><i class="bi bi-x"></i></button></div>
    </div>`;
  }).join('');
}

// ===== DRAG FROM LIB TO CANVAS =====
let libDrag=null; const ghost=document.getElementById('dragGhost');
function startLibDrag(e,libId){e.preventDefault();const item=library.find(l=>l.libId===libId);if(!item)return;libDrag={libId,sx:e.clientX,sy:e.clientY,active:false,item};}
document.addEventListener('mousemove',e=>{
  if(!libDrag)return;
  if(!libDrag.active&&(Math.abs(e.clientX-libDrag.sx)+Math.abs(e.clientY-libDrag.sy))>6){libDrag.active=true;ghost.textContent=libDrag.item.name;ghost.style.display='block';}
  if(libDrag.active){
    ghost.style.left=(e.clientX+12)+'px';ghost.style.top=(e.clientY-14)+'px';
    const cr=document.getElementById('canvas').getBoundingClientRect();
    document.getElementById('dropInd').style.display=(e.clientX>=cr.left&&e.clientX<=cr.right&&e.clientY>=cr.top&&e.clientY<=cr.bottom)?'block':'none';
  }
});
document.addEventListener('mouseup',e=>{
  if(!libDrag)return;
  ghost.style.display='none';document.getElementById('dropInd').style.display='none';
  if(libDrag.active){
    const cr=document.getElementById('canvas').getBoundingClientRect();
    if(e.clientX>=cr.left&&e.clientX<=cr.right&&e.clientY>=cr.top&&e.clientY<=cr.bottom){
      snap();const lib=libDrag.item;
      const node={nodeId:gid(),libId:lib.libId,name:lib.name,filename:lib.filename,x:(e.clientX-cr.left-cOff.x)/cZoom,y:(e.clientY-cr.top-cOff.y)/cZoom,duration:lib.duration,blobUrl:lib.blobUrl||'',src:getVideoPath(lib),startTransition:'',endTransition:''};
      nodes.push(node);
      if(!project.startNodeId)project.startNodeId=node.nodeId;
      fullRender();selectNode(node.nodeId);toast('Node created');
    }
  }
  libDrag=null;
});

// File drop on add zone
const az=document.getElementById('addZone');
az.addEventListener('dragover',e=>{e.preventDefault();az.style.borderColor='var(--accent-blue)';az.style.background='var(--accent-blue-dim)';});
az.addEventListener('dragleave',()=>{az.style.borderColor='';az.style.background='';});
az.addEventListener('drop',e=>{e.preventDefault();az.style.borderColor='';az.style.background='';const f=Array.from(e.dataTransfer.files).filter(f=>f.type.startsWith('video/'));if(f.length)addToLib(f);});

// ===== RENDER NODES =====
function renderNodes(){
  const layer=document.getElementById('nodeLayer');
  layer.querySelectorAll('.vnode').forEach(n=>n.remove());
  document.getElementById('canvasEmpty').style.display=nodes.length?'none':'flex';

  nodes.forEach(n=>{
    const el=document.createElement('div');
    const isStart=project.startNodeId===n.nodeId;
    el.className='vnode'+(selNode===n.nodeId?' selected':'')+(isStart?' start-node':'');
    el.id='nd-'+n.nodeId;
    el.style.left=(n.x*cZoom+cOff.x)+'px';
    el.style.top=(n.y*cZoom+cOff.y)+'px';
    el.style.transform=`scale(${cZoom})`;
    el.style.transformOrigin='top left';

    const vidOut=connections.find(c=>c.fromPortId===n.nodeId);
    const ints=interactions.filter(i=>i.nodeId===n.nodeId);

    el.innerHTML=`
      <div class="vnode-preview">
        <video src="${n.blobUrl||n.src||(project.settings.videoBasePath||'./videos/')+(n.filename||n.name+'.mp4')}" muted preload="metadata"></video>
      </div>
      <div class="vnode-header" data-nid="${n.nodeId}">
        <div class="vnode-badge">${isStart?'START':'VIDEO'}</div>
        <div class="vnode-name">${n.name}</div>
        <div class="vnode-dur">${fmt(n.duration)}</div>
      </div>
      <div class="vnode-ports-row">
        <div class="port port-in" data-pt="in" data-nid="${n.nodeId}" title="IN (accepts many)"></div>
        <div class="port-row-center">video flow</div>
        <div class="port port-out ${vidOut?'connected':''}" data-pt="out" data-pid="${n.nodeId}" title="OUT (single)"></div>
      </div>
      ${ints.map(i=>{
        const ic=connections.find(c=>c.fromPortId===i.intId);
        return `<div class="vnode-int-item ${selInt===i.intId?'selected':''}" data-iid="${i.intId}">
          <div class="int-icon ${i.type}"><i class="bi bi-${intIco(i.type)}"></i></div>
          <div class="vnode-int-label">${i.label||i.type}</div>
          <div class="vnode-int-time">${fmt(i.startTime)}-${fmt(i.endTime)}</div>
          <div class="port port-out ${ic?'connected':''}" data-pt="out" data-pid="${i.intId}" title="OUT (single)"></div>
          <button class="del-int" title="Delete"><i class="bi bi-x"></i></button>
        </div>`;
      }).join('')}
      <div class="vnode-add-int"><i class="bi bi-plus"></i> add interaction</div>
    `;

    // Header drag
    el.querySelector('.vnode-header').addEventListener('mousedown',e=>{if(e.button!==0)return;e.preventDefault();e.stopPropagation();selectNode(n.nodeId);ndDrag={nid:n.nodeId,sx:e.clientX,sy:e.clientY,ox:n.x,oy:n.y};});

    // Preview area drag
    el.querySelector('.vnode-preview').addEventListener('mousedown',e=>{if(e.button!==0)return;e.preventDefault();e.stopPropagation();selectNode(n.nodeId);ndDrag={nid:n.nodeId,sx:e.clientX,sy:e.clientY,ox:n.x,oy:n.y};});

    // Right-click
    el.addEventListener('contextmenu',e=>{e.preventDefault();selectNode(n.nodeId);showCtx(e.clientX,e.clientY,n.nodeId);});

    // Click body to select
    el.addEventListener('mousedown',e=>{
      if(e.target.closest('.port')||e.target.closest('.del-int')||e.target.closest('.vnode-add-int')||e.target.closest('.vnode-int-item'))return;
      if(e.button===0){e.stopPropagation();selectNode(n.nodeId);}
    });

    // Interaction items
    el.querySelectorAll('.vnode-int-item').forEach(item=>{
      item.addEventListener('mousedown',e=>{
        if(e.target.closest('.port')||e.target.closest('.del-int'))return;
        e.stopPropagation();selectInteraction(item.dataset.iid);
      });
    });
    el.querySelectorAll('.del-int').forEach(btn=>{
      btn.addEventListener('click',e=>{e.stopPropagation();const item=btn.closest('.vnode-int-item');if(item)removeInt(item.dataset.iid);});
    });

    // Add interaction button
    el.querySelector('.vnode-add-int').addEventListener('click',e=>{e.stopPropagation();openAddInt(n.nodeId);});

    // Port events
    el.querySelectorAll('.port').forEach(port=>{
      port.addEventListener('mousedown',e=>{
        e.preventDefault();e.stopPropagation();
        if(port.dataset.pt==='out'){
          const pid=port.dataset.pid;
          if(connections.find(c=>c.fromPortId===pid)){toast('Already connected — delete existing first','err');return;}
          startConn(pid,port);
        }
      });
    });

    layer.appendChild(el);
  });
}

// ===== NODE DRAG =====
let ndDrag=null;
document.addEventListener('mousemove',e=>{
  if(ndDrag){const n=nodes.find(x=>x.nodeId===ndDrag.nid);if(n){n.x=ndDrag.ox+(e.clientX-ndDrag.sx)/cZoom;n.y=ndDrag.oy+(e.clientY-ndDrag.sy)/cZoom;renderNodes();renderConns();}}
});
document.addEventListener('mouseup',()=>{ndDrag=null;});

// ===== CONNECTION DRAG =====
let connDrag=null, tempPath=null;
function startConn(fromPid,portEl){const r=portEl.getBoundingClientRect();connDrag={fromPid,sx:r.left+r.width/2,sy:r.top+r.height/2};}
document.addEventListener('mousemove',e=>{
  if(!connDrag)return;
  const svg=document.getElementById('connSvg');
  if(!tempPath){tempPath=document.createElementNS('http://www.w3.org/2000/svg','path');tempPath.setAttribute('stroke','#3ddc84');tempPath.setAttribute('stroke-width','2');tempPath.setAttribute('stroke-dasharray','6,4');tempPath.setAttribute('fill','none');tempPath.setAttribute('opacity','0.8');svg.appendChild(tempPath);}
  const cr=document.getElementById('canvas').getBoundingClientRect();
  const x1=connDrag.sx-cr.left,y1=connDrag.sy-cr.top,x2=e.clientX-cr.left,y2=e.clientY-cr.top,dx=Math.max(Math.abs(x2-x1)*0.4,40);
  tempPath.setAttribute('d',`M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`);
});
document.addEventListener('mouseup',e=>{
  if(!connDrag)return;
  if(tempPath){tempPath.remove();tempPath=null;}
  const tgt=document.elementFromPoint(e.clientX,e.clientY);
  if(tgt&&tgt.classList.contains('port')&&tgt.dataset.pt==='in'){
    const toNid=tgt.dataset.nid;
    const fromOwner=portOwner(connDrag.fromPid);
    if(fromOwner&&fromOwner!==toNid){
      snap();
      // Auto-label from interaction if applicable
      const fromInt=interactions.find(i=>i.intId===connDrag.fromPid);
      const autoLabel=fromInt?(fromInt.label||fromInt.type):'';
      connections.push({connId:gid(),fromPortId:connDrag.fromPid,toNodeId:toNid,label:autoLabel,color:'#4a9eff',transition:''});
      fullRender();toast('Connected');
    }
  }
  connDrag=null;
});

function portOwner(pid){const n=nodes.find(x=>x.nodeId===pid);if(n)return n.nodeId;const i=interactions.find(x=>x.intId===pid);if(i)return i.nodeId;return null;}

// ===== RENDER CONNECTIONS =====
function renderConns(){
  const svg=document.getElementById('connSvg');
  svg.querySelectorAll('path:not([stroke-dasharray])').forEach(p=>p.remove());
  svg.querySelectorAll('path.hit-area').forEach(p=>p.remove());
  document.querySelectorAll('.conn-label-el').forEach(l=>l.remove());
  const cr=document.getElementById('canvas').getBoundingClientRect();
  connections.forEach(conn=>{
    const fromEl=document.querySelector(`.port[data-pid="${conn.fromPortId}"]`);
    const toEl=document.querySelector(`#nd-${conn.toNodeId} .port[data-pt="in"]`);
    if(!fromEl||!toEl)return;
    const fp=fromEl.getBoundingClientRect(),tp=toEl.getBoundingClientRect();
    const x1=fp.left+fp.width/2-cr.left,y1=fp.top+fp.height/2-cr.top;
    const x2=tp.left+tp.width/2-cr.left,y2=tp.top+tp.height/2-cr.top;
    const dx=Math.max(Math.abs(x2-x1)*0.4,40);
    const pathD=`M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}`;
    // Invisible wide hit area for easy clicking
    const hitPath=document.createElementNS('http://www.w3.org/2000/svg','path');
    hitPath.setAttribute('d',pathD);
    hitPath.setAttribute('class','hit-area');
    hitPath.addEventListener('click',e=>{e.stopPropagation();selectConn(conn.connId);});
    svg.appendChild(hitPath);
    // Visible path
    const path=document.createElementNS('http://www.w3.org/2000/svg','path');
    path.setAttribute('d',pathD);
    path.setAttribute('stroke',conn.color||'#4a9eff');
    path.setAttribute('stroke-width',selConn===conn.connId?'4':'2.5');
    path.setAttribute('opacity',selConn===conn.connId?'1':'0.6');
    path.style.cursor='pointer';path.style.pointerEvents='stroke';
    path.addEventListener('click',e=>{e.stopPropagation();selectConn(conn.connId);});
    svg.appendChild(path);
    if(conn.label){
      const lbl=document.createElement('div');lbl.className='conn-label-el';
      lbl.style.cssText=`position:absolute;left:${(x1+x2)/2}px;top:${(y1+y2)/2-10}px;background:var(--bg-card);border:1px solid var(--border-dim);border-radius:4px;padding:1px 6px;font-size:9px;color:var(--text-secondary);z-index:16;cursor:pointer;white-space:nowrap;`;
      lbl.textContent=conn.label;lbl.onclick=()=>selectConn(conn.connId);
      document.getElementById('canvas').appendChild(lbl);
    }
  });
}

// ===== SELECTION =====
function selectNode(nid){selNode=nid;selInt=null;selConn=null;renderNodes();renderConns();showNodeProps(nid);showTimeline(nid);}
function selectInteraction(iid){const i=interactions.find(x=>x.intId===iid);if(!i)return;selInt=iid;selConn=null;selNode=i.nodeId;renderNodes();renderConns();showIntProps(iid);showTimeline(i.nodeId);}
function selectConn(cid){selConn=cid;selInt=null;renderNodes();renderConns();showConnProps(cid);}
function clearSel(){selNode=null;selInt=null;selConn=null;document.getElementById('propsArea').style.display='none';document.getElementById('propsEmpty').style.display='flex';document.getElementById('btnAddInt').style.display='none';document.getElementById('tlTitle').textContent='Timeline — select a node';document.getElementById('tlEmpty').style.display='flex';document.getElementById('tlContent').style.display='none';renderNodes();renderConns();}

// ===== PROPS: NODE =====
function showNodeProps(nid){
  const n=nodes.find(x=>x.nodeId===nid);if(!n)return;
  document.getElementById('propsEmpty').style.display='none';
  document.getElementById('propsArea').style.display='block';
  document.getElementById('propsTitle').textContent='Video Node';
  const isStart = project.startNodeId===nid;
  const hasOut = connections.some(c=>c.fromPortId===nid);
  const hasIntOuts = interactions.filter(i=>i.nodeId===nid).some(i=>connections.some(c=>c.fromPortId===i.intId));
  const mkSel=(opts,val)=>opts.map(o=>`<option value="${o[0]}" ${o[0]===val?'selected':''}>${o[1]}</option>`).join('');
  const startTrOpts=[['','None (instant)'],['fadeInBlack','Fade in from black'],['fadeInWhite','Fade in from white'],['blur','Blur in'],['zoomIn','Zoom in']];
  const endTrOpts=[['','Default (global)'],['cut','Cut (instant)'],['fade','Fade to black'],['fadeWhite','Fade to white'],['blur','Blur out'],['zoomIn','Zoom out']];
  const endBehOpts=[['','Use global setting'],['pause','Pause on last frame'],['replay','Replay this video'],['playthrough','Auto-continue (if connected)'],['choices','Show choices (if interactions connected)']];

  document.getElementById('propsContent').innerHTML=`
    <div class="prop-group">
      <div class="prop-group-title">Node</div>
      <div class="prop-field"><label>Name</label><input value="${n.name}" onchange="snap();nodes.find(x=>x.nodeId==='${nid}').name=this.value;fullRender();"></div>
      <div class="prop-field"><label>Video Path</label><input value="${n.src||(project.settings.videoBasePath||'./videos/')+(n.filename||n.name+'.mp4')}" onchange="snap();nodes.find(x=>x.nodeId==='${nid}').src=this.value;fullRender();" placeholder="./videos/myvideo.mp4">
        <div style="font-size:9px;color:var(--text-dim);margin-top:2px">Override the auto-generated path if needed</div>
      </div>
      <div class="prop-field"><label>Duration</label><input value="${fmt(n.duration)}" readonly style="opacity:.5"></div>
      <div class="prop-field"><label class="prop-check"><input type="checkbox" ${isStart?'checked':''} onchange="snap();project.startNodeId=this.checked?'${nid}':null;fullRender();selectNode('${nid}');"> Start node</label></div>
    </div>
    <div class="prop-group">
      <div class="prop-group-title">End of Video Behavior</div>
      <div class="prop-field"><label>When this video ends</label><select onchange="snap();nodes.find(x=>x.nodeId==='${nid}').endBehavior=this.value;">
        ${mkSel(endBehOpts, n.endBehavior||'')}
      </select></div>
    </div>
    <div class="prop-group">
      <div class="prop-group-title">Audio Override</div>
      <div class="prop-field"><label>Audio for this node</label><select onchange="snap();nodes.find(x=>x.nodeId==='${nid}').audioMode=this.value;">
        ${mkSel([['','Follow global setting'],['video','Play video audio'],['bgm','Play background music'],['custom','Play custom audio'],['mute','Mute']],n.audioMode||'')}
      </select></div>
      <div class="prop-field"><label>Custom Audio Path</label><input value="${n.audioFile||''}" onchange="snap();nodes.find(x=>x.nodeId==='${nid}').audioFile=this.value;" placeholder="./audio/scene1.mp3"></div>
      <div class="prop-field"><label class="prop-check"><input type="checkbox" ${n.audioLoop!==false?'checked':''} onchange="snap();nodes.find(x=>x.nodeId==='${nid}').audioLoop=this.checked;"> Loop custom audio</label></div>
    </div>
    <div class="prop-group">
      <div class="prop-group-title">Overlay Image Override</div>
      <div style="font-size:10px;color:var(--text-dim);margin-bottom:6px">Override the global overlay image for this video node</div>
      <div class="prop-field"><label>Overlay Image</label><div style="display:flex;gap:4px;align-items:center">
        <button class="prop-btn" style="width:auto;padding:3px 8px;font-size:10px" onclick="document.getElementById('nodeOvFile').click()"><i class="bi bi-upload"></i></button>
        <span id="nodeOvPreview" style="font-size:10px;color:var(--text-dim)">${n.overlayImg?'Set':'None (use global)'}</span>
        <button class="prop-btn" style="width:auto;padding:3px 6px;font-size:10px" onclick="snap();nodes.find(x=>x.nodeId==='${nid}').overlayImg='';selectNode('${nid}')"><i class="bi bi-x"></i></button>
      </div><input type="file" id="nodeOvFile" accept="image/*" hidden onchange="handleNodeOvImg(this,'${nid}')"></div>
      ${n.overlayImg?`<div style="margin-top:4px"><img src="${n.overlayImg}" style="max-width:120px;border-radius:4px;border:1px solid var(--border-dim)"></div>`:''}
    </div>
    ${isStart?`<div class="prop-group">
      <div class="prop-group-title">Start Transition (intro)</div>
      <div class="prop-field"><label>Transition In</label><select onchange="snap();nodes.find(x=>x.nodeId==='${nid}').startTransition=this.value;">
        ${mkSel(startTrOpts, n.startTransition||'')}
      </select></div>
    </div>`:''}
    ${!hasOut?`<div class="prop-group">
      <div class="prop-group-title">End Transition (outro)</div>
      <div class="prop-field"><label>Transition Out</label><select onchange="snap();nodes.find(x=>x.nodeId==='${nid}').endTransition=this.value;">
        ${mkSel(endTrOpts, n.endTransition||'')}
      </select></div>
    </div>`:''}
    <div class="prop-group">
      <div class="prop-group-title">Interactions (${interactions.filter(i=>i.nodeId===nid).length})</div>
      <button class="prop-btn" onclick="openAddInt('${nid}')"><i class="bi bi-plus"></i> Add Interaction</button>
    </div>
    <div class="prop-group" style="margin-top:16px"><button class="prop-btn danger" onclick="deleteNode('${nid}')"><i class="bi bi-trash3"></i> Delete Node</button></div>
  `;
}

// ===== PROPS: INTERACTION =====
function showIntProps(iid){
  const i=interactions.find(x=>x.intId===iid);if(!i)return;
  document.getElementById('propsEmpty').style.display='none';
  document.getElementById('propsArea').style.display='block';
  document.getElementById('propsTitle').textContent='Interaction';
  const ct=connections.find(c=>c.fromPortId===iid);
  const tgtName=ct?(nodes.find(n=>n.nodeId===ct.toNodeId)?.name||'?'):'Not connected';
  const st=i.style||{};
  const hasImg=st.image&&st.image.length>10;
  document.getElementById('propsContent').innerHTML=`
    <div class="prop-group">
      <div class="prop-group-title">Basic</div>
      <div class="prop-field"><label>Type</label><select onchange="snap();interactions.find(x=>x.intId==='${iid}').type=this.value;fullRender();selectInteraction('${iid}');">
        ${['hotspot','button','choice','overlay','timer'].map(t=>`<option value="${t}" ${i.type===t?'selected':''}>${t}</option>`).join('')}
      </select></div>
      <div class="prop-field"><label>Label</label><input value="${i.label||''}" onchange="snap();interactions.find(x=>x.intId==='${iid}').label=this.value;fullRender();selectInteraction('${iid}');"></div>
    </div>
    <div class="prop-group">
      <div class="prop-group-title">Timing</div>
      <div style="display:flex;gap:8px">
        <div class="prop-field" style="flex:1"><label>Start (s)</label><input type="number" step="0.1" min="0" value="${i.startTime}" onchange="snap();interactions.find(x=>x.intId==='${iid}').startTime=+this.value;fullRender();selectInteraction('${iid}');"></div>
        <div class="prop-field" style="flex:1"><label>End (s)</label><input type="number" step="0.1" min="0" value="${i.endTime}" onchange="snap();interactions.find(x=>x.intId==='${iid}').endTime=+this.value;fullRender();selectInteraction('${iid}');"></div>
      </div>
    </div>
    <div class="prop-group">
      <div class="prop-group-title">Position</div>
      <button class="prop-btn" style="margin-bottom:8px;background:var(--accent-blue-dim);border-color:rgba(74,158,255,0.3);color:var(--accent-blue)" onclick="openPosEditor('${iid}')"><i class="bi bi-bounding-box"></i> Visual Position Editor</button>
      <div style="display:flex;gap:8px">
        <div class="prop-field" style="flex:1"><label>X %</label><input type="number" min="0" max="100" value="${i.x}" onchange="snap();interactions.find(x=>x.intId==='${iid}').x=+this.value;"></div>
        <div class="prop-field" style="flex:1"><label>Y %</label><input type="number" min="0" max="100" value="${i.y}" onchange="snap();interactions.find(x=>x.intId==='${iid}').y=+this.value;"></div>
      </div>
      <div style="display:flex;gap:8px">
        <div class="prop-field" style="flex:1"><label>Width %</label><input type="number" min="1" max="100" value="${i.w}" onchange="snap();interactions.find(x=>x.intId==='${iid}').w=+this.value;"></div>
        <div class="prop-field" style="flex:1"><label>Height %</label><input type="number" min="1" max="100" value="${i.h}" onchange="snap();interactions.find(x=>x.intId==='${iid}').h=+this.value;"></div>
      </div>
    </div>
    <div class="prop-group">
      <div class="prop-group-title">Connection</div>
      <div class="prop-field"><label>Connected to</label><input value="${tgtName}" readonly style="opacity:.5"></div>
      ${ct?`<button class="prop-btn danger" style="margin-top:4px" onclick="delConn('${ct.connId}')"><i class="bi bi-x"></i> Remove connection</button>`:'<div style="font-size:10px;color:var(--text-dim)">Drag OUT port → another node IN</div>'}
    </div>
    <div class="prop-group">
      <div class="prop-group-title">Image</div>
      <div class="prop-field">
        <div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">
          <button class="prop-btn" style="width:auto;padding:4px 10px" onclick="document.getElementById('intImgUp').click()"><i class="bi bi-image"></i> Upload Image</button>
          ${hasImg?`<button class="prop-btn" style="width:auto;padding:4px 8px" onclick="snap();interactions.find(x=>x.intId==='${iid}').style.image='';selectInteraction('${iid}');"><i class="bi bi-x"></i> Remove</button>`:''}
        </div>
        <input type="file" id="intImgUp" accept="image/*" hidden onchange="handleIntImgProp(this,'${iid}')">
        ${hasImg?`<div style="margin-top:4px"><img src="${st.image}" style="max-width:120px;max-height:60px;border-radius:4px;border:1px solid var(--border-dim)"></div>`:'<div style="font-size:10px;color:var(--text-dim)">PNG, GIF, APNG, WebP</div>'}
      </div>
    </div>
    <div class="prop-group">
      <div class="prop-group-title">Style</div>
      <div class="prop-field"><label>Background</label>
        <div style="display:flex;gap:6px;align-items:center">
          <input type="color" value="${st.bg||'#4a9eff'}" style="width:40px;flex-shrink:0" onchange="snap();interactions.find(x=>x.intId==='${iid}').style.bg=this.value;">
          <label class="prop-check" style="font-size:11px"><input type="checkbox" ${st.transparent?'checked':''} onchange="snap();interactions.find(x=>x.intId==='${iid}').style.transparent=this.checked;"> Transparent</label>
        </div>
      </div>
      <div class="prop-field"><label>Text Color</label><input type="color" value="${st.color||'#ffffff'}" onchange="snap();interactions.find(x=>x.intId==='${iid}').style.color=this.value;"></div>
      <div class="prop-field"><label>Border</label>
        <div style="display:flex;gap:6px;align-items:center">
          <input type="number" min="0" max="10" value="${st.borderWidth||0}" style="width:50px" onchange="snap();interactions.find(x=>x.intId==='${iid}').style.borderWidth=+this.value;"> px
          <input type="color" value="${st.borderColor||'#ffffff'}" style="width:40px" onchange="snap();interactions.find(x=>x.intId==='${iid}').style.borderColor=this.value;">
        </div>
      </div>
      <div class="prop-field"><label>Border Radius (px)</label><input type="number" min="0" max="100" value="${st.borderRadius||8}" onchange="snap();interactions.find(x=>x.intId==='${iid}').style.borderRadius=+this.value;"></div>
      <div class="prop-field"><label>Shadow</label><select onchange="snap();interactions.find(x=>x.intId==='${iid}').style.shadow=this.value;">
        <option value="" ${!st.shadow?'selected':''}>None</option>
        <option value="sm" ${st.shadow==='sm'?'selected':''}>Small</option>
        <option value="md" ${st.shadow==='md'?'selected':''}>Medium</option>
        <option value="lg" ${st.shadow==='lg'?'selected':''}>Large</option>
        <option value="glow" ${st.shadow==='glow'?'selected':''}>Glow</option>
      </select></div>
      <div class="prop-field"><label>Entrance Animation (<a href="https://animate.style" target="_blank" style="color:var(--accent-blue);font-size:10px">animate.css</a>)</label><select onchange="snap();interactions.find(x=>x.intId==='${iid}').style.animateIn=this.value;">
        ${mkAnimSel(st.animateIn||'')}
      </select></div>
      <div class="prop-field"><label>Exit Animation</label><select onchange="snap();interactions.find(x=>x.intId==='${iid}').style.animateOut=this.value;">
        ${mkAnimSel(st.animateOut||'',true)}
      </select></div>
      <div class="prop-field"><label>Attention Animation (loops)</label><select onchange="snap();interactions.find(x=>x.intId==='${iid}').style.animateAttention=this.value;">
        ${mkAttentionSel(st.animateAttention||'')}
      </select></div>
      <div class="prop-field"><label>Icon (bi-xxx)</label><input value="${st.icon||''}" onchange="snap();interactions.find(x=>x.intId==='${iid}').style.icon=this.value;fullRender();selectInteraction('${iid}');"></div>
      <div class="prop-field"><label class="prop-check"><input type="checkbox" ${i.action?.pauseVideo!==false?'checked':''} onchange="snap();interactions.find(x=>x.intId==='${iid}').action.pauseVideo=this.checked;"> Pause video on trigger</label></div>
    </div>
    <div class="prop-group"><button class="prop-btn danger" onclick="removeInt('${iid}')"><i class="bi bi-trash3"></i> Delete Interaction</button></div>
  `;
}

function handleIntImgProp(input,iid){
  if(!input.files||!input.files[0])return;
  const r=new FileReader();
  r.onload=e=>{snap();const intr=interactions.find(i=>i.intId===iid);if(intr){if(!intr.style)intr.style={};intr.style.image=e.target.result;}selectInteraction(iid);};
  r.readAsDataURL(input.files[0]);
}

function mkAnimSel(val,isExit){
  const ins=['','bounceIn','bounceInDown','bounceInLeft','bounceInRight','bounceInUp','fadeIn','fadeInDown','fadeInLeft','fadeInRight','fadeInUp','flipInX','flipInY','lightSpeedInRight','rotateIn','slideInDown','slideInLeft','slideInRight','slideInUp','zoomIn','jackInTheBox','rollIn','heartBeat','pulse','rubberBand','shakeX','shakeY','swing','tada','wobble','jello','flash','bounce'];
  const outs=['','bounceOut','bounceOutDown','bounceOutLeft','bounceOutRight','bounceOutUp','fadeOut','fadeOutDown','fadeOutLeft','fadeOutRight','fadeOutUp','flipOutX','flipOutY','lightSpeedOutRight','rotateOut','slideOutDown','slideOutLeft','slideOutRight','slideOutUp','zoomOut','rollOut','hinge'];
  const list=isExit?outs:ins;
  return list.map(a=>`<option value="${a}" ${a===val?'selected':''}>${a||'None'}</option>`).join('');
}
function mkAttentionSel(val){
  const attn=['','bounce','flash','pulse','rubberBand','shakeX','shakeY','headShake','swing','tada','wobble','jello','heartBeat'];
  return attn.map(a=>`<option value="${a}" ${a===val?'selected':''}>${a||'None'}</option>`).join('');
}

// ===== PROPS: CONNECTION =====
function showConnProps(cid){
  const c=connections.find(x=>x.connId===cid);if(!c)return;
  document.getElementById('propsEmpty').style.display='none';
  document.getElementById('propsArea').style.display='block';
  document.getElementById('propsTitle').textContent='Connection';
  const fromOwner=portOwner(c.fromPortId);
  const fromNode=nodes.find(n=>n.nodeId===fromOwner);
  const fromInt=interactions.find(i=>i.intId===c.fromPortId);
  const fromLabel=fromInt?`${fromNode?.name||'?'} → ${fromInt.label||fromInt.type}`:fromNode?.name||'?';
  const toName=nodes.find(n=>n.nodeId===c.toNodeId)?.name||'?';
  document.getElementById('propsContent').innerHTML=`
    <div class="prop-group">
      <div class="prop-group-title">Connection</div>
      <div class="prop-field"><label>From</label><input value="${fromLabel}" readonly style="opacity:.5"></div>
      <div class="prop-field"><label>To</label><input value="${toName}" readonly style="opacity:.5"></div>
      <div class="prop-field"><label>Label</label><input value="${c.label||''}" onchange="snap();connections.find(x=>x.connId==='${cid}').label=this.value;renderConns();"></div>
      <div class="prop-field"><label>Color</label><input type="color" value="${c.color||'#4a9eff'}" onchange="snap();connections.find(x=>x.connId==='${cid}').color=this.value;renderConns();"></div>
      <div class="prop-field"><label>Transition</label><select onchange="snap();connections.find(x=>x.connId==='${cid}').transition=this.value;">
        <option value="" ${!c.transition?'selected':''}>Default (use global)</option>
        <option value="cut" ${c.transition==='cut'?'selected':''}>Cut (instant)</option>
        <option value="fade" ${c.transition==='fade'?'selected':''}>Fade to black</option>
        <option value="fadeWhite" ${c.transition==='fadeWhite'?'selected':''}>Fade to white</option>
        <option value="dissolve" ${c.transition==='dissolve'?'selected':''}>Dissolve (crossfade)</option>
        <option value="slideLeft" ${c.transition==='slideLeft'?'selected':''}>Slide Left</option>
        <option value="slideRight" ${c.transition==='slideRight'?'selected':''}>Slide Right</option>
        <option value="zoomIn" ${c.transition==='zoomIn'?'selected':''}>Zoom In</option>
        <option value="blur" ${c.transition==='blur'?'selected':''}>Blur</option>
      </select></div>
    </div>
    <div class="prop-group"><button class="prop-btn danger" onclick="delConn('${cid}')"><i class="bi bi-trash3"></i> Delete Connection</button></div>
  `;
}

// ===== INTERACTION CRUD =====
function intIco(t){return{hotspot:'cursor-fill',choice:'signpost-split',button:'hand-index',overlay:'layers',timer:'clock'}[t]||'star';}

function openAddInt(nid){
  if(!nid&&selNode)nid=selNode;if(!nid){toast('Select a node first','err');return;}
  addIntNode=nid;editIntId=null;
  const n=nodes.find(x=>x.nodeId===nid);const dur=n?.duration||30;
  document.getElementById('miTitle').textContent='Add Interaction';
  document.getElementById('miBody').innerHTML=`
    <div class="prop-field"><label>Type</label><select id="mi_type">
      <option value="hotspot">Hotspot</option><option value="button" selected>Button</option>
      <option value="choice">Choice</option><option value="overlay">Overlay</option><option value="timer">Timer</option>
    </select></div>
    <div class="prop-field"><label>Label</label><input id="mi_label" placeholder="e.g. Go left"></div>
    <div style="display:flex;gap:8px">
      <div class="prop-field" style="flex:1"><label>Start (s)</label><input type="number" id="mi_start" value="0" min="0" max="${dur}" step="0.1"></div>
      <div class="prop-field" style="flex:1"><label>End (s)</label><input type="number" id="mi_end" value="${Math.min(5,dur)}" min="0" max="${dur}" step="0.1"></div>
    </div>
    <div style="display:flex;gap:8px">
      <div class="prop-field" style="flex:1"><label>X %</label><input type="number" id="mi_x" value="50" min="0" max="100"></div>
      <div class="prop-field" style="flex:1"><label>Y %</label><input type="number" id="mi_y" value="50" min="0" max="100"></div>
    </div>
    <div style="display:flex;gap:8px">
      <div class="prop-field" style="flex:1"><label>W %</label><input type="number" id="mi_w" value="15" min="1" max="100"></div>
      <div class="prop-field" style="flex:1"><label>H %</label><input type="number" id="mi_h" value="8" min="1" max="100"></div>
    </div>
    <div class="prop-field"><label>Background</label><input type="color" id="mi_bg" value="#4a9eff"></div>
    <div class="prop-field"><label>Text Color</label><input type="color" id="mi_clr" value="#ffffff"></div>
    <div class="prop-field"><label>Icon (bi-xxx)</label><input id="mi_icon" placeholder="bi-cursor-fill"></div>
    <div class="prop-field"><label>Image (PNG, GIF, APNG)</label>
      <input type="file" id="mi_imgFile" accept="image/png,image/gif,image/apng,image/webp" onchange="handleIntImage(this)" style="font-size:11px">
      <input type="hidden" id="mi_imgData" value="">
      <div id="mi_imgPreview" style="margin-top:4px"></div>
    </div>
    <div class="prop-field"><label class="prop-check"><input type="checkbox" id="mi_pause" checked> Pause video on trigger</label></div>
  `;
  document.getElementById('modalInt').classList.add('show');
}

function handleIntImage(input){
  if(!input.files||!input.files[0])return;
  const reader=new FileReader();
  reader.onload=e=>{
    document.getElementById('mi_imgData').value=e.target.result;
    document.getElementById('mi_imgPreview').innerHTML=`<img src="${e.target.result}" style="max-width:80px;max-height:40px;border-radius:4px;border:1px solid var(--border-dim)">`;
  };
  reader.readAsDataURL(input.files[0]);
}

function saveInt(){
  snap();
  const d={
    intId:editIntId||gid(), nodeId:addIntNode,
    type:document.getElementById('mi_type').value,
    label:document.getElementById('mi_label').value,
    startTime:+document.getElementById('mi_start').value||0,
    endTime:+document.getElementById('mi_end').value||5,
    x:+document.getElementById('mi_x').value||50, y:+document.getElementById('mi_y').value||50,
    w:+document.getElementById('mi_w').value||15, h:+document.getElementById('mi_h').value||8,
    action:{type:'jumpToVideo',value:'',pauseVideo:document.getElementById('mi_pause').checked},
    style:{bg:document.getElementById('mi_bg').value,color:document.getElementById('mi_clr').value,icon:document.getElementById('mi_icon').value,image:document.getElementById('mi_imgData').value||''}
  };
  if(editIntId){const idx=interactions.findIndex(i=>i.intId===editIntId);if(idx>=0)interactions[idx]={...interactions[idx],...d};}
  else interactions.push(d);
  editIntId=null;closeMod('modalInt');fullRender();selectNode(addIntNode);toast('Interaction saved');
}

// ===== TIMELINE ZOOM =====
let tlPPS=18;
function tlZoom(d){tlPPS=Math.max(2,Math.min(120,tlPPS+d));document.getElementById('tlZoomLbl').textContent=tlPPS+'px/s';if(selNode)showTimeline(selNode);}

function removeInt(iid){snap();connections=connections.filter(c=>c.fromPortId!==iid);interactions=interactions.filter(i=>i.intId!==iid);if(selInt===iid)selInt=null;fullRender();if(selNode)selectNode(selNode);toast('Removed');}

// ===== NODE CRUD =====
function deleteNode(nid){
  snap();const ints=interactions.filter(i=>i.nodeId===nid).map(i=>i.intId);
  connections=connections.filter(c=>c.toNodeId!==nid&&c.fromPortId!==nid&&!ints.includes(c.fromPortId));
  interactions=interactions.filter(i=>i.nodeId!==nid);
  nodes=nodes.filter(n=>n.nodeId!==nid);
  if(project.startNodeId===nid)project.startNodeId=nodes.length?nodes[0].nodeId:null;
  clearSel();fullRender();toast('Deleted');
}
function delConn(cid){snap();connections=connections.filter(c=>c.connId!==cid);clearSel();fullRender();toast('Connection removed');}

// ===== CONTEXT MENU =====
let ctxNid=null;
function showCtx(x,y,nid){ctxNid=nid;const m=document.getElementById('ctxMenu');m.style.left=x+'px';m.style.top=y+'px';m.classList.add('show');}
function hideCtx(){document.getElementById('ctxMenu').classList.remove('show');}
function ctxAct(a){
  hideCtx();if(!ctxNid)return;
  if(a==='start'){snap();project.startNodeId=ctxNid;fullRender();toast('Start set');}
  else if(a==='dup'){snap();const o=nodes.find(n=>n.nodeId===ctxNid);if(o){const nn={...o,nodeId:gid(),name:o.name+' (copy)',x:o.x+30,y:o.y+30};nodes.push(nn);fullRender();selectNode(nn.nodeId);toast('Duplicated');}}
  else if(a==='del')deleteNode(ctxNid);
}

// ===== TIMELINE =====
function showTimeline(nid){
  const n=nodes.find(x=>x.nodeId===nid);if(!n)return;
  document.getElementById('tlTitle').textContent=`Timeline — ${n.name}`;
  document.getElementById('btnAddInt').style.display='flex';
  document.getElementById('btnAddInt').onclick=()=>openAddInt(nid);
  document.getElementById('tlEmpty').style.display='none';
  document.getElementById('tlContent').style.display='block';

  const dur=Math.max(n.duration||30,5);
  const PPS=tlPPS;
  const totalW=dur*PPS+40;

  const ruler=document.getElementById('tlRuler');ruler.style.width=totalW+'px';ruler.innerHTML='';
  const step=dur>120?10:dur>30?5:1;
  for(let t=0;t<=dur;t+=step){const tk=document.createElement('div');tk.className='tl-tick';tk.style.left=(t*PPS)+'px';const lb=document.createElement('div');lb.className='tl-tick-label';lb.textContent=fmt(t);tk.appendChild(lb);ruler.appendChild(tk);}

  const tracks=document.getElementById('tlTracks');tracks.style.width=totalW+'px';tracks.innerHTML='';
  const ints=interactions.filter(i=>i.nodeId===nid);

  ints.forEach(intr=>{
    const track=document.createElement('div');track.className='tl-track';
    const bar=document.createElement('div');
    bar.className=`tl-bar type-${intr.type} ${selInt===intr.intId?'selected':''}`;
    bar.style.left=(intr.startTime*PPS)+'px';
    bar.style.width=Math.max(20,(intr.endTime-intr.startTime)*PPS)+'px';
    bar.textContent=intr.label||intr.type;

    const hl=document.createElement('div');hl.className='tl-handle tl-handle-l';
    const hr=document.createElement('div');hr.className='tl-handle tl-handle-r';
    bar.appendChild(hl);bar.appendChild(hr);

    // Click to select
    bar.addEventListener('mousedown',e=>{
      if(e.target.classList.contains('tl-handle'))return;
      e.stopPropagation();
      selectInteraction(intr.intId);
      // Start move drag
      const bDrag={iid:intr.intId,mode:'move',sx:e.clientX,os:intr.startTime,oe:intr.endTime};
      const onM=ev=>{
        const dx=(ev.clientX-bDrag.sx)/PPS;
        const it=interactions.find(i=>i.intId===bDrag.iid);if(!it)return;
        const len=bDrag.oe-bDrag.os;
        let ns=Math.max(0,bDrag.os+dx);if(ns+len>dur)ns=dur-len;
        it.startTime=Math.round(ns*10)/10;it.endTime=Math.round((ns+len)*10)/10;
        bar.style.left=(it.startTime*PPS)+'px';bar.style.width=Math.max(20,(it.endTime-it.startTime)*PPS)+'px';
      };
      const onU=()=>{snap();document.removeEventListener('mousemove',onM);document.removeEventListener('mouseup',onU);fullRender();if(selInt)selectInteraction(selInt);else if(selNode)selectNode(selNode);};
      document.addEventListener('mousemove',onM);document.addEventListener('mouseup',onU);
    });

    // Left handle
    hl.addEventListener('mousedown',e=>{
      e.stopPropagation();
      const bDrag={iid:intr.intId,sx:e.clientX,os:intr.startTime};
      const onM=ev=>{
        const dx=(ev.clientX-bDrag.sx)/PPS;
        const it=interactions.find(i=>i.intId===bDrag.iid);if(!it)return;
        it.startTime=Math.max(0,Math.min(it.endTime-0.1,Math.round((bDrag.os+dx)*10)/10));
        bar.style.left=(it.startTime*PPS)+'px';bar.style.width=Math.max(20,(it.endTime-it.startTime)*PPS)+'px';
      };
      const onU=()=>{snap();document.removeEventListener('mousemove',onM);document.removeEventListener('mouseup',onU);fullRender();if(selInt)selectInteraction(selInt);};
      document.addEventListener('mousemove',onM);document.addEventListener('mouseup',onU);
    });

    // Right handle
    hr.addEventListener('mousedown',e=>{
      e.stopPropagation();
      const bDrag={iid:intr.intId,sx:e.clientX,oe:intr.endTime};
      const onM=ev=>{
        const dx=(ev.clientX-bDrag.sx)/PPS;
        const it=interactions.find(i=>i.intId===bDrag.iid);if(!it)return;
        it.endTime=Math.min(dur,Math.max(it.startTime+0.1,Math.round((bDrag.oe+dx)*10)/10));
        bar.style.width=Math.max(20,(it.endTime-it.startTime)*PPS)+'px';
      };
      const onU=()=>{snap();document.removeEventListener('mousemove',onM);document.removeEventListener('mouseup',onU);fullRender();if(selInt)selectInteraction(selInt);};
      document.addEventListener('mousemove',onM);document.addEventListener('mouseup',onU);
    });

    track.appendChild(bar);tracks.appendChild(track);
  });

  if(!ints.length){const e=document.createElement('div');e.style.cssText='padding:14px;text-align:center;font-size:11px;color:var(--text-dim);';e.textContent='No interactions. Click "+ Add" above.';tracks.appendChild(e);}
}

// ===== CANVAS PAN/ZOOM =====
let cPan=null;
document.getElementById('canvas').addEventListener('mousedown',e=>{
  // Don't intercept clicks on nodes, controls, SVG paths, or connection labels
  if(e.target.closest('.vnode')||e.target.closest('.canvas-controls')||e.target.closest('.conn-label-el'))return;
  if(e.target.tagName==='path'||e.target.closest('svg.conn-svg'))return;
  if(e.button===0){cPan={sx:e.clientX-cOff.x,sy:e.clientY-cOff.y};clearSel();}
});
document.addEventListener('mousemove',e=>{if(cPan){cOff.x=e.clientX-cPan.sx;cOff.y=e.clientY-cPan.sy;renderNodes();renderConns();}});
document.addEventListener('mouseup',()=>{cPan=null;});
document.getElementById('canvas').addEventListener('wheel',e=>{e.preventDefault();doZoom(e.deltaY>0?-0.06:0.06);},{passive:false});

function doZoom(d){cZoom=Math.max(0.3,Math.min(2,cZoom+d));document.getElementById('zoomLbl').textContent=Math.round(cZoom*100)+'%';renderNodes();renderConns();}
function fitView(){cZoom=1;cOff={x:60,y:60};document.getElementById('zoomLbl').textContent='100%';renderNodes();renderConns();}

// ===== KEYBOARD =====
document.addEventListener('keydown',e=>{
  if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;
  if(e.key==='Delete'||e.key==='Backspace'){if(selConn)delConn(selConn);else if(selInt)removeInt(selInt);else if(selNode)deleteNode(selNode);}
  if(e.ctrlKey&&e.key==='z'){e.preventDefault();doUndo();}
  if(e.ctrlKey&&e.key==='y'){e.preventDefault();doRedo();}
});
document.addEventListener('click',hideCtx);

// ===== SETTINGS =====
function saveSettings(){
  var cm=document.getElementById('sCtrlMode').value;
  var prev=project.settings||{};
  project.settings={
    showControls:document.getElementById('sShowCtrl').checked,
    controlMode:cm,
    controlPosition:document.getElementById('sCtrlPos').value,
    autoHide:cm==='custom'?(document.getElementById('sAutoHide2')||{}).checked:document.getElementById('sAutoHide').checked,
    allowSeeking:cm==='custom'?(document.getElementById('sSeek2')||{}).checked:document.getElementById('sSeek').checked,
    showProgress:cm==='custom'?(document.getElementById('sProgress2')||{}).checked:document.getElementById('sProgress').checked,
    showPlayBtn:(document.getElementById('sShowPlay')||{checked:true}).checked,
    showPauseBtn:(document.getElementById('sShowPause')||{checked:true}).checked,
    showRewindBtn:(document.getElementById('sShowRewind')||{checked:true}).checked,
    showForwardBtn:(document.getElementById('sShowForward')||{checked:true}).checked,
    showMuteBtn:(document.getElementById('sShowMute')||{checked:true}).checked,
    showFsBtn:(document.getElementById('sShowFs')||{checked:true}).checked,
    showReplayBtn:(document.getElementById('sShowReplay')||{checked:true}).checked,
    accentColor:document.getElementById('sAccent').value,
    playIcon:document.getElementById('sPlayIcon').value,
    pauseIcon:document.getElementById('sPauseIcon').value,
    seekIcon:document.getElementById('sSeekIcon').value,
    muteIcon:document.getElementById('sMuteIcon').value,
    fsIcon:document.getElementById('sFsIcon').value,
    replayIcon:document.getElementById('sReplayIcon').value,
    overlayImg:document.getElementById('sOvImg').value,
    overlayImgSize:document.getElementById('sOvImgSize').value,
    overlayImgPos:document.getElementById('sOvImgPos').value,
    overlayImgOpacity:+document.getElementById('sOvImgOp').value,
    endBehavior:document.getElementById('sEnd').value,
    videoFit:document.getElementById('sVideoFit').value,
    videoBasePath:document.getElementById('sVideoBase').value||'./videos/',
    transition:document.getElementById('sTransition').value,
    transitionDuration:+document.getElementById('sTransDur').value||600,
    choiceTitle:document.getElementById('sChoiceTitle').value,
    choiceSubtitle:document.getElementById('sChoiceSub').value,
    showChoiceOverlay:document.getElementById('sChoiceShow').checked,
    loadingText:document.getElementById('sLoadText').value,
    loadingBg:document.getElementById('sLoadBg').value,
    autoPlay:document.getElementById('sAutoPlay').checked,
    audioSource:document.getElementById('sAudioSrc').value,
    bgmFile:document.getElementById('sAudioFile').value,
    bgmVolume:+document.getElementById('sAudioVol').value,
    // Preserve ctrlPositions from previous save (set by position editor)
    ctrlPositions:prev.ctrlPositions||null
  };
}

function toggleCtrlUI(){
  var mode=document.getElementById('sCtrlMode').value;
  document.getElementById('standardCtrlOpts').style.display=mode==='standard'?'block':'none';
  document.getElementById('customCtrlOpts').style.display=mode==='custom'?'block':'none';
}

function handleOverlayImg(input){
  if(!input.files||!input.files[0])return;
  var r=new FileReader();r.onload=function(e){
    document.getElementById('sOvImg').value=e.target.result;
    document.getElementById('sOvImgPreview').innerHTML='<img src="'+e.target.result+'" style="height:20px;border-radius:2px">';
    saveSettings();
  };r.readAsDataURL(input.files[0]);
}
function clearOverlayImg(){document.getElementById('sOvImg').value='';document.getElementById('sOvImgPreview').textContent='None';saveSettings();}

function handleNodeOvImg(input,nid){
  if(!input.files||!input.files[0])return;
  var r=new FileReader();r.onload=function(e){snap();var n=nodes.find(x=>x.nodeId===nid);if(n)n.overlayImg=e.target.result;selectNode(nid);};
  r.readAsDataURL(input.files[0]);
}

function handleIconUpload(input,type){
  if(!input.files||!input.files[0])return;
  const r=new FileReader();
  r.onload=e=>{
    const data=e.target.result;
    if(type==='play'){document.getElementById('sPlayIcon').value=data;document.getElementById('sPlayPreview').innerHTML=`<img src="${data}" style="height:20px;border-radius:2px">`;}
    else if(type==='pause'){document.getElementById('sPauseIcon').value=data;document.getElementById('sPausePreview').innerHTML=`<img src="${data}" style="height:20px;border-radius:2px">`;}
    else if(type==='seek'){document.getElementById('sSeekIcon').value=data;document.getElementById('sSeekPreview').innerHTML=`<img src="${data}" style="height:20px;border-radius:2px">`;}
    else if(type==='replay'){document.getElementById('sReplayIcon').value=data;document.getElementById('sReplayPreview').innerHTML=`<img src="${data}" style="height:20px;border-radius:2px">`;}
    else if(type==='mute'){document.getElementById('sMuteIcon').value=data;document.getElementById('sMutePreview').innerHTML=`<img src="${data}" style="height:20px;border-radius:2px">`;}
    else if(type==='fs'){document.getElementById('sFsIcon').value=data;document.getElementById('sFsPreview').innerHTML=`<img src="${data}" style="height:20px;border-radius:2px">`;}
    saveSettings();
  };
  r.readAsDataURL(input.files[0]);
}
function clearIconUpload(type){
  if(type==='play'){document.getElementById('sPlayIcon').value='';document.getElementById('sPlayPreview').textContent='Default';}
  else if(type==='pause'){document.getElementById('sPauseIcon').value='';document.getElementById('sPausePreview').textContent='Default';}
  else if(type==='seek'){document.getElementById('sSeekIcon').value='';document.getElementById('sSeekPreview').textContent='Default';}
  else if(type==='replay'){document.getElementById('sReplayIcon').value='';document.getElementById('sReplayPreview').textContent='Default';}
  else if(type==='mute'){document.getElementById('sMuteIcon').value='';document.getElementById('sMutePreview').textContent='Default';}
  else if(type==='fs'){document.getElementById('sFsIcon').value='';document.getElementById('sFsPreview').textContent='Default';}
  saveSettings();
}

// ===== POSITION EDITOR =====
let posIntId=null;
const VP_SIZES={desktop:{w:1920,h:1080},tablet:{w:1024,h:768},mobileP:{w:390,h:844},mobileL:{w:844,h:390}};
let curVP='desktop';
let posUnit='%';
let vpData={desktop:{x:50,y:50,w:15,h:8},tablet:{x:50,y:50,w:15,h:8},mobileP:{x:50,y:50,w:20,h:6},mobileL:{x:50,y:50,w:15,h:8}};

function openPosEditor(iid){
  const intr=interactions.find(i=>i.intId===iid);if(!intr)return;
  posIntId=iid;
  document.getElementById('posTitle').textContent=intr.label||intr.type;
  const saved=intr.vpPos||{};
  const def={x:intr.x||50,y:intr.y||50,w:intr.w||15,h:intr.h||8};
  ['desktop','tablet','mobileP','mobileL'].forEach(vp=>{vpData[vp]=saved[vp]?{...saved[vp]}:{...def};});
  const el=document.getElementById('posDragEl');
  el.style.backgroundColor=intr.style?.transparent?'transparent':(intr.style?.bg||'#4a9eff');
  el.style.color=intr.style?.color||'#fff';
  Array.from(el.childNodes).forEach(n=>{if(n.nodeType===3||n.tagName==='SPAN')n.remove();});
  const lbl=document.createElement('span');lbl.textContent=intr.label||intr.type;lbl.style.cssText='pointer-events:none;position:relative;z-index:1';
  el.insertBefore(lbl,el.querySelector('.pos-handle'));
  if(intr.style?.image){el.style.backgroundImage='url('+intr.style.image+')';el.style.backgroundSize='cover';}
  else{el.style.backgroundImage='none';}
  // Set video frame as background of the position editor
  const node=nodes.find(n=>n.nodeId===intr.nodeId);
  const frame=document.getElementById('posFrame');
  if(node){
    const vidSrc=node.blobUrl||node.src||(project.settings.videoBasePath||'./videos/')+(node.filename||node.name+'.mp4');
    frame.style.background='#111';
    // Use a video element as bg via poster or canvas capture
    let bgVid=frame.querySelector('.pos-bg-video');
    if(!bgVid){bgVid=document.createElement('video');bgVid.className='pos-bg-video';bgVid.style.cssText='position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0.4;pointer-events:none;z-index:0';bgVid.muted=true;frame.insertBefore(bgVid,frame.firstChild);}
    bgVid.src=vidSrc;bgVid.currentTime=0.5;
  }
  posUnit='%';document.getElementById('posUnitBtn').textContent='%';
  document.getElementById('modalPos').classList.add('show');
  curVP=null;switchVP('desktop');
}

function togglePosUnit(){
  saveCurrentVP();const vp=VP_SIZES[curVP];const d=vpData[curVP];
  if(posUnit==='%'){posUnit='px';document.getElementById('posUnitBtn').textContent='px';
    document.getElementById('posX').value=Math.round(d.x/100*vp.w);document.getElementById('posY').value=Math.round(d.y/100*vp.h);
    document.getElementById('posW').value=Math.round(d.w/100*vp.w);document.getElementById('posH').value=Math.round(d.h/100*vp.h);
  }else{posUnit='%';document.getElementById('posUnitBtn').textContent='%';
    document.getElementById('posX').value=Math.round(d.x);document.getElementById('posY').value=Math.round(d.y);
    document.getElementById('posW').value=Math.round(d.w);document.getElementById('posH').value=Math.round(d.h);
  }
  renderPosEl();
}

function saveCurrentVP(){
  if(!curVP)return;const vp=VP_SIZES[curVP];
  if(posUnit==='px'){
    vpData[curVP]={x:Math.round(+document.getElementById('posX').value/vp.w*100),y:Math.round(+document.getElementById('posY').value/vp.h*100),w:Math.round(+document.getElementById('posW').value/vp.w*100),h:Math.round(+document.getElementById('posH').value/vp.h*100)};
  }else{
    vpData[curVP]={x:+document.getElementById('posX').value||50,y:+document.getElementById('posY').value||50,w:+document.getElementById('posW').value||15,h:+document.getElementById('posH').value||8};
  }
}

function switchVP(vp){
  if(curVP&&posIntId)saveCurrentVP();
  curVP=vp;
  document.querySelectorAll('[id^="vp"]').forEach(b=>b.classList.remove('primary'));
  ({desktop:'vpDesk',tablet:'vpTab',mobileP:'vpMobP',mobileL:'vpMobL'})[vp]&&document.getElementById(({desktop:'vpDesk',tablet:'vpTab',mobileP:'vpMobP',mobileL:'vpMobL'})[vp]).classList.add('primary');
  const sz=VP_SIZES[vp];const container=document.getElementById('posViewport');
  const maxW=container.clientWidth-16,maxH=420;
  const scale=Math.min(maxW/sz.w,maxH/sz.h);
  const frame=document.getElementById('posFrame');
  frame.style.width=Math.round(sz.w*scale)+'px';frame.style.height=Math.round(sz.h*scale)+'px';
  const d=vpData[vp];
  if(posUnit==='px'){
    document.getElementById('posX').value=Math.round(d.x/100*sz.w);document.getElementById('posY').value=Math.round(d.y/100*sz.h);
    document.getElementById('posW').value=Math.round(d.w/100*sz.w);document.getElementById('posH').value=Math.round(d.h/100*sz.h);
  }else{
    document.getElementById('posX').value=Math.round(d.x);document.getElementById('posY').value=Math.round(d.y);
    document.getElementById('posW').value=Math.round(d.w);document.getElementById('posH').value=Math.round(d.h);
  }
  renderPosEl();
}

function renderPosEl(){
  const frame=document.getElementById('posFrame'),el=document.getElementById('posDragEl');
  const d=vpData[curVP];const fw=frame.clientWidth,fh=frame.clientHeight;
  el.style.width=Math.round(d.w/100*fw)+'px';el.style.height=Math.round(d.h/100*fh)+'px';
  el.style.left=Math.round((d.x-d.w/2)/100*fw)+'px';el.style.top=Math.round((d.y-d.h/2)/100*fh)+'px';
  document.getElementById('posCoords').textContent=curVP+': x:'+Math.round(d.x)+'% y:'+Math.round(d.y)+'% w:'+Math.round(d.w)+'% h:'+Math.round(d.h)+'%';
  el.onmousedown=e=>{
    if(e.target.classList.contains('pos-handle')){
      e.preventDefault();e.stopPropagation();
      const cls=e.target.className;const sL=el.offsetLeft,sT=el.offsetTop,sW=el.clientWidth,sH=el.clientHeight;
      const onM=ev=>{
        let nL=sL,nT=sT,nW=sW,nH=sH;const dx=ev.clientX-e.clientX,dy=ev.clientY-e.clientY;
        if(cls.includes('h-br')){nW=Math.max(20,sW+dx);nH=Math.max(14,sH+dy);}
        else if(cls.includes('h-bl')){nL=sL+dx;nW=Math.max(20,sW-dx);nH=Math.max(14,sH+dy);}
        else if(cls.includes('h-tr')){nT=sT+dy;nW=Math.max(20,sW+dx);nH=Math.max(14,sH-dy);}
        else if(cls.includes('h-tl')){nL=sL+dx;nT=sT+dy;nW=Math.max(20,sW-dx);nH=Math.max(14,sH-dy);}
        else if(cls.includes('h-tm')){nT=sT+dy;nH=Math.max(14,sH-dy);}
        else if(cls.includes('h-bm')){nH=Math.max(14,sH+dy);}
        else if(cls.includes('h-ml')){nL=sL+dx;nW=Math.max(20,sW-dx);}
        else if(cls.includes('h-mr')){nW=Math.max(20,sW+dx);}
        nL=Math.max(0,nL);nT=Math.max(0,nT);
        el.style.left=nL+'px';el.style.top=nT+'px';el.style.width=nW+'px';el.style.height=nH+'px';
        const wp=nW/fw*100,hp=nH/fh*100,xp=nL/fw*100+wp/2,yp=nT/fh*100+hp/2;
        vpData[curVP]={x:Math.round(xp*10)/10,y:Math.round(yp*10)/10,w:Math.round(wp*10)/10,h:Math.round(hp*10)/10};
        updPosInputs();
      };
      const onU=()=>{document.removeEventListener('mousemove',onM);document.removeEventListener('mouseup',onU);};
      document.addEventListener('mousemove',onM);document.addEventListener('mouseup',onU);return;
    }
    e.preventDefault();const fr=frame.getBoundingClientRect();
    const ox=e.clientX-el.getBoundingClientRect().left,oy=e.clientY-el.getBoundingClientRect().top;
    const onM=ev=>{
      const cx=Math.max(0,Math.min(fw-el.clientWidth,ev.clientX-fr.left-ox));
      const cy=Math.max(0,Math.min(fh-el.clientHeight,ev.clientY-fr.top-oy));
      el.style.left=cx+'px';el.style.top=cy+'px';
      const wp=el.clientWidth/fw*100,hp=el.clientHeight/fh*100;
      vpData[curVP].x=Math.round((cx/fw*100+wp/2)*10)/10;vpData[curVP].y=Math.round((cy/fh*100+hp/2)*10)/10;
      updPosInputs();
    };
    const onU=()=>{document.removeEventListener('mousemove',onM);document.removeEventListener('mouseup',onU);};
    document.addEventListener('mousemove',onM);document.addEventListener('mouseup',onU);
  };
}

function updPosInputs(){
  const d=vpData[curVP],vp=VP_SIZES[curVP];
  if(posUnit==='px'){
    document.getElementById('posX').value=Math.round(d.x/100*vp.w);document.getElementById('posY').value=Math.round(d.y/100*vp.h);
    document.getElementById('posW').value=Math.round(d.w/100*vp.w);document.getElementById('posH').value=Math.round(d.h/100*vp.h);
  }else{
    document.getElementById('posX').value=Math.round(d.x);document.getElementById('posY').value=Math.round(d.y);
    document.getElementById('posW').value=Math.round(d.w);document.getElementById('posH').value=Math.round(d.h);
  }
  document.getElementById('posCoords').textContent=curVP+': x:'+Math.round(d.x)+'% y:'+Math.round(d.y)+'% w:'+Math.round(d.w)+'% h:'+Math.round(d.h)+'%';
}

function updatePosFromInput(){saveCurrentVP();renderPosEl();}

function savePosEditor(){
  if(!posIntId)return;saveCurrentVP();snap();
  const intr=interactions.find(i=>i.intId===posIntId);
  if(intr){
    const dd=vpData.desktop;intr.x=dd.x;intr.y=dd.y;intr.w=dd.w;intr.h=dd.h;
    intr.vpPos={desktop:{...vpData.desktop},tablet:{...vpData.tablet},mobileP:{...vpData.mobileP},mobileL:{...vpData.mobileL}};
  }
  closeMod('modalPos');fullRender();selectInteraction(posIntId);toast('Positions saved for all viewports');
}

// ===== SERVER BROWSER =====
function openServerBrowser(){
  document.getElementById('srvPath').value=project.settings.videoBasePath||'./videos/';
  document.getElementById('modalServer').classList.add('show');
}
function scanServerDir(){
  const path=document.getElementById('srvPath').value.trim();
  if(!path){toast('Enter a path','err');return;}
  const list=document.getElementById('srvList');
  list.innerHTML='<div style="padding:20px;text-align:center"><div class="spin" style="width:24px;height:24px;border-width:2px;margin:0 auto"></div><div style="margin-top:8px;font-size:11px;color:var(--text-dim)">Scanning...</div></div>';
  // Try fetching as directory listing (HTML) or JSON index
  fetch(path).then(r=>r.text()).then(html=>{
    const files=[];
    const dirs=[];
    // Try JSON first
    try{
      const json=JSON.parse(html);
      if(Array.isArray(json))json.forEach(f=>{if(typeof f==='string')files.push(f);else if(f.name)files.push(f.name);});
    }catch(e){
      // Parse HTML directory listing (Apache/nginx style)
      const parser=new DOMParser();
      const doc=parser.parseFromString(html,'text/html');
      doc.querySelectorAll('a').forEach(a=>{
        const href=a.getAttribute('href');
        if(!href||href==='../'||href==='../') return;
        if(href.endsWith('/'))dirs.push(href);
        else if(/\.(mp4|webm|mov|avi|mkv|m4v)$/i.test(href))files.push(href);
      });
    }
    renderServerList(path,dirs,files);
  }).catch(e=>{
    list.innerHTML='<div style="padding:20px;text-align:center;color:var(--accent-red);font-size:12px"><i class="bi bi-exclamation-circle"></i> Could not access directory. Make sure directory listing is enabled on your server, or place a JSON index file.<br><br><span style="color:var(--text-dim)">'+e.message+'</span></div>';
  });
}
function renderServerList(basePath,dirs,files){
  const list=document.getElementById('srvList');
  if(!dirs.length&&!files.length){
    list.innerHTML='<div style="padding:20px;text-align:center;color:var(--text-dim);font-size:12px">No video files found in this directory</div>';
    return;
  }
  let html='';
  // Parent dir
  if(basePath!=='./videos/'&&basePath!=='/'){
    const parent=basePath.replace(/[^/]+\/$/,'')||'./';
    html+=`<div class="lib-item" onclick="document.getElementById('srvPath').value='${parent}';scanServerDir()" style="cursor:pointer"><div class="thumb" style="background:var(--accent-blue-dim);color:var(--accent-blue)"><i class="bi bi-arrow-up"></i></div><div class="info"><div class="name">..</div><div class="meta">Parent directory</div></div></div>`;
  }
  dirs.forEach(d=>{
    const fullPath=basePath+(basePath.endsWith('/')?'':'/')+d;
    html+=`<div class="lib-item" onclick="document.getElementById('srvPath').value='${fullPath}';scanServerDir()" style="cursor:pointer"><div class="thumb" style="background:var(--accent-blue-dim);color:var(--accent-blue)"><i class="bi bi-folder-fill"></i></div><div class="info"><div class="name">${d}</div><div class="meta">Directory</div></div></div>`;
  });
  files.forEach(f=>{
    const fullPath=basePath+(basePath.endsWith('/')?'':'/')+f;
    html+=`<div class="lib-item" style="cursor:pointer" onclick="addServerVideo('${fullPath}','${f}')"><div class="thumb"><video src="${fullPath}" muted preload="metadata" style="width:100%;height:100%;object-fit:cover"></video></div><div class="info"><div class="name">${f}</div><div class="meta">${fullPath}</div></div><div class="actions"><button onclick="event.stopPropagation();addServerVideo('${fullPath}','${f}')" style="background:none;border:none;color:var(--accent-green);cursor:pointer;font-size:14px"><i class="bi bi-plus-circle"></i></button></div></div>`;
  });
  list.innerHTML=html;
}
function addServerVideo(src,filename){
  const libId=gid();
  const name=filename.replace(/\.[^.]+$/,'');
  const item={libId,name,filename,blobUrl:'',src:src,duration:0};
  // Try to get duration
  const v=document.createElement('video');v.src=src;v.preload='metadata';
  v.onloadedmetadata=()=>{item.duration=v.duration;renderLib();};
  library.push(item);
  renderLib();
  toast('Added: '+name);
}

// ===== CONTROL POSITION EDITOR =====
const CTRL_BTNS=['play','rewind','forward','mute','fullscreen','replay'];
const CTRL_LABELS={play:'▶ Play',rewind:'⏪ Rew',forward:'⏩ Fwd',mute:'🔊 Vol',fullscreen:'⛶ FS',replay:'↻ Replay'};
const CTRL_DEFAULTS={
  play:{x:50,y:50,w:5,h:8},rewind:{x:40,y:50,w:4,h:7},forward:{x:60,y:50,w:4,h:7},
  mute:{x:8,y:92,w:3,h:5},fullscreen:{x:95,y:92,w:3,h:5},replay:{x:50,y:50,w:5,h:8}
};
let ctrlVPData={};
let ctrlCurVP='desktop';

function openCtrlPosEditor(){
  // Load saved positions or defaults
  const saved=project.settings.ctrlPositions||{};
  ['desktop','tablet','mobileP','mobileL'].forEach(vp=>{
    ctrlVPData[vp]=saved[vp]?{...saved[vp]}:{};
    CTRL_BTNS.forEach(btn=>{
      if(!ctrlVPData[vp][btn])ctrlVPData[vp][btn]={...CTRL_DEFAULTS[btn]};
    });
  });
  document.getElementById('modalCtrlPos').classList.add('show');
  ctrlCurVP=null;
  switchCtrlVP('desktop');
}

function switchCtrlVP(vp){
  if(ctrlCurVP)saveCtrlVPFromDOM();
  ctrlCurVP=vp;
  ['vpDesk','vpTab','vpMobP','vpMobL'].forEach(id=>{
    const el=document.getElementById('c'+id);if(el)el.classList.remove('primary');
  });
  const map={desktop:'cvpDesk',tablet:'cvpTab',mobileP:'cvpMobP',mobileL:'cvpMobL'};
  document.getElementById(map[vp]).classList.add('primary');
  const sz=VP_SIZES[vp];
  const container=document.getElementById('ctrlPosViewport');
  const maxW=container.clientWidth-16,maxH=420;
  const scale=Math.min(maxW/sz.w,maxH/sz.h);
  const fw=Math.round(sz.w*scale),fh=Math.round(sz.h*scale);
  const frame=document.getElementById('ctrlPosFrame');
  frame.style.width=fw+'px';frame.style.height=fh+'px';
  renderCtrlBtns(frame,fw,fh);
}

function renderCtrlBtns(frame,fw,fh){
  frame.innerHTML='';
  const data=ctrlVPData[ctrlCurVP];
  CTRL_BTNS.forEach(btn=>{
    const d=data[btn]||CTRL_DEFAULTS[btn];
    const el=document.createElement('div');
    el.dataset.btn=btn;
    const icon=project.settings[btn+'Icon'];
    el.style.cssText='position:absolute;cursor:move;border:2px dashed rgba(255,255,255,0.5);border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:#fff;z-index:5;overflow:hidden;'
      +'left:'+Math.round((d.x-d.w/2)/100*fw)+'px;top:'+Math.round((d.y-d.h/2)/100*fh)+'px;'
      +'width:'+Math.round(d.w/100*fw)+'px;height:'+Math.round(d.h/100*fh)+'px;'
      +'background:rgba(0,0,0,0.7);';
    if(icon){el.innerHTML='<img src="'+icon+'" style="width:80%;height:80%;object-fit:contain">';}
    else{el.textContent=CTRL_LABELS[btn];}
    // Resize handle
    const rh=document.createElement('div');
    rh.className='ctrl-rh';
    rh.style.cssText='position:absolute;bottom:-4px;right:-4px;width:12px;height:12px;background:#fff;border:2px solid var(--accent-blue);border-radius:2px;cursor:se-resize;z-index:6';
    el.appendChild(rh);
    // Drag
    el.onmousedown=function(e){
      if(e.target===rh){
        e.preventDefault();e.stopPropagation();
        const sW=el.clientWidth,sH=el.clientHeight;
        const onM=function(ev){
          el.style.width=Math.max(16,sW+(ev.clientX-e.clientX))+'px';
          el.style.height=Math.max(12,sH+(ev.clientY-e.clientY))+'px';
        };
        const onU=function(){document.removeEventListener('mousemove',onM);document.removeEventListener('mouseup',onU);};
        document.addEventListener('mousemove',onM);document.addEventListener('mouseup',onU);
        return;
      }
      e.preventDefault();
      const fr=frame.getBoundingClientRect();
      const ox=e.clientX-el.getBoundingClientRect().left,oy=e.clientY-el.getBoundingClientRect().top;
      const onM=function(ev){
        el.style.left=Math.max(0,Math.min(fw-el.clientWidth,ev.clientX-fr.left-ox))+'px';
        el.style.top=Math.max(0,Math.min(fh-el.clientHeight,ev.clientY-fr.top-oy))+'px';
      };
      const onU=function(){document.removeEventListener('mousemove',onM);document.removeEventListener('mouseup',onU);};
      document.addEventListener('mousemove',onM);document.addEventListener('mouseup',onU);
    };
    frame.appendChild(el);
  });
}

function saveCtrlVPFromDOM(){
  const frame=document.getElementById('ctrlPosFrame');
  const fw=frame.clientWidth,fh=frame.clientHeight;
  frame.querySelectorAll('[data-btn]').forEach(el=>{
    const btn=el.dataset.btn;
    const wp=el.clientWidth/fw*100,hp=el.clientHeight/fh*100;
    const xp=el.offsetLeft/fw*100+wp/2,yp=el.offsetTop/fh*100+hp/2;
    ctrlVPData[ctrlCurVP][btn]={x:Math.round(xp*10)/10,y:Math.round(yp*10)/10,w:Math.round(wp*10)/10,h:Math.round(hp*10)/10};
  });
}

function saveCtrlPositions(){
  saveCtrlVPFromDOM();
  snap();
  project.settings.ctrlPositions={
    desktop:{...ctrlVPData.desktop},tablet:{...ctrlVPData.tablet},
    mobileP:{...ctrlVPData.mobileP},mobileL:{...ctrlVPData.mobileL}
  };
  closeMod('modalCtrlPos');
  toast('Control positions saved for all viewports');
}

// ===== EXPORT/IMPORT =====
function buildExport(){
  const basePath=project.settings.videoBasePath||'./videos/';
  const vids=nodes.map(n=>{
    const fn=n.filename||n.name+'.mp4';
    const src=n.src||(basePath+fn);
    return{id:n.nodeId,name:n.name,src:src,filename:fn,duration:n.duration,nodeX:n.x,nodeY:n.y,startTransition:n.startTransition||'',endTransition:n.endTransition||'',endBehavior:n.endBehavior||'',audioMode:n.audioMode||'',audioFile:n.audioFile||'',audioLoop:n.audioLoop!==false,overlayImg:n.overlayImg||''};
  });
  const conns=connections.map(c=>{const owner=portOwner(c.fromPortId);const isInt=!!interactions.find(i=>i.intId===c.fromPortId);return{id:c.connId,fromVideoId:owner,toVideoId:c.toNodeId,label:c.label||'',color:c.color,fromPortId:c.fromPortId,isDirectFlow:!isInt,transition:c.transition||''};});
  const ints=interactions.map(i=>{const cn=connections.find(c=>c.fromPortId===i.intId);const s=i.style||{};return{id:i.intId,videoId:i.nodeId,type:i.type,label:i.label,startTime:i.startTime,endTime:i.endTime,x:i.x,y:i.y,w:i.w,h:i.h,action:{type:cn?'jumpToVideo':(i.action?.type||'showOverlay'),targetVideoId:cn?cn.toNodeId:null,value:i.action?.value||'',pauseVideo:i.action?.pauseVideo!==false},style:{backgroundColor:s.transparent?'transparent':s.bg||'#4a9eff',textColor:s.color||'#fff',icon:s.icon||'',image:s.image||'',borderWidth:s.borderWidth||0,borderColor:s.borderColor||'#fff',borderRadius:s.borderRadius||8,shadow:s.shadow||'',transparent:!!s.transparent,animateIn:s.animateIn||'',animateOut:s.animateOut||'',animateAttention:s.animateAttention||''},vpPos:i.vpPos||null};});
  // Strip blobUrls from _admin (session-only, can't survive save/load)
  // Sync project name from input
  project.name=document.getElementById('projectName').value;
  const adminLib=library.map(l=>({libId:l.libId,name:l.name,filename:l.filename||l.name+'.mp4',duration:l.duration}));
  const adminNodes=nodes.map(n=>({...n,blobUrl:undefined}));
  return{project:{...project,name:document.getElementById('projectName').value,startVideoId:project.startNodeId},videos:vids,connections:conns,interactions:ints,_admin:{library:adminLib,nodes:adminNodes,interactions,connections,project}};
}

function exportProject(){
  const name=document.getElementById('projectName').value.trim()||'project';
  const d=buildExport();const json=JSON.stringify(d,null,2);
  const blob=new Blob([json],{type:'application/json'});const url=URL.createObjectURL(blob);
  const ts=new Date().toISOString().replace(/[:.]/g,'-').slice(0,19);
  const fname=name.replace(/[^a-zA-Z0-9_-]/g,'_')+'_'+ts+'.json';
  const a=document.createElement('a');a.href=url;a.download=fname;a.click();URL.revokeObjectURL(url);toast('Exported: '+fname);
}

function showImportModal(){document.getElementById('importJson').value='';document.getElementById('modalImport').classList.add('show');}
function loadImpFile(f){if(!f)return;const r=new FileReader();r.onload=e=>{document.getElementById('importJson').value=e.target.result;};r.readAsText(f);}
function doImport(){
  try{
    const d=JSON.parse(document.getElementById('importJson').value);snap();
    if(d._admin){
      library=(d._admin.library||[]).map(l=>({...l,blobUrl:'',filename:l.filename||l.src?.split('/').pop()||l.name+'.mp4'}));
      nodes=(d._admin.nodes||[]).map(n=>({...n,blobUrl:'',filename:n.filename||n.src?.split('/').pop()||n.name+'.mp4'}));
      interactions=d._admin.interactions||[];
      connections=(d._admin.connections||[]).map(c=>({...c,transition:c.transition||''}));
      if(d._admin.project){
        project.name=d._admin.project.name||d.project?.name||'';
        project.startNodeId=d._admin.project.startNodeId||d.project?.startVideoId||null;
        if(d._admin.project.settings)project.settings={...project.settings,...d._admin.project.settings};
      }
    }else{
      if(d.videos)nodes=d.videos.map(v=>({nodeId:v.id,libId:'',name:v.name,filename:v.filename||v.src?.split('/').pop()||v.name+'.mp4',x:v.nodeX||0,y:v.nodeY||0,duration:v.duration||0,blobUrl:'',src:v.src||'',startTransition:v.startTransition||'',endTransition:v.endTransition||'',endBehavior:v.endBehavior||'',audioMode:v.audioMode||'',audioFile:v.audioFile||'',audioLoop:v.audioLoop!==false}));
      if(d.connections)connections=d.connections.map(c=>({connId:c.id,fromPortId:c.fromPortId||c.fromVideoId,toNodeId:c.toVideoId,label:c.label||'',color:c.color||'#4a9eff',transition:c.transition||''}));
      if(d.interactions)interactions=d.interactions.map(i=>({intId:i.id,nodeId:i.videoId,type:i.type,label:i.label,startTime:i.startTime,endTime:i.endTime,x:i.x,y:i.y,w:i.w,h:i.h,vpPos:i.vpPos||null,action:{type:i.action?.type,value:i.action?.value,pauseVideo:i.action?.pauseVideo},style:{bg:i.style?.backgroundColor,color:i.style?.textColor,icon:i.style?.icon,image:i.style?.image,transparent:i.style?.transparent,borderWidth:i.style?.borderWidth,borderColor:i.style?.borderColor,borderRadius:i.style?.borderRadius,shadow:i.style?.shadow,animateIn:i.style?.animateIn,animateOut:i.style?.animateOut,animateAttention:i.style?.animateAttention}}));
      if(d.project){project.name=d.project.name||'';project.startNodeId=d.project.startVideoId;Object.assign(project.settings,d.project.settings||{});}
    }
    document.getElementById('projectName').value=project.name;
    loadSettingsToUI();
    closeMod('modalImport');fullRender();renderLib();toast('Imported');
  }catch(e){toast('Invalid JSON: '+e.message,'err');}
}

function loadSettingsToUI(){
  var s=project.settings;
  document.getElementById('sShowCtrl').checked=s.showControls!==false;
  document.getElementById('sCtrlMode').value=s.controlMode||'standard';
  document.getElementById('sCtrlPos').value=s.controlPosition||'bottom';
  document.getElementById('sAutoHide').checked=s.autoHide!==false;
  document.getElementById('sSeek').checked=s.allowSeeking!==false;
  document.getElementById('sProgress').checked=s.showProgress!==false;
  if(document.getElementById('sAutoHide2'))document.getElementById('sAutoHide2').checked=s.autoHide!==false;
  if(document.getElementById('sProgress2'))document.getElementById('sProgress2').checked=s.showProgress!==false;
  if(document.getElementById('sShowPlay'))document.getElementById('sShowPlay').checked=s.showPlayBtn!==false;
  if(document.getElementById('sShowPause'))document.getElementById('sShowPause').checked=s.showPauseBtn!==false;
  if(document.getElementById('sShowRewind'))document.getElementById('sShowRewind').checked=s.showRewindBtn!==false;
  if(document.getElementById('sShowForward'))document.getElementById('sShowForward').checked=s.showForwardBtn!==false;
  if(document.getElementById('sShowMute'))document.getElementById('sShowMute').checked=s.showMuteBtn!==false;
  if(document.getElementById('sShowFs'))document.getElementById('sShowFs').checked=s.showFsBtn!==false;
  if(document.getElementById('sShowReplay'))document.getElementById('sShowReplay').checked=s.showReplayBtn!==false;
  document.getElementById('sAccent').value=s.accentColor||'#4a9eff';
  document.getElementById('sPlayIcon').value=s.playIcon||'';
  document.getElementById('sPauseIcon').value=s.pauseIcon||'';
  document.getElementById('sSeekIcon').value=s.seekIcon||'';
  document.getElementById('sMuteIcon').value=s.muteIcon||'';
  document.getElementById('sFsIcon').value=s.fsIcon||'';
  document.getElementById('sReplayIcon').value=s.replayIcon||'';
  document.getElementById('sOvImg').value=s.overlayImg||'';
  document.getElementById('sOvImgSize').value=s.overlayImgSize||'cover';
  document.getElementById('sOvImgPos').value=s.overlayImgPos||'center';
  document.getElementById('sOvImgOp').value=s.overlayImgOpacity||1;
  document.getElementById('sAutoPlay').checked=s.autoPlay!==false;
  document.getElementById('sEnd').value=s.endBehavior||'pause';
  document.getElementById('sVideoFit').value=s.videoFit||'contain';
  document.getElementById('sVideoBase').value=s.videoBasePath||'./videos/';
  document.getElementById('sChoiceTitle').value=s.choiceTitle||'What do you choose?';
  document.getElementById('sChoiceSub').value=s.choiceSubtitle||'Select an option to continue';
  document.getElementById('sChoiceShow').checked=s.showChoiceOverlay!==false;
  document.getElementById('sLoadText').value=s.loadingText||'Loading...';
  document.getElementById('sLoadBg').value=s.loadingBg||'#000000';
  document.getElementById('sAudioSrc').value=s.audioSource||'video';
  document.getElementById('sAudioFile').value=s.bgmFile||'';
  document.getElementById('sAudioVol').value=s.bgmVolume||0.5;
  document.getElementById('sTransition').value=s.transition||'fade';
  document.getElementById('sTransDur').value=s.transitionDuration||600;
  // Previews
  var icons=['play','pause','seek','mute','fs','replay'];
  icons.forEach(function(k){var v=s[k+'Icon'];var el=document.getElementById('s'+k.charAt(0).toUpperCase()+k.slice(1)+'Preview');if(!el)return;if(v)el.innerHTML='<img src="'+v+'" style="height:20px">';else el.textContent='Default';});
  if(s.playIcon)document.getElementById('sPlayPreview').innerHTML='<img src="'+s.playIcon+'" style="height:20px">';
  if(s.replayIcon)document.getElementById('sReplayPreview').innerHTML='<img src="'+s.replayIcon+'" style="height:20px">';
  if(s.overlayImg)document.getElementById('sOvImgPreview').innerHTML='<img src="'+s.overlayImg+'" style="height:20px">';
  toggleCtrlUI();
}

function previewProject(){
  const d=buildExport();
  // Inject blobUrls for preview playback (these are session-only)
  d.videos.forEach(v=>{
    const node=nodes.find(n=>n.nodeId===v.id);
    if(node&&node.blobUrl) v.blobUrl=node.blobUrl;
  });
  const w=window.open('videoplayer.html','_blank');
  if(w){
    let sent=false;
    const send=()=>{if(sent)return;sent=true;w.postMessage({type:'loadProject',data:d},'*');};
    setTimeout(send,1200);
  }
  toast('Opening preview...');
}

// ===== MODAL =====
function closeMod(id){document.getElementById(id).classList.remove('show');}

// ===== HELPERS =====
function fmt(s){if(!s||isNaN(s))return '0:00';const m=Math.floor(s/60),sc=Math.floor(s%60);return m+':'+(sc<10?'0':'')+sc;}

// ===== FULL RENDER =====
function fullRender(){renderNodes();setTimeout(()=>renderConns(),15);renderLib();if(selNode)showTimeline(selNode);}

// ===== THEME =====
function toggleTheme(){
  const isLight=document.body.classList.toggle('light');
  document.getElementById('themeIcon').className=isLight?'bi bi-moon':'bi bi-sun';
  localStorage.setItem('ivs-theme',isLight?'light':'dark');
}
(function initTheme(){
  const saved=localStorage.getItem('ivs-theme');
  if(saved==='light'){document.body.classList.add('light');document.getElementById('themeIcon').className='bi bi-moon';}
})();

fullRender();
</script>
</body>
</html>