Your IP : 216.73.216.79


Current Path : /var/www/html/unity/development/ui-builder/
Upload File :
Current File : /var/www/html/unity/development/ui-builder/ui-builder.html

<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI Builder — Game Interface Designer</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.theme.default.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script>
<script src="https://unpkg.com/blockly/blockly_compressed.js"></script>
<script src="https://unpkg.com/blockly/blocks_compressed.js"></script>
<script src="https://unpkg.com/blockly/javascript_compressed.js"></script>
<script src="https://unpkg.com/blockly/msg/en.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Orbitron:wght@400;500;600;700&display=swap');

:root {
  --toolbar-height: 56px;
  --palette-width: 200px;
  --props-width: 280px;
  --bg-dark: #0a0c14;
  --bg-panel: rgba(18,20,30,0.95);
  --bg-panel-hover: #252836;
  --bg-input: rgba(15,17,23,0.8);
  --accent: #00e5a0;
  --accent-glow: rgba(0,229,160,0.3);
  --accent2: #a78bfa;
  --accent2-glow: rgba(167,139,250,0.3);
  --accent3: #f59e0b;
  --danger: #ff4d6a;
  --text-primary: #e8eaf0;
  --text-secondary: #7a7f94;
  --text-muted: #555a6e;
  --border-color: #2a2d3a;
  --canvas-bg: #14161f;
}

/* ── Light Theme ── */
[data-theme="light"] {
  --bg-dark: #d5d7e2;
  --bg-panel: rgba(245,245,250,0.96);
  --bg-panel-hover: #dddde8;
  --bg-input: rgba(255,255,255,0.8);
  --accent: #059669;
  --accent-glow: rgba(5,150,105,0.25);
  --accent2: #7c3aed;
  --accent2-glow: rgba(124,58,237,0.2);
  --accent3: #b45309;
  --danger: #dc2626;
  --text-primary: #1a1a2e;
  --text-secondary: #4a4a65;
  --text-muted: #888;
  --border-color: #c8c8d8;
  --canvas-bg: #cccdd6;
}
[data-theme="light"] .pp-input, [data-theme="light"] .pp-textarea,
[data-theme="light"] .pp-select { color:#1a1a2e; }
[data-theme="light"] #canvas-viewport { background:#e8e8f0; }
[data-theme="light"] .toast { background:rgba(255,255,255,0.95); }
[data-theme="light"] .pal-item .bi { color:var(--text-secondary); }

* { margin:0; padding:0; box-sizing:border-box; }
body {
  font-family:'Rajdhani',sans-serif;
  background: var(--bg-dark);
  color: var(--text-primary);
  overflow: hidden;
  height: 100vh;
  user-select: none;
}

/* ── Toolbar (top) ── */
#toolbar {
  position: fixed; top:0; left:0; right:0;
  height: var(--toolbar-height);
  background: var(--bg-panel);
  border-bottom: 1px solid var(--border-color);
  display: flex; align-items: center;
  padding: 0 12px; gap: 8px; z-index: 100;
}
.tb-logo {
  font-family:'Orbitron',sans-serif;
  font-size: 0.7rem; font-weight: 700;
  color: var(--accent); letter-spacing: 2px;
  margin-right: 12px; white-space: nowrap;
}
.tb-sep { width:1px; height:28px; background:var(--border-color); margin:0 4px; }
.tb-btn {
  display: flex; align-items: center; gap: 5px;
  padding: 6px 14px; border-radius: 8px;
  font-family:'Rajdhani',sans-serif; font-size: 0.75rem; font-weight: 600;
  border: 1px solid var(--border-color);
  background: transparent; color: var(--text-secondary);
  cursor: pointer; transition: all 0.15s; white-space: nowrap;
}
.tb-btn:hover { background: var(--bg-panel-hover); color: var(--text-primary); }
.tb-btn.active { background: rgba(0,229,160,0.1); border-color: var(--accent); color: var(--accent); }
.tb-btn.accent2 { border-color: var(--accent2); color: var(--accent2); }
.tb-btn.accent2:hover { background: rgba(167,139,250,0.1); }
.tb-btn.accent3 { border-color: var(--accent3); color: var(--accent3); }
.tb-btn.accent3:hover { background: rgba(245,158,11,0.1); }
.tb-btn.danger { border-color: var(--danger); color: var(--danger); }
.tb-btn.danger:hover { background: rgba(255,77,106,0.1); }
.tb-spacer { flex:1; }
.tb-name {
  font-family:'Rajdhani',sans-serif; font-size:0.8rem; font-weight:600;
  color:var(--text-primary); background:transparent; border:1px solid var(--border-color);
  border-radius:6px; padding:5px 10px; width:180px;
}
.tb-name:focus { outline:none; border-color:var(--accent); }

/* ── Device preview tabs ── */
.device-tabs { display:flex; gap:2px; background:rgba(0,0,0,0.3); border-radius:8px; padding:2px; }
.device-tab {
  padding:5px 10px; border-radius:6px; border:none;
  background:transparent; color:var(--text-muted); cursor:pointer;
  font-family:'Rajdhani',sans-serif; font-size:0.7rem; font-weight:600;
  transition:all 0.15s;
}
.device-tab.active { background:var(--accent); color:#000; }

/* ── Left palette ── */
#palette {
  position: fixed; top: var(--toolbar-height); left:0; bottom:0;
  width: var(--palette-width);
  background: var(--bg-panel);
  border-right: 1px solid var(--border-color);
  overflow: hidden; z-index: 50;
  display:flex; flex-direction:column;
}
/* ── Left panel tabs ── */
#pal-tabs {
  display:flex; flex-shrink:0;
  border-bottom:1px solid var(--border-color);
  background:rgba(0,0,0,0.2);
}
.pal-tab {
  flex:1; display:flex; align-items:center; justify-content:center; gap:5px;
  padding:9px 4px; border:none; background:transparent;
  color:var(--text-muted); cursor:pointer;
  font-family:'Orbitron',sans-serif; font-size:0.5rem;
  letter-spacing:1px; text-transform:uppercase;
  border-bottom:2px solid transparent;
  transition:all 0.15s;
}
.pal-tab .bi { font-size:0.75rem; }
.pal-tab:hover { color:var(--text-secondary); background:var(--bg-panel-hover); }
.pal-tab.active { color:var(--accent); border-bottom-color:var(--accent); background:rgba(0,229,160,0.05); }
.pal-tab-count {
  font-family:'Rajdhani',sans-serif; font-size:0.5rem; font-weight:700;
  background:rgba(255,255,255,0.08); color:var(--text-muted);
  border-radius:8px; padding:0 5px; min-width:16px; line-height:14px;
}
.pal-tab.active .pal-tab-count { background:rgba(0,229,160,0.15); color:var(--accent); }
.pal-tab-panel { display:none; flex:1; overflow-y:auto; }
.pal-tab-panel.active { display:block; }
.pal-cat {
  border-bottom: 1px solid var(--border-color);
}
.pal-cat-header {
  display:flex; align-items:center; gap:6px;
  padding: 10px 12px; cursor:pointer;
  font-size:0.7rem; font-weight:700; text-transform:uppercase;
  letter-spacing:1px; color:var(--accent);
  font-family:'Orbitron',sans-serif;
  transition: background 0.15s;
}
.pal-cat-header:hover { background: var(--bg-panel-hover); }
.pal-cat-header .bi { font-size:0.8rem; }
.pal-cat-header .arrow { margin-left:auto; transition:transform 0.2s; font-size:0.6rem; color:var(--text-muted); }
.pal-cat.collapsed .pal-cat-items { display:none; }
.pal-cat.collapsed .arrow { transform:rotate(-90deg); }
.pal-cat-items { padding:4px 8px 8px; display:grid; grid-template-columns:1fr 1fr; gap:4px; }
.pal-item {
  display:flex; flex-direction:column; align-items:center; gap:3px;
  padding:8px 4px; border-radius:8px;
  border:1px solid transparent;
  background:rgba(255,255,255,0.02);
  cursor:grab; transition:all 0.15s;
}
.pal-item:hover { background:rgba(0,229,160,0.06); border-color:rgba(0,229,160,0.2); }
.pal-item:active { cursor:grabbing; }
.pal-item .bi { font-size:1.1rem; color:var(--text-secondary); }
.pal-item span { font-size:0.55rem; color:var(--text-muted); text-align:center; line-height:1.2; }

/* ── Right properties panel ── */
#props-panel {
  position:fixed; top:var(--toolbar-height); right:0; bottom:0;
  width:var(--props-width);
  background:var(--bg-panel);
  border-left:1px solid var(--border-color);
  overflow:hidden; z-index:50;
  display:none;
}
#props-panel.visible { display:flex; flex-direction:column; }
#pp-context { flex-shrink:0; }
/* ── Right panel tabs (Style / Properties) ── */
#pp-tabs {
  display:flex; flex-shrink:0;
  border-top:1px solid var(--border-color);
  border-bottom:1px solid var(--border-color);
  background:rgba(0,0,0,0.2);
}
.pp-tab {
  flex:1; display:flex; align-items:center; justify-content:center; gap:5px;
  padding:8px 4px; border:none; background:transparent;
  color:var(--text-muted); cursor:pointer;
  font-family:'Orbitron',sans-serif; font-size:0.48rem;
  letter-spacing:1px; text-transform:uppercase;
  border-bottom:2px solid transparent; transition:all 0.15s;
}
.pp-tab .bi { font-size:0.72rem; }
.pp-tab:hover { color:var(--text-secondary); background:var(--bg-panel-hover); }
.pp-tab.active { color:var(--accent); border-bottom-color:var(--accent); background:rgba(0,229,160,0.05); }
#pp-content { flex:1; overflow-y:auto; }
.pp-tab-panel { display:none; }
.pp-tab-panel.active { display:block; }
.pp-header {
  display:flex; align-items:center; justify-content:space-between;
  padding:12px 14px; border-bottom:1px solid var(--border-color);
}
.pp-header h6 {
  font-family:'Orbitron',sans-serif; font-size:0.6rem;
  letter-spacing:1.5px; text-transform:uppercase; color:var(--accent);
  margin:0;
}
.pp-close {
  background:none; border:none; color:var(--text-muted);
  cursor:pointer; font-size:0.9rem; opacity:0.6;
}
.pp-close:hover { opacity:1; }
.pp-section {
  padding:10px 14px; border-bottom:1px solid var(--border-color);
}
.pp-section h6 {
  font-family:'Orbitron',sans-serif; font-size:0.5rem;
  letter-spacing:1px; text-transform:uppercase; color:var(--accent3);
  margin-bottom:8px;
}
.pp-row {
  display:flex; align-items:center; gap:6px; margin-bottom:5px;
}
.pp-label {
  font-size:0.65rem; color:var(--text-secondary); min-width:55px;
  flex-shrink:0;
}
.pp-input {
  flex:1; background:var(--bg-input); border:1px solid var(--border-color);
  border-radius:4px; padding:4px 6px; font-size:0.7rem;
  color:var(--text-primary); font-family:'Rajdhani',sans-serif;
  min-width:0;
}
.pp-input:focus { outline:none; border-color:var(--accent); }
.pp-select {
  flex:1; background:var(--bg-input); border:1px solid var(--border-color);
  border-radius:4px; padding:4px 6px; font-size:0.7rem;
  color:var(--text-primary); font-family:'Rajdhani',sans-serif;
  cursor:pointer;
}
.pp-textarea {
  width:100%; background:var(--bg-input); border:1px solid var(--border-color);
  border-radius:4px; padding:6px; font-size:0.7rem;
  color:var(--text-primary); font-family:'Rajdhani',sans-serif;
  resize:vertical; min-height:50px;
}
.pp-textarea:focus { outline:none; border-color:var(--accent); }
.pp-color {
  width:32px; height:24px; border:1px solid var(--border-color);
  border-radius:4px; cursor:pointer; padding:1px;
}
.pp-slider {
  flex:1; accent-color:var(--accent); cursor:pointer;
}
.pp-val {
  min-width:28px; text-align:right; font-size:0.65rem; color:var(--text-secondary);
}
.pp-check {
  width:15px; height:15px; accent-color:var(--accent); cursor:pointer;
}
.pp-btn {
  padding:6px 12px; border-radius:6px; font-family:'Rajdhani',sans-serif;
  font-size:0.7rem; font-weight:600; cursor:pointer; border:1px solid;
  transition:all 0.15s;
}

/* ── Canvas area ── */
#canvas-area {
  position: fixed;
  top: var(--toolbar-height);
  left: var(--palette-width);
  right: var(--props-width);
  bottom: 0;
  background: var(--canvas-bg);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
#canvas-area.props-hidden { right:0; }

#canvas-viewport {
  position: relative;
  background: #0f1118;
  border: 1px solid var(--border-color);
  overflow: hidden;
  transition: width 0.3s, height 0.3s;
  box-shadow: 0 0 40px rgba(0,0,0,0.5);
}

/* Grid overlay */
#canvas-viewport.show-grid::before {
  content:'';
  position:absolute; top:0; left:0; right:0; bottom:0;
  background-image:
    linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
  background-size: 5% 5%;
  pointer-events:none; z-index:1;
}

/* Safe area guides */
#canvas-viewport.show-guides::after {
  content:'';
  position:absolute;
  top:5%; left:5%; right:5%; bottom:5%;
  border:1px dashed rgba(0,229,160,0.15);
  pointer-events:none; z-index:1;
}

/* ── Canvas elements ── */
.ui-el {
  position: absolute;
  cursor: move;
  outline: 2px solid transparent;
  outline-offset: 2px;
  transition: outline-color 0.1s;
  z-index: 10;
}
.ui-el:hover { outline-color: rgba(0,229,160,0.25); }
.ui-el.selected {
  outline-color: var(--accent) !important;
  outline-style: solid;
}
.ui-el.locked { cursor:default; }

/* Resize handles */
.resize-handle {
  position:absolute; width:8px; height:8px;
  background:var(--accent); border:1px solid #000;
  border-radius:2px; z-index:20; display:none;
}
.ui-el.selected .resize-handle { display:block; }
.rh-tl { top:-4px; left:-4px; cursor:nwse-resize; }
.rh-tr { top:-4px; right:-4px; cursor:nesw-resize; }
.rh-bl { bottom:-4px; left:-4px; cursor:nesw-resize; }
.rh-br { bottom:-4px; right:-4px; cursor:nwse-resize; }
.rh-t { top:-4px; left:50%; transform:translateX(-50%); cursor:ns-resize; }
.rh-b { bottom:-4px; left:50%; transform:translateX(-50%); cursor:ns-resize; }
.rh-l { top:50%; left:-4px; transform:translateY(-50%); cursor:ew-resize; }
.rh-r { top:50%; right:-4px; transform:translateY(-50%); cursor:ew-resize; }

/* ── Layer list (Hierarchy tab) ── */
#layer-list { padding:6px 0 12px; }
#layer-empty {
  display:none; padding:26px 16px; text-align:center;
  font-size:0.65rem; color:var(--text-muted); line-height:1.6;
}
#layer-empty span { font-size:0.58rem; opacity:0.7; }
#layer-list.empty #layer-empty { display:block; }
.layer-item {
  display:flex; align-items:center; gap:6px;
  padding:5px 12px; cursor:pointer; font-size:0.7rem;
  color:var(--text-secondary); transition:background 0.1s;
}
.layer-item:hover { background:var(--bg-panel-hover); }
.layer-item.selected { background:rgba(0,229,160,0.08); color:var(--accent); }
.layer-item .li-icon { font-size:0.75rem; opacity:0.6; }
.layer-item .li-name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.layer-item .li-vis { background:none; border:none; color:var(--text-muted); cursor:pointer; font-size:0.7rem; padding:2px; }
.layer-item .li-vis:hover { color:var(--text-primary); }

/* ── Carousel item editor (props panel) ── */
.car-ed-item {
  border:1px solid var(--border-color); border-radius:8px;
  margin-bottom:6px; background:rgba(255,255,255,0.02); overflow:hidden;
}
.car-ed-head {
  display:flex; align-items:center; gap:6px; padding:6px 8px;
  cursor:pointer; font-size:0.62rem; color:var(--text-secondary);
}
.car-ed-head:hover { background:var(--bg-panel-hover); }
.car-ed-head .car-ed-thumb {
  width:22px; height:22px; border-radius:4px; object-fit:cover;
  background:rgba(255,255,255,0.06); flex-shrink:0;
  display:flex; align-items:center; justify-content:center; font-size:0.6rem; color:var(--text-muted);
}
.car-ed-head .car-ed-name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.car-ed-head .car-ed-btn {
  background:none; border:none; color:var(--text-muted); cursor:pointer;
  font-size:0.6rem; padding:2px 3px;
}
.car-ed-head .car-ed-btn:hover { color:var(--text-primary); }
.car-ed-head .car-ed-btn.del:hover { color:var(--danger); }
.car-ed-body { display:none; padding:6px 8px 8px; border-top:1px solid var(--border-color); }
.car-ed-item.open .car-ed-body { display:block; }
.car-ed-add {
  width:100%; padding:7px; border-radius:8px; cursor:pointer;
  background:rgba(0,229,160,0.08); border:1px dashed var(--accent); color:var(--accent);
  font-family:'Rajdhani',sans-serif; font-size:0.62rem; font-weight:600;
}
.car-ed-add:hover { background:rgba(0,229,160,0.14); }

/* ── Toast ── */
#toast-container {
  position:fixed; bottom:20px; left:50%; transform:translateX(-50%);
  z-index:9999; display:flex; flex-direction:column; gap:6px; align-items:center;
}
.toast {
  padding:8px 20px; border-radius:8px; font-size:0.75rem;
  font-family:'Rajdhani',sans-serif; font-weight:600;
  background:var(--bg-panel); border:1px solid var(--accent);
  color:var(--accent); backdrop-filter:blur(12px);
  animation:fadeInUp 0.3s;
}
.toast.error { border-color:var(--danger); color:var(--danger); }
.toast.warn { border-color:var(--accent3); color:var(--accent3); }
@keyframes fadeInUp { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:translateY(0); } }

/* ── Drag ghost ── */
#drag-ghost {
  position:fixed; pointer-events:none; z-index:9000;
  padding:6px 14px; border-radius:8px;
  background:rgba(0,229,160,0.15); border:1px solid var(--accent);
  color:var(--accent); font-size:0.7rem; font-weight:600;
  display:none;
}

/* ── Settings Overlay ── */
#settings-overlay {
  position:fixed; inset:0; z-index:200;
  background:rgba(0,0,0,0.5); backdrop-filter:blur(4px);
  display:none; align-items:center; justify-content:center;
}
#settings-overlay.visible { display:flex; }
#settings-box {
  background:var(--bg-panel); border:1px solid var(--border-color);
  border-radius:16px; padding:24px; width:340px; max-height:80vh;
  overflow-y:auto; box-shadow:0 8px 40px rgba(0,0,0,0.5);
}
#settings-box h5 {
  font-family:'Orbitron',sans-serif; font-size:0.65rem;
  letter-spacing:2px; text-transform:uppercase; color:var(--accent);
  margin-bottom:16px;
}
.setting-row {
  display:flex; align-items:center; justify-content:space-between;
  padding:8px 0; border-bottom:1px solid var(--border-color);
}
.setting-row:last-child { border-bottom:none; }
.setting-label { font-size:0.7rem; color:var(--text-secondary); }
.toggle-switch {
  position:relative; width:40px; height:22px; cursor:pointer;
}
.toggle-switch input { opacity:0; width:0; height:0; }
.toggle-track {
  position:absolute; inset:0; background:var(--border-color);
  border-radius:11px; transition:background 0.2s;
}
.toggle-switch input:checked + .toggle-track { background:var(--accent); }
.toggle-track::after {
  content:''; position:absolute; left:3px; top:3px;
  width:16px; height:16px; border-radius:50%;
  background:white; transition:transform 0.2s;
}
.toggle-switch input:checked + .toggle-track::after { transform:translateX(18px); }

/* ── Action Chain Cards ── */
.act-chain { margin-top:6px; }
.act-card {
  background:rgba(255,255,255,0.02); border:1px solid var(--border-color);
  border-radius:8px; padding:8px; margin-bottom:4px;
}
.act-card-header {
  display:flex; align-items:center; justify-content:space-between;
  margin-bottom:4px;
}
.act-card-header span {
  font-family:'Orbitron',sans-serif; font-size:0.45rem;
  letter-spacing:1px; color:var(--accent3);
}
.act-remove {
  background:none; border:none; color:var(--danger);
  cursor:pointer; font-size:0.65rem; padding:0 2px;
  opacity:0.5;
}
.act-remove:hover { opacity:1; }

/* ── Parent-child layer indentation ── */
.layer-item.child { padding-left:28px; }
.layer-item.child .li-icon { font-size:0.6rem; }
.layer-item .li-parent-badge {
  font-size:0.45rem; background:rgba(167,139,250,0.15); color:var(--accent2);
  padding:1px 4px; border-radius:3px; margin-left:4px;
}

/* ── Panel child highlight ── */
.ui-el.drop-target { outline:2px dashed var(--accent2) !important; }
.pp-device-badge {
  display:inline-flex; align-items:center; gap:4px;
  font-family:'Orbitron',sans-serif; font-size:0.4rem;
  letter-spacing:1px; text-transform:uppercase;
  padding:3px 8px; border-radius:4px; margin-bottom:6px;
  background:rgba(167,139,250,0.1); border:1px solid var(--accent2);
  color:var(--accent2);
}
.pp-device-badge.desktop { background:rgba(0,229,160,0.08); border-color:var(--accent); color:var(--accent); }
.pp-row.has-override .pp-label::after {
  content:'●'; color:var(--accent2); margin-left:3px; font-size:0.5rem;
}
.pp-responsive-toggle {
  display:flex; align-items:center; gap:8px; padding:4px 0 8px;
  border-bottom:1px solid var(--border-color); margin-bottom:6px;
}
.pp-responsive-toggle span { font-size:0.55rem; color:var(--text-secondary); }

/* ── Code editor ── */
.code-textarea {
  width:100%; min-height:120px; background:#0d0f18; color:#9cdcfe;
  border:1px solid var(--border-color); border-radius:6px;
  font-family:'Courier New',monospace; font-size:0.65rem;
  padding:8px; resize:vertical; tab-size:2; line-height:1.5;
}
[data-theme="light"] .code-textarea { background:#f5f5fa; color:#1a1a2e; }

/* ── Code editor overlay (full) ── */
#code-overlay {
  position:fixed; inset:0; z-index:250;
  background:rgba(0,0,0,0.7); backdrop-filter:blur(6px);
  display:none; flex-direction:column;
}
#code-overlay.visible { display:flex; }
#code-header {
  display:flex; align-items:center; justify-content:space-between;
  padding:10px 16px; background:var(--bg-panel);
  border-bottom:1px solid var(--border-color);
}
#code-header h5 {
  font-family:'Orbitron',sans-serif; font-size:0.6rem;
  letter-spacing:2px; color:var(--accent); margin:0;
}
#code-header .code-btns { display:flex; gap:6px; }
#code-body { display:flex; flex:1; overflow:hidden; }
#code-editor-area {
  flex:1; display:flex; flex-direction:column; background:#0d0f18;
  border-right:1px solid var(--border-color);
}
[data-theme="light"] #code-editor-area { background:#f5f5fa; }
.code-tabs {
  display:flex; gap:0; background:var(--bg-panel);
  border-bottom:1px solid var(--border-color);
}
.code-tab {
  padding:6px 16px; font-family:'Orbitron',sans-serif; font-size:0.4rem;
  letter-spacing:1px; text-transform:uppercase; color:var(--text-muted);
  cursor:pointer; border:none; background:none; border-bottom:2px solid transparent;
  transition:color 0.2s, border-color 0.2s;
}
.code-tab:hover { color:var(--text-primary); }
.code-tab.active { color:var(--accent); border-bottom-color:var(--accent); }
.code-tab-content { display:none; flex:1; flex-direction:column; overflow:hidden; }
.code-tab-content.active { display:flex; }
.func-bar {
  display:flex; gap:6px; align-items:center; padding:4px 8px;
  background:rgba(0,0,0,0.15); border-bottom:1px solid var(--border-color);
}
.func-bar .func-chip {
  padding:3px 10px; font-size:0.45rem; border-radius:4px; cursor:pointer;
  background:rgba(255,255,255,0.04); border:1px solid var(--border-color);
  color:var(--text-secondary); font-family:'Courier New',monospace;
}
.func-bar .func-chip.active { background:rgba(0,229,160,0.08); border-color:var(--accent); color:var(--accent); }
.func-bar .func-add {
  padding:3px 8px; font-size:0.45rem; border-radius:4px; cursor:pointer;
  background:rgba(167,139,250,0.08); border:1px dashed var(--accent2);
  color:var(--accent2);
}
#blockly-area { flex:1; position:relative; }
#blockly-generated {
  height:120px; overflow:auto; background:#0a0c14; border-top:1px solid var(--border-color);
  padding:8px 12px; font-family:'Courier New',monospace; font-size:0.55rem;
  color:#6a9955; white-space:pre-wrap;
}
[data-theme="light"] #blockly-generated { background:#eee; color:#2e7d32; }
#code-editor-area .code-label {
  padding:6px 12px; font-family:'Orbitron',sans-serif; font-size:0.45rem;
  letter-spacing:1px; color:var(--accent3); text-transform:uppercase;
  border-bottom:1px solid var(--border-color); background:var(--bg-panel);
}
#code-main {
  flex:1; width:100%; background:transparent; color:#9cdcfe;
  border:none; outline:none; font-family:'Courier New',monospace;
  font-size:0.75rem; padding:12px 16px; resize:none;
  tab-size:2; line-height:1.6;
}
[data-theme="light"] #code-main { color:#1a1a2e; }
#code-ref {
  width:280px; overflow-y:auto; background:var(--bg-panel);
  padding:14px; font-size:0.6rem; color:var(--text-secondary);
  line-height:1.6;
}
#code-ref h6 {
  font-family:'Orbitron',sans-serif; font-size:0.5rem;
  color:var(--accent); margin:10px 0 6px; letter-spacing:1px;
}
#code-ref h6:first-child { margin-top:0; }
#code-ref code {
  background:rgba(0,229,160,0.08); color:var(--accent);
  padding:1px 4px; border-radius:3px; font-size:0.55rem;
}
#code-ref .sig {
  font-family:'Courier New',monospace; color:#9cdcfe;
  font-size:0.55rem; display:block; margin:3px 0;
  padding:3px 6px; background:rgba(0,0,0,0.2); border-radius:4px;
}
[data-theme="light"] #code-ref .sig { background:rgba(0,0,0,0.05); color:#1a1a2e; }
#code-ref .desc { color:var(--text-muted); font-size:0.5rem; margin-bottom:6px; }

/* ── Color+text combo ── */
.color-combo {
  display:flex; align-items:center; gap:4px; flex:1;
}
.color-combo .pp-color { flex-shrink:0; }
.color-combo .pp-input { flex:1; min-width:0; }

/* ── Scrollbar ── */
::-webkit-scrollbar { width:5px; }
::-webkit-scrollbar-track { background:transparent; }
::-webkit-scrollbar-thumb { background:var(--border-color); border-radius:3px; }
::-webkit-scrollbar-thumb:hover { background:var(--text-muted); }

/* ── Mobile ── */
@media (max-width:900px) {
  :root { --palette-width:0px; --props-width:0px; }
  #palette, #props-panel { display:none !important; }
}
</style>
</head>
<body>

<!-- ══ Toolbar ══ -->
<div id="toolbar">
  <span class="tb-logo"><i class="bi bi-layout-wtf"></i> UI BUILDER</span>
  <div class="tb-sep"></div>
  <input type="text" class="tb-name" id="ui-name" value="Game HUD" placeholder="UI Name...">
  <div class="tb-sep"></div>
  <div class="device-tabs">
    <button class="device-tab active" data-device="desktop" title="Desktop"><i class="bi bi-display"></i></button>
    <button class="device-tab" data-device="tablet" title="Tablet"><i class="bi bi-tablet"></i></button>
    <button class="device-tab" data-device="phone" title="Phone"><i class="bi bi-phone"></i></button>
    <button class="device-tab" data-device="phone-land" title="Phone Landscape"><i class="bi bi-phone-landscape"></i></button>
  </div>
  <div class="tb-sep"></div>
  <button class="tb-btn" id="btn-grid" title="Toggle Grid"><i class="bi bi-grid-3x3"></i> Grid</button>
  <button class="tb-btn" id="btn-guides" title="Toggle Safe Area"><i class="bi bi-bounding-box"></i> Guides</button>
  <button class="tb-btn" id="btn-snap" title="Toggle Snap"><i class="bi bi-magnet"></i> Snap</button>
  <span class="tb-spacer"></span>
  <button class="tb-btn" id="btn-bg-image" title="Set background image"><i class="bi bi-image"></i> BG</button>
  <button class="tb-btn accent2" id="btn-preview" title="Preview Mode"><i class="bi bi-eye"></i> Preview</button>
  <div class="tb-sep"></div>
  <button class="tb-btn accent3" id="btn-save"><i class="bi bi-download"></i> Save</button>
  <button class="tb-btn" id="btn-load"><i class="bi bi-upload"></i> Load</button>
  <input type="file" id="file-load" accept=".json,.ui.json" style="display:none">
  <div class="tb-sep"></div>
  <button class="tb-btn" id="btn-code" title="Code Editor"><i class="bi bi-code-slash"></i></button>
  <button class="tb-btn" id="btn-settings" title="Settings"><i class="bi bi-gear"></i></button>
</div>

<!-- ══ Code Editor Overlay ══ -->
<div id="code-overlay">
  <div id="code-header">
    <h5><i class="bi bi-code-slash"></i> CODE EDITOR</h5>
    <div class="code-btns">
      <button class="pp-btn" id="code-run" style="background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);">
        <i class="bi bi-play-fill"></i> Run
      </button>
      <button class="pp-btn" id="code-save" style="background:rgba(245,158,11,0.08);border-color:var(--accent3);color:var(--accent3);">
        <i class="bi bi-check-lg"></i> Save &amp; Close
      </button>
      <button class="pp-btn" id="code-close" style="background:rgba(255,255,255,0.04);border-color:var(--border-color);color:var(--text-secondary);">
        <i class="bi bi-x-lg"></i>
      </button>
    </div>
  </div>
  <div id="code-body">
    <div id="code-editor-area">
      <div class="code-tabs">
        <button class="code-tab active" data-tab="code"><i class="bi bi-code-slash"></i> Code</button>
        <button class="code-tab" data-tab="blocks"><i class="bi bi-puzzle"></i> Blocks</button>
      </div>

      <!-- Code Tab -->
      <div class="code-tab-content active" id="tab-code">
        <div class="func-bar" id="func-bar">
          <span style="font-size:0.4rem;color:var(--text-muted);font-family:'Orbitron',sans-serif;letter-spacing:1px;">FN:</span>
          <select id="code-func-select" style="background:var(--bg-panel);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:2px 8px;font-size:0.5rem;font-family:'Courier New',monospace;max-width:200px;">
            <option value="">(select function)</option>
          </select>
          <button class="func-add" id="btn-add-func"><i class="bi bi-plus"></i> New</button>
          <button class="func-add" id="btn-edit-func" style="background:rgba(245,158,11,0.06);border-color:var(--accent3);color:var(--accent3);border-style:solid;"><i class="bi bi-pencil"></i> Edit Args</button>
          <button class="func-add" id="btn-del-func" style="background:rgba(255,77,106,0.06);border-color:var(--danger);color:var(--danger);border-style:solid;"><i class="bi bi-trash3"></i> Delete</button>
        </div>
        <textarea id="code-main" spellcheck="false" placeholder="// Functions are called by element actions (callFunction)&#10;function onButtonClick(sourceEl, targetEl) {&#10;  GameAPI.setUIValue('score', 0);&#10;  GameAPI.setUIVisible('el_3', true);&#10;}&#10;&#10;// onReady() runs automatically after UI loads&#10;function onReady() {&#10;  console.log('UI initialized!');&#10;  GameAPI.setUIValue('lives', 3);&#10;}"></textarea>
      </div>

      <!-- Blocks Tab -->
      <div class="code-tab-content" id="tab-blocks">
        <div class="func-bar" id="blockly-func-bar">
          <span style="font-size:0.4rem;color:var(--text-muted);font-family:'Orbitron',sans-serif;letter-spacing:1px;">FN:</span>
          <select id="blockly-func-select" style="background:var(--bg-panel);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:2px 8px;font-size:0.5rem;font-family:'Courier New',monospace;">
            <option value="">(select function)</option>
          </select>
          <button class="func-add" id="blockly-add-func"><i class="bi bi-plus"></i> New Function</button>
          <button class="func-add" id="blockly-gen-func" style="background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);border-style:solid;"><i class="bi bi-arrow-down-circle"></i> Generate &amp; Insert</button>
          <button class="func-add" id="blockly-clear" style="background:rgba(255,77,106,0.06);border-color:var(--danger);color:var(--danger);border-style:solid;"><i class="bi bi-trash3"></i> Clear</button>
          <button class="func-add" id="blockly-del-func" style="background:rgba(255,77,106,0.06);border-color:var(--danger);color:var(--danger);border-style:solid;"><i class="bi bi-x-circle"></i> Delete Fn</button>
          <button class="func-add" id="blockly-edit-args" style="background:rgba(245,158,11,0.06);border-color:var(--accent3);color:var(--accent3);border-style:solid;"><i class="bi bi-pencil"></i> Edit Args</button>
          <span style="flex:1;"></span>
          <button class="func-add" id="blockly-build-block" style="background:rgba(0,229,160,0.06);border-color:var(--accent);color:var(--accent);border-style:solid;"><i class="bi bi-bricks"></i> Block Builder</button>
        </div>
        <div id="blockly-area"></div>
        <div id="blockly-generated">
          <div style="color:var(--text-muted);font-size:0.45rem;font-family:'Orbitron',sans-serif;letter-spacing:1px;padding:0 0 4px;">GENERATED CODE PREVIEW</div>
          <pre id="blockly-code-output" style="margin:0;"></pre>
        </div>
      </div>
    </div>
    <div id="code-ref">
      <h6>UI API</h6>
      <span class="sig">GameAPI.setUIValue(var, value)</span>
      <span class="sig">GameAPI.getUIValue(var) → value</span>
      <span class="sig">GameAPI.setUIVisible(elId, bool)</span>
      <span class="sig">GameAPI.setUIContent(elId, text)</span>
      <span class="sig">GameAPI.setUIStyle(elId, prop, val)</span>

      <h6>Key Simulation</h6>
      <span class="sig">GameAPI.pressKey(key)</span>
      <span class="sig">GameAPI.releaseKey(key)</span>
      <span class="sig">GameAPI.simulateKey(key, ms)</span>
      <span class="sig">GameAPI.releaseAllKeys()</span>
      <span class="cat">Globals</span>
      <span class="sig">GameAPI.setGlobal(name, value)</span>
      <span class="sig">GameAPI.getGlobal(name) &rarr; value</span>

      <h6>3D Objects</h6>
      <span class="sig">GameAPI.getObject(id) → entry</span>
      <span class="sig">GameAPI.getObjects(category) → []</span>
      <span class="sig">GameAPI.setPosition(id, x, y, z)</span>
      <span class="sig">GameAPI.setRotation(id, rx, ry, rz)</span>
      <span class="sig">GameAPI.setScale(id, sx, sy, sz)</span>
      <span class="sig">GameAPI.setAnimation(id, clipName)</span>
      <span class="sig">GameAPI.getAnimations(id) → []</span>
      <span class="sig">GameAPI.setVisible(id, bool)</span>
      <span class="sig">GameAPI.destroy(id)</span>

      <h6>Queries</h6>
      <span class="sig">GameAPI.getPosition(id) → {x,y,z}</span>
      <span class="sig">GameAPI.getDistance(id1, id2) → num</span>
      <span class="sig">GameAPI.getProperty(id, prop) → val</span>
      <span class="sig">GameAPI.setProperty(id, prop, val)</span>

      <h6>Scene</h6>
      <span class="sig">GameAPI.switchCamera(camId)</span>
      <span class="sig">GameAPI.showDialog(text, opts)</span>
      <span class="sig">GameAPI.showToast(message)</span>
      <span class="sig">GameAPI.playAudio(url) → Audio</span>

      <h6>Function Signature</h6>
      <span class="sig">function myFunc(sourceEl, targetEl) { }</span>
      <span class="desc"><code>sourceEl</code> = clicked element, <code>targetEl</code> = action target.</span>

      <h6>Example: D-Pad Button</h6>
      <span class="sig">function moveUp() {
  GameAPI.simulateKey('w', 200);
}</span>

      <h6>Example: Collect Coin</h6>
      <span class="sig">function collectCoin() {
  let s = GameAPI.getUIValue('score')||0;
  GameAPI.setUIValue('score', s+10);
  GameAPI.destroy('coin_1');
  GameAPI.playAudio('coin.mp3');
}</span>
    </div>
  </div>
</div>

<!-- ══ Settings Overlay ══ -->
<div id="settings-overlay">
  <div id="settings-box">
    <h5><i class="bi bi-gear"></i> Settings</h5>
    <div class="setting-row">
      <span class="setting-label">Theme</span>
      <label class="toggle-switch">
        <input type="checkbox" id="set-theme">
        <span class="toggle-track"></span>
      </label>
    </div>
    <div class="setting-row">
      <span class="setting-label">Snap to Grid</span>
      <label class="toggle-switch">
        <input type="checkbox" id="set-snap" checked>
        <span class="toggle-track"></span>
      </label>
    </div>
    <div class="setting-row">
      <span class="setting-label">Show Grid</span>
      <label class="toggle-switch">
        <input type="checkbox" id="set-grid" checked>
        <span class="toggle-track"></span>
      </label>
    </div>
    <div class="setting-row" style="flex-direction:column;align-items:stretch;gap:6px;">
      <span class="setting-label">Custom Game Functions</span>
      <textarea class="code-textarea" id="set-custom-code" placeholder="// Write JS functions callable from UI actions&#10;// e.g. function openInventory() { ... }&#10;// Then select 'Call Function' action on a button"></textarea>
    </div>
    <div style="display:flex;gap:8px;margin-top:16px;">
      <button class="pp-btn" id="set-save" style="flex:1;background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);">
        <i class="bi bi-check-lg"></i> Save Config
      </button>
      <button class="pp-btn" id="set-close" style="flex:1;background:rgba(255,255,255,0.04);border-color:var(--border-color);color:var(--text-secondary);">
        Close
      </button>
    </div>
  </div>
</div>

<!-- ══ Left Panel (Tools / Hierarchy tabs) ══ -->
<div id="palette">
  <div id="pal-tabs">
    <button class="pal-tab active" data-tab="tools"><i class="bi bi-tools"></i> Tools</button>
    <button class="pal-tab" data-tab="hierarchy"><i class="bi bi-diagram-3"></i> Hierarchy <span class="pal-tab-count" id="hierarchy-count">0</span></button>
  </div>
  <div class="pal-tab-panel active" id="pal-panel-tools">
    <div id="palette-items"></div>
  </div>
  <div class="pal-tab-panel" id="pal-panel-hierarchy">
    <div id="layer-list">
      <div id="layer-items"></div>
      <div id="layer-empty">No elements yet.<br><span>Add one from the Tools tab.</span></div>
    </div>
  </div>
</div>

<!-- ══ Canvas Area ══ -->
<div id="canvas-area">
  <div id="canvas-viewport" class="show-grid show-guides"></div>
</div>

<!-- ══ Properties Panel ══ -->
<div id="props-panel">
  <div class="pp-header">
    <h6 id="pp-title">Properties</h6>
    <button class="pp-close" id="pp-close"><i class="bi bi-x-lg"></i></button>
  </div>
  <div id="pp-context"></div>
  <div id="pp-tabs">
    <button class="pp-tab" data-tab="style"><i class="bi bi-palette"></i> Style</button>
    <button class="pp-tab active" data-tab="props"><i class="bi bi-sliders2"></i> Properties</button>
  </div>
  <div id="pp-content">
    <div class="pp-tab-panel" id="pp-panel-style"></div>
    <div class="pp-tab-panel active" id="pp-panel-props"></div>
  </div>
</div>

<!-- ══ Drag Ghost ══ -->
<div id="drag-ghost"></div>

<!-- ══ Toast Container ══ -->
<div id="toast-container"></div>

<script>
/* ═══════════════════════════════════════════════════════════
   UI BUILDER — Game Interface Designer
   ═══════════════════════════════════════════════════════════ */

// ── Config ──
let ELEMENT_CONFIG = [];
const DEVICE_SIZES = {
  desktop: { w:960, h:540 },
  tablet: { w:512, h:683 },
  phone: { w:320, h:568 },
  'phone-land': { w:568, h:320 },
};

// ── State ──
let elements = [];
let selectedId = null;
let nextId = 1;
let deviceMode = 'desktop';
let projectScaleMode = 'fit';   // 'fit' = keep design proportions (letterbox), 'fill' = cover, 'stretch' = legacy
let snapEnabled = true;
let snapSize = 1; // percentage
let uiName = 'Game HUD';
let bgImage = '';
let previewMode = false;
let isDragging = false;
let isResizing = false;
let dragData = null;
let clipboard = null; // cut/copy buffer
let customCode = ''; // user custom JS functions

// DOM refs
const viewport = document.getElementById('canvas-viewport');
const canvasArea = document.getElementById('canvas-area');
const propsPanel = document.getElementById('props-panel');
const ppContent = document.getElementById('pp-content');
const ppContext = document.getElementById('pp-context');
const ppStyle = document.getElementById('pp-panel-style');
const ppProps = document.getElementById('pp-panel-props');
const ghostEl = document.getElementById('drag-ghost');

// ── Load element config ──
async function loadConfig() {
  try {
    const resp = await fetch('ui-builder.json');
    const data = await resp.json();
    ELEMENT_CONFIG = data.categories || [];
  } catch(e) {
    console.warn('Could not load ui-builder.json, using built-in defaults');
    ELEMENT_CONFIG = getBuiltinConfig();
  }
  buildPalette();
  resizeViewport();
}

function getBuiltinConfig() {
  // Minimal fallback
  return [
    { id:'text', name:'Text', icon:'bi-fonts', items:[
      { id:'heading', name:'Heading', icon:'bi-type-h1', type:'text', defaults:{ content:'Heading', fontFamily:"'Orbitron',sans-serif", fontSize:2.0, fontWeight:'700', color:'#ffffff', textAlign:'center' }},
      { id:'label', name:'Label', icon:'bi-type', type:'text', defaults:{ content:'Label', fontFamily:"'Rajdhani',sans-serif", fontSize:0.9, fontWeight:'500', color:'#c0c4d0' }},
    ]},
    { id:'buttons', name:'Buttons', icon:'bi-hand-index', items:[
      { id:'button_primary', name:'Button', icon:'bi-play-circle', type:'button', defaults:{ content:'Button', fontFamily:"'Rajdhani',sans-serif", fontSize:1.0, fontWeight:'700', color:'#ffffff', background:'linear-gradient(135deg,#00e5a0,#00b87a)', borderRadius:'8px', padding:'12px 32px', border:'none', cursor:'pointer' }},
    ]},
    { id:'layout', name:'Layout', icon:'bi-grid', items:[
      { id:'panel', name:'Panel', icon:'bi-square', type:'panel', defaults:{ w:25, h:20, background:'rgba(10,12,20,0.75)', borderRadius:'12px', border:'1px solid rgba(255,255,255,0.08)', padding:'16px' }},
    ]},
  ];
}

// ── Toast ──
function showToast(msg, type='') {
  const t = document.createElement('div');
  t.className = 'toast' + (type ? ' '+type : '');
  t.textContent = msg;
  document.getElementById('toast-container').appendChild(t);
  setTimeout(() => t.remove(), 2500);
}

// ══════════════════════════════════════
//  PALETTE
// ══════════════════════════════════════
function buildPalette() {
  const container = document.getElementById('palette-items');
  container.innerHTML = '';
  ELEMENT_CONFIG.forEach(cat => {
    const div = document.createElement('div');
    div.className = 'pal-cat';
    const header = document.createElement('div');
    header.className = 'pal-cat-header';
    header.innerHTML = `<i class="bi ${cat.icon}"></i> ${cat.name} <span class="arrow"><i class="bi bi-chevron-down"></i></span>`;
    header.onclick = () => div.classList.toggle('collapsed');
    div.appendChild(header);

    const items = document.createElement('div');
    items.className = 'pal-cat-items';
    (cat.items || []).forEach(item => {
      const el = document.createElement('div');
      el.className = 'pal-item';
      el.innerHTML = `<i class="bi ${item.icon}"></i><span>${item.name}</span>`;
      el.draggable = true;
      el.addEventListener('dragstart', e => {
        dragData = { source:'palette', itemId:item.id, catId:cat.id };
        ghostEl.textContent = item.name;
        ghostEl.style.display = 'block';
        e.dataTransfer.setDragImage(ghostEl, 0, 0);
        e.dataTransfer.effectAllowed = 'copy';
      });
      el.addEventListener('dragend', () => { ghostEl.style.display = 'none'; dragData = null; });
      // Click to add at center
      el.addEventListener('dblclick', () => { addElementAtCenter(item, cat); });
      items.appendChild(el);
    });
    div.appendChild(items);
    container.appendChild(div);
  });
}

// ══════════════════════════════════════
//  ELEMENT MANAGEMENT
// ══════════════════════════════════════
function findTemplate(itemId) {
  for (const cat of ELEMENT_CONFIG) {
    const item = (cat.items || []).find(i => i.id === itemId);
    if (item) return item;
  }
  return null;
}

function createElement(template, x, y) {
  const id = 'el_' + (nextId++);
  const d = template.defaults || {};
  const el = {
    id,
    templateId: template.id,
    type: template.type || 'text',
    name: template.name,
    // Position (percentage 0-100)
    x: x !== undefined ? x : 50,
    y: y !== undefined ? y : 50,
    // Size (percentage, 0 = auto)
    w: d.w || 0,
    h: d.h || 0,
    // Anchor: how x,y relate to element (affects CSS transform)
    anchor: 'center',
    // Content
    content: d.content || '',
    src: d.src || '',
    iconClass: d.iconClass || '',
    // Style
    fontFamily: d.fontFamily || "'Rajdhani',sans-serif",
    fontSize: d.fontSize || 1,
    fontUnit: d.fontUnit || 'rem',
    fontWeight: d.fontWeight || '400',
    color: d.color || '#ffffff',
    background: d.background || 'transparent',
    borderRadius: d.borderRadius || '0',
    padding: d.padding || '0',
    border: d.border || 'none',
    opacity: d.opacity !== undefined ? d.opacity : 1,
    textAlign: d.textAlign || 'center',
    textShadow: d.textShadow || '',
    boxShadow: d.boxShadow || '',
    letterSpacing: d.letterSpacing || '',
    lineHeight: d.lineHeight || '',
    textTransform: d.textTransform || '',
    backdropFilter: d.backdropFilter || '',
    objectFit: d.objectFit || 'contain',
    aspectRatio: d.aspectRatio || '',
    minWidth: d.minWidth || '',
    minHeight: d.minHeight || '',
    cursor: d.cursor || '',
    display: d.display || '',
    justifyContent: d.justifyContent || '',
    alignItems: d.alignItems || '',
    borderTop: d.borderTop || '',
    // Video
    videoFit: d.videoFit || 'contain',
    playMode: d.playMode || 'auto',
    loopMode: d.loopMode || 'loop',
    loopCount: d.loopCount !== undefined ? d.loopCount : 1,
    muted: d.muted !== undefined ? d.muted : true,
    controls: d.controls || false,
    volume: d.volume !== undefined ? d.volume : 1,
    videoPoster: d.videoPoster || '',
    // Chroma key (make a colour transparent)
    chromaKey: d.chromaKey || false,
    chromaColor: d.chromaColor || '#00ff00',
    chromaSimilarity: d.chromaSimilarity !== undefined ? d.chromaSimilarity : 0.4,
    chromaSmoothness: d.chromaSmoothness !== undefined ? d.chromaSmoothness : 0.08,
    chromaSpill: d.chromaSpill !== undefined ? d.chromaSpill : 0.1,
    videoBlend: d.videoBlend || 'none',
    // ── Carousel (Owl Carousel 2) ──
    carouselItems: d.carouselItems ? JSON.parse(JSON.stringify(d.carouselItems)) : [],
    carItems: d.carItems !== undefined ? d.carItems : 3,
    carItemsTablet: d.carItemsTablet !== undefined ? d.carItemsTablet : 2,
    carItemsPhone: d.carItemsPhone !== undefined ? d.carItemsPhone : 1,
    carMargin: d.carMargin !== undefined ? d.carMargin : 12,
    carStagePadding: d.carStagePadding !== undefined ? d.carStagePadding : 0,
    carAutoWidth: d.carAutoWidth || false,
    carLoop: d.carLoop !== undefined ? d.carLoop : true,
    carCenter: d.carCenter !== undefined ? d.carCenter : true,
    carAutoplay: d.carAutoplay || false,
    carAutoplayTimeout: d.carAutoplayTimeout !== undefined ? d.carAutoplayTimeout : 4000,
    carAutoplayHoverPause: d.carAutoplayHoverPause !== undefined ? d.carAutoplayHoverPause : true,
    carSpeed: d.carSpeed !== undefined ? d.carSpeed : 450,
    carMouseDrag: d.carMouseDrag !== undefined ? d.carMouseDrag : true,
    carNav: d.carNav !== undefined ? d.carNav : true,
    carDots: d.carDots !== undefined ? d.carDots : true,
    carStart: d.carStart !== undefined ? d.carStart : 0,
    // Center emphasis (active item bigger)
    carCenterScale: d.carCenterScale !== undefined ? d.carCenterScale : 1.18,
    carSideScale: d.carSideScale !== undefined ? d.carSideScale : 0.88,
    carSideOpacity: d.carSideOpacity !== undefined ? d.carSideOpacity : 0.55,
    carCenterOffsetX: d.carCenterOffsetX !== undefined ? d.carCenterOffsetX : 0,
    carCenterOffsetY: d.carCenterOffsetY !== undefined ? d.carCenterOffsetY : 0,
    carCenterWidth: d.carCenterWidth || '',
    carCenterHeight: d.carCenterHeight || '',
    carItemWidth: d.carItemWidth || '',
    carItemHeight: d.carItemHeight || '',
    carClickToCenter: d.carClickToCenter !== undefined ? d.carClickToCenter : true,
    carActiveSlot: d.carActiveSlot !== undefined ? d.carActiveSlot : '',
    carActiveZ: d.carActiveZ !== undefined ? d.carActiveZ : 10,
    carPadTop: d.carPadTop !== undefined ? d.carPadTop : 0,
    carPadBottom: d.carPadBottom !== undefined ? d.carPadBottom : 0,
    carDeviceOverrides: d.carDeviceOverrides ? JSON.parse(JSON.stringify(d.carDeviceOverrides)) : { tablet:{}, phone:{}, 'phone-land':{} },
    carNavOffsetX: d.carNavOffsetX !== undefined ? d.carNavOffsetX : 0,
    carNavOffsetY: d.carNavOffsetY !== undefined ? d.carNavOffsetY : 0,
    carNavPrevX: d.carNavPrevX !== undefined ? d.carNavPrevX : 0,
    carNavPrevY: d.carNavPrevY !== undefined ? d.carNavPrevY : 0,
    carNavNextX: d.carNavNextX !== undefined ? d.carNavNextX : 0,
    carNavNextY: d.carNavNextY !== undefined ? d.carNavNextY : 0,
    carNavInset: d.carNavInset !== undefined ? d.carNavInset : (d.carNavOffset !== undefined ? d.carNavOffset : 0),
    carDotOffsetX: d.carDotOffsetX !== undefined ? d.carDotOffsetX : 0,
    carDotOffsetY: d.carDotOffsetY !== undefined ? d.carDotOffsetY : 0,
    carItemsPhoneLand: d.carItemsPhoneLand !== undefined ? d.carItemsPhoneLand : '',
    carDotPosition: d.carDotPosition || 'below',
    carDotBottom: d.carDotBottom !== undefined ? d.carDotBottom : 8,
    carResetVisibility: d.carResetVisibility !== undefined ? d.carResetVisibility : true,
    carItemMargin: d.carItemMargin || '0',
    carCenterPadding: d.carCenterPadding || '',
    carCenterMargin: d.carCenterMargin || '',
    carCenterRadius: d.carCenterRadius || '',
    carCenterBg: d.carCenterBg || '',
    carCenterBorder: d.carCenterBorder || '',
    carCenterShadow: d.carCenterShadow || '',
    carShowFields: d.carShowFields ? d.carShowFields.slice() : [],
    carFieldSize: d.carFieldSize !== undefined ? d.carFieldSize : 0.6,
    carTitleUnit: d.carTitleUnit || 'rem',
    carTextUnit: d.carTextUnit || 'rem',
    carFieldUnit: d.carFieldUnit || 'rem',
    carFieldColor: d.carFieldColor || '#8a93a8',
    carNavSize: d.carNavSize !== undefined ? d.carNavSize : 28,
    carNavRadius: d.carNavRadius || '50%',
    carNavIconSize: d.carNavIconSize !== undefined ? d.carNavIconSize : 0.8,
    carNavBorder: d.carNavBorder || 'none',
    carNavOffset: d.carNavOffset !== undefined ? d.carNavOffset : 0,
    carNavPrevIcon: d.carNavPrevIcon || 'bi-chevron-left',
    carNavNextIcon: d.carNavNextIcon || 'bi-chevron-right',
    carDotWidth: d.carDotWidth !== undefined ? d.carDotWidth : 7,
    carDotHeight: d.carDotHeight !== undefined ? d.carDotHeight : 7,
    carDotRadius: d.carDotRadius || '50%',
    carDotGap: d.carDotGap !== undefined ? d.carDotGap : 6,
    carDotActiveWidth: d.carDotActiveWidth || '',
    carDotActiveHeight: d.carDotActiveHeight || '',
    carDotMarginTop: d.carDotMarginTop !== undefined ? d.carDotMarginTop : 6,
    carLinks: d.carLinks ? JSON.parse(JSON.stringify(d.carLinks)) : [],
    carChangeFunc: d.carChangeFunc || '',
    // Item design (replicated for every item)
    carItemBg: d.carItemBg || 'rgba(255,255,255,0.05)',
    carItemBorder: d.carItemBorder || '1px solid rgba(255,255,255,0.1)',
    carItemRadius: d.carItemRadius || '12px',
    carItemPadding: d.carItemPadding || '10px',
    carItemShadow: d.carItemShadow || '0 6px 20px rgba(0,0,0,0.35)',
    carItemAlign: d.carItemAlign || 'center',
    carShowImage: d.carShowImage !== undefined ? d.carShowImage : true,
    carShowTitle: d.carShowTitle !== undefined ? d.carShowTitle : true,
    carShowText: d.carShowText !== undefined ? d.carShowText : true,
    carImgHeight: d.carImgHeight || '110px',
    carImgFit: d.carImgFit || 'cover',
    carImgRadius: d.carImgRadius || '8px',
    carFgInset: d.carFgInset !== undefined ? d.carFgInset : 12,
    carFgPadding: d.carFgPadding !== undefined ? d.carFgPadding : 0,
    carFgWidth: d.carFgWidth || '100%',
    carFgHeight: d.carFgHeight || '100%',
    carFgAlignH: d.carFgAlignH || 'center',
    carFgAlignV: d.carFgAlignV || 'center',
    carFgFit: d.carFgFit || 'contain',
    carFgPosition: d.carFgPosition || 'center',
    carFgRadius: d.carFgRadius || '0',
    carFgShadow: d.carFgShadow || '',
    carModelX: d.carModelX !== undefined ? d.carModelX : 0,
    carModelZ: d.carModelZ !== undefined ? d.carModelZ : 0,
    carImgMode: d.carImgMode || 'inline',      // 'inline' | 'cover'
    carImgPosition: d.carImgPosition || 'center',
    carCoverAlign: d.carCoverAlign || 'bottom',
    carTitleSize: d.carTitleSize !== undefined ? d.carTitleSize : 0.85,
    carTitleColor: d.carTitleColor || '#ffffff',
    carTitleWeight: d.carTitleWeight || '700',
    carTextSize: d.carTextSize !== undefined ? d.carTextSize : 0.65,
    carTextColor: d.carTextColor || '#a0b0c0',
    carNavColor: d.carNavColor || '#ffffff',
    carNavBg: d.carNavBg || 'rgba(0,0,0,0.4)',
    carDotColor: d.carDotColor || 'rgba(255,255,255,0.25)',
    carDotActiveColor: d.carDotActiveColor || '#00e5a0',
    carItemTemplate: d.carItemTemplate || '',
    carItemClick: d.carItemClick || 'none',   // none | link | function
    carItemFunc: d.carItemFunc || '',
    carLinkTarget: d.carLinkTarget || 'newTab',
    // ── Hero section ──
    heroSlides: d.heroSlides ? JSON.parse(JSON.stringify(d.heroSlides)) : [],
    heroTitle: d.heroTitle !== undefined ? d.heroTitle : '',
    heroSubtitle: d.heroSubtitle !== undefined ? d.heroSubtitle : '',
    heroAutoplay: d.heroAutoplay !== undefined ? d.heroAutoplay : true,
    heroInterval: d.heroInterval !== undefined ? d.heroInterval : 6000,
    heroTransition: d.heroTransition || 'fade',
    heroTransDuration: d.heroTransDuration !== undefined ? d.heroTransDuration : 900,
    heroKenBurns: d.heroKenBurns !== undefined ? d.heroKenBurns : false,
    heroKenScale: d.heroKenScale !== undefined ? d.heroKenScale : 1.12,
    heroStart: d.heroStart !== undefined ? d.heroStart : 0,
    heroBgFit: d.heroBgFit || 'cover',
    heroBgPosition: d.heroBgPosition || 'center',
    heroEmptyBg: d.heroEmptyBg || 'linear-gradient(135deg,#1b2030,#0b0e17)',
    heroOverlay: d.heroOverlay || 'linear-gradient(180deg,rgba(0,0,0,0.15),rgba(0,0,0,0.55))',
    heroOverlayOpacity: d.heroOverlayOpacity !== undefined ? d.heroOverlayOpacity : 1,
    heroAlignH: d.heroAlignH || 'center',
    heroAlignV: d.heroAlignV || 'center',
    heroPadding: d.heroPadding || '6%',
    heroGap: d.heroGap || '10px',
    heroMaxWidth: d.heroMaxWidth || '70%',
    heroTitleFont: d.heroTitleFont || "'Orbitron',sans-serif",
    heroTitleSize: d.heroTitleSize !== undefined ? d.heroTitleSize : 2.4,
    heroTitleUnit: d.heroTitleUnit || 'rem',
    heroTitleWeight: d.heroTitleWeight || '700',
    heroTitleColor: d.heroTitleColor || '#ffffff',
    heroTitleLineHeight: d.heroTitleLineHeight || '1.1',
    heroTitleSpacing: d.heroTitleSpacing || 'normal',
    heroTitleShadow: d.heroTitleShadow || '0 2px 18px rgba(0,0,0,0.5)',
    heroSubFont: d.heroSubFont || "'Rajdhani',sans-serif",
    heroSubSize: d.heroSubSize !== undefined ? d.heroSubSize : 1,
    heroSubUnit: d.heroSubUnit || 'rem',
    heroSubWeight: d.heroSubWeight || '400',
    heroSubColor: d.heroSubColor || '#d5dce8',
    heroSubLineHeight: d.heroSubLineHeight || '1.5',
    heroSubShadow: d.heroSubShadow || '0 1px 10px rgba(0,0,0,0.45)',
    heroDots: d.heroDots !== undefined ? d.heroDots : true,
    heroDotWidth: d.heroDotWidth !== undefined ? d.heroDotWidth : 10,
    heroDotHeight: d.heroDotHeight !== undefined ? d.heroDotHeight : 10,
    heroDotRadius: d.heroDotRadius || '50%',
    heroDotGap: d.heroDotGap !== undefined ? d.heroDotGap : 8,
    heroDotBottom: d.heroDotBottom !== undefined ? d.heroDotBottom : 18,
    heroDotColor: d.heroDotColor || 'rgba(255,255,255,0.35)',
    heroDotActiveColor: d.heroDotActiveColor || '#ffffff',
    heroDotActiveWidth: d.heroDotActiveWidth || '',
    heroArrows: d.heroArrows !== undefined ? d.heroArrows : false,
    heroArrowSize: d.heroArrowSize !== undefined ? d.heroArrowSize : 36,
    heroArrowRadius: d.heroArrowRadius || '50%',
    heroArrowBg: d.heroArrowBg || 'rgba(0,0,0,0.35)',
    heroArrowColor: d.heroArrowColor || '#ffffff',
    heroArrowIconSize: d.heroArrowIconSize !== undefined ? d.heroArrowIconSize : 1,
    heroArrowInset: d.heroArrowInset !== undefined ? d.heroArrowInset : 12,
    heroPrevIcon: d.heroPrevIcon || 'bi-chevron-left',
    heroNextIcon: d.heroNextIcon || 'bi-chevron-right',
    heroChangeFunc: d.heroChangeFunc || '',
    // ── Iframe / embed ──
    frameTitle: d.frameTitle || '',
    frameAllow: d.frameAllow || 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
    frameAllowFullscreen: d.frameAllowFullscreen !== undefined ? d.frameAllowFullscreen : true,
    frameLazy: d.frameLazy !== undefined ? d.frameLazy : false,
    frameSandbox: d.frameSandbox !== undefined ? d.frameSandbox : false,
    frameSandboxRules: d.frameSandboxRules || 'allow-scripts allow-same-origin allow-popups allow-forms',
    frameScroll: d.frameScroll !== undefined ? d.frameScroll : true,
    frameBorder: d.frameBorder || 'none',
    frameBg: d.frameBg || 'rgba(0,0,0,0.2)',
    framePreviewOnly: d.framePreviewOnly !== undefined ? d.framePreviewOnly : true,
    // ── 3D model (GLB) ──
    modelBg: d.modelBg || 'transparent',
    modelAutoRotate: d.modelAutoRotate !== undefined ? d.modelAutoRotate : true,
    modelRotateSpeed: d.modelRotateSpeed !== undefined ? d.modelRotateSpeed : 2,
    modelControls: d.modelControls !== undefined ? d.modelControls : false,
    modelZoom: d.modelZoom !== undefined ? d.modelZoom : true,
    modelPan: d.modelPan !== undefined ? d.modelPan : false,
    modelDragRotate: d.modelDragRotate !== undefined ? d.modelDragRotate : true,
    modelDragSpeed: d.modelDragSpeed !== undefined ? d.modelDragSpeed : 1,
    modelZoomSpeed: d.modelZoomSpeed !== undefined ? d.modelZoomSpeed : 1,
    modelDamping: d.modelDamping !== undefined ? d.modelDamping : true,
    modelStopSpinOnInteract: d.modelStopSpinOnInteract !== undefined ? d.modelStopSpinOnInteract : true,
    modelResumeSpin: d.modelResumeSpin !== undefined ? d.modelResumeSpin : 0,
    modelMinDistance: d.modelMinDistance !== undefined ? d.modelMinDistance : '',
    modelMaxDistance: d.modelMaxDistance !== undefined ? d.modelMaxDistance : '',
    modelMinPolar: d.modelMinPolar !== undefined ? d.modelMinPolar : '',
    modelMaxPolar: d.modelMaxPolar !== undefined ? d.modelMaxPolar : '',
    modelAutoFit: d.modelAutoFit !== undefined ? d.modelAutoFit : true,
    modelDistance: d.modelDistance !== undefined ? d.modelDistance : 4,
    modelFov: d.modelFov !== undefined ? d.modelFov : 45,
    modelCamHeight: d.modelCamHeight !== undefined ? d.modelCamHeight : 0.4,
    modelTargetY: d.modelTargetY !== undefined ? d.modelTargetY : 0,
    modelX: d.modelX !== undefined ? d.modelX : 0,
    modelY: d.modelY !== undefined ? d.modelY : 0,
    modelZ: d.modelZ !== undefined ? d.modelZ : 0,
    modelScale: d.modelScale !== undefined ? d.modelScale : 1,
    modelRotX: d.modelRotX !== undefined ? d.modelRotX : 0,
    modelRotY: d.modelRotY !== undefined ? d.modelRotY : 0,
    modelRotZ: d.modelRotZ !== undefined ? d.modelRotZ : 0,
    modelLighting: d.modelLighting || 'studio',
    modelLightIntensity: d.modelLightIntensity !== undefined ? d.modelLightIntensity : 1,
    modelShadows: d.modelShadows !== undefined ? d.modelShadows : false,
    modelHemi: d.modelHemi !== undefined ? d.modelHemi : true,
    modelPlayClips: d.modelPlayClips !== undefined ? d.modelPlayClips : true,
    modelLiveOnCanvas: d.modelLiveOnCanvas !== undefined ? d.modelLiveOnCanvas : true,
    // carousel-wide model options
    carModelHeight: d.carModelHeight || '',
    carModelBg: d.carModelBg || 'transparent',
    carModelAutoRotate: d.carModelAutoRotate !== undefined ? d.carModelAutoRotate : true,
    carModelRotateSpeed: d.carModelRotateSpeed !== undefined ? d.carModelRotateSpeed : 2,
    carModelControls: d.carModelControls !== undefined ? d.carModelControls : false,
    carModelDragRotate: d.carModelDragRotate !== undefined ? d.carModelDragRotate : true,
    carModelZoom: d.carModelZoom !== undefined ? d.carModelZoom : false,
    carModelPan: d.carModelPan !== undefined ? d.carModelPan : false,
    carModelDragSpeed: d.carModelDragSpeed !== undefined ? d.carModelDragSpeed : 1,
    carModelZoomSpeed: d.carModelZoomSpeed !== undefined ? d.carModelZoomSpeed : 1,
    carModelDamping: d.carModelDamping !== undefined ? d.carModelDamping : true,
    carModelStopSpinOnInteract: d.carModelStopSpinOnInteract !== undefined ? d.carModelStopSpinOnInteract : true,
    carModelResumeSpin: d.carModelResumeSpin !== undefined ? d.carModelResumeSpin : 0,
    carModelMinDistance: d.carModelMinDistance !== undefined ? d.carModelMinDistance : '',
    carModelMaxDistance: d.carModelMaxDistance !== undefined ? d.carModelMaxDistance : '',
    carModelMinPolar: d.carModelMinPolar !== undefined ? d.carModelMinPolar : '',
    carModelMaxPolar: d.carModelMaxPolar !== undefined ? d.carModelMaxPolar : '',
    carModelDistance: d.carModelDistance !== undefined ? d.carModelDistance : 4,
    carModelFov: d.carModelFov !== undefined ? d.carModelFov : 45,
    carModelLighting: d.carModelLighting || 'studio',
    carModelLightIntensity: d.carModelLightIntensity !== undefined ? d.carModelLightIntensity : 1,
    carModelScale: d.carModelScale !== undefined ? d.carModelScale : 1,
    carModelWidth: d.carModelWidth || '',
    carModelInset: d.carModelInset !== undefined ? d.carModelInset : 0,
    carModelRotX: d.carModelRotX !== undefined ? d.carModelRotX : 0,
    carModelRotY: d.carModelRotY !== undefined ? d.carModelRotY : 0,
    carModelRotZ: d.carModelRotZ !== undefined ? d.carModelRotZ : 0,
    carModelY: d.carModelY !== undefined ? d.carModelY : 0,
    carModelCamHeight: d.carModelCamHeight !== undefined ? d.carModelCamHeight : 0.4,
    carModelActiveRotX: d.carModelActiveRotX !== undefined ? d.carModelActiveRotX : '',
    carModelActiveRotY: d.carModelActiveRotY !== undefined ? d.carModelActiveRotY : '',
    carModelActiveRotZ: d.carModelActiveRotZ !== undefined ? d.carModelActiveRotZ : '',
    carModelActiveScale: d.carModelActiveScale !== undefined ? d.carModelActiveScale : '',
    carModelActiveDistance: d.carModelActiveDistance !== undefined ? d.carModelActiveDistance : '',
    carModelSideRotX: d.carModelSideRotX !== undefined ? d.carModelSideRotX : '',
    carModelSideRotY: d.carModelSideRotY !== undefined ? d.carModelSideRotY : '',
    carModelSideRotZ: d.carModelSideRotZ !== undefined ? d.carModelSideRotZ : '',
    carModelSideScale: d.carModelSideScale !== undefined ? d.carModelSideScale : '',
    carModelSideDistance: d.carModelSideDistance !== undefined ? d.carModelSideDistance : '',
    carModelSpinActiveOnly: d.carModelSpinActiveOnly !== undefined ? d.carModelSpinActiveOnly : false,
    carSideGap: d.carSideGap !== undefined ? d.carSideGap : 0,
    carSideGapStacks: d.carSideGapStacks !== undefined ? d.carSideGapStacks : true,
    // Binding
    binding: d.binding || '',
    onClick: d.onClick || '',
    // Progress bar
    barValue: d.barValue || 0,
    barColor: d.barColor || '#00e5a0',
    barBgColor: d.barBgColor || 'rgba(255,255,255,0.08)',
    barHeight: d.barHeight || '12px',
    showLabel: d.showLabel || false,
    labelText: d.labelText || '',
    // Layer
    zIndex: 10 + elements.length,
    locked: false,
    visible: true,
    disabled: false, // permanently hidden, ignores all actions
    // Hierarchy
    parentId: null,
    // Events → action chains keyed by event type
    events: { click:[], hover:[], mousedown:[], mouseup:[], dblclick:[] },
    // Interaction states
    hoverBackground: '', hoverColor: '', hoverScale: '', hoverSrc: '', hoverBoxShadow: '',
    activeBackground: '', activeColor: '', activeScale: '', activeSrc: '', activeBoxShadow: '',
    // Image background mode
    bgSize: d.bgSize || '', bgPosition: d.bgPosition || '', bgRepeat: d.bgRepeat || '',
    // Responsive: per-device overrides (desktop values are the base on el itself)
    responsive: false,
    overrides: { tablet:{}, phone:{}, 'phone-land':{} },
    // Viewport visibility: which devices this element appears on (empty = all)
    showOn: d.showOn || [],
    // Control mode for panels: 'none', 'dpad', 'joystick'
    controlMode: d.controlMode || 'none',
    controlBg: d.controlBg || '',
    controlColor: d.controlColor || '',
    controlKnob: d.controlKnob || '',
    // Custom CSS
    customCSS: d.customCSS || '',
    // Animate.css (legacy single animation — kept for old projects)
    animateClass: d.animateClass || '',
    animateDelay: d.animateDelay || '',
    animateDuration: d.animateDuration || '',
    animateRepeat: d.animateRepeat || '',
    // Animation sequence: entrance → attention → exit (all optional, none by default)
    animTrigger: d.animTrigger || 'load',       // 'load' | 'manual'
    animEnterClass: d.animEnterClass || '',
    animEnterDelay: d.animEnterDelay !== undefined ? d.animEnterDelay : 0,
    animEnterDuration: d.animEnterDuration !== undefined ? d.animEnterDuration : 800,
    animEnterRepeat: d.animEnterRepeat || '1',
    animAttnClass: d.animAttnClass || '',
    animAttnDelay: d.animAttnDelay !== undefined ? d.animAttnDelay : 400,
    animAttnDuration: d.animAttnDuration !== undefined ? d.animAttnDuration : 1000,
    animAttnRepeat: d.animAttnRepeat || '1',
    animExitClass: d.animExitClass || '',
    animExitDelay: d.animExitDelay !== undefined ? d.animExitDelay : 1000,
    animExitDuration: d.animExitDuration !== undefined ? d.animExitDuration : 800,
    animExitRepeat: d.animExitRepeat || '1',
    animExitHide: d.animExitHide !== undefined ? d.animExitHide : true,
  };
  elements.push(el);
  renderElement(el);
  buildLayerList();
  selectElement(id);
  return el;
}

function addElementAtCenter(template) {
  createElement(template, 50, 50);
  showToast(`Added ${template.name}`);
}

function removeElement(id) {
  const idx = elements.findIndex(e => e.id === id);
  if (idx < 0) return;
  // Unparent any children
  elements.forEach(e => { if (e.parentId === id) e.parentId = null; });
  const dom = viewport.querySelector(`[data-id="${id}"]`);
  if (dom) dom.remove();
  elements.splice(idx, 1);
  if (selectedId === id) { selectedId = null; propsPanel.classList.remove('visible'); canvasArea.classList.add('props-hidden'); }
  renderAllElements();
  buildLayerList();
  showToast('Element removed');
}

function duplicateElement(id) {
  const src = elements.find(e => e.id === id);
  if (!src) return;
  const copy = JSON.parse(JSON.stringify(src));
  copy.id = 'el_' + (nextId++);
  copy.name = src.name + ' copy';
  copy.x = Math.min(src.x + 3, 95);
  copy.y = Math.min(src.y + 3, 95);
  copy.zIndex = 10 + elements.length;
  elements.push(copy);
  renderElement(copy);
  buildLayerList();
  selectElement(copy.id);
  showToast('Duplicated');
}

// ══════════════════════════════════════
//  RENDER ELEMENT ON CANVAS
// ══════════════════════════════════════
function isEffectivelyVisible(el) {
  if (getVal(el,'visible') === false) return false;
  if (!el.visible) return false;
  if (el.parentId) {
    const parent = elements.find(e => e.id === el.parentId);
    if (parent) return isEffectivelyVisible(parent);
  }
  return true;
}

function rerenderChildren(parentId) {
  elements.filter(e => e.parentId === parentId).forEach(child => {
    renderElement(child);
    rerenderChildren(child.id); // recurse for nested panels
  });
}

/* ── Multi-stage element animation (entrance → attention → exit) ── */
function uiAnimHasSequence(el) {
  return !!(el.animEnterClass || el.animAttnClass || el.animExitClass || el.animateClass);
}

function clearUIAnim(inner) {
  if (!inner) return;
  if (inner._animTimers) inner._animTimers.forEach(function(t) { window.clearTimeout(t); });
  inner._animTimers = [];
  Array.prototype.slice.call(inner.classList).forEach(function(c) {
    if (c.indexOf('animate__') === 0) inner.classList.remove(c);
  });
  inner.style.removeProperty('--animate-duration');
  inner.style.removeProperty('animation-delay');
  inner.style.removeProperty('animation-duration');
  inner.style.removeProperty('animation-iteration-count');
  inner.style.removeProperty('opacity');
  inner.style.removeProperty('pointer-events');
}

/** Plays one animate.css class. `done` is not called for infinite repeats. */
function playUIAnimStage(inner, cls, durationMs, repeat, done) {
  if (!inner) return;
  if (!cls) { if (done) done(); return; }
  Array.prototype.slice.call(inner.classList).forEach(function(c) {
    if (c.indexOf('animate__') === 0) inner.classList.remove(c);
  });
  void inner.offsetWidth; // reflow so the animation can restart
  var dur = parseInt(durationMs);
  if (!dur || dur < 0) dur = 800;
  var infinite = (String(repeat) === 'infinite');
  var rep = infinite ? 'infinite' : (parseInt(repeat) || 1);
  inner.style.setProperty('--animate-duration', (dur / 1000) + 's');
  inner.style.animationDuration = (dur / 1000) + 's';
  inner.style.animationIterationCount = rep;
  inner.classList.add('animate__animated', cls.indexOf('animate__') === 0 ? cls : 'animate__' + cls);
  if (infinite || !done) return;
  var t = window.setTimeout(done, dur * rep + 30);
  (inner._animTimers = inner._animTimers || []).push(t);
}

/**
 * Runs the element's animation sequence.
 * stage: 'all' (default) | 'entrance' | 'attention' | 'exit' | 'stop'
 */
function runUIAnimSequence(inner, el, stage) {
  if (!inner || !el) return;
  stage = stage || 'all';
  clearUIAnim(inner);
  if (stage === 'stop') return;
  inner._animTimers = inner._animTimers || [];

  // Legacy single-animation projects behave like an entrance
  var enterCls = el.animEnterClass || (!el.animAttnClass && !el.animExitClass ? el.animateClass : '') || '';
  var enterDur = el.animEnterDuration !== undefined && el.animEnterDuration !== ''
    ? el.animEnterDuration
    : (parseFloat(el.animateDuration) ? parseFloat(el.animateDuration) * 1000 : 800);
  var enterDelay = el.animEnterDelay !== undefined && el.animEnterDelay !== ''
    ? el.animEnterDelay
    : (parseFloat(el.animateDelay) ? parseFloat(el.animateDelay) * 1000 : 0);
  var enterRep = el.animEnterRepeat || (el.animateRepeat && !el.animEnterClass ? el.animateRepeat : 1);

  function wait(ms, fn) {
    var t = window.setTimeout(fn, Math.max(0, parseInt(ms) || 0));
    inner._animTimers.push(t);
    return t;
  }

  function doExit() {
    if (!el.animExitClass) return;
    wait(el.animExitDelay, function() {
      playUIAnimStage(inner, el.animExitClass, el.animExitDuration, el.animExitRepeat || 1, function() {
        if (el.animExitHide !== false) {
          inner.style.opacity = '0';
          inner.style.pointerEvents = 'none';
        }
      });
    });
  }

  function doAttention(next) {
    if (!el.animAttnClass) { if (next) next(); return; }
    wait(el.animAttnDelay, function() {
      var infinite = String(el.animAttnRepeat) === 'infinite';
      playUIAnimStage(inner, el.animAttnClass, el.animAttnDuration, el.animAttnRepeat || 1, infinite ? null : next);
      if (infinite && next) next(); // exit is timed from when the loop starts
    });
  }

  // While an entrance is waiting out its delay, keep the element invisible —
  // otherwise it pops in first and only then animates.
  function armEntrance(then) {
    var d = Math.max(0, parseInt(enterDelay) || 0);
    if (enterCls && d > 0) inner.style.opacity = '0';
    wait(d, function() {
      inner.style.removeProperty('opacity');
      then();
    });
  }

  if (stage === 'entrance') { armEntrance(function() { playUIAnimStage(inner, enterCls, enterDur, enterRep, null); }); return; }
  if (stage === 'attention') { doAttention(null); return; }
  if (stage === 'exit') { doExit(); return; }

  // Full sequence
  if (enterCls) {
    armEntrance(function() {
      playUIAnimStage(inner, enterCls, enterDur, enterRep, function() { doAttention(doExit); });
    });
  } else {
    doAttention(doExit);
  }
}

/** Visible-item count for the builder's current device preview */
function carItemsForDevice(el) {
  const desktop = Math.max(1, parseInt(el.carItems) || 3);
  const tablet = Math.max(1, parseInt(el.carItemsTablet) || desktop);
  if (deviceMode === 'phone') return Math.max(1, parseInt(el.carItemsPhone) || 1);
  if (deviceMode === 'tablet') return tablet;
  if (deviceMode === 'phone-land') return el.carItemsPhoneLand ? Math.max(1, parseInt(el.carItemsPhoneLand)) : tablet;
  return desktop;
}

window.__uiCarouselItemsResolver = el => carItemsForDevice(el);

// ── Carousel helpers (Owl Carousel 2) — shared shape with runtimes ──
function escAttr(s) { return String(s == null ? '' : s).replace(/"/g, '&quot;'); }

/** Overlay image: an absolutely placed box (margin + padding) holding a sized image */
function carFgHTML(el, item, boxStyle, isCover) {
  var pad = item.fgPadding !== undefined && item.fgPadding !== '' ? item.fgPadding : el.carFgPadding;
  var w = item.fgWidth !== undefined && item.fgWidth !== '' ? item.fgWidth : el.carFgWidth;
  var hgt = item.fgHeight !== undefined && item.fgHeight !== '' ? item.fgHeight : el.carFgHeight;
  var fit = item.fgFit || el.carFgFit || 'contain';
  if (fit === 'stretch') fit = 'fill';
  var imgStyle = 'width:' + cssLen(w, '100%') + ';height:' + cssLen(hgt, '100%') +
    ';object-fit:' + fit + ';';
  return '<div class="car-fg-box' + (isCover ? ' car-fg-cover' : '') + '" style="' + boxStyle +
    'padding:' + cssLen(pad, '0px') + ';">' +
    '<img class="car-fg" src="' + escAttr(item.foreground) + '" style="' + imgStyle + '" draggable="false"></div>';
}

function carouselItemInnerHTML(el, item) {
  if (el.carItemTemplate) {
    var out = el.carItemTemplate
      .replace(/\{image\}/g, item.image || '')
      .replace(/\{title\}/g, item.title || '')
      .replace(/\{text\}/g, item.text || '')
      .replace(/\{link\}/g, item.link || '');
    // Any custom field can be used as {fieldName}
    if (item.fields) {
      Object.keys(item.fields).forEach(function(k) {
        out = out.split('{' + k + '}').join(item.fields[k] == null ? '' : item.fields[k]);
      });
    }
    return out;
  }
  var h = '';
  var cover = el.carImgMode === 'cover';
  // What sits on top of the card image: a 3D model, another image, or nothing
  var layer = item.layer || (item.model ? 'model' : (item.foreground ? 'image' : 'none'));
  var hasModel = layer === 'model' && item.model;
  var hasFg = layer === 'image' && item.foreground;
  var fgInset = item.fgInset !== undefined && item.fgInset !== '' ? item.fgInset : el.carFgInset;
  var fgStyle = 'inset:' + cssLen(fgInset, '0px') + ';';

  if (cover) {
    // The image fills the whole card and everything else stacks on top of it
    if (el.carShowImage !== false && item.image) {
      h += '<img class="car-bgimg" src="' + escAttr(item.image) + '" alt="' + escAttr(item.title) + '" draggable="false">';
    }
    if (hasModel) h += '<div class="car-model car-model-cover" data-model="' + escAttr(item.model) + '"></div>';
    else if (hasFg) h += carFgHTML(el, item, fgStyle, true);
  } else if (hasModel || hasFg) {
    // Background image keeps its normal size; the model / overlay image is layered over it,
    // so a card with either is exactly the same size as one without.
    h += '<div class="car-media">';
    if (el.carShowImage !== false && item.image) {
      h += '<img class="car-img" src="' + escAttr(item.image) + '" alt="' + escAttr(item.title) + '" draggable="false">';
    }
    if (hasModel) h += '<div class="car-model" data-model="' + escAttr(item.model) + '"></div>';
    else h += carFgHTML(el, item, fgStyle, false);
    h += '</div>';
  } else if (el.carShowImage !== false && item.image) {
    h += '<img class="car-img" src="' + escAttr(item.image) + '" alt="' + escAttr(item.title) + '" draggable="false">';
  }
  if (el.carShowTitle !== false && item.title) h += '<div class="car-title">' + item.title + '</div>';
  if (el.carShowText !== false && item.text) h += '<div class="car-text">' + item.text + '</div>';
  // Extra fields marked "show in card"
  (el.carShowFields || []).forEach(function(key) {
    var v = item.fields ? item.fields[key] : undefined;
    if (v !== undefined && v !== '') h += '<div class="car-field car-field-' + escAttr(key).replace(/[^\w-]/g, '') + '">' + v + '</div>';
  });
  return h;
}

/** Active-item settings can differ per device; these are the keys that support it */
var CAR_DEVICE_PROPS = ['carActiveSlot','carCenterScale','carSideScale','carSideOpacity',
  'carCenterOffsetX','carCenterOffsetY','carCenterWidth','carCenterHeight',
  'carCenterPadding','carCenterMargin','carCenterRadius',
  // breathing room around the strip, per-device
  'carPadTop','carPadBottom',
  // arrow + dot placement is per-device too
  'carNavInset','carNavOffsetX','carNavOffsetY',
  'carNavPrevX','carNavPrevY','carNavNextX','carNavNextY',
  'carDotOffsetX','carDotOffsetY','carDotMarginTop','carDotBottom'];

/** el merged with its overrides for `device` ('tablet' | 'phone' | 'phone-land') */
function carMergedFor(el, device) {
  var ov = (device && el.carDeviceOverrides) ? el.carDeviceOverrides[device] : null;
  if (!ov) return el;
  var m = {}, k;
  for (k in el) m[k] = el[k];
  CAR_DEVICE_PROPS.forEach(function(prop) {
    if (ov[prop] !== undefined && ov[prop] !== null && ov[prop] !== '') m[prop] = ov[prop];
    else if (prop === 'carActiveSlot' && ov[prop] === '') m[prop] = '';
  });
  return m;
}

function carHasDeviceOverride(el, device) {
  var ov = (el.carDeviceOverrides || {})[device];
  if (!ov) return false;
  return CAR_DEVICE_PROPS.some(function(p) { return ov[p] !== undefined && ov[p] !== null; });
}

/** Explicit active-slot mode: the active card sits in a chosen visible slot
 *  instead of being geometrically centred (e.g. 1 card left, 2 cards right). */
function carActiveSlotOf(el, perView) {
  var v = el.carActiveSlot;
  if (v === '' || v === undefined || v === null || v === 'auto') return null;
  var n = parseInt(v);
  if (isNaN(n)) return null;
  return Math.max(0, Math.min((parseInt(perView) || 3) - 1, n));
}

function carUsesSlotMode(el) {
  return carActiveSlotOf(el, el.carItems) !== null;
}

/** Media queries matching the builder's device modes */
var CAR_DEVICE_QUERIES = {
  tablet: ['@media (max-width:1024px) and (min-width:481px)'],
  phone: ['@media (max-width:480px) and (orientation:portrait)'],
  'phone-land': ['@media (orientation:landscape) and (max-height:500px)', '@media (max-width:768px) and (orientation:landscape)']
};

/** The active-item + stage-shift rules for one configuration (base or a device override) */
function carActiveRules(el, cfg, itemCount) {
  var s = '.car-' + el.id;
  var cScale = cfg.carCenterScale !== undefined ? cfg.carCenterScale : 1;
  var sScale = cfg.carSideScale !== undefined ? cfg.carSideScale : 1;
  var sOpac = cfg.carSideOpacity !== undefined ? cfg.carSideOpacity : 1;
  var cx = parseFloat(cfg.carCenterOffsetX) || 0;
  var cy = parseFloat(cfg.carCenterOffsetY) || 0;
  var grow = Math.max(0, (cScale - 1) * 60);
  var padTop = Math.round(grow + Math.max(0, -cy) + (parseFloat(cfg.carPadTop) || 0));
  var padBottom = Math.round(grow + Math.max(0, cy) + (parseFloat(cfg.carPadBottom) || 0));
  var centerSizeCss = '';
  if (cfg.carCenterWidth) centerSizeCss += 'width:' + cssLen(cfg.carCenterWidth) + '; margin-left:auto; margin-right:auto;';
  if (cfg.carCenterHeight) centerSizeCss += 'height:' + cssLen(cfg.carCenterHeight) + ';';
  var centerSel = s + ' .owl-item.center .car-item, ' + s + ' .owl-item.car-active .car-item, ' + s + ' .car-fallback .car-item.center';
  var out = [
    s + ' .owl-stage-outer { padding:' + padTop + 'px 0 ' + padBottom + 'px; }',
    s + ' .car-item { transform:translateX(var(--car-shift, 0px)) scale(' + sScale + '); opacity:' + sOpac + '; }',
    centerSel + ' { transform:translate(' + cx + 'px, ' + cy + 'px) scale(' + cScale + '); opacity:1; ' + centerSizeCss +
      (cfg.carCenterPadding ? ' padding:' + cfg.carCenterPadding + ';' : '') +
      (cfg.carCenterMargin ? ' margin:' + cfg.carCenterMargin + ';' : '') +
      (cfg.carCenterRadius ? ' border-radius:' + cfg.carCenterRadius + ';' : '') +
      (cfg.carCenterBg ? ' background:' + cfg.carCenterBg + ';' : '') +
      (cfg.carCenterBorder ? ' border:' + cfg.carCenterBorder + ';' : '') +
      (cfg.carCenterShadow ? ' box-shadow:' + cfg.carCenterShadow + ';' : '') + ' }'
  ];
  // Off-balance layout: push the whole strip right by whole slots
  var c = Math.max(1, parseInt(itemCount) || 3);
  var slot = carActiveSlotOf(cfg, c);
  if (slot !== null) out.push(s + ' .owl-stage { margin-left:' + ((slot / c) * 100).toFixed(4) + '%; }');
  else out.push(s + ' .owl-stage { margin-left:0; }');
  return out.join('\n');
}

/** Arrow + dot placement for one configuration (base or a device override) */
function carPositionRules(el, cfg) {
  var s = '.car-' + el.id;
  var num = function(v) { return parseFloat(v) || 0; };
  var inset = num(cfg.carNavInset !== undefined ? cfg.carNavInset : cfg.carNavOffset);
  var prevT = 'translate(' + num(cfg.carNavPrevX) + 'px, ' + num(cfg.carNavPrevY) + 'px)';
  var nextT = 'translate(' + num(cfg.carNavNextX) + 'px, ' + num(cfg.carNavNextY) + 'px)';
  var dotT = 'translate(' + num(cfg.carDotOffsetX) + 'px, ' + num(cfg.carDotOffsetY) + 'px)';
  var topSpace = (cfg.carDotMarginTop === '' || cfg.carDotMarginTop === undefined) ? 6 : num(cfg.carDotMarginTop);
  var out = [
    s + ' .owl-nav { position:absolute; top:calc(50% + ' + num(cfg.carNavOffsetY) + 'px); left:' + num(cfg.carNavOffsetX) +
      'px; width:100%; margin:0 !important; transform:translateY(-50%); display:flex; justify-content:space-between; pointer-events:none; box-sizing:border-box; }',
    s + ' .owl-nav button.owl-prev { margin:0 ' + inset + 'px !important; transform:' + prevT + '; }',
    s + ' .owl-nav button.owl-next { margin:0 ' + inset + 'px !important; transform:' + nextT + '; }',
    // keep the nudge when hovering, or the button would jump back to its default spot
    s + ' .owl-nav button.owl-prev:hover { transform:' + prevT + ' scale(1.12); }',
    s + ' .owl-nav button.owl-next:hover { transform:' + nextT + ' scale(1.12); }'
  ];
  if (cfg.carDotPosition === 'overlay') {
    out.push(s + ' { position:relative; }');
    out.push(s + ' .owl-dots { position:absolute; left:0; right:0; bottom:' +
      ((cfg.carDotBottom === '' || cfg.carDotBottom === undefined) ? 8 : num(cfg.carDotBottom)) +
      'px; z-index:3; margin:0 !important; transform:' + dotT + '; }');
  } else {
    if (cfg.carDotPosition === 'above') {
      out.push(s + ' .owl-dots { order:-1; }');
      out.push(s + ' .car-wrap { display:flex; flex-direction:column; }');
    }
    // !important because owl.theme.default.css has a more specific rule
    // (`.owl-theme .owl-nav.disabled + .owl-dots { margin-top:10px }`) that would
    // otherwise pin the spacing and make "Top space" do nothing.
    out.push(s + ' .owl-dots { margin-top:' + topSpace + 'px !important; position:static; transform:' + dotT + '; }');
  }
  return out.join('\n');
}

/** "160" → "160px"; leaves "160px", "50%", "auto", "" alone */
function cssLen(v, fallback) {
  if (v === undefined || v === null || v === '') return fallback;
  var str = String(v).trim();
  return /^-?\d*\.?\d+$/.test(str) ? str + 'px' : str;
}

function buildCarouselCSS(el) {
  var s = '.car-' + el.id;
  // The builder previews one device at a time and can't rely on media queries,
  // so it asks for that device's values to be baked into the base rules.
  var previewDevice = (typeof window !== 'undefined') ? window.__uiCarouselDevice : null;
  var base = previewDevice ? carMergedFor(el, previewDevice) : el;
  var per = parseInt(el.carItems) || 3;
  var gap = parseInt(el.carMargin) || 0;
  // Item box sizing
  var sizeCss = '';
  if (el.carItemWidth) sizeCss += 'width:' + cssLen(el.carItemWidth) + '; margin-left:auto; margin-right:auto;';
  if (el.carItemHeight) sizeCss += 'height:' + cssLen(el.carItemHeight) + ';';

  // Active-item rules: base first, then a block per device that overrides them
  var basePer = previewDevice
    ? (previewDevice === 'phone' ? (parseInt(el.carItemsPhone) || 1)
      : previewDevice === 'tablet' ? (parseInt(el.carItemsTablet) || per)
      : (parseInt(el.carItemsPhoneLand) || parseInt(el.carItemsTablet) || per))
    : per;
  var activeCss = [carActiveRules(el, base, basePer), carPositionRules(el, base)];
  if (!previewDevice) {
    var counts = {
      tablet: parseInt(el.carItemsTablet) || per,
      phone: parseInt(el.carItemsPhone) || 1,
      'phone-land': parseInt(el.carItemsPhoneLand) || parseInt(el.carItemsTablet) || per
    };
    Object.keys(CAR_DEVICE_QUERIES).forEach(function(dev) {
      var cfg = carMergedFor(el, dev);
      if (cfg === el && counts[dev] === per) return;   // nothing device-specific
      // (cfg !== el means this device carries at least one override)
      var rules = carActiveRules(el, cfg, counts[dev]) + '\n' + carPositionRules(el, cfg);
      CAR_DEVICE_QUERIES[dev].forEach(function(q) { activeCss.push(q + ' {\n' + rules + '\n}'); });
    });
  }
  var slotCss = activeCss.join('\n');
  return [
    s + ' { width:100%; }',
    // Owl ships `.owl-carousel { display:none }` and only reveals it once it has
    // initialised — if anything goes wrong the whole carousel vanishes. Keep it shown.
    s + ' .owl-carousel { display:block; }',
    s + ' .owl-carousel .owl-item { float:left; }',
    s + ' .owl-stage { display:flex; align-items:center; }',
    // z-index only applies to positioned elements — without this the *next* card in
    // DOM order paints over the enlarged active one.
    s + ' .owl-item { display:flex; align-items:center; position:relative; z-index:1; }',
    s + ' .owl-item.center, ' + s + ' .owl-item.car-active { z-index:' + (parseInt(el.carActiveZ) || 10) + '; }',
    s + ' .car-item { position:relative; }',
    s + ' .car-fallback .car-item.center { z-index:' + (parseInt(el.carActiveZ) || 10) + '; }',
    s + ' .car-item { background:' + (el.carItemBg || 'transparent') + '; border:' + (el.carItemBorder || 'none') +
      '; border-radius:' + (el.carItemRadius || '0') + '; padding:' + (el.carItemPadding || '0') +
      '; margin:' + (el.carItemMargin || '0') +
      '; box-shadow:' + (el.carItemShadow || 'none') + '; text-align:' + (el.carItemAlign || 'center') +
      '; overflow:hidden; box-sizing:border-box; ' + sizeCss +
      ' transition:transform 0.35s ease, opacity 0.35s ease; }',
    s + ' .car-media { position:relative; width:' + cssLen(el.carModelWidth, '100%') + '; height:' +
      cssLen(el.carModelHeight, cssLen(el.carImgHeight, '140px')) + '; margin:0 auto; }',
    // The canvas covers the media box, so adding a model never resizes the card
    s + ' .car-media .car-model { position:absolute; inset:' + (parseFloat(el.carModelInset) || 0) + 'px; }',
    s + ' .car-media .car-fg-box { position:absolute; }',
    s + ' .car-media .car-img { position:absolute; inset:0; width:100%; height:100%; }',
    s + ' .car-model { border-radius:' + (el.carImgRadius || '0') + '; overflow:hidden; z-index:1; }',
    // Full-bleed image mode: the picture covers the card, content sits above it
    s + ' .car-bgimg { position:absolute; inset:0; width:100%; height:100%; object-fit:' +
      (el.carImgFit || 'cover') + '; object-position:' + (el.carImgPosition || 'center') +
      '; border-radius:inherit; z-index:0; display:block; }',
    s + ' .car-model-cover { position:absolute; inset:' + (parseFloat(el.carModelInset) || 0) + 'px; z-index:1; }',
    // Overlay image: sits above the card image, its inset acts as a margin from the edges
    // Overlay image: the box handles margin/padding/alignment, the image its own size + fit
    s + ' .car-fg-box { position:absolute; inset:' + cssLen(el.carFgInset, '0px') + '; padding:' + cssLen(el.carFgPadding, '0px') +
      '; box-sizing:border-box; display:flex; z-index:1; pointer-events:none;' +
      ' align-items:' + (el.carFgAlignV === 'top' ? 'flex-start' : el.carFgAlignV === 'bottom' ? 'flex-end' : 'center') +
      '; justify-content:' + (el.carFgAlignH === 'left' ? 'flex-start' : el.carFgAlignH === 'right' ? 'flex-end' : 'center') + '; }',
    s + ' .car-fg { max-width:100%; max-height:100%; object-position:' + (el.carFgPosition || 'center') +
      '; border-radius:' + (el.carFgRadius || '0') + '; display:block; ' +
      (el.carFgShadow ? 'filter:drop-shadow(' + el.carFgShadow + ');' : '') + ' }',
    s + ' .car-item > .car-title, ' + s + ' .car-item > .car-text, ' + s + ' .car-item > .car-field { position:relative; z-index:2; }',
    (el.carImgMode === 'cover' ? s + ' .car-item { padding:' + (el.carItemPadding || '0') + '; display:flex; flex-direction:column; justify-content:' +
      (el.carCoverAlign === 'top' ? 'flex-start' : el.carCoverAlign === 'center' ? 'center' : 'flex-end') + '; }' : ''),
    s + ' .car-img { width:100%; height:' + cssLen(el.carImgHeight, 'auto') + '; object-fit:' + (el.carImgFit || 'cover') +
      '; border-radius:' + (el.carImgRadius || '0') + '; display:block; }',
    s + ' .car-title { font-size:' + (el.carTitleSize !== undefined ? el.carTitleSize : 0.85) + (el.carTitleUnit || 'rem') + '; color:' + (el.carTitleColor || '#fff') +
      '; font-weight:' + (el.carTitleWeight || '700') + '; margin-top:6px; line-height:1.3; }',
    s + ' .car-text { font-size:' + (el.carTextSize !== undefined ? el.carTextSize : 0.65) + (el.carTextUnit || 'rem') + '; color:' + (el.carTextColor || '#a0b0c0') +
      '; margin-top:3px; line-height:1.4; }',
    s + ' .car-field { font-size:' + (el.carFieldSize !== undefined ? el.carFieldSize : 0.6) + (el.carFieldUnit || 'rem') + '; color:' + (el.carFieldColor || '#8a93a8') +
      '; margin-top:2px; line-height:1.4; }',
    s + ' .owl-nav button.owl-prev, ' + s + ' .owl-nav button.owl-next { pointer-events:auto; color:' + (el.carNavColor || '#fff') +
      ' !important; background:' + (el.carNavBg || 'rgba(0,0,0,0.4)') + ' !important; width:' + (parseFloat(el.carNavSize) || 28) +
      'px; height:' + (parseFloat(el.carNavSize) || 28) + 'px; border-radius:' + (el.carNavRadius || '50%') +
      '; border:' + (el.carNavBorder || 'none') + ' !important; display:flex; align-items:center; justify-content:center; font-size:' +
      (el.carNavIconSize !== undefined ? el.carNavIconSize : 0.8) + 'rem !important; transition:transform 0.15s, background 0.15s; }',
    // Owl adds `disabled` to the nav/dots wrapper whenever every slide already fits on
    // screen, and its core CSS hides `.disabled` outright — which silently removes the
    // arrows and dots as soon as "Visible" reaches the number of slides. With loop on
    // they still work, so keep them shown when the user asked for them.
    (el.carNav !== false ? s + ' .owl-nav.disabled { display:flex !important; }' : ''),
    (el.carDots !== false ? s + ' .owl-dots.disabled { display:flex !important; }' : ''),
    s + ' .owl-dots { display:flex; justify-content:center; align-items:center; }',
    s + ' .owl-dots .owl-dot span { background:' + (el.carDotColor || 'rgba(255,255,255,0.25)') + ' !important; width:' +
      (parseFloat(el.carDotWidth) || 7) + 'px; height:' + (parseFloat(el.carDotHeight) || 7) + 'px; border-radius:' +
      (el.carDotRadius || '50%') + ' !important; margin:0 ' + ((parseFloat(el.carDotGap) || 6) / 2) + 'px !important; transition:all 0.2s; }',
    s + ' .owl-dots .owl-dot.active span { background:' + (el.carDotActiveColor || '#00e5a0') + ' !important;' +
      (el.carDotActiveWidth ? ' width:' + parseFloat(el.carDotActiveWidth) + 'px !important;' : '') +
      (el.carDotActiveHeight ? ' height:' + parseFloat(el.carDotActiveHeight) + 'px !important;' : '') + ' }',
    s + ' .car-fallback { display:flex; gap:' + gap + 'px; align-items:center; justify-content:center; width:100%; overflow:hidden; }',
    s + ' .car-fallback .car-item { flex:0 0 calc((100% - ' + (gap * (per - 1)) + 'px) / ' + per + '); }',
    (el.carItemClick && el.carItemClick !== 'none') ? s + ' .car-item { cursor:pointer; }' : '',
    slotCss
  ].join('\n');
}

function applyCarouselCSS(el) {
  var st = document.getElementById('car-style-' + el.id);
  if (!st) { st = document.createElement('style'); st.id = 'car-style-' + el.id; document.head.appendChild(st); }
  st.textContent = buildCarouselCSS(el);
}

function owlAvailable() {
  return typeof window.jQuery !== 'undefined' && window.jQuery.fn && typeof window.jQuery.fn.owlCarousel === 'function';
}

/** Visible item count for the current viewport — mirrors the builder's device modes.
 *  window.__uiCarouselForceItems lets the builder pin a count for its device preview. */
function carItemsForViewport(el) {
  // The builder previews devices by resizing its canvas, not the window, so it installs
  // a resolver that answers from the selected device instead of window.innerWidth.
  if (typeof window !== 'undefined' && typeof window.__uiCarouselItemsResolver === 'function') {
    try {
      var r = parseInt(window.__uiCarouselItemsResolver(el));
      if (r > 0) return r;
    } catch(e) {}
  }
  var forced = (typeof window !== 'undefined') ? window.__uiCarouselForceItems : null;
  if (forced) return Math.max(1, parseInt(forced) || 1);
  var desktop = Math.max(1, parseInt(el.carItems) || 3);
  var tablet = Math.max(1, parseInt(el.carItemsTablet) || desktop);
  var phone = Math.max(1, parseInt(el.carItemsPhone) || 1);
  var land = el.carItemsPhoneLand ? Math.max(1, parseInt(el.carItemsPhoneLand)) : tablet;
  var w = (typeof window !== 'undefined' && window.innerWidth) ? window.innerWidth : 1280;
  var h = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800;
  var landscape = w > h;
  // Phones held sideways are wide but short (844×390, 932×430…), so height matters
  // more than width for spotting them.
  if (landscape && (w <= 768 || (h <= 500 && w <= 1200))) return land;
  if (w <= 480) return phone;
  if (w <= 1024) return tablet;
  return desktop;
}

function owlOptionsFor(el, live) {
  var per = carItemsForViewport(el);
  return {
    items: per,
    margin: parseInt(el.carMargin) || 0,
    loop: el.carLoop !== false,
    center: carUsesSlotMode(el) ? false : !!el.carCenter,
    autoWidth: !!el.carAutoWidth,
    stagePadding: parseInt(el.carStagePadding) || 0,
    nav: el.carNav !== false,
    dots: el.carDots !== false,
    dotsEach: 1,          // one dot per item (Owl otherwise draws one dot per "page",
                          // which gives a single dot when every item is already visible)
    navText: ['<i class="bi ' + (el.carNavPrevIcon || 'bi-chevron-left') + '"></i>', '<i class="bi ' + (el.carNavNextIcon || 'bi-chevron-right') + '"></i>'],
    autoplay: !!(live && el.carAutoplay),
    autoplayTimeout: parseInt(el.carAutoplayTimeout) || 4000,
    autoplayHoverPause: el.carAutoplayHoverPause !== false,
    smartSpeed: parseInt(el.carSpeed) || 450,
    mouseDrag: live ? el.carMouseDrag !== false : false,
    touchDrag: live ? el.carMouseDrag !== false : false,
    pullDrag: false,
    rewind: el.carLoop === false,
    startPosition: parseInt(el.carStart) || 0,
    // One bucket only: the count is resolved by carItemsForViewport(), which understands
    // portrait/landscape. Owl's own breakpoints can't express orientation.
    responsive: { 0: { items: per } }
  };
}

/* ── Linked elements: update other UI elements when the active item changes ── */
function carouselTargetDom(id) {
  if (!id) return null;
  try { if (typeof gameUIElements !== 'undefined' && gameUIElements && gameUIElements[id]) return gameUIElements[id].dom; } catch(e) {}
  try { if (typeof viewport !== 'undefined' && viewport) return viewport.querySelector('[data-id="' + id + '"]'); } catch(e) {}
  return document.getElementById('gui-' + id);
}

function carouselFieldValue(item, field, index) {
  if (!item) return '';
  if (field === 'index') return String(index + 1);
  if (field && field.indexOf('field:') === 0) {
    var key = field.slice(6);
    return (item.fields && item.fields[key] !== undefined) ? item.fields[key] : '';
  }
  return item[field || 'title'] !== undefined ? item[field || 'title'] : '';
}

/** Tags the current .owl-item with `car-active` (Owl only adds `center` in centre mode) */
function markCarouselActive(el, wrap, $t) {
  if (!wrap) return;
  var nodes = wrap.querySelectorAll('.owl-item');
  if (!nodes.length) return;
  var target = wrap.querySelector('.owl-item.center');
  if (!target) {
    var pos = -1;
    try {
      var inst = $t && $t.data ? $t.data('owl.carousel') : null;
      if (inst) pos = inst.current();
    } catch(e) {}
    if (pos >= 0 && nodes[pos]) target = nodes[pos];
  }
  Array.prototype.forEach.call(nodes, function(n) { n.classList.remove('car-active'); });
  if (target) target.classList.add('car-active');
  applyCarouselSpread(el, wrap, nodes, target);
}

/** Pushes the side cards away from the active one and poses their 3D models */
function applyCarouselSpread(el, wrap, nodes, activeNode) {
  nodes = nodes || wrap.querySelectorAll('.owl-item');
  if (!nodes.length) return;
  var activeIdx = -1;
  Array.prototype.forEach.call(nodes, function(n, i) { if (n === activeNode) activeIdx = i; });
  var gap = parseFloat(el.carSideGap) || 0;
  var stack = el.carSideGapStacks !== false;   // 2nd neighbour moves twice as far
  Array.prototype.forEach.call(nodes, function(n, i) {
    var card = n.querySelector('.car-item');
    if (!card) return;
    var shift = 0;
    if (activeIdx >= 0 && i !== activeIdx && gap) {
      var dist = i - activeIdx;
      var steps = stack ? Math.abs(dist) : 1;
      shift = (dist < 0 ? -1 : 1) * gap * steps;
    }
    card.style.setProperty('--car-shift', shift + 'px');

    // Per-card 3D pose: the active card can sit differently from the side ones
    var host = n.querySelector('.car-model .ui-model');
    if (host && host._modelApi && host._modelApi.setPose) {
      var isActive = (i === activeIdx);
      var item = null;
      var ci = card.dataset ? parseInt(card.dataset.index) : NaN;
      if (!isNaN(ci)) item = (el.carouselItems || [])[ci];
      var pose = {
        rotX: pickNum(isActive ? el.carModelActiveRotX : el.carModelSideRotX, item && item.modelRotX, el.carModelRotX, 0),
        rotY: pickNum(isActive ? el.carModelActiveRotY : el.carModelSideRotY, item && item.modelRotY, el.carModelRotY, 0),
        rotZ: pickNum(isActive ? el.carModelActiveRotZ : el.carModelSideRotZ, item && item.modelRotZ, el.carModelRotZ, 0),
        scale: pickNum(isActive ? el.carModelActiveScale : el.carModelSideScale, item && item.modelScale, el.carModelScale, 1),
        distance: pickNum(isActive ? el.carModelActiveDistance : el.carModelSideDistance, item && item.modelDistance, el.carModelDistance, 4)
      };
      host._modelApi.setPose(pose);
      var spins = carItemSpins(el, item, false);
      host._modelApi.setSpin(el.carModelSpinActiveOnly ? (isActive && spins) : spins);
    }
  });
}

/** Whether an item's model should spin — per item, falling back to the carousel */
function carItemSpins(el, item, atCreation) {
  var v = item ? item.modelAutoRotate : undefined;
  var on = (v === undefined || v === null || v === '') ? (el.carModelAutoRotate !== false) : (v === true || v === 'true');
  // "spin active only" is applied per card once we know which one is active
  if (atCreation && el.carModelSpinActiveOnly) return false;
  return on;
}

/** First value that is actually set */
function pickNum() {
  for (var i = 0; i < arguments.length; i++) {
    var v = arguments[i];
    if (v !== undefined && v !== null && v !== '' && !isNaN(parseFloat(v))) return parseFloat(v);
  }
  return 0;
}

function carouselActiveIndex(wrap) {
  if (!wrap) return 0;
  var node = wrap.querySelector('.owl-item.center .car-item')
    || wrap.querySelector('.owl-item.car-active .car-item')
    || wrap.querySelector('.owl-item.active .car-item')
    || wrap.querySelector('.car-item.center')
    || wrap.querySelector('.car-item');
  if (!node) return 0;
  var i = parseInt(node.dataset.index);
  return isNaN(i) ? 0 : i;
}

/** Base (designed) visibility of another element, so per-item show/hide can be reset */
function carouselTargetBaseVisible(id) {
  try { if (typeof gameUIElements !== 'undefined' && gameUIElements && gameUIElements[id]) return gameUIElements[id].data.visible !== false; } catch(e) {}
  try {
    if (typeof elements !== 'undefined' && elements && elements.find) {
      var m = elements.find(function(o) { return o.id === id; });
      if (m) return m.visible !== false;
    }
  } catch(e) {}
  return true;
}

function carouselSetTargetVisible(dom, show) {
  if (!dom) return;
  if (dom.classList.contains('gui-el')) {
    dom.classList.toggle('gui-hidden', !show);
    if (show) dom.style.removeProperty('display');
  } else {
    dom.style.display = show ? '' : 'none';
    dom.classList.toggle('el-hidden', !show);
  }
}

/** Applies one action (shared by carousel links and per-item triggers) */
function applyCarouselAction(dom, action, value) {
  if (!dom) return;
  var inner = dom.querySelector('.gui-inner') || dom.querySelector('.ui-el-inner') || dom.querySelector('.el-inner') || dom.firstElementChild || dom;
  switch (action || 'setText') {
    case 'setText': inner.textContent = value; break;
    case 'setHTML': inner.innerHTML = value; break;
    case 'setImage': {
      var img = inner.querySelector('img');
      if (img) img.src = value;
      else {
        inner.style.backgroundImage = value ? 'url(' + value + ')' : 'none';
        inner.style.backgroundSize = inner.style.backgroundSize || 'cover';
        inner.style.backgroundPosition = inner.style.backgroundPosition || 'center';
      }
      break;
    }
    case 'setBackground':
      inner.style.backgroundImage = value ? 'url(' + value + ')' : 'none';
      inner.style.backgroundSize = 'cover';
      inner.style.backgroundPosition = 'center';
      break;
    case 'setVideo': {
      var vid = inner.querySelector('video');
      if (vid && value) { vid.src = value; vid.load(); }
      break;
    }
    case 'setColor': inner.style.color = value; break;
    case 'show': carouselSetTargetVisible(dom, true); break;
    case 'hide': carouselSetTargetVisible(dom, false); break;
  }
}

/** Every element touched by any item's show/hide actions — reset before each slide */
function carouselManagedVisibility(el) {
  var ids = {};
  (el.carouselItems || []).forEach(function(it) {
    (it.actions || []).forEach(function(a) {
      if (a && a.targetId && (a.action === 'show' || a.action === 'hide')) ids[a.targetId] = true;
    });
  });
  return Object.keys(ids);
}

function applyCarouselLinks(el, index) {
  var items = el.carouselItems || [];
  if (!items.length) return;
  if (index < 0 || index >= items.length) index = 0;
  var item = items[index];
  // 1. Reset anything a slide controls, so show/hide is per-slide rather than sticky
  if (el.carResetVisibility !== false) {
    carouselManagedVisibility(el).forEach(function(id) {
      carouselSetTargetVisible(carouselTargetDom(id), carouselTargetBaseVisible(id));
    });
  }

  // 2. Carousel-wide links (same mapping for every slide)
  (el.carLinks || []).forEach(function(link) {
    if (!link || !link.targetId) return;
    applyCarouselAction(carouselTargetDom(link.targetId), link.action, carouselFieldValue(item, link.field, index));
  });

  // 3. This slide's own triggers
  (item.actions || []).forEach(function(a) {
    if (!a || !a.targetId) return;
    var val = a.value;
    if (a.useField) val = carouselFieldValue(item, a.useField, index);
    applyCarouselAction(carouselTargetDom(a.targetId), a.action, val);
  });
  if (el.carChangeFunc && typeof window[el.carChangeFunc] === 'function') {
    try { window[el.carChangeFunc](item, index, el); } catch(err) { console.warn('[Carousel] change function error:', err); }
  }
}

/** Current real item index — asks Owl first (clone-safe), then falls back to the DOM */
function carouselCurrentIndex(el, wrap, $t) {
  var n = (el.carouselItems || []).length;
  if (n) {
    try {
      var inst = $t && $t.data ? $t.data('owl.carousel') : null;
      if (inst && typeof inst.relative === 'function') {
        var rel = inst.relative(inst.current());
        if (!isNaN(rel)) return ((rel % n) + n) % n;
      }
    } catch(e) {}
  }
  return carouselActiveIndex(wrap);
}

function syncCarouselLinks(el, wrap, $t) {
  applyCarouselLinks(el, carouselCurrentIndex(el, wrap, $t));
}

function renderUICarousel(container, el, live) {
  var items = el.carouselItems || [];
  if (typeof disposeUIModel === 'function') {
    Array.prototype.forEach.call(container.querySelectorAll('.car-model'), function(m) { disposeUIModel(m); });
  }
  container.innerHTML = '';
  applyCarouselCSS(el);
  if (items.length === 0) return;

  var wrap = document.createElement('div');
  wrap.className = 'car-wrap car-' + el.id;
  wrap.style.cssText = 'width:100%;';

  var ready = owlAvailable();
  var track = document.createElement('div');
  track.className = ready ? 'owl-carousel owl-theme' : 'car-fallback';

  items.forEach(function(it, idx) {
    var box = document.createElement('div');
    box.className = 'car-item';
    box.dataset.index = idx;
    box.innerHTML = carouselItemInnerHTML(el, it);
    if (!ready) {
      var per = parseInt(el.carItems) || 3;
      var slotIdx = carUsesSlotMode(el) ? carActiveSlotOf(el, per) : (el.carCenter ? Math.floor(Math.min(items.length, per) / 2) : -1);
      if (slotIdx >= 0 && idx === slotIdx) box.classList.add('center');
    }
    var modelBox = box.querySelector('.car-model');
    if (modelBox && typeof renderUIModel === 'function') {
      renderUIModel(modelBox, {
        id: el.id + '_m' + idx,
        src: it.model,
        borderRadius: el.carImgRadius,
        modelBg: it.modelBg || el.carModelBg || 'transparent',
        modelStubBg: (el.carShowImage !== false && it.image) ? 'transparent' : null,
        modelAutoRotate: carItemSpins(el, it, true),
        modelRotateSpeed: el.carModelRotateSpeed,
        modelControls: el.carModelControls,
        modelDragRotate: el.carModelDragRotate,
        modelZoom: el.carModelZoom,
        modelPan: el.carModelPan,
        modelDragSpeed: el.carModelDragSpeed,
        modelZoomSpeed: el.carModelZoomSpeed,
        modelMinDistance: el.carModelMinDistance,
        modelMaxDistance: el.carModelMaxDistance,
        modelMinPolar: el.carModelMinPolar,
        modelMaxPolar: el.carModelMaxPolar,
        modelDamping: el.carModelDamping,
        modelStopSpinOnInteract: el.carModelStopSpinOnInteract,
        modelResumeSpin: el.carModelResumeSpin,
        modelDistance: it.modelDistance || el.carModelDistance,
        modelFov: el.carModelFov,
        modelLighting: el.carModelLighting,
        modelLightIntensity: el.carModelLightIntensity,
        modelScale: it.modelScale !== undefined ? it.modelScale : el.carModelScale,
        modelRotX: it.modelRotX !== undefined ? it.modelRotX : el.carModelRotX,
        modelRotY: it.modelRotY !== undefined ? it.modelRotY : el.carModelRotY,
        modelRotZ: it.modelRotZ !== undefined ? it.modelRotZ : el.carModelRotZ,
        modelY: it.modelY !== undefined ? it.modelY : el.carModelY,
        modelX: it.modelX !== undefined ? it.modelX : el.carModelX,
        modelZ: it.modelZ !== undefined ? it.modelZ : el.carModelZ,
        modelInset: it.modelInset,
        onModelReady: function() { markCarouselActive(el, wrap, $track); },
        modelCamHeight: el.carModelCamHeight,
        modelAutoFit: true
      }, live);
    }
    if (live && el.carItemClick && el.carItemClick !== 'none') {
      box.addEventListener('click', function() {
        if (el.carItemClick === 'link' && it.link) {
          var t = el.carLinkTarget || 'newTab';
          if (t === 'same') window.location.href = it.link;
          else if (t === 'iframe' && typeof showIframeOverlay === 'function') showIframeOverlay(it.link);
          else window.open(it.link, '_blank');
        } else if (el.carItemClick === 'function' && el.carItemFunc && typeof window[el.carItemFunc] === 'function') {
          try { window[el.carItemFunc](it, idx, el); } catch(err) { console.warn('[Carousel]', err); }
        }
      });
    }
    track.appendChild(box);
  });

  wrap.appendChild(track);
  container.appendChild(wrap);

  var $track = null;
  if (ready) {
    try {
      var $t = window.jQuery(track);
      $track = $t;
      $t.on('initialized.owl.carousel changed.owl.carousel translated.owl.carousel', function(e) {
        // Prefer Owl's own index (clone-safe), fall back to reading the DOM
        var idx = null;
        try {
          if (e && e.item && e.relatedTarget && typeof e.relatedTarget.relative === 'function') {
            idx = e.relatedTarget.relative(e.item.index);
          }
        } catch(err) {}
        markCarouselActive(el, wrap, $t);
        var n = items.length;
        if (idx !== null && !isNaN(idx) && n) applyCarouselLinks(el, ((idx % n) + n) % n);
        else syncCarouselLinks(el, wrap, $t);
        // Re-read once the slide has settled
        window.setTimeout(function() {
          markCarouselActive(el, wrap, $t);
          syncCarouselLinks(el, wrap, $t);
        }, (parseInt(el.carSpeed) || 450) + 40);
      });
      // Clicking a side item brings it to the centre (feels natural with center mode)
      if ((el.carCenter || carUsesSlotMode(el)) && live && el.carClickToCenter !== false) {
        $t.on('click', '.owl-item:not(.center):not(.car-active) .car-item', function(e) {
          if (el.carModelControls && e.target && e.target.closest && e.target.closest('.car-model')) return;
          var i = parseInt(this.dataset.index);
          if (!isNaN(i)) $t.trigger('to.owl.carousel', [i, parseInt(el.carSpeed) || 450]);
        });
      }
      $t.owlCarousel(owlOptionsFor(el, live));
    } catch(err) {
      console.warn('[Carousel] init failed:', err);
      track.classList.add('owl-loaded');     // show the cards rather than nothing
    }
  }
  window.setTimeout(function() {
    markCarouselActive(el, wrap, $track);
    syncCarouselLinks(el, wrap, $track);
  }, 30);

  // Keep Owl in step with size / orientation changes. Owl caches item widths at init,
  // so without this a resized (or rotated) carousel shows the wrong number of items.
  if (ready && $track) {
    var lastPer = owlOptionsFor(el, live).items;
    var reflowTimer = null;
    var reflow = function() {
      window.clearTimeout(reflowTimer);
      reflowTimer = window.setTimeout(function() {
        if (!document.body.contains(track)) return; // element was re-rendered
        var per = carItemsForViewport(el);
        if (per !== lastPer) {
          lastPer = per;
          try {
            $track.trigger('destroy.owl.carousel');
          } catch(e) { console.warn('[Carousel] destroy failed:', e); }
          try {
            $track.addClass('owl-carousel owl-theme');
            $track.owlCarousel(owlOptionsFor(el, live));
          } catch(e) {
            console.warn('[Carousel] re-init failed:', e);
            $track.addClass('owl-loaded');   // fall back to a plain visible row
          }
        } else {
          try { $track.trigger('refresh.owl.carousel'); } catch(e) {}
        }
        markCarouselActive(el, wrap, $track);
        syncCarouselLinks(el, wrap, $track);
      }, 120);
    };
    window.addEventListener('resize', reflow);
    window.addEventListener('orientationchange', reflow);
    if (typeof ResizeObserver !== 'undefined') {
      try { new ResizeObserver(reflow).observe(wrap); } catch(e) {}
    }
  }
}

function controlUICarousel(dom, cmd, index) {
  if (!owlAvailable() || !dom) return;
  var $t = window.jQuery(dom).find('.owl-carousel');
  if (!$t.length) return;
  switch (cmd || 'next') {
    case 'next': $t.trigger('next.owl.carousel'); break;
    case 'prev': $t.trigger('prev.owl.carousel'); break;
    case 'goTo': $t.trigger('to.owl.carousel', [parseInt(index) || 0, 300]); break;
    case 'play': $t.trigger('play.owl.autoplay'); break;
    case 'stop': $t.trigger('stop.owl.autoplay'); break;
    case 'refresh': $t.trigger('refresh.owl.carousel'); break;
  }
}

/* ── Design stage: reproduce the builder's canvas exactly ───────────────────
   The builder lays out on a fixed canvas (960×540 on desktop) and scales it to
   fit. Positions are percentages, but font sizes, image heights, paddings and
   radii are px/rem — so rendering the same UI into a raw browser window of a
   different size changes every one of those proportions. Runtimes therefore
   size the overlay to the design resolution and scale it, exactly like the
   builder does. */
var UI_DESIGN_SIZES = {
  desktop: { w: 960, h: 540 },
  tablet: { w: 512, h: 683 },
  phone: { w: 320, h: 568 },
  'phone-land': { w: 568, h: 320 }
};

/** Anchor → translate offsets (the element's own position is a % of the stage) */
function uiAnchorTranslate(anchor) {
  switch (anchor || 'center') {
    case 'top-left': return { tx: '0', ty: '0' };
    case 'top-center': return { tx: '-50%', ty: '0' };
    case 'top-right': return { tx: '-100%', ty: '0' };
    case 'center-left': return { tx: '0', ty: '-50%' };
    case 'center-right': return { tx: '-100%', ty: '-50%' };
    case 'bottom-left': return { tx: '0', ty: '-100%' };
    case 'bottom-center': return { tx: '-50%', ty: '-100%' };
    case 'bottom-right': return { tx: '-100%', ty: '-100%' };
    default: return { tx: '-50%', ty: '-50%' };
  }
}

/** The transform an element carries: anchor offset plus a hover/active scale hook */
function uiAnchorTransform(anchor) {
  var a = uiAnchorTranslate(anchor);
  return 'translate(' + a.tx + ', ' + a.ty + ') scale(var(--gui-scale, 1))';
}

function uiDeviceBucket() {
  var w = (typeof window !== 'undefined' && window.innerWidth) ? window.innerWidth : 1280;
  var h = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800;
  var landscape = w > h;
  if (landscape && (w <= 768 || (h <= 500 && w <= 1200))) return 'phone-land';
  if (w <= 480) return 'phone';
  if (w <= 1024) return 'tablet';
  return 'desktop';
}

function uiDesignSize(uiData) {
  var sizes = (uiData && uiData.designSizes) || UI_DESIGN_SIZES;
  return sizes[uiDeviceBucket()] || sizes.desktop || UI_DESIGN_SIZES.desktop;
}

function applyUIStage(uiData, overlayId) {
  var overlay = document.getElementById(overlayId || 'game-ui-overlay');
  if (!overlay) return;
  var mode = (uiData && uiData.scaleMode) || 'fit';

  if (mode === 'stretch') {   // legacy: fill the window, px sizes unscaled
    overlay.style.width = '100%';
    overlay.style.height = '100%';
    overlay.style.left = '0';
    overlay.style.top = '0';
    overlay.style.transform = '';
    overlay.style.transformOrigin = '';
    return;
  }

  var size = uiDesignSize(uiData);
  var w = window.innerWidth, h = window.innerHeight;
  var scale = mode === 'fill'
    ? Math.max(w / size.w, h / size.h)
    : Math.min(w / size.w, h / size.h);

  overlay.style.width = size.w + 'px';
  overlay.style.height = size.h + 'px';
  overlay.style.left = '50%';
  overlay.style.top = '50%';
  overlay.style.transformOrigin = 'center center';
  overlay.style.transform = 'translate(-50%, -50%) scale(' + scale + ')';
}

/** Re-apply on resize / rotate; safe to call repeatedly. */
function watchUIStage(getUIData, overlayId) {
  if (window.__uiStageWatch) window.removeEventListener('resize', window.__uiStageWatch);
  var timer = null;
  window.__uiStageWatch = function() {
    window.clearTimeout(timer);
    timer = window.setTimeout(function() { applyUIStage(getUIData(), overlayId); }, 60);
  };
  window.addEventListener('resize', window.__uiStageWatch);
  window.addEventListener('orientationchange', window.__uiStageWatch);
}

/* ── 3D model viewer (Three.js + GLB) ──────────────────────────────────────
   Loads Three.js on demand the first time a model element is rendered, so a
   project without one pays nothing. */
var UI_THREE_VERSION = '0.160.0';
var UI_THREE_CDN = 'https://cdn.jsdelivr.net/npm/three@' + UI_THREE_VERSION;
var UI_THREE_ESM = 'https://esm.sh/three@' + UI_THREE_VERSION;
var _uiThreePromise = null;
var _uiThreeError = '';

/**
 * Three's add-ons (GLTFLoader, OrbitControls) are published as ES modules that
 * `import ... from 'three'` — a bare specifier the browser can only resolve through
 * an import map. Loading them straight from a CDN therefore fails with
 * "Failed to resolve module specifier". So: make sure an import map exists first,
 * and keep a self-resolving CDN as a backup.
 */
function ensureThreeImportMap() {
  if (typeof document === 'undefined') return false;
  var existing = document.querySelector('script[type="importmap"]');
  if (existing) {
    // A host page (game-level) already maps "three" — use whatever it points at
    return true;
  }
  try {
    var map = document.createElement('script');
    map.type = 'importmap';
    map.textContent = JSON.stringify({
      imports: {
        'three': UI_THREE_CDN + '/build/three.module.js',
        'three/': UI_THREE_CDN + '/',
        'three/addons/': UI_THREE_CDN + '/examples/jsm/'
      }
    });
    document.head.appendChild(map);
    return true;
  } catch(e) {
    return false;
  }
}

function loadUIThree() {
  if (typeof window !== 'undefined' && window.__uiThreeStub) return Promise.resolve(window.__uiThreeStub);
  if (_uiThreePromise) return _uiThreePromise;

  var viaImportMap = function() {
    ensureThreeImportMap();
    return Promise.all([
      import(/* webpackIgnore: true */ 'three'),
      import(/* webpackIgnore: true */ 'three/addons/loaders/GLTFLoader.js'),
      import(/* webpackIgnore: true */ 'three/addons/controls/OrbitControls.js')
    ]);
  };
  // esm.sh rewrites the add-ons' internal "three" imports, so it needs no map
  var viaEsmSh = function() {
    return Promise.all([
      import(/* webpackIgnore: true */ UI_THREE_ESM),
      import(/* webpackIgnore: true */ UI_THREE_ESM + '/examples/jsm/loaders/GLTFLoader.js'),
      import(/* webpackIgnore: true */ UI_THREE_ESM + '/examples/jsm/controls/OrbitControls.js')
    ]);
  };

  _uiThreePromise = viaImportMap()
    .catch(function(err) {
      console.warn('[Model3D] import-map load failed (' + (err && err.message) + '), retrying via esm.sh');
      return viaEsmSh();
    })
    .then(function(m) {
      return { THREE: m[0], GLTFLoader: m[1].GLTFLoader, OrbitControls: m[2].OrbitControls };
    })
    .catch(function(err) {
      _uiThreeError = (err && err.message) || 'unknown error';
      console.warn('[Model3D] Three.js could not be loaded:', _uiThreeError,
        '\nIf you are offline, host three.module.js yourself and add an import map to the page.');
      return null;
    });
  return _uiThreePromise;
}

function disposeUIModel(container) {
  if (!container) return;
  var host = container.querySelector('.ui-model');
  if (host && host._modelCleanup) { try { host._modelCleanup(); } catch(e) {} }
}

function modelPlaceholder(el, message, spinner) {
  return '<div class="ui-model-stub" style="width:100%;height:100%;min-height:60px;display:flex;flex-direction:column;' +
    'gap:6px;align-items:center;justify-content:center;background:' +
    (el.modelStubBg ? el.modelStubBg : (el.modelBg && el.modelBg !== 'transparent' ? el.modelBg : 'rgba(0,0,0,0.3)')) +
    ';border-radius:' + (el.borderRadius || '8px') + ';color:#c9d2e0;text-align:center;padding:8px;box-sizing:border-box;">' +
    '<i class="bi ' + (spinner ? 'bi-arrow-repeat' : 'bi-box') + '" style="font-size:1.5rem;opacity:0.65;' +
    (spinner ? 'animation:uiModelSpin 1.1s linear infinite;' : '') + '"></i>' +
    '<span style="font-size:0.5rem;opacity:0.75;">' + message + '</span></div>';
}

function renderUIModel(container, el, live) {
  disposeUIModel(container);
  container.innerHTML = '';

  if (!document.getElementById('ui-model-spin-css')) {
    var st = document.createElement('style');
    st.id = 'ui-model-spin-css';
    st.textContent = '@keyframes uiModelSpin { to { transform:rotate(360deg); } }';
    document.head.appendChild(st);
  }

  if (!el.src) {
    container.innerHTML = modelPlaceholder(el, 'Set a .glb / .gltf path', false);
    return null;
  }

  var host = document.createElement('div');
  host.className = 'ui-model';
  host.style.cssText = 'position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;border-radius:' +
    (el.borderRadius || '0') + ';' + (el.modelBg && el.modelBg !== 'transparent' ? 'background:' + el.modelBg + ';' : '');
  var loading = document.createElement('div');
  loading.className = 'ui-model-loading';
  loading.style.cssText = 'position:absolute;inset:0;';
  loading.innerHTML = modelPlaceholder(el, 'Loading 3D model…', true);
  host.appendChild(loading);
  container.appendChild(host);

  var cancelled = false;
  host._modelEl = el;
  host._modelCleanup = function() { cancelled = true; };

  loadUIThree().then(function(libs) {
    if (cancelled || !host.isConnected) return;
    if (!libs || !libs.THREE) {
      host.innerHTML = modelPlaceholder(el, '3D viewer unavailable' + (_uiThreeError ? ' — ' + _uiThreeError : ''), false);
      return;
    }
    var THREE = libs.THREE;
    var w = host.clientWidth || 300, h = host.clientHeight || 200;

    var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: false });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    renderer.setSize(w, h, false);
    renderer.domElement.style.cssText = 'width:100%;height:100%;display:block;';
    if (renderer.outputColorSpace !== undefined && THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;
    if (el.modelShadows && renderer.shadowMap) { renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; }

    var scene = new THREE.Scene();
    if (el.modelBg && el.modelBg !== 'transparent') {
      try { scene.background = new THREE.Color(el.modelBg); } catch(e) {}
    } else {
      renderer.setClearColor(0x000000, 0);
    }

    var camera = new THREE.PerspectiveCamera(parseFloat(el.modelFov) || 45, w / h, 0.01, 2000);

    // Lighting presets
    var preset = el.modelLighting || 'studio';
    var intensity = el.modelLightIntensity !== undefined ? parseFloat(el.modelLightIntensity) : 1;
    var amb = new THREE.AmbientLight(0xffffff, (preset === 'dramatic' ? 0.25 : preset === 'soft' ? 0.9 : 0.55) * intensity);
    scene.add(amb);
    var key = new THREE.DirectionalLight(0xffffff, (preset === 'dramatic' ? 2.2 : preset === 'soft' ? 0.8 : 1.4) * intensity);
    key.position.set(3, 5, 4);
    if (el.modelShadows) { key.castShadow = true; }
    scene.add(key);
    if (preset !== 'dramatic') {
      var fill = new THREE.DirectionalLight(0xffffff, 0.6 * intensity);
      fill.position.set(-4, 2, -3);
      scene.add(fill);
    }
    if (el.modelHemi !== false && THREE.HemisphereLight) {
      scene.add(new THREE.HemisphereLight(0xffffff, 0x223344, 0.4 * intensity));
    }

    var root = new THREE.Group();
    scene.add(root);

    var controls = null;
    if (el.modelControls && libs.OrbitControls) {
      controls = new libs.OrbitControls(camera, renderer.domElement);
      controls.enableDamping = el.modelDamping !== false;
      controls.dampingFactor = parseFloat(el.modelDampingFactor) || 0.08;
      controls.enableRotate = el.modelDragRotate !== false;
      controls.enableZoom = el.modelZoom !== false;
      controls.enablePan = !!el.modelPan;
      controls.rotateSpeed = parseFloat(el.modelDragSpeed) || 1;
      controls.zoomSpeed = parseFloat(el.modelZoomSpeed) || 1;
      controls.autoRotate = !!(live && el.modelAutoRotate);
      controls.autoRotateSpeed = parseFloat(el.modelRotateSpeed) || 2;
      if (el.modelMinDistance !== undefined && el.modelMinDistance !== '') controls.minDistance = parseFloat(el.modelMinDistance);
      if (el.modelMaxDistance !== undefined && el.modelMaxDistance !== '') controls.maxDistance = parseFloat(el.modelMaxDistance);
      if (el.modelMinPolar !== undefined && el.modelMinPolar !== '') controls.minPolarAngle = parseFloat(el.modelMinPolar) * Math.PI / 180;
      if (el.modelMaxPolar !== undefined && el.modelMaxPolar !== '') controls.maxPolarAngle = parseFloat(el.modelMaxPolar) * Math.PI / 180;

      // Touch gestures: one finger turns, two fingers zoom (and pan when allowed)
      if (THREE.TOUCH) {
        controls.touches = {
          ONE: el.modelDragRotate === false ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE,
          TWO: el.modelPan ? THREE.TOUCH.DOLLY_PAN : THREE.TOUCH.DOLLY_ROTATE
        };
      }
      renderer.domElement.style.touchAction = 'none';
      renderer.domElement.style.cursor = 'grab';

      // Don't let a carousel (or any parent) steal the gesture
      ['pointerdown', 'mousedown', 'touchstart', 'wheel', 'click'].forEach(function(evt) {
        renderer.domElement.addEventListener(evt, function(e) { e.stopPropagation(); }, { passive: evt === 'wheel' ? false : true });
      });

      // Spinning pauses while the user is dragging, and can come back afterwards
      controls.addEventListener('start', function() {
        renderer.domElement.style.cursor = 'grabbing';
        if (el.modelStopSpinOnInteract !== false) {
          controls.autoRotate = false;
          if (host._modelState) host._modelState.spinning = false;
        }
        if (host._resumeTimer) { window.clearTimeout(host._resumeTimer); host._resumeTimer = null; }
      });
      controls.addEventListener('end', function() {
        renderer.domElement.style.cursor = 'grab';
        var delay = parseFloat(el.modelResumeSpin);
        if (el.modelStopSpinOnInteract !== false && el.modelAutoRotate && delay > 0) {
          host._resumeTimer = window.setTimeout(function() {
            controls.autoRotate = true;
            if (host._modelState) host._modelState.spinning = true;
          }, delay);
        }
      });
    }

    function frameModel(obj) {
      var box = new THREE.Box3().setFromObject(obj);
      if (!isFinite(box.min.x)) return;
      var size = box.getSize(new THREE.Vector3());
      var center = box.getCenter(new THREE.Vector3());
      var maxDim = Math.max(size.x, size.y, size.z) || 1;
      if (el.modelAutoFit !== false) {
        obj.position.sub(center);                       // sit on the origin
        var target = 1.6;
        obj.scale.multiplyScalar(target / maxDim);
      }
      var dist = (parseFloat(el.modelDistance) || 4);
      camera.position.set(0, parseFloat(el.modelCamHeight) || 0.4, dist);
      camera.lookAt(0, parseFloat(el.modelTargetY) || 0, 0);
      if (controls) { controls.target.set(0, parseFloat(el.modelTargetY) || 0, 0); controls.update(); }
    }

    function applyTransform(obj) {
      obj.position.x += parseFloat(el.modelX) || 0;
      obj.position.y += parseFloat(el.modelY) || 0;
      obj.position.z += parseFloat(el.modelZ) || 0;
      var sc = parseFloat(el.modelScale);
      if (!sc && sc !== 0) sc = 1;
      obj.scale.multiplyScalar(sc);
      obj.rotation.set(
        (parseFloat(el.modelRotX) || 0) * Math.PI / 180,
        (parseFloat(el.modelRotY) || 0) * Math.PI / 180,
        (parseFloat(el.modelRotZ) || 0) * Math.PI / 180
      );
    }

    var mixer = null, clock = new THREE.Clock();
    var loader = new libs.GLTFLoader();
    loader.load(el.src, function(gltf) {
      if (cancelled || !host.isConnected) return;
      var obj = gltf.scene || gltf.scenes[0];
      if (el.modelShadows) {
        obj.traverse(function(n) { if (n.isMesh) { n.castShadow = true; n.receiveShadow = true; } });
      }
      frameModel(obj);
      host._modelBaseScale = obj.scale.x;   // scale after auto-fit, before user scale
      applyTransform(obj);
      root.add(obj);
      host._modelObject = obj;
      if (gltf.animations && gltf.animations.length && el.modelPlayClips !== false) {
        mixer = new THREE.AnimationMixer(obj);
        gltf.animations.forEach(function(clip) { mixer.clipAction(clip).play(); });
      }
      if (loading.parentNode) loading.remove();
      // Let the host (e.g. a carousel) re-apply its pose now the model exists
      if (typeof el.onModelReady === 'function') {
        try { el.onModelReady(host); } catch(e) { console.warn('[Model3D] onModelReady:', e); }
      }
    }, undefined, function(err) {
      var isFile = (location.protocol === 'file:');
      console.warn('[Model3D] failed to load', el.src, err && err.message,
        isFile ? '\nThe page was opened from disk (file://) — browsers refuse to fetch .glb files that way. ' +
                 'Tick "Embed 3D models" when exporting, or serve the folder over http.' : '');
      host.innerHTML = modelPlaceholder(el,
        isFile ? 'Cannot load models from file:// — embed them on export or serve over http'
               : 'Could not load ' + el.src, false);
    });

    host.appendChild(renderer.domElement);

    // Keep the drawing buffer in step with the element
    var ro = null;
    function resize() {
      var nw = host.clientWidth || w, nh = host.clientHeight || h;
      if (!nw || !nh) return;
      camera.aspect = nw / nh;
      camera.updateProjectionMatrix();
      renderer.setSize(nw, nh, false);
    }
    if (typeof ResizeObserver !== 'undefined') {
      try { ro = new ResizeObserver(resize); ro.observe(host); } catch(e) {}
    }
    window.addEventListener('resize', resize);

    var raf = null, spinning = !!(live && el.modelAutoRotate && !controls);
    host._modelState = { spinning: spinning };
    function tick() {
      if (cancelled || !host.isConnected) return;
      raf = requestAnimationFrame(tick);
      var dt = clock.getDelta();
      if (mixer) mixer.update(dt);
      if (host._modelState.spinning && host._modelObject) {
        host._modelObject.rotation.y += (parseFloat(el.modelRotateSpeed) || 2) * 0.25 * dt;
      }
      if (controls) controls.update();
      renderer.render(scene, camera);
    }
    tick();

    host._modelCleanup = function() {
      cancelled = true;
      if (raf) cancelAnimationFrame(raf);
      window.removeEventListener('resize', resize);
      if (ro) { try { ro.disconnect(); } catch(e) {} }
      if (controls) { try { controls.dispose(); } catch(e) {} }
      try {
        scene.traverse(function(n) {
          if (n.geometry) n.geometry.dispose();
          if (n.material) {
            var mats = Array.isArray(n.material) ? n.material : [n.material];
            mats.forEach(function(m) {
              Object.keys(m).forEach(function(k) { if (m[k] && m[k].isTexture) m[k].dispose(); });
              m.dispose();
            });
          }
        });
        renderer.dispose();
      } catch(e) {}
    };
    host._modelApi = {
      renderer: renderer, scene: scene, camera: camera, root: root, controls: controls,
      setSpin: function(on) {
        host._modelState.spinning = !!on && !controls;
        if (controls) controls.autoRotate = !!on;
      },
      setPose: function(pose) {
        if (!pose) return;
        var obj = host._modelObject;
        if (obj) {
          if (pose.rotX !== undefined || pose.rotY !== undefined || pose.rotZ !== undefined) {
            obj.rotation.set(
              (pose.rotX || 0) * Math.PI / 180,
              (pose.rotY || 0) * Math.PI / 180,
              (pose.rotZ || 0) * Math.PI / 180
            );
          }
          if (pose.scale !== undefined && host._modelBaseScale) {
            var sc = host._modelBaseScale * pose.scale;
            obj.scale.set(sc, sc, sc);
          }
        }
        if (pose.distance !== undefined && camera) {
          camera.position.z = pose.distance;
          if (controls) controls.update();
        }
      },
      controlsFor: function() { return controls; },
      reset: function() {
        if (host._modelObject) {
          host._modelObject.rotation.set(
            (parseFloat(el.modelRotX) || 0) * Math.PI / 180,
            (parseFloat(el.modelRotY) || 0) * Math.PI / 180,
            (parseFloat(el.modelRotZ) || 0) * Math.PI / 180
          );
        }
        if (controls) controls.reset();
      }
    };
  });

  return host;
}

function controlUIModel(dom, cmd, value) {
  var host = dom ? dom.querySelector('.ui-model') : null;
  if (!host) return;
  var api = host._modelApi;
  switch (cmd || 'toggleSpin') {
    case 'spin': if (api) api.setSpin(true); break;
    case 'stop': if (api) api.setSpin(false); break;
    case 'toggleSpin': if (api) api.setSpin(!(host._modelState && host._modelState.spinning)); break;
    case 'reset': if (api) api.reset(); break;
    case 'setModel':
      if (value && host.parentNode) {
        var el = host._modelEl || {};
        var next = {};
        for (var k in el) next[k] = el[k];
        next.src = value;
        renderUIModel(host.parentNode, next, true);
      }
      break;
  }
}

/* ── Iframe / embed element ── */
function uiEmbedURL(src) {
  var u = String(src || '').trim();
  if (!u) return '';
  // Friendly conversions so a pasted share link just works
  var yt = u.match(/(?:youtube\.com\/(?:watch\?v=|live\/|shorts\/)|youtu\.be\/)([\w-]{6,})/i);
  if (yt) return 'https://www.youtube.com/embed/' + yt[1];
  var vim = u.match(/vimeo\.com\/(?:video\/)?(\d+)/i);
  if (vim) return 'https://player.vimeo.com/video/' + vim[1];
  var gmap = u.match(/google\.[a-z.]+\/maps\/place\/([^/?]+)/i);
  if (gmap && u.indexOf('/embed') === -1) {
    return 'https://www.google.com/maps?q=' + encodeURIComponent(decodeURIComponent(gmap[1])) + '&output=embed';
  }
  return u;
}

function buildUIFrame(el) {
  var frame = document.createElement('iframe');
  frame.className = 'ui-frame';
  frame.src = uiEmbedURL(el.src);
  frame.style.cssText = 'width:100%;height:100%;display:block;border:' + (el.frameBorder || 'none') +
    ';border-radius:' + (el.borderRadius || '0') + ';background:' + (el.frameBg || 'transparent') + ';';
  if (el.frameTitle) frame.title = el.frameTitle;
  frame.setAttribute('allow', el.frameAllow || 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
  if (el.frameAllowFullscreen !== false) frame.setAttribute('allowfullscreen', '');
  if (el.frameLazy) frame.setAttribute('loading', 'lazy');
  if (el.frameSandbox) frame.setAttribute('sandbox', el.frameSandboxRules || 'allow-scripts allow-same-origin allow-popups allow-forms');
  if (el.frameScroll === false) frame.setAttribute('scrolling', 'no');
  return frame;
}

function renderUIFrame(container, el, live) {
  container.innerHTML = '';
  if (!el.src) {
    container.innerHTML = '<div style="width:100%;height:100%;min-height:60px;display:flex;flex-direction:column;gap:4px;' +
      'align-items:center;justify-content:center;background:rgba(0,0,0,0.25);border:1px dashed rgba(255,255,255,0.2);' +
      'border-radius:' + (el.borderRadius || '8px') + ';"><i class="bi bi-window" style="font-size:1.5rem;opacity:0.5;"></i>' +
      '<span style="font-size:0.5rem;opacity:0.6;">Set a page URL</span></div>';
    return null;
  }
  // While designing, a placeholder avoids loading (and autoplaying) the real page
  if (!live && el.framePreviewOnly !== false) {
    var box = document.createElement('div');
    box.className = 'ui-frame-stub';
    box.style.cssText = 'width:100%;height:100%;display:flex;flex-direction:column;gap:6px;align-items:center;justify-content:center;' +
      'background:' + (el.frameBg || 'rgba(0,0,0,0.35)') + ';border:' + (el.frameBorder || '1px solid rgba(255,255,255,0.12)') +
      ';border-radius:' + (el.borderRadius || '8px') + ';color:#cfd6e4;text-align:center;padding:8px;box-sizing:border-box;overflow:hidden;';
    box.innerHTML = '<i class="bi bi-window-stack" style="font-size:1.4rem;opacity:0.7;"></i>' +
      '<div style="font-size:0.5rem;font-weight:700;letter-spacing:1px;">EMBEDDED PAGE</div>' +
      '<div style="font-size:0.45rem;opacity:0.7;word-break:break-all;max-width:90%;">' + escAttr(uiEmbedURL(el.src)) + '</div>' +
      '<div style="font-size:0.4rem;opacity:0.5;">loads in Preview and in the player</div>';
    container.appendChild(box);
    return null;
  }
  var frame = buildUIFrame(el);
  container.appendChild(frame);
  return frame;
}

function controlUIFrame(dom, cmd, value) {
  var frame = dom ? dom.querySelector('iframe') : null;
  if (!frame) return;
  switch (cmd || 'reload') {
    case 'reload': frame.src = frame.src; break;
    case 'setSrc': if (value) frame.src = uiEmbedURL(value); break;
    case 'blank': frame.src = 'about:blank'; break;
  }
}

/* ── Hero section (full-bleed backgrounds that cross-fade) ── */
function heroSlidesOf(el) {
  var list = el.heroSlides || [];
  return list.length ? list : [{ id: 'hs_empty', image: '', title: '', text: '' }];
}

function buildHeroCSS(el) {
  var s = '.hero-' + el.id;
  var dur = parseInt(el.heroTransDuration);
  if (!dur && dur !== 0) dur = 900;
  var trans = el.heroTransition || 'fade';
  var pad = el.heroPadding || '6%';
  var alignH = el.heroAlignH || 'center';
  var alignV = el.heroAlignV || 'center';
  var justify = alignV === 'top' ? 'flex-start' : alignV === 'bottom' ? 'flex-end' : 'center';
  var items = alignH === 'left' ? 'flex-start' : alignH === 'right' ? 'flex-end' : 'center';
  var textAlign = alignH === 'left' ? 'left' : alignH === 'right' ? 'right' : 'center';

  // How an inactive slide sits before/after it becomes the active one
  var restState, activeState;
  if (trans === 'slide-left') { restState = 'transform:translateX(100%);'; activeState = 'transform:translateX(0);'; }
  else if (trans === 'slide-right') { restState = 'transform:translateX(-100%);'; activeState = 'transform:translateX(0);'; }
  else if (trans === 'slide-up') { restState = 'transform:translateY(100%);'; activeState = 'transform:translateY(0);'; }
  else if (trans === 'zoom') { restState = 'opacity:0; transform:scale(1.15);'; activeState = 'opacity:1; transform:scale(1);'; }
  else { restState = 'opacity:0;'; activeState = 'opacity:1;'; }

  var out = [
    s + ' { position:relative; width:100%; height:100%; overflow:hidden; }',
    s + ' .hero-bgs { position:absolute; inset:0; overflow:hidden; }',
    s + ' .hero-bg { position:absolute; inset:0; background-size:' + (el.heroBgFit || 'cover') +
      '; background-position:' + (el.heroBgPosition || 'center') + '; background-repeat:no-repeat; ' +
      restState + ' transition:opacity ' + dur + 'ms ease, transform ' + dur + 'ms ease; }',
    s + ' .hero-bg.active { ' + activeState + ' z-index:1; }',
    s + ' .hero-bg video { width:100%; height:100%; object-fit:' + (el.heroBgFit || 'cover') + '; display:block; }',
    s + ' .hero-overlay { position:absolute; inset:0; z-index:2; pointer-events:none; background:' +
      (el.heroOverlay || 'none') + '; opacity:' + (el.heroOverlayOpacity !== undefined ? el.heroOverlayOpacity : 1) + '; }',
    s + ' .hero-content { position:absolute; inset:0; z-index:3; display:flex; flex-direction:column; box-sizing:border-box;' +
      ' justify-content:' + justify + '; align-items:' + items + '; text-align:' + textAlign +
      '; padding:' + pad + '; gap:' + (el.heroGap || '10px') + '; pointer-events:none; }',
    s + ' .hero-content > * { pointer-events:auto; max-width:' + (el.heroMaxWidth || '100%') + '; }',
    s + ' .hero-title { font-family:' + (el.heroTitleFont || el.fontFamily || "'Orbitron',sans-serif") +
      '; font-size:' + (el.heroTitleSize !== undefined ? el.heroTitleSize : 2.4) + (el.heroTitleUnit || 'rem') +
      '; font-weight:' + (el.heroTitleWeight || '700') + '; color:' + (el.heroTitleColor || '#ffffff') +
      '; line-height:' + (el.heroTitleLineHeight || '1.1') + '; letter-spacing:' + (el.heroTitleSpacing || 'normal') +
      '; text-shadow:' + (el.heroTitleShadow || 'none') + '; margin:0; }',
    s + ' .hero-sub { font-family:' + (el.heroSubFont || el.fontFamily || "'Rajdhani',sans-serif") +
      '; font-size:' + (el.heroSubSize !== undefined ? el.heroSubSize : 1) + (el.heroSubUnit || 'rem') +
      '; font-weight:' + (el.heroSubWeight || '400') + '; color:' + (el.heroSubColor || '#d0d8e4') +
      '; line-height:' + (el.heroSubLineHeight || '1.5') + '; text-shadow:' + (el.heroSubShadow || 'none') + '; margin:0; }',
    s + ' .hero-children { position:absolute; inset:0; z-index:4; pointer-events:none; }',
    s + ' .hero-dots { position:absolute; z-index:5; left:0; right:0; bottom:' + (parseFloat(el.heroDotBottom) || 18) +
      'px; display:flex; justify-content:center; align-items:center; gap:' + (parseFloat(el.heroDotGap) || 8) + 'px; }',
    s + ' .hero-dot { width:' + (parseFloat(el.heroDotWidth) || 10) + 'px; height:' + (parseFloat(el.heroDotHeight) || 10) +
      'px; border-radius:' + (el.heroDotRadius || '50%') + '; background:' + (el.heroDotColor || 'rgba(255,255,255,0.35)') +
      '; cursor:pointer; transition:all 0.25s; border:none; padding:0; }',
    s + ' .hero-dot.active { background:' + (el.heroDotActiveColor || '#ffffff') + ';' +
      (el.heroDotActiveWidth ? ' width:' + parseFloat(el.heroDotActiveWidth) + 'px;' : '') + ' }',
    s + ' .hero-arrow { position:absolute; z-index:5; top:50%; transform:translateY(-50%); width:' +
      (parseFloat(el.heroArrowSize) || 36) + 'px; height:' + (parseFloat(el.heroArrowSize) || 36) +
      'px; border-radius:' + (el.heroArrowRadius || '50%') + '; background:' + (el.heroArrowBg || 'rgba(0,0,0,0.35)') +
      '; color:' + (el.heroArrowColor || '#fff') + '; border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:' +
      (el.heroArrowIconSize !== undefined ? el.heroArrowIconSize : 1) + 'rem; }',
    s + ' .hero-arrow.prev { left:' + (parseFloat(el.heroArrowInset) || 12) + 'px; }',
    s + ' .hero-arrow.next { right:' + (parseFloat(el.heroArrowInset) || 12) + 'px; }'
  ];
  if (el.heroKenBurns) {
    out.push('@keyframes heroKen-' + el.id + ' { from { transform:scale(1); } to { transform:scale(' +
      (el.heroKenScale || 1.12) + '); } }');
    out.push(s + ' .hero-bg.active { animation:heroKen-' + el.id + ' ' +
      ((parseInt(el.heroInterval) || 6000) + dur) + 'ms ease-out forwards; }');
  }
  return out.join('\n');
}

function applyHeroCSS(el) {
  var st = document.getElementById('hero-style-' + el.id);
  if (!st) { st = document.createElement('style'); st.id = 'hero-style-' + el.id; document.head.appendChild(st); }
  st.textContent = buildHeroCSS(el);
}

function heroShowSlide(wrap, el, index, live) {
  var slides = heroSlidesOf(el);
  var n = slides.length;
  if (!n) return;
  index = ((index % n) + n) % n;
  wrap._heroIndex = index;
  var bgs = wrap.querySelectorAll('.hero-bg');
  Array.prototype.forEach.call(bgs, function(b, i) { b.classList.toggle('active', i === index); });
  var dots = wrap.querySelectorAll('.hero-dot');
  Array.prototype.forEach.call(dots, function(d, i) { d.classList.toggle('active', i === index); });

  // Per-slide headings fall back to the hero's own text
  var slide = slides[index] || {};
  var t = wrap.querySelector('.hero-title');
  var sub = wrap.querySelector('.hero-sub');
  if (t) {
    var title = slide.title !== undefined && slide.title !== '' ? slide.title : (el.heroTitle || '');
    t.innerHTML = title;
    t.style.display = title ? '' : 'none';
  }
  if (sub) {
    var text = slide.text !== undefined && slide.text !== '' ? slide.text : (el.heroSubtitle || '');
    sub.innerHTML = text;
    sub.style.display = text ? '' : 'none';
  }
  // Videos only play while their slide is on screen
  Array.prototype.forEach.call(bgs, function(b, i) {
    var v = b.querySelector('video');
    if (!v) return;
    if (i === index && live) { v.play().catch(function(){}); }
    else { v.pause(); }
  });
  if (live && el.heroChangeFunc && typeof window[el.heroChangeFunc] === 'function') {
    try { window[el.heroChangeFunc](slide, index, el); } catch(e) { console.warn('[Hero]', e); }
  }
}

function heroStopAuto(wrap) {
  if (wrap && wrap._heroTimer) { window.clearInterval(wrap._heroTimer); wrap._heroTimer = null; }
}

function heroStartAuto(wrap, el, live) {
  heroStopAuto(wrap);
  if (!live || !el.heroAutoplay) return;
  var slides = heroSlidesOf(el);
  if (slides.length < 2) return;
  wrap._heroTimer = window.setInterval(function() {
    if (!document.body.contains(wrap)) { heroStopAuto(wrap); return; }
    heroShowSlide(wrap, el, (wrap._heroIndex || 0) + 1, live);
  }, Math.max(600, parseInt(el.heroInterval) || 6000));
}

function renderUIHero(container, el, live) {
  var prev = container.querySelector('.hero-wrap');
  if (prev) heroStopAuto(prev);
  container.innerHTML = '';
  applyHeroCSS(el);

  var wrap = document.createElement('div');
  wrap.className = 'hero-wrap hero-' + el.id;

  var slides = heroSlidesOf(el);
  var bgs = document.createElement('div');
  bgs.className = 'hero-bgs';
  slides.forEach(function(sl, i) {
    var b = document.createElement('div');
    b.className = 'hero-bg';
    if (sl.video) {
      var v = document.createElement('video');
      v.src = sl.video; v.muted = true; v.loop = true; v.playsInline = true; v.preload = 'auto';
      b.appendChild(v);
    } else if (sl.image) {
      b.style.backgroundImage = 'url(' + sl.image + ')';
    } else {
      b.style.background = sl.color || (el.heroEmptyBg || 'linear-gradient(135deg,#1b2030,#0b0e17)');
    }
    bgs.appendChild(b);
  });
  wrap.appendChild(bgs);

  var ov = document.createElement('div');
  ov.className = 'hero-overlay';
  wrap.appendChild(ov);

  var content = document.createElement('div');
  content.className = 'hero-content';
  var title = document.createElement('div');
  title.className = 'hero-title';
  var sub = document.createElement('div');
  sub.className = 'hero-sub';
  content.appendChild(title);
  content.appendChild(sub);
  wrap.appendChild(content);

  // Anything parented onto the hero lands here
  var kids = document.createElement('div');
  kids.className = 'hero-children gui-children ui-el-children';
  wrap.appendChild(kids);

  if (el.heroArrows && slides.length > 1) {
    ['prev', 'next'].forEach(function(dir) {
      var b = document.createElement('button');
      b.className = 'hero-arrow ' + dir;
      b.innerHTML = '<i class="bi ' + (dir === 'prev'
        ? (el.heroPrevIcon || 'bi-chevron-left')
        : (el.heroNextIcon || 'bi-chevron-right')) + '"></i>';
      b.addEventListener('click', function(e) {
        e.stopPropagation();
        heroShowSlide(wrap, el, (wrap._heroIndex || 0) + (dir === 'next' ? 1 : -1), live);
        heroStartAuto(wrap, el, live);
      });
      wrap.appendChild(b);
    });
  }

  if (el.heroDots && slides.length > 1) {
    var dotBox = document.createElement('div');
    dotBox.className = 'hero-dots';
    slides.forEach(function(sl, i) {
      var dt = document.createElement('button');
      dt.className = 'hero-dot';
      dt.addEventListener('click', function(e) {
        e.stopPropagation();
        heroShowSlide(wrap, el, i, live);
        heroStartAuto(wrap, el, live);
      });
      dotBox.appendChild(dt);
    });
    wrap.appendChild(dotBox);
  }

  wrap._heroEl = el;
  wrap._heroLive = live;
  container.appendChild(wrap);
  heroShowSlide(wrap, el, parseInt(el.heroStart) || 0, live);
  heroStartAuto(wrap, el, live);
  return wrap;
}

function controlUIHero(dom, cmd, index) {
  var wrap = dom ? dom.querySelector('.hero-wrap') : null;
  if (!wrap) return;
  var el = wrap._heroEl;
  if (!el) return;
  switch (cmd || 'next') {
    case 'next': heroShowSlide(wrap, el, (wrap._heroIndex || 0) + 1, true); break;
    case 'prev': heroShowSlide(wrap, el, (wrap._heroIndex || 0) - 1, true); break;
    case 'goTo': heroShowSlide(wrap, el, parseInt(index) || 0, true); break;
    case 'play': heroStartAuto(wrap, el, true); break;
    case 'stop': heroStopAuto(wrap); break;
  }
}

/* ── Video chroma key (colour → transparency) ── */
function uiHexToRgb(hex) {
  var h = String(hex || '#00ff00').trim();
  if (h.charAt(0) === '#') h = h.slice(1);
  if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
  var n = parseInt(h, 16);
  if (isNaN(n)) return { r: 0, g: 255, b: 0 };
  return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}

function stopUIVideoChroma(container) {
  if (!container) return;
  var old = container.querySelector('canvas.ui-chroma');
  if (old) { old._stop = true; old.remove(); }
}

/**
 * Replaces the <video> with a canvas whose keyed colour is made transparent.
 * Falls back to the plain video if the source taints the canvas (cross-origin).
 */
function setupUIVideoChroma(container, vid, el) {
  stopUIVideoChroma(container);
  if (!container || !vid) return;
  vid.style.removeProperty('display');
  if (!el.chromaKey) return;

  var canvas = document.createElement('canvas');
  canvas.className = 'ui-chroma';
  canvas.style.cssText = vid.style.cssText;
  canvas.style.background = 'transparent';
  var ctx = null;
  try { ctx = canvas.getContext('2d', { willReadFrequently: true }); } catch(e) {}
  if (!ctx) return;

  vid.style.display = 'none';
  container.appendChild(canvas);

  var key = uiHexToRgb(el.chromaColor || '#00ff00');
  var sim = el.chromaSimilarity !== undefined ? parseFloat(el.chromaSimilarity) : 0.4;
  var smooth = el.chromaSmoothness !== undefined ? parseFloat(el.chromaSmoothness) : 0.08;
  var spill = el.chromaSpill !== undefined ? parseFloat(el.chromaSpill) : 0;
  var lastTime = -1, lastReady = -1;

  function draw() {
    if (canvas._stop || !canvas.isConnected) return;
    var w = vid.videoWidth, h = vid.videoHeight;
    // Only re-key when there is a new frame (a paused video stays cheap)
    if (w && h && (vid.currentTime !== lastTime || vid.readyState !== lastReady)) {
      lastTime = vid.currentTime; lastReady = vid.readyState;
      if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
      try {
        ctx.clearRect(0, 0, w, h);
        ctx.drawImage(vid, 0, 0, w, h);
        var img = ctx.getImageData(0, 0, w, h), d = img.data;
        var range = Math.max(0.0001, smooth);
        for (var i = 0; i < d.length; i += 4) {
          var dr = d[i] - key.r, dg = d[i + 1] - key.g, db = d[i + 2] - key.b;
          var dist = Math.sqrt(dr * dr + dg * dg + db * db) / 441.6729;
          if (dist < sim) {
            d[i + 3] = 0;
          } else if (dist < sim + range) {
            var a = (dist - sim) / range;
            d[i + 3] = d[i + 3] * a;
            if (spill > 0) {   // pull the key colour out of the soft edge
              d[i] = d[i] + (d[i] - key.r) * spill * (1 - a);
              d[i + 1] = d[i + 1] + (d[i + 1] - key.g) * spill * (1 - a);
              d[i + 2] = d[i + 2] + (d[i + 2] - key.b) * spill * (1 - a);
            }
          }
        }
        ctx.putImageData(img, 0, 0);
      } catch(err) {
        // Tainted canvas: the browser refuses to read frames from a cross-origin or
        // file:// video. Try once with CORS, then fall back to the untouched video.
        if (!vid._chromaRetried && /^https?:/i.test(vid.src)) {
          vid._chromaRetried = true;
          var wasTime = vid.currentTime, wasPlaying = !vid.paused;
          vid.crossOrigin = 'anonymous';
          vid.load();
          vid.addEventListener('loadeddata', function once() {
            vid.removeEventListener('loadeddata', once);
            try { vid.currentTime = wasTime; } catch(e) {}
            if (wasPlaying) vid.play().catch(function(){});
          });
          lastTime = -1; lastReady = -1;
          if (typeof requestAnimationFrame === 'function') requestAnimationFrame(draw);
          return;
        }
        console.warn('[Video] chroma key unavailable for this source (' + (err && err.message) + '). ' +
          'Frames cannot be read from a file:// or cross-origin video — embed the video in the export, ' +
          'or serve it from the same origin with CORS enabled.');
        canvas._stop = true;
        canvas.remove();
        vid.style.removeProperty('display');
        return;
      }
    }
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(draw);
    else window.setTimeout(draw, 40);
  }
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(draw);
  else window.setTimeout(draw, 40);
}

// ── Video helpers (shared by canvas + preview) ──
function applyVideoFit(vid, el) {
  const fit = el.videoFit || 'contain';
  let css = 'display:block;border-radius:' + (el.borderRadius || 0) + ';';
  if (fit === 'fit-width') css += 'width:100%;height:auto;';
  else if (fit === 'fit-height') css += 'height:100%;width:auto;';
  else css += 'width:100%;height:100%;object-fit:' + (fit === 'stretch' ? 'fill' : fit) + ';';
  vid.style.cssText = css;
}

function applyVideoPlayback(vid, el, live) {
  vid.muted = el.muted !== false;
  vid.volume = Math.max(0, Math.min(1, el.volume !== undefined ? el.volume : 1));
  vid.loop = el.loopMode === 'loop';
  vid.controls = !!(live && el.controls);
  // Clear previous handlers
  if (vid._endedHandler) { vid.removeEventListener('ended', vid._endedHandler); vid._endedHandler = null; }
  if (vid._clickHandler) { vid.removeEventListener('click', vid._clickHandler); vid._clickHandler = null; }
  if (!live) {
    vid.autoplay = false;
    if (!vid.paused) vid.pause();
    try { vid.currentTime = 0; } catch(e) {}
    return;
  }
  // Live (preview / runtime) behavior
  if (el.loopMode === 'count') {
    let played = 1;
    vid._endedHandler = () => {
      if (played < (parseInt(el.loopCount) || 1)) { played++; vid.currentTime = 0; vid.play().catch(() => {}); }
    };
    vid.addEventListener('ended', vid._endedHandler);
  }
  if ((el.playMode || 'auto') === 'auto') {
    vid.autoplay = true;
    vid.play().catch(() => {});
  } else if (el.playMode === 'click' && !el.controls) {
    vid.style.cursor = 'pointer';
    vid._clickHandler = () => { if (vid.paused) vid.play().catch(() => {}); else vid.pause(); };
    vid.addEventListener('click', vid._clickHandler);
  }
}

function renderElement(el) {
  let dom = viewport.querySelector(`[data-id="${el.id}"]`);
  if (!dom) {
    dom = document.createElement('div');
    dom.className = 'ui-el';
    dom.dataset.id = el.id;
    // Resize handles
    ['tl','tr','bl','br','t','b','l','r'].forEach(pos => {
      const h = document.createElement('div');
      h.className = `resize-handle rh-${pos}`;
      h.dataset.handle = pos;
      dom.appendChild(h);
    });
    // Element interaction
    dom.addEventListener('pointerdown', e => onElementPointerDown(e, el.id));
  }

  // Place in correct parent container
  let parentContainer = viewport;
  if (el.parentId) {
    const parentInner = viewport.querySelector(`[data-id="${el.parentId}"] .ui-el-inner .ui-el-children`);
    if (parentInner) parentContainer = parentInner;
  }
  if (dom.parentElement !== parentContainer) {
    parentContainer.appendChild(dom);
  }

  // Position — children use relative positioning within parent
  const isChild = el.parentId && parentContainer !== viewport;
  const anchor = getVal(el, 'anchor') || 'center';

  dom.style.left = getVal(el,'x') + '%';
  dom.style.top = getVal(el,'y') + '%';
  dom.style.transform = uiAnchorTransform(anchor);
  const rw = getVal(el,'w'); const rh = getVal(el,'h');
  dom.style.width = rw > 0 ? rw + '%' : 'auto';
  dom.style.height = rh > 0 ? rh + '%' : 'auto';
  dom.style.zIndex = el.zIndex;

  // ── Visibility resolution: reset first, then apply correct state ──
  dom.classList.remove('el-hidden-edit', 'el-disabled');
  dom.style.display = '';
  dom.style.pointerEvents = previewMode && el.locked ? 'none' : 'auto';
  dom.style.opacity = getVal(el, 'opacity');

  if (el.disabled) {
    dom.style.display = 'none';
    dom.classList.add('el-hidden-edit', 'el-disabled');
  } else if (el.showOn && el.showOn.length > 0 && !el.showOn.includes(deviceMode)) {
    if (previewMode) {
      dom.style.display = 'none';
    } else {
      dom.style.opacity = '0.15';
    }
    dom.classList.add('el-hidden-edit');
  } else {
    const elVisible = getVal(el,'visible');
    const effectivelyVisible = elVisible !== false && isEffectivelyVisible(el);
    if (!effectivelyVisible) {
      dom.style.opacity = '0';
      dom.style.pointerEvents = 'none';
      dom.classList.add('el-hidden-edit');
    }
  }

  // Build inner content based on type
  let inner = dom.querySelector('.ui-el-inner');
  if (!inner) {
    inner = document.createElement('div');
    inner.className = 'ui-el-inner';
    dom.insertBefore(inner, dom.firstChild);
  }

  // Apply styles (using getVal for overridable properties)
  const s = inner.style;
  s.width = '100%'; s.height = '100%';
  s.fontFamily = el.fontFamily;
  s.fontSize = getVal(el,'fontSize') + (getVal(el,'fontUnit') || 'rem');
  s.fontWeight = getVal(el,'fontWeight');
  s.color = getVal(el,'color');
  s.background = getVal(el,'background');
  s.borderRadius = getVal(el,'borderRadius');
  s.padding = getVal(el,'padding');
  s.border = getVal(el,'border');
  s.textAlign = getVal(el,'textAlign');
  s.textShadow = el.textShadow;
  s.boxShadow = getVal(el,'boxShadow');
  s.letterSpacing = getVal(el,'letterSpacing');
  s.lineHeight = el.lineHeight || 'normal';
  s.textTransform = el.textTransform;
  s.overflow = el.type === 'panel' ? 'visible' : 'hidden';
  s.position = (el.type === 'panel' || el.type === 'hero') ? 'relative' : '';
  if (el.backdropFilter) s.backdropFilter = el.backdropFilter;
  if (el.objectFit) s.objectFit = el.objectFit;
  if (el.aspectRatio) s.aspectRatio = el.aspectRatio;
  if (el.minWidth) s.minWidth = el.minWidth;
  if (el.minHeight) s.minHeight = el.minHeight;
  if (el.cursor) s.cursor = el.cursor;
  if (el.display) s.display = el.display;
  if (el.justifyContent) s.justifyContent = el.justifyContent;
  if (el.alignItems) s.alignItems = el.alignItems;
  if (el.borderTop) s.borderTop = el.borderTop;

  // Apply custom CSS
  if (el.customCSS) {
    try {
      const pairs = el.customCSS.split(';').filter(p => p.includes(':'));
      pairs.forEach(p => {
        const [prop, ...rest] = p.split(':');
        const cssProp = prop.trim();
        const val = rest.join(':').trim();
        if (cssProp && val) {
          // Convert CSS prop to camelCase for style assignment
          const camel = cssProp.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
          inner.style[camel] = val;
        }
      });
    } catch(e) {}
  }

  // Animation sequences run in Preview only — the canvas stays still while designing.
  // (Use the Play buttons in the Animation section to test a stage on the canvas.)
  if (el.animEnterClass || el.animAttnClass || el.animExitClass) {
    if (previewMode) {
      window.setTimeout(() => {
        const iv = dom.querySelector('.ui-el-inner');
        if (iv) runUIAnimSequence(iv, el, 'all');
      }, 0);
    } else {
      clearUIAnim(inner);
    }
  } else {
  // Legacy single animation (older projects)
  inner.classList.forEach(c => { if (c.startsWith('animate__')) inner.classList.remove(c); });
  if (el.animateClass) {
    inner.classList.add('animate__animated', el.animateClass);
    if (el.animateDelay) inner.style.animationDelay = el.animateDelay;
    else inner.style.removeProperty('animation-delay');
    if (el.animateDuration) inner.style.animationDuration = el.animateDuration;
    else inner.style.removeProperty('animation-duration');
    if (el.animateRepeat) inner.style.animationIterationCount = el.animateRepeat;
    else inner.style.removeProperty('animation-iteration-count');
  } else {
    inner.style.removeProperty('animation-delay');
    inner.style.removeProperty('animation-duration');
    inner.style.removeProperty('animation-iteration-count');
  }
  }

  // Type-specific rendering
  if (el.type === 'text' || el.type === 'button') {
    inner.textContent = el.content;
    if (el.type === 'button' && !previewMode) { s.cursor = 'inherit'; }
  } else if (el.type === 'image') {
    if (el.src && el.objectFit === 'background') {
      inner.innerHTML = '';
      inner.style.backgroundImage = `url(${el.src})`;
      inner.style.backgroundSize = el.bgSize || 'cover';
      inner.style.backgroundPosition = el.bgPosition || 'center';
      inner.style.backgroundRepeat = el.bgRepeat || 'no-repeat';
    } else if (el.src) {
      inner.innerHTML = `<img src="${el.src}" style="width:100%;height:100%;object-fit:${el.objectFit};border-radius:${el.borderRadius};display:block;" draggable="false">`;
    } else {
      inner.innerHTML = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.03);border:1px dashed rgba(255,255,255,0.15);border-radius:' + el.borderRadius + ';"><i class="bi bi-image" style="font-size:1.5rem;color:var(--text-muted);"></i></div>';
    }
  } else if (el.type === 'video') {
    if (el.src) {
      let vid = inner.querySelector('video');
      if (!vid || vid.dataset.src !== el.src) {
        inner.innerHTML = '';
        vid = document.createElement('video');
        vid.dataset.src = el.src;
        vid.src = el.src;
        vid.preload = 'metadata';
        vid.playsInline = true;
        inner.appendChild(vid);
      }
      vid.poster = el.videoPoster || '';
      applyVideoFit(vid, el);
      vid.style.mixBlendMode = (el.videoBlend && el.videoBlend !== 'none') ? el.videoBlend : '';
      setupUIVideoChroma(inner, vid, el);
      if (el.videoFit === 'fit-width' || el.videoFit === 'fit-height') {
        s.display = 'flex'; s.alignItems = 'center'; s.justifyContent = 'center';
      }
      applyVideoPlayback(vid, el, previewMode);
      // Edit-mode badge showing play mode
      let badge = inner.querySelector('.video-badge');
      if (!previewMode) {
        if (!badge) {
          badge = document.createElement('div');
          badge.className = 'video-badge';
          badge.style.cssText = 'position:absolute;top:4px;left:4px;background:rgba(0,0,0,0.65);color:#00e5a0;font-family:"Orbitron",sans-serif;font-size:0.42rem;letter-spacing:1px;padding:2px 6px;border-radius:4px;pointer-events:none;z-index:2;';
          inner.style.position = 'relative';
          inner.appendChild(badge);
        }
        const loopTxt = el.loopMode === 'loop' ? '∞' : (el.loopMode === 'count' ? '×' + (el.loopCount || 1) : '×1');
        badge.innerHTML = '<i class="bi bi-film"></i> ' + (el.playMode || 'auto').toUpperCase() + ' ' + loopTxt;
      } else if (badge) {
        badge.remove();
      }
    } else {
      inner.innerHTML = '<div style="width:100%;height:100%;display:flex;flex-direction:column;gap:4px;align-items:center;justify-content:center;background:rgba(0,0,0,0.35);border:1px dashed rgba(255,255,255,0.2);border-radius:' + el.borderRadius + ';"><i class="bi bi-film" style="font-size:1.6rem;color:var(--text-muted);"></i><span style="font-size:0.5rem;color:var(--text-muted);">Set video src</span></div>';
    }
  } else if (el.type === 'model3d') {
    const modelSig = JSON.stringify([el.src, el.modelBg, el.modelAutoRotate, el.modelRotateSpeed,
      el.modelControls, el.modelZoom, el.modelPan, el.modelAutoFit, el.modelDistance, el.modelFov,
      el.modelCamHeight, el.modelTargetY, el.modelX, el.modelY, el.modelZ, el.modelScale,
      el.modelRotX, el.modelRotY, el.modelRotZ, el.modelLighting, el.modelLightIntensity,
      el.modelShadows, el.modelHemi, el.modelPlayClips, el.modelLiveOnCanvas,
      el.modelDragRotate, el.modelDragSpeed, el.modelZoomSpeed, el.modelDamping,
      el.modelStopSpinOnInteract, el.modelResumeSpin, el.modelMinDistance, el.modelMaxDistance,
      el.modelMinPolar, el.modelMaxPolar, previewMode]);
    if (dom.dataset.modelSig !== modelSig || !inner.firstChild) {
      dom.dataset.modelSig = modelSig;
      // On the canvas the model renders but only spins when "Live on canvas" is on
      renderUIModel(inner, el, previewMode || el.modelLiveOnCanvas !== false);
    }
    s.overflow = 'hidden';
  } else if (el.type === 'iframe') {
    const frameSig = JSON.stringify([el.src, el.frameAllow, el.frameSandbox, el.frameSandboxRules,
      el.frameScroll, el.frameLazy, el.frameAllowFullscreen, el.framePreviewOnly, el.borderRadius,
      el.frameBorder, el.frameBg, previewMode]);
    if (dom.dataset.frameSig !== frameSig || !inner.firstChild) {
      dom.dataset.frameSig = frameSig;
      renderUIFrame(inner, el, previewMode);
    }
  } else if (el.type === 'hero') {
    const heroSig = JSON.stringify([el.heroSlides, el.heroTitle, el.heroSubtitle, el.heroTransition,
      el.heroTransDuration, el.heroKenBurns, el.heroDots, el.heroArrows, el.heroStart,
      el.heroAutoplay, el.heroInterval, previewMode]);
    if (dom.dataset.heroSig !== heroSig || !inner.querySelector('.hero-wrap')) {
      dom.dataset.heroSig = heroSig;
      renderUIHero(inner, el, previewMode);
      // children live inside the hero, so re-mount them after a rebuild
      elements.filter(c => c.parentId === el.id).forEach(c => renderElement(c));
    } else {
      applyHeroCSS(el);
      const hw = inner.querySelector('.hero-wrap');
      if (hw) { hw._heroEl = el; heroShowSlide(hw, el, hw._heroIndex || 0, previewMode); }
    }
    s.overflow = 'hidden'; s.position = 'relative';
  } else if (el.type === 'carousel') {
    // Only rebuild (and re-init Owl) when content/options change — not on drag/resize
    const carSig = JSON.stringify([el.carouselItems, el.carItems, el.carItemsTablet, el.carItemsPhone, el.carMargin,
      el.carStagePadding, el.carAutoWidth, el.carLoop, el.carCenter, el.carAutoplay, el.carAutoplayTimeout,
      el.carAutoplayHoverPause, el.carSpeed, el.carMouseDrag, el.carNav, el.carDots, el.carStart,
      el.carItemTemplate, el.carShowImage, el.carShowTitle, el.carShowText, el.carItemClick,
      el.carImgMode, el.carImgPosition, el.carCoverAlign,
      el.carFgInset, el.carFgFit, el.carFgPosition, el.carFgRadius, el.carFgShadow, el.carModelX, el.carModelZ,
      el.carFgPadding, el.carFgWidth, el.carFgHeight, el.carFgAlignH, el.carFgAlignV,
      el.carModelDragRotate, el.carModelZoom, el.carModelPan, el.carModelDragSpeed, el.carModelZoomSpeed,
      el.carModelDamping, el.carModelStopSpinOnInteract, el.carModelResumeSpin,
      el.carModelMinDistance, el.carModelMaxDistance, el.carModelMinPolar, el.carModelMaxPolar,
      el.carLinks, el.carChangeFunc, el.carClickToCenter, el.carShowFields, el.carActiveSlot,
      el.carActiveZ, el.carPadTop, el.carPadBottom,
      el.carItemsPhoneLand, el.carDotPosition, el.carResetVisibility, deviceMode, el.carDeviceOverrides,
      el.carModelHeight, el.carModelBg, el.carModelAutoRotate, el.carModelRotateSpeed, el.carModelControls,
      el.carModelDistance, el.carModelFov, el.carModelLighting, el.carModelLightIntensity, el.carModelScale,
      el.carModelWidth, el.carModelInset, el.carModelRotX, el.carModelRotY, el.carModelRotZ, el.carModelY,
      el.carSideGap, el.carSideGapStacks, el.carModelSpinActiveOnly,
      el.carNavPrevIcon, el.carNavNextIcon, previewMode]);
    window.__uiCarouselDevice = deviceMode === 'desktop' ? null : deviceMode;
    if (dom.dataset.carSig !== carSig || !inner.firstChild) {
      dom.dataset.carSig = carSig;
      renderUICarousel(inner, el, previewMode);
    } else {
      applyCarouselCSS(el); // pure styling change → just refresh the CSS
      const wrapEl = inner.querySelector('.car-wrap');
      if (wrapEl) syncCarouselLinks(el, wrapEl);
    }
    s.display = 'flex'; s.alignItems = 'center'; s.overflow = 'visible';
  } else if (el.type === 'icon') {
    inner.innerHTML = `<i class="bi ${el.iconClass}" style="font-size:${el.fontSize}rem;color:${el.color};text-shadow:${el.textShadow};"></i>`;
  } else if (el.type === 'progressbar') {
    const pct = Math.max(0, Math.min(100, el.barValue));
    let labelHtml = '';
    if (el.showLabel && el.labelText) {
      labelHtml = `<span style="position:absolute;left:6px;top:50%;transform:translateY(-50%);font-family:'Rajdhani',sans-serif;font-size:0.55rem;font-weight:700;color:rgba(255,255,255,0.8);text-shadow:0 1px 2px rgba(0,0,0,0.5);z-index:1;">${el.labelText}</span>`;
    }
    inner.innerHTML = `
      <div style="position:relative;width:100%;height:${el.barHeight};background:${el.barBgColor};border-radius:${el.borderRadius};overflow:hidden;">
        ${labelHtml}
        <div style="width:${pct}%;height:100%;background:${el.barColor};border-radius:${el.borderRadius};transition:width 0.3s;"></div>
      </div>`;
  } else if (el.type === 'panel') {
    // Only clear text, preserve children container
    const existingChildren = inner.querySelector('.ui-el-children');
    if (!existingChildren) {
      inner.innerHTML = '';
      const childBox = document.createElement('div');
      childBox.className = 'ui-el-children';
      childBox.style.cssText = 'position:absolute;inset:0;pointer-events:none;';
      inner.appendChild(childBox);
    }
    // Ensure children container pointer events pass through but children catch them
    const cbox = inner.querySelector('.ui-el-children');
    if (cbox) {
      cbox.querySelectorAll('.ui-el').forEach(c => { c.style.pointerEvents = 'auto'; });
    }
    // Control mode indicator
    let ctrlIndicator = inner.querySelector('.ctrl-indicator');
    const cArrow = el.controlKnob || '#00e5a0';
    const cBg = el.controlBg || 'rgba(255,255,255,0.08)';
    if (el.controlMode === 'dpad') {
      if (!ctrlIndicator) { ctrlIndicator = document.createElement('div'); ctrlIndicator.className = 'ctrl-indicator'; inner.appendChild(ctrlIndicator); }
      ctrlIndicator.innerHTML = `<div style="position:absolute;inset:5%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;pointer-events:none;">
        <div style="display:flex;align-items:center;justify-content:center;width:36%;aspect-ratio:1;border-radius:8px;background:${cBg};border:1px solid rgba(255,255,255,0.15);"><i class="bi bi-caret-up-fill" style="font-size:1.2rem;color:${cArrow};"></i></div>
        <div style="display:flex;gap:2px;width:100%;justify-content:center;">
          <div style="display:flex;align-items:center;justify-content:center;width:36%;aspect-ratio:1;border-radius:8px;background:${cBg};border:1px solid rgba(255,255,255,0.15);"><i class="bi bi-caret-left-fill" style="font-size:1.2rem;color:${cArrow};"></i></div>
          <div style="display:flex;align-items:center;justify-content:center;width:36%;aspect-ratio:1;border-radius:8px;background:${cBg};border:1px solid rgba(255,255,255,0.15);"><i class="bi bi-caret-right-fill" style="font-size:1.2rem;color:${cArrow};"></i></div>
        </div>
        <div style="display:flex;align-items:center;justify-content:center;width:36%;aspect-ratio:1;border-radius:8px;background:${cBg};border:1px solid rgba(255,255,255,0.15);"><i class="bi bi-caret-down-fill" style="font-size:1.2rem;color:${cArrow};"></i></div>
      </div>`;
    } else if (el.controlMode === 'joystick') {
      if (!ctrlIndicator) { ctrlIndicator = document.createElement('div'); ctrlIndicator.className = 'ctrl-indicator'; inner.appendChild(ctrlIndicator); }
      const knobC = el.controlKnob || 'rgba(0,229,160,0.5)';
      ctrlIndicator.innerHTML = `<div style="position:absolute;inset:10%;border-radius:50%;background:${cBg};border:1px solid rgba(255,255,255,0.1);display:flex;justify-content:center;align-items:center;pointer-events:none;"><div style="width:40%;height:40%;border-radius:50%;background:${knobC};border:2px solid ${knobC};opacity:0.7;"></div></div>`;
    } else if (ctrlIndicator) {
      ctrlIndicator.remove();
    }
  }

  // Locked / selected state
  dom.classList.toggle('locked', el.locked);
  dom.classList.toggle('selected', el.id === selectedId);

  // Store actions ref for preview click delegation
  dom._elEvents = el.events || {};
  dom._elData = el;

  // ── Hover / Active interaction states ──
  const inner2 = dom.querySelector('.ui-el-inner');
  if (inner2) {
    // Store originals for restore
    const origBg = el.background;
    const origColor = el.color;
    const origTransform = dom.style.transform;

    // Remove old listeners (use named refs on dom)
    if (dom._hEnter) inner2.removeEventListener('mouseenter', dom._hEnter);
    if (dom._hLeave) inner2.removeEventListener('mouseleave', dom._hLeave);
    if (dom._aDown) inner2.removeEventListener('mousedown', dom._aDown);
    if (dom._aUp) inner2.removeEventListener('mouseup', dom._aUp);

    const hasHover = el.hoverBackground || el.hoverColor || el.hoverScale || el.hoverSrc || el.hoverBoxShadow;
    const hasActive = el.activeBackground || el.activeColor || el.activeScale || el.activeSrc || el.activeBoxShadow;

    if (hasHover || hasActive) {
      dom._hEnter = () => {
        if (!previewMode) return;
        if (el.hoverBackground) inner2.style.background = el.hoverBackground;
        if (el.hoverColor) inner2.style.color = el.hoverColor;
        if (el.hoverScale) dom.style.transform = origTransform + ` scale(${el.hoverScale})`;
        if (el.hoverBoxShadow) inner2.style.boxShadow = el.hoverBoxShadow;
        if (el.hoverSrc && el.type === 'image') {
          const img = inner2.querySelector('img');
          if (img) img.src = el.hoverSrc;
        }
        inner2.style.transition = 'all 0.15s ease';
      };
      dom._hLeave = () => {
        if (!previewMode) return;
        inner2.style.background = origBg;
        inner2.style.color = el.color;
        inner2.style.boxShadow = el.boxShadow || '';
        dom.style.transform = origTransform;
        if (el.hoverSrc && el.type === 'image') {
          const img = inner2.querySelector('img');
          if (img) img.src = el.src;
        }
      };
      dom._aDown = () => {
        if (!previewMode) return;
        if (el.activeBackground) inner2.style.background = el.activeBackground;
        if (el.activeColor) inner2.style.color = el.activeColor;
        if (el.activeScale) dom.style.transform = origTransform + ` scale(${el.activeScale})`;
        if (el.activeBoxShadow) inner2.style.boxShadow = el.activeBoxShadow;
        if (el.activeSrc && el.type === 'image') {
          const img = inner2.querySelector('img');
          if (img) img.src = el.activeSrc;
        }
      };
      dom._aUp = () => {
        if (!previewMode) return;
        if (el.hoverBackground) inner2.style.background = el.hoverBackground;
        else inner2.style.background = origBg;
        if (el.hoverColor) inner2.style.color = el.hoverColor;
        else inner2.style.color = el.color;
        inner2.style.boxShadow = el.hoverBoxShadow || el.boxShadow || '';
        dom.style.transform = origTransform;
      };
      inner2.addEventListener('mouseenter', dom._hEnter);
      inner2.addEventListener('mouseleave', dom._hLeave);
      inner2.addEventListener('mousedown', dom._aDown);
      inner2.addEventListener('mouseup', dom._aUp);
    }
  }
}

// ── Preview action execution (mirrors game-level) ──
function previewSetVisible(target, visible) {
  if (target.disabled) return;
  if (target.showOn && target.showOn.length > 0 && !target.showOn.includes(deviceMode)) return;
  target.visible = visible;
  const dom = viewport.querySelector(`[data-id="${target.id}"]`);
  if (dom) {
    dom.style.opacity = visible ? target.opacity : '0';
    dom.style.pointerEvents = visible ? 'auto' : 'none';
    dom.classList.toggle('el-hidden-edit', !visible);
  }
  // Cascade DOM-only to children — do NOT modify child.visible
  previewCascadeChildren(target.id, visible);
}

function previewCascadeChildren(parentId, parentVisible) {
  elements.filter(e => e.parentId === parentId).forEach(child => {
    if (child.disabled) return;
    if (child.showOn && child.showOn.length > 0 && !child.showOn.includes(deviceMode)) return;
    const childShouldShow = parentVisible && child.visible;
    const childDom = viewport.querySelector(`[data-id="${child.id}"]`);
    if (childDom) {
      childDom.style.opacity = childShouldShow ? child.opacity : '0';
      childDom.style.pointerEvents = childShouldShow ? 'auto' : 'none';
      childDom.classList.toggle('el-hidden-edit', !childShouldShow);
    }
    previewCascadeChildren(child.id, childShouldShow);
  });
}

function executePreviewActions(actions, sourceEl, mode) {
  const isChain = mode === 'chain';
  let cumDelay = 0;
  actions.forEach((act, i) => {
    if (isChain) {
      cumDelay += (i > 0 ? (act.delay || 0) : 0);
    }
    const delay = isChain ? cumDelay : (act.delay || 0);
    setTimeout(() => {
      const targetId = act.targetId || sourceEl.id;
      const target = elements.find(e => e.id === targetId);
      if (!target && !['callFunction','addAnimation','removeAnimation','playSound','loadScene','playMusic','stopMusic','setVolume','loadURL','showHTML'].includes(act.type)) return;
      const targetDom = viewport.querySelector(`[data-id="${targetId}"]`);

      switch (act.type) {
        case 'show':
          if (target) previewSetVisible(target, true);
          break;
        case 'hide':
          if (target) previewSetVisible(target, false);
          break;
        case 'toggle':
          if (target) previewSetVisible(target, !target.visible);
          break;
        case 'setText':
          if (target) {
            target.content = act.value || '';
            const inn = targetDom?.querySelector('.ui-el-inner');
            if (inn && (target.type === 'text' || target.type === 'button')) inn.textContent = act.value || '';
          }
          break;
        case 'setProgress':
          if (targetDom) {
            const fill = targetDom.querySelector('[style*="transition:width"]');
            if (fill) fill.style.width = Math.max(0, Math.min(100, act.value || 0)) + '%';
          }
          break;
        case 'playAnimation':
          {
            const aDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            const aInner = aDom ? aDom.querySelector('.ui-el-inner') : null;
            const aEl = (act.targetId ? elements.find(e => e.id === act.targetId) : sourceEl) || sourceEl;
            if (aInner) runUIAnimSequence(aInner, aEl, act.animStage || 'all');
          }
          break;
        case 'modelControl':
          {
            const mDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            if (mDom) controlUIModel(mDom, act.modelCmd, act.modelValue);
          }
          break;
        case 'frameControl':
          {
            const fDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            if (fDom) controlUIFrame(fDom, act.frameCmd, act.frameValue);
          }
          break;
        case 'heroControl':
          {
            const hDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            if (hDom) controlUIHero(hDom, act.heroCmd, act.heroIndex);
          }
          break;
        case 'carouselControl':
          {
            const cDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            if (cDom) controlUICarousel(cDom, act.carCmd, act.carIndex);
            else showToast('carouselControl: target not found', 'warn');
          }
          break;
        case 'videoControl':
          {
            const vDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            const vid = vDom ? vDom.querySelector('video') : null;
            if (vid) {
              switch (act.videoCmd || 'play') {
                case 'play': vid.play().catch(() => {}); break;
                case 'pause': vid.pause(); break;
                case 'toggle': vid.paused ? vid.play().catch(() => {}) : vid.pause(); break;
                case 'stop': vid.pause(); try { vid.currentTime = 0; } catch(e) {} break;
                case 'restart': try { vid.currentTime = 0; } catch(e) {} vid.play().catch(() => {}); break;
              }
            } else {
              showToast('videoControl: target has no video', 'warn');
            }
          }
          break;
        case 'playSound':
          if (act.soundUrl) {
            try {
              const audio = new Audio(act.soundUrl);
              audio.volume = act.volume !== undefined ? act.volume : 1;
              audio.play().catch(e => showToast('Audio: ' + e.message, 'warn'));
            } catch(e) { showToast('Sound error', 'error'); }
          }
          break;
        case 'callFunction':
          if (act.funcName) {
            try {
              if (typeof window[act.funcName] === 'function') {
                let extraArgs = [];
                if (act.funcArgs) {
                  try { extraArgs = Function(`return [${act.funcArgs}]`)(); } catch(e) {}
                }
                window[act.funcName](...extraArgs, sourceEl, target);
              }
            }
            catch(err) { showToast('Func error: ' + err.message, 'error'); }
          }
          break;
        case 'addAnimation':
          {
            // Apply to INNER element to avoid overriding wrapper's position transform
            const aDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            const aInner = aDom ? aDom.querySelector('.ui-el-inner') : null;
            if (aInner && act.animClass) {
              // Clear any previous action animation classes
              [...aInner.classList].filter(c => c.startsWith('animate__') && c !== 'animate__animated').forEach(c => aInner.classList.remove(c));
              aInner.classList.remove('animate__animated');
              void aInner.offsetWidth; // force reflow
              aInner.classList.add('animate__animated', act.animClass);
              if (act.animDuration) aInner.style.setProperty('--animate-duration', act.animDuration);
              else aInner.style.removeProperty('--animate-duration');
              if (act.animDelay) aInner.style.animationDelay = act.animDelay;
              else aInner.style.animationDelay = '';
              if (act.animRepeat) aInner.style.animationIterationCount = act.animRepeat;
              else aInner.style.animationIterationCount = '';
            }
          }
          break;
        case 'removeAnimation':
          {
            const rDom = targetDom || viewport.querySelector(`[data-id="${sourceEl.id}"]`);
            const rInner = rDom ? rDom.querySelector('.ui-el-inner') : null;
            if (rInner) {
              if (act.animClass) {
                rInner.classList.remove(act.animClass);
                const hasOtherAnim = [...rInner.classList].some(c => c.startsWith('animate__') && c !== 'animate__animated');
                if (!hasOtherAnim) rInner.classList.remove('animate__animated');
              } else {
                [...rInner.classList].filter(c => c.startsWith('animate__')).forEach(c => rInner.classList.remove(c));
              }
              rInner.style.removeProperty('--animate-duration');
              rInner.style.animationDelay = '';
              rInner.style.animationIterationCount = '';
            }
          }
          break;
        case 'loadScene':
          showToast('Load Scene: ' + (act.levelFile || '?') + ' — runs in game-level', 'info');
          break;
        case 'playMusic':
          showToast('Play Music: ' + (act.musicSrc || '?'), 'info');
          break;
        case 'stopMusic':
          showToast('Stop Music', 'info');
          break;
        case 'setVolume':
          showToast('Set Volume: ' + Math.round((act.volume || 0.5) * 100) + '%', 'info');
          break;
        case 'loadURL':
          showToast('Load URL: ' + (act.url || '?') + ' (' + (act.urlTarget || 'newTab') + ')', 'info');
          break;
        case 'showHTML':
          showToast('Show HTML (' + (act.htmlMode || 'popup') + ')', 'info');
          break;
      }
    }, delay);
  });
}

// Delegated preview event handlers on viewport
function previewEventHandler(eventType, e) {
  if (!previewMode) return;
  const elDom = e.target.closest('.ui-el');
  if (!elDom || !elDom._elEvents) return;
  const acts = elDom._elEvents[eventType];
  if (acts && acts.length > 0) {
    e.stopPropagation();
    const mode = elDom._elEvents[eventType + 'Mode'] || 'parallel';
    executePreviewActions(acts, elDom._elData, mode);
  }
}
viewport.addEventListener('click', e => previewEventHandler('click', e));
viewport.addEventListener('dblclick', e => previewEventHandler('dblclick', e));
viewport.addEventListener('mousedown', e => previewEventHandler('mousedown', e));
viewport.addEventListener('mouseup', e => previewEventHandler('mouseup', e));
viewport.addEventListener('mouseenter', e => {
  if (!previewMode) return;
  const elDom = e.target.closest('.ui-el');
  if (elDom && elDom._elEvents && elDom._elEvents.hover && elDom._elEvents.hover.length > 0) {
    const mode = elDom._elEvents.hoverMode || 'parallel';
    executePreviewActions(elDom._elEvents.hover, elDom._elData, mode);
  }
}, true);

function renderAllElements() {
  viewport.querySelectorAll('.ui-el').forEach(d => d.remove());
  // Render parents first, then children
  const parents = elements.filter(e => !e.parentId);
  const children = elements.filter(e => e.parentId);
  parents.forEach(el => renderElement(el));
  children.forEach(el => renderElement(el));
  // Carousels drive other elements — re-apply their links after a full rebuild
  elements.filter(e => e.type === 'carousel').forEach(el => {
    const dom = viewport.querySelector(`[data-id="${el.id}"]`);
    const wrapEl = dom ? dom.querySelector('.car-wrap') : null;
    if (wrapEl) syncCarouselLinks(el, wrapEl);
  });
}

// ══════════════════════════════════════
//  SELECTION
// ══════════════════════════════════════
function selectElement(id) {
  selectedId = id;
  viewport.querySelectorAll('.ui-el').forEach(d => d.classList.toggle('selected', d.dataset.id === id));
  const el = elements.find(e => e.id === id);
  if (el) {
    propsPanel.classList.add('visible');
    canvasArea.classList.remove('props-hidden');
    buildPropsPanel(el);
  }
  buildLayerList();
}

function deselectAll() {
  selectedId = null;
  viewport.querySelectorAll('.ui-el.selected').forEach(d => d.classList.remove('selected'));
  propsPanel.classList.remove('visible');
  canvasArea.classList.add('props-hidden');
  buildLayerList();
}

// ══════════════════════════════════════
//  PROPERTIES PANEL
// ══════════════════════════════════════
// Overridable properties per device
const OVERRIDE_PROPS = ['x','y','w','h','fontSize','fontWeight','padding','background',
  'borderRadius','border','opacity','color','textAlign','boxShadow','letterSpacing','visible'];

function getVal(el, prop) {
  if (el.responsive && deviceMode !== 'desktop') {
    const ov = (el.overrides || {})[deviceMode] || {};
    if (ov[prop] !== undefined && ov[prop] !== '') return ov[prop];
  }
  return el[prop];
}
function setVal(el, prop, val) {
  if (el.responsive && deviceMode !== 'desktop') {
    if (!el.overrides) el.overrides = {};
    if (!el.overrides[deviceMode]) el.overrides[deviceMode] = {};
    el.overrides[deviceMode][prop] = val;
  } else {
    el[prop] = val;
  }
  renderElement(el);
}
function clearOverride(el, prop) {
  if (el.overrides && el.overrides[deviceMode]) {
    delete el.overrides[deviceMode][prop];
  }
}
function hasOverride(el, prop) {
  if (!el.responsive || deviceMode === 'desktop') return false;
  return el.overrides && el.overrides[deviceMode] && el.overrides[deviceMode][prop] !== undefined;
}

function buildPropsPanel(el) {
  document.getElementById('pp-title').textContent = el.name || 'Properties';
  ppContext.innerHTML = '';
  ppStyle.innerHTML = '';
  ppProps.innerHTML = '';

  // ── Device badge + Responsive toggle ──
  const devNames = { desktop:'Desktop', tablet:'Tablet', phone:'Phone', 'phone-land':'Phone Land' };
  const badge = document.createElement('div');
  badge.className = 'pp-device-badge' + (deviceMode === 'desktop' ? ' desktop' : '');
  badge.innerHTML = `<i class="bi ${deviceMode === 'desktop' ? 'bi-display' : deviceMode === 'tablet' ? 'bi-tablet' : 'bi-phone'}"></i> ${devNames[deviceMode] || deviceMode}`;
  ppContext.appendChild(badge);

  const respRow = document.createElement('div');
  respRow.className = 'pp-responsive-toggle';
  const respCheck = document.createElement('input');
  respCheck.type = 'checkbox'; respCheck.className = 'pp-check';
  if (!el.overrides) el.overrides = { tablet:{}, phone:{}, 'phone-land':{} };
  respCheck.checked = !!el.responsive;
  respCheck.onchange = () => { el.responsive = respCheck.checked; buildPropsPanel(el); };
  const respLabel = document.createElement('span');
  respLabel.textContent = 'Per-device layout & style';
  respRow.appendChild(respCheck); respRow.appendChild(respLabel);
  if (el.responsive && deviceMode !== 'desktop') {
    const resetBtn = document.createElement('button');
    resetBtn.className = 'pp-btn'; resetBtn.style.cssText = 'margin-left:auto;padding:2px 8px;font-size:0.45rem;background:rgba(255,77,106,0.08);border-color:var(--danger);color:var(--danger);';
    resetBtn.textContent = 'Reset device';
    resetBtn.onclick = () => { el.overrides[deviceMode] = {}; renderElement(el); buildPropsPanel(el); showToast('Device overrides cleared'); };
    respRow.appendChild(resetBtn);
  }
  ppContext.appendChild(respRow);

  // ── Info section ──
  const info = makeSection('Info', 'bi-info-circle');
  info.appendChild(makeRow('Name', makeInput(el.name, v => { el.name = v; buildLayerList(); })));
  info.appendChild(makeRow('ID', makeReadonly(el.id)));
  info.appendChild(makeRow('Type', makeReadonly(el.type)));
  ppProps.appendChild(info);

  // ── Position section ──
  const pos = makeSection('Position', 'bi-arrows-move');
  pos.appendChild(makeOvRow(el,'X %', 'x', makeNumInput(getVal(el,'x'), 0, 100, 0.5, v => setVal(el,'x',v))));
  pos.appendChild(makeOvRow(el,'Y %', 'y', makeNumInput(getVal(el,'y'), 0, 100, 0.5, v => setVal(el,'y',v))));
  pos.appendChild(makeOvRow(el,'W %', 'w', makeNumInput(getVal(el,'w'), 0, 100, 0.5, v => setVal(el,'w',v), '0=auto')));
  pos.appendChild(makeOvRow(el,'H %', 'h', makeNumInput(getVal(el,'h'), 0, 100, 0.5, v => setVal(el,'h',v), '0=auto')));
  pos.appendChild(makeOvRow(el, 'Anchor', 'anchor', makeSelect(getVal(el, 'anchor') || 'center',
    ['center','top-left','top-center','top-right','center-left','center-right','bottom-left','bottom-center','bottom-right'],
    v => { setVal(el, 'anchor', v); buildPropsPanel(el); })));
  pos.appendChild(makeRow('Z-Index', makeNumInput(el.zIndex, 0, 100, 1, v => { el.zIndex = v; renderElement(el); })));
  // Parent dropdown — only panels can be parents
  const panelEls = elements.filter(e => (e.type === 'panel' || e.type === 'hero') && e.id !== el.id);
  const parentOpts = ['(none)', ...panelEls.map(p => p.id + ' — ' + (p.name || p.id))];
  const parentVals = ['', ...panelEls.map(p => p.id)];
  const parentSel = document.createElement('select');
  parentSel.className = 'pp-select';
  parentVals.forEach((v, i) => {
    const opt = document.createElement('option');
    opt.value = v; opt.textContent = parentOpts[i];
    if (v === (el.parentId || '')) opt.selected = true;
    parentSel.appendChild(opt);
  });
  parentSel.onchange = () => { el.parentId = parentSel.value || null; renderAllElements(); buildLayerList(); };
  pos.appendChild(makeRow('Parent', parentSel));
  ppProps.appendChild(pos);

  // ── Content section ──
  if (el.type === 'text' || el.type === 'button') {
    const cont = makeSection('Content', 'bi-text-left');
    const ta = document.createElement('textarea');
    ta.className = 'pp-textarea'; ta.value = el.content;
    ta.oninput = () => { el.content = ta.value; renderElement(el); };
    cont.appendChild(ta);
    ppProps.appendChild(cont);
  } else if (el.type === 'image') {
    const cont = makeSection('Image', 'bi-image');
    cont.appendChild(makeRow('Image', makeImageInput(el.src || '', v => { el.src = v; renderElement(el); })));
    cont.appendChild(makeRow('Fit', makeSelect(el.objectFit, ['contain','cover','fill','none','background'], v => { el.objectFit = v; renderElement(el); })));
    if (el.objectFit === 'background') {
      cont.appendChild(makeRow('BG Size', makeInput(el.bgSize || 'auto 100%', v => { el.bgSize = v; renderElement(el); }, 'auto 100%, cover, 50%')));
      cont.appendChild(makeRow('BG Pos', makeInput(el.bgPosition || 'center', v => { el.bgPosition = v; renderElement(el); }, 'center, top left')));
      cont.appendChild(makeRow('BG Repeat', makeSelect(el.bgRepeat || 'no-repeat', ['no-repeat','repeat','repeat-x','repeat-y','space','round'], v => { el.bgRepeat = v; renderElement(el); })));
    }
    ppProps.appendChild(cont);
  } else if (el.type === 'model3d') {
    const md = makeSection('3D Model', 'bi-box');
    md.appendChild(makeRow('GLB / GLTF', makeInput(el.src || '', v => { el.src = v; renderElement(el); }, './models/product.glb')));
    const mdHint = document.createElement('div');
    mdHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    mdHint.textContent = 'A path next to your HTML, or a full URL. .glb is a single file and travels best; .gltf needs its textures alongside it.';
    md.appendChild(mdHint);
    md.appendChild(makeRow('Auto Rotate', makeCheck(el.modelAutoRotate, v => { el.modelAutoRotate = v; renderElement(el); })));
    md.appendChild(makeRow('Spin Speed', makeSlider(el.modelRotateSpeed, 0.1, 12, 0.1, v => { el.modelRotateSpeed = v; renderElement(el); })));
    md.appendChild(makeRow('Interactive', makeCheck(el.modelControls, v => { el.modelControls = v; buildPropsPanel(el); renderElement(el); })));
    if (el.modelControls) {
      const inter = document.createElement('div');
      inter.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
      inter.textContent = 'Drag (or one finger) turns the model, wheel / pinch zooms, and two fingers pan when allowed.';
      md.appendChild(inter);
      md.appendChild(makeRow('Drag Rotate', makeCheck(el.modelDragRotate, v => { el.modelDragRotate = v; renderElement(el); })));
      md.appendChild(makeRow('Allow Zoom', makeCheck(el.modelZoom, v => { el.modelZoom = v; renderElement(el); })));
      md.appendChild(makeRow('Allow Pan', makeCheck(el.modelPan, v => { el.modelPan = v; renderElement(el); })));
      md.appendChild(makeRow('Drag Speed', makeSlider(el.modelDragSpeed, 0.1, 4, 0.05, v => { el.modelDragSpeed = v; renderElement(el); })));
      md.appendChild(makeRow('Zoom Speed', makeSlider(el.modelZoomSpeed, 0.1, 4, 0.05, v => { el.modelZoomSpeed = v; renderElement(el); })));
      md.appendChild(makeRow('Smoothing', makeCheck(el.modelDamping, v => { el.modelDamping = v; renderElement(el); })));
      md.appendChild(makeRow('Pause Spin', makeCheck(el.modelStopSpinOnInteract, v => { el.modelStopSpinOnInteract = v; buildPropsPanel(el); renderElement(el); })));
      if (el.modelStopSpinOnInteract) {
        md.appendChild(makeRow('Resume After', makeNumInput(el.modelResumeSpin, 0, 60000, 250, v => { el.modelResumeSpin = Math.round(v); renderElement(el); })));
        const rh = document.createElement('div');
        rh.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
        rh.textContent = 'Milliseconds of stillness before auto-rotation starts again. 0 keeps it stopped once the visitor has taken over.';
        md.appendChild(rh);
      }
      const limLbl = document.createElement('div');
      limLbl.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
      limLbl.textContent = '● Limits (blank = free)';
      md.appendChild(limLbl);
      const lim = (label, key, ph) => {
        const inp = document.createElement('input');
        inp.type = 'number'; inp.className = 'pp-input'; inp.style.cssText = 'flex:1;min-width:0;';
        inp.value = el[key] === '' || el[key] === undefined ? '' : el[key];
        inp.placeholder = ph;
        inp.onchange = () => { el[key] = inp.value === '' ? '' : parseFloat(inp.value); renderElement(el); };
        return makeRow(label, inp);
      };
      md.appendChild(lim('Min Zoom', 'modelMinDistance', 'e.g. 2'));
      md.appendChild(lim('Max Zoom', 'modelMaxDistance', 'e.g. 10'));
      md.appendChild(lim('Min Tilt°', 'modelMinPolar', '0 = top'));
      md.appendChild(lim('Max Tilt°', 'modelMaxPolar', '180 = bottom'));
    }
    md.appendChild(makeRow('Play Clips', makeCheck(el.modelPlayClips, v => { el.modelPlayClips = v; renderElement(el); })));
    md.appendChild(makeRow('Live on canvas', makeCheck(el.modelLiveOnCanvas, v => { el.modelLiveOnCanvas = v; renderElement(el); })));
    const liveHint = document.createElement('div');
    liveHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
    liveHint.textContent = 'Turn this off if several models make the canvas sluggish — they still animate in Preview and in the player.';
    md.appendChild(liveHint);
    ppProps.appendChild(md);

    const tr = makeSection('Model Transform', 'bi-arrows-move');
    tr.appendChild(makeRow('Auto Fit', makeCheck(el.modelAutoFit, v => { el.modelAutoFit = v; renderElement(el); })));
    const fitHint = document.createElement('div');
    fitHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    fitHint.textContent = 'Auto Fit centres the model and normalises its size first; the values below are applied on top.';
    tr.appendChild(fitHint);
    tr.appendChild(makeRow('Scale', makeSlider(el.modelScale, 0.05, 5, 0.05, v => { el.modelScale = v; renderElement(el); })));
    tr.appendChild(makeRow('Pos X', makeNumInput(el.modelX, -20, 20, 0.1, v => { el.modelX = v; renderElement(el); })));
    tr.appendChild(makeRow('Pos Y', makeNumInput(el.modelY, -20, 20, 0.1, v => { el.modelY = v; renderElement(el); })));
    tr.appendChild(makeRow('Pos Z', makeNumInput(el.modelZ, -20, 20, 0.1, v => { el.modelZ = v; renderElement(el); })));
    tr.appendChild(makeRow('Rot X°', makeNumInput(el.modelRotX, -360, 360, 1, v => { el.modelRotX = v; renderElement(el); })));
    tr.appendChild(makeRow('Rot Y°', makeNumInput(el.modelRotY, -360, 360, 1, v => { el.modelRotY = v; renderElement(el); })));
    tr.appendChild(makeRow('Rot Z°', makeNumInput(el.modelRotZ, -360, 360, 1, v => { el.modelRotZ = v; renderElement(el); })));
    ppProps.appendChild(tr);

    const cam = makeSection('Camera & Light', 'bi-camera-reels');
    cam.appendChild(makeRow('Distance', makeSlider(el.modelDistance, 0.5, 20, 0.1, v => { el.modelDistance = v; renderElement(el); })));
    cam.appendChild(makeRow('Height', makeNumInput(el.modelCamHeight, -10, 10, 0.1, v => { el.modelCamHeight = v; renderElement(el); })));
    cam.appendChild(makeRow('Look At Y', makeNumInput(el.modelTargetY, -10, 10, 0.1, v => { el.modelTargetY = v; renderElement(el); })));
    cam.appendChild(makeRow('FOV', makeSlider(el.modelFov, 10, 110, 1, v => { el.modelFov = v; renderElement(el); })));
    cam.appendChild(makeRow('Lighting', makeSelect(el.modelLighting, ['studio','soft','dramatic'], v => { el.modelLighting = v; renderElement(el); })));
    cam.appendChild(makeRow('Intensity', makeSlider(el.modelLightIntensity, 0, 3, 0.05, v => { el.modelLightIntensity = v; renderElement(el); })));
    cam.appendChild(makeRow('Sky Light', makeCheck(el.modelHemi, v => { el.modelHemi = v; renderElement(el); })));
    cam.appendChild(makeRow('Shadows', makeCheck(el.modelShadows, v => { el.modelShadows = v; renderElement(el); })));
    ppStyle.appendChild(cam);

    const mstyle = makeSection('Model Background', 'bi-palette');
    mstyle.appendChild(makeRow('Background', makeColorCombo(el.modelBg, v => { el.modelBg = v; renderElement(el); })));
    const bgHint2 = document.createElement('div');
    bgHint2.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
    bgHint2.textContent = 'Type "transparent" to let whatever sits behind the element show through.';
    mstyle.appendChild(bgHint2);
    mstyle.appendChild(makeRow('Radius', makeInput(el.borderRadius, v => { el.borderRadius = v; renderElement(el); }, '10px')));
    ppStyle.appendChild(mstyle);
  } else if (el.type === 'iframe') {
    const fr = makeSection('Embedded Page', 'bi-window');
    fr.appendChild(makeRow('URL', makeInput(el.src || '', v => { el.src = v; renderElement(el); }, 'https://… (YouTube / Vimeo / Maps links auto-convert)')));
    if (el.src) {
      const resolved = document.createElement('div');
      resolved.style.cssText = 'font-size:0.4rem;color:var(--accent);padding:0 0 4px;line-height:1.35;word-break:break-all;';
      resolved.textContent = 'Loads: ' + uiEmbedURL(el.src);
      fr.appendChild(resolved);
    }
    fr.appendChild(makeRow('Title', makeInput(el.frameTitle, v => { el.frameTitle = v; renderElement(el); }, 'for screen readers')));
    fr.appendChild(makeRow('Scrollbars', makeCheck(el.frameScroll, v => { el.frameScroll = v; renderElement(el); })));
    fr.appendChild(makeRow('Fullscreen', makeCheck(el.frameAllowFullscreen, v => { el.frameAllowFullscreen = v; renderElement(el); })));
    fr.appendChild(makeRow('Lazy Load', makeCheck(el.frameLazy, v => { el.frameLazy = v; renderElement(el); })));
    fr.appendChild(makeRow('Stub on canvas', makeCheck(el.framePreviewOnly, v => { el.framePreviewOnly = v; renderElement(el); })));
    const frHint = document.createElement('div');
    frHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
    frHint.textContent = 'With the stub on, the canvas shows a placeholder and the real page only loads in Preview and in the player — much lighter while designing.';
    fr.appendChild(frHint);
    fr.appendChild(makeRow('Allow', makeInput(el.frameAllow, v => { el.frameAllow = v; renderElement(el); }, 'autoplay; fullscreen…')));
    fr.appendChild(makeRow('Sandbox', makeCheck(el.frameSandbox, v => { el.frameSandbox = v; buildPropsPanel(el); renderElement(el); })));
    if (el.frameSandbox) {
      fr.appendChild(makeRow('Rules', makeInput(el.frameSandboxRules, v => { el.frameSandboxRules = v; renderElement(el); }, 'allow-scripts allow-same-origin')));
    }
    const secHint = document.createElement('div');
    secHint.style.cssText = 'font-size:0.42rem;color:var(--accent3);padding:2px 0;line-height:1.35;';
    secHint.textContent = '⚠ Many sites refuse to be embedded (X-Frame-Options / CSP). If a page stays blank, that site is blocking it — nothing you can change here.';
    fr.appendChild(secHint);
    ppProps.appendChild(fr);

    const frStyle = makeSection('Frame Style', 'bi-border-style');
    frStyle.appendChild(makeRow('Border', makeInput(el.frameBorder, v => { el.frameBorder = v; renderElement(el); }, 'none / 1px solid #fff2')));
    frStyle.appendChild(makeRow('Radius', makeInput(el.borderRadius, v => { el.borderRadius = v; renderElement(el); }, '8px')));
    frStyle.appendChild(makeRow('Background', makeColorCombo(el.frameBg, v => { el.frameBg = v; renderElement(el); })));
    ppStyle.appendChild(frStyle);
  } else if (el.type === 'hero') {
    buildHeroProps(el);
  } else if (el.type === 'carousel') {
    buildCarouselProps(el);
  } else if (el.type === 'video') {
    const cont = makeSection('Video', 'bi-film');
    cont.appendChild(makeRow('Src', makeVideoInput(el.src || '', v => { el.src = v; renderElement(el); })));
    cont.appendChild(makeRow('Poster', makeImageInput(el.videoPoster || '', v => { el.videoPoster = v; renderElement(el); })));
    cont.appendChild(makeRow('Play', makeSelect(el.playMode || 'auto', ['auto','click','manual'], v => { el.playMode = v; renderElement(el); })));
    const playHint = document.createElement('div');
    playHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
    playHint.textContent = 'auto = play on load · click = tap video to play/pause · manual = only via actions/API';
    cont.appendChild(playHint);
    cont.appendChild(makeRow('Fit', makeSelect(el.videoFit || 'contain', ['contain','fit-width','fit-height','cover','stretch'], v => { el.videoFit = v; renderElement(el); })));
    cont.appendChild(makeRow('Playback', makeSelect(el.loopMode || 'loop', ['once','count','loop'], v => { el.loopMode = v; buildPropsPanel(el); renderElement(el); })));
    if (el.loopMode === 'count') {
      cont.appendChild(makeRow('Times', makeNumInput(el.loopCount || 1, 1, 999, 1, v => { el.loopCount = Math.max(1, Math.round(v)); renderElement(el); })));
    }
    const mutedChk = document.createElement('input');
    mutedChk.type = 'checkbox'; mutedChk.className = 'pp-check'; mutedChk.checked = el.muted !== false;
    mutedChk.onchange = () => { el.muted = mutedChk.checked; renderElement(el); };
    cont.appendChild(makeRow('Muted', mutedChk));
    const ctrlChk = document.createElement('input');
    ctrlChk.type = 'checkbox'; ctrlChk.className = 'pp-check'; ctrlChk.checked = !!el.controls;
    ctrlChk.onchange = () => { el.controls = ctrlChk.checked; renderElement(el); };
    cont.appendChild(makeRow('Controls', ctrlChk));
    cont.appendChild(makeRow('Volume', makeSlider(el.volume !== undefined ? el.volume : 1, 0, 1, 0.05, v => { el.volume = v; renderElement(el); })));
    const vHint = document.createElement('div');
    vHint.style.cssText = 'font-size:0.42rem;color:var(--accent3);padding:2px 0;line-height:1.3;';
    vHint.textContent = '⚠ Browsers block un-muted autoplay. Keep Muted ON for auto, or use click/manual.';
    cont.appendChild(vHint);
    ppProps.appendChild(cont);

    // ── Transparency / chroma key ──
    const ck = makeSection('Transparency', 'bi-magic');
    const ckHint = document.createElement('div');
    ckHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
    ckHint.textContent = 'Knock a colour out of the video so whatever is behind it shows through — green/blue screen footage, logos on a flat background, and so on.';
    ck.appendChild(ckHint);
    ck.appendChild(makeRow('Chroma Key', makeCheck(el.chromaKey, v => { el.chromaKey = v; buildPropsPanel(el); renderElement(el); })));
    if (el.chromaKey) {
      ck.appendChild(makeRow('Colour', makeColor(el.chromaColor, v => { el.chromaColor = v; renderElement(el); })));
      const presets = document.createElement('div');
      presets.style.cssText = 'display:flex;gap:4px;padding:2px 0 6px;';
      [['#00ff00','Green'],['#0000ff','Blue'],['#000000','Black'],['#ffffff','White'],['#ff00ff','Magenta']].forEach(([c, label]) => {
        const b = document.createElement('button');
        b.className = 'pp-btn';
        b.style.cssText = 'flex:1;padding:3px 2px;font-size:0.42rem;border-color:' + c + ';color:' + c + ';';
        b.textContent = label;
        b.onclick = () => { el.chromaColor = c; buildPropsPanel(el); renderElement(el); };
        presets.appendChild(b);
      });
      ck.appendChild(presets);
      ck.appendChild(makeRow('Similarity', makeSlider(el.chromaSimilarity, 0.01, 1, 0.01, v => { el.chromaSimilarity = v; renderElement(el); })));
      ck.appendChild(makeRow('Smoothness', makeSlider(el.chromaSmoothness, 0, 0.5, 0.01, v => { el.chromaSmoothness = v; renderElement(el); })));
      ck.appendChild(makeRow('Spill', makeSlider(el.chromaSpill, 0, 1, 0.05, v => { el.chromaSpill = v; renderElement(el); })));
      const tuneHint = document.createElement('div');
      tuneHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
      tuneHint.textContent = 'Similarity = how much of the colour range is removed · Smoothness = softness of the cut-out edge · Spill = removes colour bleeding onto the subject.';
      ck.appendChild(tuneHint);
      const cors = document.createElement('div');
      cors.style.cssText = 'font-size:0.42rem;color:var(--accent3);padding:2px 0;line-height:1.35;';
      cors.textContent = '⚠ Keying reads the video frames, so the file must be local, embedded, or served with CORS enabled — otherwise the plain video is shown.';
      ck.appendChild(cors);
    }
    ck.appendChild(makeRow('Blend', makeSelect(el.videoBlend || 'none',
      ['none','screen','multiply','lighten','darken','overlay','difference','luminosity'],
      v => { el.videoBlend = v; renderElement(el); })));
    const blendHint = document.createElement('div');
    blendHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
    blendHint.textContent = 'A cheaper alternative for simple cases: screen drops a black background, multiply drops a white one. Works without reading the frames.';
    ck.appendChild(blendHint);
    ppStyle.appendChild(ck);
  } else if (el.type === 'icon') {
    const cont = makeSection('Icon', 'bi-emoji-smile');
    cont.appendChild(makeRow('Class', makeInput(el.iconClass || '', v => { el.iconClass = v; renderElement(el); })));
    const hint = document.createElement('div');
    hint.style.cssText = 'font-size:0.5rem;color:var(--text-muted);padding:4px 0;';
    hint.textContent = 'e.g. bi-heart-fill, bi-star, bi-gear';
    cont.appendChild(hint);
    ppProps.appendChild(cont);
  } else if (el.type === 'progressbar') {
    const cont = makeSection('Progress', 'bi-battery-half');
    cont.appendChild(makeRow('Value', makeSlider(el.barValue, 0, 100, 1, v => { el.barValue = v; renderElement(el); })));
    cont.appendChild(makeRow('Bar Color', makeColor(el.barColor, v => { el.barColor = v; renderElement(el); })));
    cont.appendChild(makeRow('BG Color', makeColor(el.barBgColor, v => { el.barBgColor = v; renderElement(el); })));
    cont.appendChild(makeRow('Height', makeInput(el.barHeight, v => { el.barHeight = v; renderElement(el); })));
    cont.appendChild(makeRow('Label', makeInput(el.labelText || '', v => { el.labelText = v; el.showLabel = !!v; renderElement(el); })));
    ppProps.appendChild(cont);
  }

  // ── Style section ──
  const style = makeSection('Style', 'bi-palette');
  style.appendChild(makeRow('Font', makeSelect(el.fontFamily, [
    "'Orbitron',sans-serif", "'Rajdhani',sans-serif", "'Courier New',monospace",
    "Georgia,serif", "'Arial',sans-serif", "'Trebuchet MS',sans-serif", "system-ui",
  ], v => { el.fontFamily = v; renderElement(el); })));
  // Font size + unit (px, rem, em, %, vw, vh, pt)
  const FONT_UNITS = ['rem','px','em','%','vw','vh','vmin','pt'];
  const sizeWrap = document.createElement('div');
  sizeWrap.style.cssText = 'display:flex;gap:4px;flex:1;align-items:center;';
  const curUnit = getVal(el, 'fontUnit') || 'rem';
  const sizeNum = document.createElement('input');
  sizeNum.type = 'number'; sizeNum.className = 'pp-input';
  sizeNum.style.cssText = 'flex:1;min-width:0;';
  sizeNum.step = (curUnit === 'px' || curUnit === 'pt' || curUnit === '%') ? '1' : '0.05';
  sizeNum.value = getVal(el, 'fontSize');
  sizeNum.onchange = () => { setVal(el, 'fontSize', parseFloat(sizeNum.value) || 0); };
  const unitSel = document.createElement('select');
  unitSel.className = 'pp-select';
  unitSel.style.cssText = 'flex:0 0 58px;';
  FONT_UNITS.forEach(u => {
    const o = document.createElement('option');
    o.value = u; o.textContent = u;
    if (u === curUnit) o.selected = true;
    unitSel.appendChild(o);
  });
  unitSel.onchange = () => {
    const from = curUnit, to = unitSel.value;
    // Convert the number so the text keeps roughly the same size
    const toPx = { rem:16, em:16, px:1, pt:1.333, '%':0.16, vw:0, vh:0, vmin:0 };
    let val = parseFloat(sizeNum.value) || 0;
    if (toPx[from] && toPx[to]) val = Math.round((val * toPx[from] / toPx[to]) * 1000) / 1000;
    setVal(el, 'fontUnit', to);
    setVal(el, 'fontSize', val);
    buildPropsPanel(el);
  };
  sizeWrap.appendChild(sizeNum); sizeWrap.appendChild(unitSel);
  style.appendChild(makeOvRow(el, 'Size', 'fontSize', sizeWrap));
  const sizeSliderMax = curUnit === 'px' ? 120 : curUnit === 'pt' ? 90 : curUnit === '%' ? 400 : curUnit === 'vw' || curUnit === 'vh' || curUnit === 'vmin' ? 20 : 6;
  style.appendChild(makeRow('', makeSlider(getVal(el,'fontSize'), 0, sizeSliderMax, sizeSliderMax > 20 ? 1 : 0.05, v => { setVal(el,'fontSize',v); sizeNum.value = v; })));
  style.appendChild(makeOvRow(el,'Weight', 'fontWeight', makeSelect(getVal(el,'fontWeight'), ['300','400','500','600','700'], v => setVal(el,'fontWeight',v))));
  style.appendChild(makeOvRow(el,'Color', 'color', makeColor(getVal(el,'color'), v => setVal(el,'color',v))));
  style.appendChild(makeOvRow(el,'BG', 'background', makeColorCombo(getVal(el,'background'), v => setVal(el,'background',v))));
  style.appendChild(makeOvRow(el,'Radius', 'borderRadius', makeInput(getVal(el,'borderRadius'), v => setVal(el,'borderRadius',v))));
  style.appendChild(makeOvRow(el,'Padding', 'padding', makeInput(getVal(el,'padding'), v => setVal(el,'padding',v))));
  style.appendChild(makeOvRow(el,'Border', 'border', makeColorCombo(getVal(el,'border'), v => setVal(el,'border',v))));
  style.appendChild(makeOvRow(el,'Opacity', 'opacity', makeSlider(getVal(el,'opacity'), 0, 1, 0.05, v => setVal(el,'opacity',v))));
  style.appendChild(makeOvRow(el,'Shadow', 'boxShadow', makeInput(getVal(el,'boxShadow'), v => setVal(el,'boxShadow',v))));
  style.appendChild(makeRow('Txt Shd', makeInput(el.textShadow, v => { el.textShadow = v; renderElement(el); })));
  style.appendChild(makeOvRow(el,'Align', 'textAlign', makeSelect(getVal(el,'textAlign'), ['left','center','right','justify'], v => setVal(el,'textAlign',v))));
  style.appendChild(makeOvRow(el,'Spacing', 'letterSpacing', makeInput(getVal(el,'letterSpacing'), v => setVal(el,'letterSpacing',v))));
  // Custom CSS
  const cssRow = document.createElement('div');
  cssRow.style.cssText = 'margin-top:6px;';
  const cssLbl = document.createElement('div');
  cssLbl.style.cssText = "font-size:0.38rem;color:var(--accent3);font-family:'Orbitron',sans-serif;letter-spacing:1px;margin-bottom:3px;";
  cssLbl.textContent = 'CUSTOM CSS';
  const cssTa = document.createElement('textarea');
  cssTa.className = 'pp-input';
  cssTa.style.cssText = 'width:100%;min-height:40px;max-height:120px;resize:vertical;font-family:"Courier New",monospace;font-size:0.5rem;line-height:1.4;';
  cssTa.value = el.customCSS || '';
  cssTa.placeholder = 'e.g. transform: rotate(5deg);\nfilter: blur(2px);';
  cssTa.onchange = () => { el.customCSS = cssTa.value; renderElement(el); };
  cssRow.appendChild(cssLbl); cssRow.appendChild(cssTa);
  style.appendChild(cssRow);
  ppStyle.appendChild(style);

  // ── Animation section: entrance → attention → exit ──
  const animSec = makeSection('Animation', 'bi-play-circle');
  const ANIM_GROUPS = {
    'Entrances': ['animate__fadeIn','animate__fadeInUp','animate__fadeInDown','animate__fadeInLeft','animate__fadeInRight','animate__bounceIn','animate__bounceInUp','animate__bounceInDown','animate__bounceInLeft','animate__bounceInRight','animate__zoomIn','animate__zoomInUp','animate__zoomInDown','animate__slideInUp','animate__slideInDown','animate__slideInLeft','animate__slideInRight','animate__flipInX','animate__flipInY','animate__rotateIn','animate__jackInTheBox','animate__rollIn','animate__lightSpeedInLeft','animate__lightSpeedInRight','animate__backInUp','animate__backInDown','animate__backInLeft','animate__backInRight'],
    'Attention': ['animate__bounce','animate__flash','animate__pulse','animate__rubberBand','animate__shakeX','animate__shakeY','animate__headShake','animate__swing','animate__tada','animate__wobble','animate__jello','animate__heartBeat','animate__flip'],
    'Exits': ['animate__fadeOut','animate__fadeOutUp','animate__fadeOutDown','animate__fadeOutLeft','animate__fadeOutRight','animate__bounceOut','animate__bounceOutUp','animate__bounceOutDown','animate__zoomOut','animate__zoomOutUp','animate__zoomOutDown','animate__slideOutUp','animate__slideOutDown','animate__slideOutLeft','animate__slideOutRight','animate__flipOutX','animate__flipOutY','animate__rotateOut','animate__rollOut','animate__lightSpeedOutLeft','animate__lightSpeedOutRight','animate__backOutUp','animate__backOutDown','animate__backOutLeft','animate__backOutRight'],
  };

  function makeAnimSelect(val, preferredGroup, onChange) {
    const sel = document.createElement('select');
    sel.className = 'pp-select';
    const none = document.createElement('option');
    none.value = ''; none.textContent = '(none)';
    if (!val) none.selected = true;
    sel.appendChild(none);
    const order = preferredGroup ? [preferredGroup].concat(Object.keys(ANIM_GROUPS).filter(g => g !== preferredGroup)) : Object.keys(ANIM_GROUPS);
    order.forEach(group => {
      const og = document.createElement('optgroup');
      og.label = group;
      ANIM_GROUPS[group].forEach(a => {
        const o = document.createElement('option');
        o.value = a; o.textContent = a.replace('animate__', '');
        if (a === val) o.selected = true;
        og.appendChild(o);
      });
      sel.appendChild(og);
    });
    sel.onchange = () => onChange(sel.value);
    return sel;
  }

  const playStage = stage => {
    const dom = viewport.querySelector(`[data-id="${el.id}"]`);
    const inner = dom ? dom.querySelector('.ui-el-inner') : null;
    if (inner) runUIAnimSequence(inner, el, stage);
  };

  // Trigger + play whole sequence
  animSec.appendChild(makeRow('Trigger', makeSelect(el.animTrigger || 'load', ['load','manual'], v => { el.animTrigger = v; renderElement(el); })));
  const trigHint = document.createElement('div');
  trigHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  trigHint.textContent = 'load = plays when the UI opens · manual = only via a Play Animation action or GameAPI.playAnimation().';
  animSec.appendChild(trigHint);

  const seqBtn = document.createElement('button');
  seqBtn.className = 'pp-btn';
  seqBtn.style.cssText = 'width:100%;margin-bottom:6px;background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);font-size:0.55rem;padding:5px;';
  seqBtn.innerHTML = '<i class="bi bi-play-circle"></i> Play full sequence';
  seqBtn.onclick = () => playStage('all');
  animSec.appendChild(seqBtn);

  const STAGES = [
    { key:'Enter', label:'1 · Entrance', icon:'bi-box-arrow-in-down', group:'Entrances',
      hint:'Plays once when the element appears. Delay is counted from when the UI opens.' },
    { key:'Attn', label:'2 · Attention', icon:'bi-stars', group:'Attention',
      hint:'Delay is counted from the moment the entrance finishes. Set Repeat to infinite for a permanent loop.' },
    { key:'Exit', label:'3 · Exit', icon:'bi-box-arrow-up', group:'Exits',
      hint:'Delay is counted from the moment the attention stage finishes (or starts, if it loops forever).' },
  ];

  STAGES.forEach(st => {
    const box = document.createElement('div');
    box.style.cssText = 'border:1px solid var(--border-color);border-radius:8px;padding:6px 8px;margin-bottom:6px;background:rgba(255,255,255,0.02);';
    const head = document.createElement('div');
    head.style.cssText = 'display:flex;align-items:center;gap:5px;margin-bottom:4px;';
    const ttl = document.createElement('span');
    ttl.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.42rem;letter-spacing:1px;color:' + (el['anim' + st.key + 'Class'] ? 'var(--accent)' : 'var(--text-muted)') + ';text-transform:uppercase;';
    ttl.innerHTML = '<i class="bi ' + st.icon + '"></i> ' + st.label;
    const play = document.createElement('button');
    play.className = 'car-ed-btn'; play.style.marginLeft = 'auto';
    play.innerHTML = '<i class="bi bi-play-fill"></i>';
    play.title = 'Test this stage on the canvas';
    play.onclick = () => playStage(st.key === 'Enter' ? 'entrance' : st.key === 'Attn' ? 'attention' : 'exit');
    head.appendChild(ttl); head.appendChild(play);
    box.appendChild(head);

    box.appendChild(makeRow('Animation', makeAnimSelect(el['anim' + st.key + 'Class'], st.group, v => {
      el['anim' + st.key + 'Class'] = v; buildPropsPanel(el); renderElement(el);
    })));

    if (el['anim' + st.key + 'Class']) {
      box.appendChild(makeRow('Delay (ms)', makeNumInput(el['anim' + st.key + 'Delay'], 0, 60000, 50, v => {
        el['anim' + st.key + 'Delay'] = Math.max(0, Math.round(v)); renderElement(el);
      })));
      box.appendChild(makeRow('Duration', makeNumInput(el['anim' + st.key + 'Duration'], 50, 20000, 50, v => {
        el['anim' + st.key + 'Duration'] = Math.max(50, Math.round(v)); renderElement(el);
      })));
      box.appendChild(makeRow('Repeat', makeSelect(String(el['anim' + st.key + 'Repeat'] || '1'), ['1','2','3','5','infinite'], v => {
        el['anim' + st.key + 'Repeat'] = v; renderElement(el);
      })));
      if (st.key === 'Exit') {
        box.appendChild(makeRow('Hide after', makeCheck(el.animExitHide, v => { el.animExitHide = v; renderElement(el); })));
      }
      const h = document.createElement('div');
      h.style.cssText = 'font-size:0.4rem;color:var(--text-muted);padding:2px 0 0;line-height:1.35;';
      h.textContent = st.hint;
      box.appendChild(h);
    }
    animSec.appendChild(box);
  });

  const animNote = document.createElement('div');
  animNote.style.cssText = 'font-size:0.42rem;color:var(--accent3);padding:2px 0;line-height:1.35;';
  animNote.textContent = 'All three stages are optional — leave them on (none) for no animation. Delays and durations are in milliseconds. Sequences run in Preview and in the player, not on the canvas.';
  animSec.appendChild(animNote);

  if (el.animateClass && !el.animEnterClass && !el.animAttnClass && !el.animExitClass) {
    const legacy = document.createElement('div');
    legacy.style.cssText = 'font-size:0.42rem;color:var(--accent2);padding:4px 0 0;line-height:1.35;';
    legacy.textContent = 'This element still uses the old single animation (' + el.animateClass.replace('animate__','') + '). Pick an Entrance above to switch it to the new sequence.';
    animSec.appendChild(legacy);
  }

  ppStyle.appendChild(animSec);

  // ── Interactions section (hover / active states) ──
  if (['button','image','icon','panel','text','video','carousel','hero','iframe','model3d'].includes(el.type)) {
    const inter = makeSection('Interactions', 'bi-cursor');
    const hintI = document.createElement('div');
    hintI.style.cssText = 'font-size:0.45rem;color:var(--text-muted);padding:0 0 6px;line-height:1.3;';
    hintI.textContent = 'CSS overrides on hover / click. Leave blank = no change.';
    inter.appendChild(hintI);

    // Hover
    const hLabel = document.createElement('div');
    hLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:6px 0 4px;text-transform:uppercase;';
    hLabel.textContent = '● On Hover';
    inter.appendChild(hLabel);
    inter.appendChild(makeRow('BG', makeColorCombo(el.hoverBackground || '', v => { el.hoverBackground = v; })));
    inter.appendChild(makeRow('Color', makeColor(el.hoverColor || '#ffffff', v => { el.hoverColor = v; })));
    inter.appendChild(makeRow('Scale', makeInput(el.hoverScale || '', v => { el.hoverScale = v; }, 'e.g. 1.05')));
    inter.appendChild(makeRow('Shadow', makeInput(el.hoverBoxShadow || '', v => { el.hoverBoxShadow = v; }, '0 0 20px rgba(0,229,160,0.5)')));
    if (el.type === 'image') inter.appendChild(makeRow('Img Src', makeImageInput(el.hoverSrc || '', v => { el.hoverSrc = v; })));

    // Active / mousedown
    const aLabel = document.createElement('div');
    aLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--danger);margin:6px 0 4px;text-transform:uppercase;';
    aLabel.textContent = '● On Click / Mousedown';
    inter.appendChild(aLabel);
    inter.appendChild(makeRow('BG', makeColorCombo(el.activeBackground || '', v => { el.activeBackground = v; })));
    inter.appendChild(makeRow('Color', makeColor(el.activeColor || '#ffffff', v => { el.activeColor = v; })));
    inter.appendChild(makeRow('Scale', makeInput(el.activeScale || '', v => { el.activeScale = v; }, 'e.g. 0.95')));
    inter.appendChild(makeRow('Shadow', makeInput(el.activeBoxShadow || '', v => { el.activeBoxShadow = v; })));
    if (el.type === 'image') inter.appendChild(makeRow('Img Src', makeImageInput(el.activeSrc || '', v => { el.activeSrc = v; })));

    ppStyle.appendChild(inter);
  }

  // ── Binding section ──
  const bind = makeSection('Binding', 'bi-link-45deg');
  bind.appendChild(makeRow('Variable', makeInput(el.binding || '', v => { el.binding = v; })));
  const hint = document.createElement('div');
  hint.style.cssText = 'font-size:0.5rem;color:var(--text-muted);padding:2px 0;line-height:1.4;';
  hint.textContent = 'Bind to game variable: score, timer, health, level, coins, lives, mana, xp, stamina, custom...';
  bind.appendChild(hint);
  ppProps.appendChild(bind);

  // ── Events / Actions section ──
  if (['button','image','icon','panel','text','video','carousel','hero','iframe','model3d'].includes(el.type)) {
    if (!el.events) el.events = { click:[], hover:[], mousedown:[], mouseup:[], dblclick:[] };
    const evtSec = makeSection('Events & Actions', 'bi-lightning-charge');

    const EVENT_TYPES = [
      { key:'click', label:'On Click', icon:'bi-cursor' },
      { key:'hover', label:'On Hover', icon:'bi-hand-index' },
      { key:'mousedown', label:'On Mouse Down', icon:'bi-caret-down-fill' },
      { key:'mouseup', label:'On Mouse Up', icon:'bi-caret-up-fill' },
      { key:'dblclick', label:'On Double Click', icon:'bi-cursor-fill' },
    ];

    EVENT_TYPES.forEach(evt => {
      if (!el.events[evt.key]) el.events[evt.key] = [];
      const acts = el.events[evt.key];
      const modeKey = evt.key + 'Mode';
      if (!el.events[modeKey]) el.events[modeKey] = 'parallel'; // default parallel

      // Collapsible event header
      const evtHeader = document.createElement('div');
      evtHeader.style.cssText = 'display:flex;align-items:center;gap:4px;padding:4px 0;cursor:pointer;border-bottom:1px solid var(--border-color);margin-top:4px;';
      const badge = acts.length > 0 ? `<span style="font-size:0.45rem;background:var(--accent);color:#000;border-radius:3px;padding:0 4px;margin-left:auto;">${acts.length}</span>` : '';
      evtHeader.innerHTML = `<i class="bi ${evt.icon}" style="font-size:0.6rem;color:var(--accent3);"></i><span style="font-family:'Orbitron',sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--text-secondary);">${evt.label}</span>${badge}<i class="bi bi-chevron-down" style="font-size:0.5rem;color:var(--text-muted);margin-left:${badge?'4':'auto'}px;transition:transform 0.2s;"></i>`;
      const evtBody = document.createElement('div');
      evtBody.style.display = acts.length > 0 ? '' : 'none';
      const arrow = evtHeader.querySelector('.bi-chevron-down');
      if (acts.length === 0) arrow.style.transform = 'rotate(-90deg)';
      evtHeader.onclick = () => {
        const open = evtBody.style.display !== 'none';
        evtBody.style.display = open ? 'none' : '';
        arrow.style.transform = open ? 'rotate(-90deg)' : '';
      };
      evtSec.appendChild(evtHeader);

      // Execution mode toggle
      const modeRow = document.createElement('div');
      modeRow.style.cssText = 'display:flex;align-items:center;gap:6px;padding:3px 0;margin-bottom:2px;';
      const modeLabel = document.createElement('span');
      modeLabel.style.cssText = 'font-size:0.38rem;color:var(--text-muted);';
      modeLabel.textContent = 'Execution:';
      modeRow.appendChild(modeLabel);

      const isChain = el.events[modeKey] === 'chain';
      const modeParBtn = document.createElement('button');
      modeParBtn.style.cssText = `font-size:0.38rem;padding:1px 6px;border-radius:3px;cursor:pointer;border:1px solid;transition:all 0.15s;${!isChain ? 'background:rgba(0,229,160,0.15);border-color:var(--accent);color:var(--accent);' : 'background:transparent;border-color:var(--border-color);color:var(--text-muted);'}`;
      modeParBtn.textContent = '⚡ All at once';
      const modeChainBtn = document.createElement('button');
      modeChainBtn.style.cssText = `font-size:0.38rem;padding:1px 6px;border-radius:3px;cursor:pointer;border:1px solid;transition:all 0.15s;${isChain ? 'background:rgba(245,158,11,0.15);border-color:var(--accent3);color:var(--accent3);' : 'background:transparent;border-color:var(--border-color);color:var(--text-muted);'}`;
      modeChainBtn.textContent = '🔗 Chain sequence';

      function updateModeBtns() {
        const c = el.events[modeKey] === 'chain';
        modeParBtn.style.background = !c ? 'rgba(0,229,160,0.15)' : 'transparent';
        modeParBtn.style.borderColor = !c ? 'var(--accent)' : 'var(--border-color)';
        modeParBtn.style.color = !c ? 'var(--accent)' : 'var(--text-muted)';
        modeChainBtn.style.background = c ? 'rgba(245,158,11,0.15)' : 'transparent';
        modeChainBtn.style.borderColor = c ? 'var(--accent3)' : 'var(--border-color)';
        modeChainBtn.style.color = c ? 'var(--accent3)' : 'var(--text-muted)';
      }
      modeParBtn.onclick = (e) => { e.stopPropagation(); el.events[modeKey] = 'parallel'; updateModeBtns(); buildChain(); };
      modeChainBtn.onclick = (e) => { e.stopPropagation(); el.events[modeKey] = 'chain'; updateModeBtns(); buildChain(); };
      modeRow.appendChild(modeParBtn);
      modeRow.appendChild(modeChainBtn);
      evtBody.appendChild(modeRow);

      const chainBox = document.createElement('div');
      chainBox.className = 'act-chain';

      function buildChain() {
        chainBox.innerHTML = '';
        acts.forEach((act, i) => {
          const card = document.createElement('div');
          card.className = 'act-card';
          const hdr = document.createElement('div');
          hdr.className = 'act-card-header';
          hdr.innerHTML = `<span>STEP ${i + 1}</span>`;
          const rm = document.createElement('button');
          rm.className = 'act-remove'; rm.innerHTML = '<i class="bi bi-x"></i>';
          rm.onclick = () => { acts.splice(i, 1); buildChain(); buildPropsPanel(el); };
          hdr.appendChild(rm);
          card.appendChild(hdr);

          // Action type
          const typeRow = document.createElement('div'); typeRow.className = 'pp-row';
          const typeSel = document.createElement('select'); typeSel.className = 'pp-select';
          ['show','hide','toggle','setText','setProgress','videoControl','carouselControl','heroControl','frameControl','modelControl','playAnimation','playSound','callFunction','addAnimation','removeAnimation','loadScene','playMusic','stopMusic','setVolume','loadURL','showHTML'].forEach(t => {
            const o = document.createElement('option');
            o.value = t; o.textContent = t; if (t === act.type) o.selected = true;
            typeSel.appendChild(o);
          });
          typeSel.onchange = () => { act.type = typeSel.value; buildChain(); };
          typeRow.appendChild(makeLbl('Type')); typeRow.appendChild(typeSel);
          card.appendChild(typeRow);

          // Target element ID
          if (['show','hide','toggle','setText','setProgress','videoControl','carouselControl','heroControl','frameControl','modelControl','playAnimation','addAnimation','removeAnimation'].includes(act.type)) {
            const tgtRow = document.createElement('div'); tgtRow.className = 'pp-row';
            const tgtSel = document.createElement('select'); tgtSel.className = 'pp-select';
            const selfOpt = document.createElement('option');
            selfOpt.value = ''; selfOpt.textContent = '(self)'; if (!act.targetId) selfOpt.selected = true;
            tgtSel.appendChild(selfOpt);
            elements.forEach(other => {
              const o = document.createElement('option');
              o.value = other.id; o.textContent = other.id + ' — ' + (other.name || other.type);
              if (other.id === act.targetId) o.selected = true;
              tgtSel.appendChild(o);
            });
            tgtSel.onchange = () => { act.targetId = tgtSel.value || ''; };
            tgtRow.appendChild(makeLbl('Target')); tgtRow.appendChild(tgtSel);
            card.appendChild(tgtRow);
          }

          // Type-specific params
          if (act.type === 'setText') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const inp = document.createElement('input'); inp.className = 'pp-input';
            inp.value = act.value || ''; inp.placeholder = 'New text...';
            inp.onchange = () => { act.value = inp.value; };
            r.appendChild(makeLbl('Text')); r.appendChild(inp); card.appendChild(r);
          } else if (act.type === 'setProgress') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const inp = document.createElement('input'); inp.className = 'pp-input'; inp.type = 'number'; inp.min = 0; inp.max = 100;
            inp.value = act.value || 50;
            inp.onchange = () => { act.value = parseFloat(inp.value) || 0; };
            r.appendChild(makeLbl('Value %')); r.appendChild(inp); card.appendChild(r);
          } else if (act.type === 'playAnimation') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const cs = document.createElement('select'); cs.className = 'pp-select';
            [['all','▶ Full sequence'],['entrance','1 · Entrance'],['attention','2 · Attention'],['exit','3 · Exit'],['stop','⏹ Stop / reset']].forEach(([v,t]) => {
              const o = document.createElement('option'); o.value = v; o.textContent = t;
              if (v === (act.animStage || 'all')) o.selected = true; cs.appendChild(o);
            });
            cs.onchange = () => { act.animStage = cs.value; };
            r.appendChild(makeLbl('Stage')); r.appendChild(cs); card.appendChild(r);
            const ah = document.createElement('div');
            ah.style.cssText = 'font-size:0.4rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
            ah.textContent = 'Runs the target element\'s Animation stages (set them in the Style tab).';
            card.appendChild(ah);
          } else if (act.type === 'modelControl') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const cs = document.createElement('select'); cs.className = 'pp-select';
            [['spin','⟳ Start spinning'],['stop','⏸ Stop spinning'],['toggleSpin','⏯ Toggle spin'],
             ['reset','↺ Reset rotation'],['setModel','⇢ Load another model']].forEach(([v,t]) => {
              const o = document.createElement('option'); o.value = v; o.textContent = t;
              if (v === (act.modelCmd || 'toggleSpin')) o.selected = true; cs.appendChild(o);
            });
            cs.onchange = () => { act.modelCmd = cs.value; buildPropsPanel(el); };
            r.appendChild(makeLbl('Command')); r.appendChild(cs); card.appendChild(r);
            if (act.modelCmd === 'setModel') {
              const r2 = document.createElement('div'); r2.className = 'pp-row';
              const inp = document.createElement('input');
              inp.className = 'pp-input'; inp.value = act.modelValue || ''; inp.placeholder = './models/thing.glb';
              inp.onchange = () => { act.modelValue = inp.value; };
              r2.appendChild(makeLbl('GLB path')); r2.appendChild(inp); card.appendChild(r2);
            }
          } else if (act.type === 'frameControl') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const cs = document.createElement('select'); cs.className = 'pp-select';
            [['reload','↻ Reload page'],['setSrc','⇢ Load a URL'],['blank','⏹ Clear (about:blank)']].forEach(([v,t]) => {
              const o = document.createElement('option'); o.value = v; o.textContent = t;
              if (v === (act.frameCmd || 'reload')) o.selected = true; cs.appendChild(o);
            });
            cs.onchange = () => { act.frameCmd = cs.value; buildPropsPanel(el); };
            r.appendChild(makeLbl('Command')); r.appendChild(cs); card.appendChild(r);
            if (act.frameCmd === 'setSrc') {
              const r2 = document.createElement('div'); r2.className = 'pp-row';
              const inp = document.createElement('input');
              inp.className = 'pp-input'; inp.value = act.frameValue || ''; inp.placeholder = 'https://…';
              inp.onchange = () => { act.frameValue = inp.value; };
              r2.appendChild(makeLbl('URL')); r2.appendChild(inp); card.appendChild(r2);
            }
          } else if (act.type === 'heroControl') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const cs = document.createElement('select'); cs.className = 'pp-select';
            [['next','› Next background'],['prev','‹ Previous'],['goTo','⇥ Go to index'],['play','▶ Start auto'],['stop','⏸ Stop auto']].forEach(([v,t]) => {
              const o = document.createElement('option'); o.value = v; o.textContent = t;
              if (v === (act.heroCmd || 'next')) o.selected = true; cs.appendChild(o);
            });
            cs.onchange = () => { act.heroCmd = cs.value; buildPropsPanel(el); };
            r.appendChild(makeLbl('Command')); r.appendChild(cs); card.appendChild(r);
            if ((act.heroCmd || 'next') === 'goTo') {
              const r2 = document.createElement('div'); r2.className = 'pp-row';
              const inp = document.createElement('input');
              inp.className = 'pp-input'; inp.type = 'number'; inp.min = '0';
              inp.value = act.heroIndex !== undefined ? act.heroIndex : 0;
              inp.onchange = () => { act.heroIndex = parseInt(inp.value) || 0; };
              r2.appendChild(makeLbl('Index')); r2.appendChild(inp); card.appendChild(r2);
            }
            const hh = document.createElement('div');
            hh.style.cssText = 'font-size:0.4rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
            hh.textContent = 'Target must be a Hero element (or self).';
            card.appendChild(hh);
          } else if (act.type === 'carouselControl') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const cs = document.createElement('select'); cs.className = 'pp-select';
            [['next','› Next'],['prev','‹ Previous'],['goTo','⇥ Go to index'],['play','▶ Start autoplay'],['stop','⏸ Stop autoplay'],['refresh','↻ Refresh']].forEach(([v,t]) => {
              const o = document.createElement('option'); o.value = v; o.textContent = t;
              if (v === (act.carCmd || 'next')) o.selected = true; cs.appendChild(o);
            });
            cs.onchange = () => { act.carCmd = cs.value; buildPropsPanel(el); };
            r.appendChild(makeLbl('Command')); r.appendChild(cs); card.appendChild(r);
            if ((act.carCmd || 'next') === 'goTo') {
              const r2 = document.createElement('div'); r2.className = 'pp-row';
              const inp = document.createElement('input');
              inp.className = 'pp-input'; inp.type = 'number'; inp.min = '0';
              inp.value = act.carIndex !== undefined ? act.carIndex : 0;
              inp.onchange = () => { act.carIndex = parseInt(inp.value) || 0; };
              r2.appendChild(makeLbl('Index')); r2.appendChild(inp); card.appendChild(r2);
            }
            const ch = document.createElement('div');
            ch.style.cssText = 'font-size:0.4rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
            ch.textContent = 'Target must be a Carousel element (or self).';
            card.appendChild(ch);
          } else if (act.type === 'videoControl') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const cs = document.createElement('select'); cs.className = 'pp-select';
            [['play','▶ Play'],['pause','⏸ Pause'],['toggle','⏯ Toggle'],['stop','⏹ Stop (reset)'],['restart','↺ Restart']].forEach(([v,t]) => {
              const o = document.createElement('option'); o.value = v; o.textContent = t;
              if (v === (act.videoCmd || 'play')) o.selected = true; cs.appendChild(o);
            });
            cs.onchange = () => { act.videoCmd = cs.value; };
            r.appendChild(makeLbl('Command')); r.appendChild(cs); card.appendChild(r);
            const vh = document.createElement('div');
            vh.style.cssText = 'font-size:0.4rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
            vh.textContent = 'Target must be a Video element (or self).';
            card.appendChild(vh);
          } else if (act.type === 'playSound') {
            const r = document.createElement('div'); r.className = 'pp-row';
            const inp = document.createElement('input'); inp.className = 'pp-input';
            inp.value = act.soundUrl || ''; inp.placeholder = 'https://...mp3';
            inp.onchange = () => { act.soundUrl = inp.value.trim(); };
            r.appendChild(makeLbl('URL')); r.appendChild(inp); card.appendChild(r);
            const vr = document.createElement('div'); vr.className = 'pp-row';
            const vi = document.createElement('input'); vi.className = 'pp-input'; vi.type = 'number'; vi.min = 0; vi.max = 1; vi.step = 0.1;
            vi.value = act.volume !== undefined ? act.volume : 1;
            vi.onchange = () => { act.volume = parseFloat(vi.value); };
            vr.appendChild(makeLbl('Vol')); vr.appendChild(vi); card.appendChild(vr);
          } else if (act.type === 'callFunction') {
            const fnR = document.createElement('div'); fnR.className = 'pp-row';
            // Combo: dropdown + text input
            const fnWrap = document.createElement('div');
            fnWrap.style.cssText = 'display:flex;gap:3px;flex:1;';
            const fnSel = document.createElement('select'); fnSel.className = 'pp-select';
            fnSel.style.cssText = 'flex:1;min-width:0;';
            const customOpt = document.createElement('option');
            customOpt.value = '__custom__'; customOpt.textContent = '(type custom…)';
            fnSel.appendChild(customOpt);
            // Parse all functions from code
            const codeVal = customCode || document.getElementById('code-main')?.value || '';
            const funcMatches = [...codeVal.matchAll(/function\s+(\w+)\s*\(([^)]*)\)/g)];
            let foundInList = false;
            funcMatches.forEach(fm => {
              const o = document.createElement('option');
              o.value = fm[1]; o.textContent = `${fm[1]}(${fm[2]})`;
              if (fm[1] === act.funcName) { o.selected = true; foundInList = true; }
              fnSel.appendChild(o);
            });
            const fnI = document.createElement('input'); fnI.className = 'pp-input';
            fnI.value = act.funcName || ''; fnI.placeholder = 'functionName';
            fnI.style.cssText = "font-family:'Courier New',monospace;flex:1;min-width:0;";
            if (foundInList) { fnI.style.display = 'none'; fnSel.value = act.funcName; }
            else { fnSel.value = '__custom__'; }
            fnSel.onchange = () => {
              if (fnSel.value === '__custom__') {
                fnI.style.display = ''; fnI.focus();
              } else {
                fnI.style.display = 'none';
                act.funcName = fnSel.value; fnI.value = fnSel.value;
              }
            };
            fnI.onchange = () => { act.funcName = fnI.value.trim(); };
            fnWrap.appendChild(fnSel); fnWrap.appendChild(fnI);
            fnR.appendChild(makeLbl('Func')); fnR.appendChild(fnWrap); card.appendChild(fnR);
            const arR = document.createElement('div'); arR.className = 'pp-row';
            const arI = document.createElement('input'); arI.className = 'pp-input';
            arI.value = act.funcArgs || ''; arI.placeholder = '"hello", 42';
            arI.style.fontFamily = "'Courier New',monospace";
            arI.onchange = () => { act.funcArgs = arI.value.trim(); };
            arR.appendChild(makeLbl('Args')); arR.appendChild(arI); card.appendChild(arR);
          } else if (act.type === 'addAnimation' || act.type === 'removeAnimation') {
            const aR = document.createElement('div'); aR.className = 'pp-row';
            const aSel = document.createElement('select'); aSel.className = 'pp-select';
            const anOpt = document.createElement('option'); anOpt.value = ''; anOpt.textContent = '(select)'; aSel.appendChild(anOpt);
            const AG = {
              'Entrances': ['animate__fadeIn','animate__fadeInUp','animate__fadeInDown','animate__fadeInLeft','animate__fadeInRight','animate__bounceIn','animate__bounceInUp','animate__zoomIn','animate__slideInUp','animate__slideInDown','animate__slideInLeft','animate__slideInRight','animate__flipInX','animate__flipInY','animate__jackInTheBox','animate__rollIn','animate__backInUp','animate__backInDown'],
              'Exits': ['animate__fadeOut','animate__fadeOutUp','animate__fadeOutDown','animate__fadeOutLeft','animate__fadeOutRight','animate__bounceOut','animate__zoomOut','animate__slideOutUp','animate__slideOutDown','animate__slideOutLeft','animate__slideOutRight','animate__flipOutX','animate__flipOutY','animate__rollOut','animate__backOutUp','animate__backOutDown'],
              'Attention': ['animate__bounce','animate__flash','animate__pulse','animate__rubberBand','animate__shakeX','animate__shakeY','animate__headShake','animate__swing','animate__tada','animate__wobble','animate__jello','animate__heartBeat','animate__flip'],
            };
            Object.entries(AG).forEach(([g, a]) => {
              const og = document.createElement('optgroup'); og.label = g;
              a.forEach(c => { const o = document.createElement('option'); o.value = c; o.textContent = c.replace('animate__',''); if (c === act.animClass) o.selected = true; og.appendChild(o); });
              aSel.appendChild(og);
            });
            aSel.onchange = () => { act.animClass = aSel.value; };
            aR.appendChild(makeLbl('Anim')); aR.appendChild(aSel); card.appendChild(aR);
            if (act.type === 'addAnimation') {
              const durR = document.createElement('div'); durR.className = 'pp-row';
              const durI = document.createElement('input'); durI.className = 'pp-input';
              durI.value = act.animDuration || ''; durI.placeholder = '0.5s, 2s';
              durI.onchange = () => { act.animDuration = durI.value.trim(); };
              durR.appendChild(makeLbl('Duration')); durR.appendChild(durI); card.appendChild(durR);

              const delR = document.createElement('div'); delR.className = 'pp-row';
              const delI = document.createElement('input'); delI.className = 'pp-input';
              delI.value = act.animDelay || ''; delI.placeholder = '0s, 0.5s, 1s';
              delI.onchange = () => { act.animDelay = delI.value.trim(); };
              delR.appendChild(makeLbl('Anim Delay')); delR.appendChild(delI); card.appendChild(delR);

              const repR = document.createElement('div'); repR.className = 'pp-row';
              const repS = document.createElement('select'); repS.className = 'pp-select';
              ['','1','2','3','5','10','infinite'].forEach(v => {
                const o = document.createElement('option'); o.value = v; o.textContent = v || '(default)';
                if (v === (act.animRepeat || '')) o.selected = true; repS.appendChild(o);
              });
              repS.onchange = () => { act.animRepeat = repS.value; };
              repR.appendChild(makeLbl('Repeat')); repR.appendChild(repS); card.appendChild(repR);
            }
          }

          // loadScene fields
          if (act.type === 'loadScene') {
            const lr = document.createElement('div'); lr.className = 'pp-row';
            const li = document.createElement('input'); li.className = 'pp-input';
            li.value = act.levelFile || ''; li.placeholder = 'saves/level2.json';
            li.onchange = () => { act.levelFile = li.value.trim(); };
            lr.appendChild(makeLbl('Level File')); lr.appendChild(li); card.appendChild(lr);
            const ur = document.createElement('div'); ur.className = 'pp-row';
            const ui = document.createElement('input'); ui.className = 'pp-input';
            ui.value = act.uiFile || ''; ui.placeholder = '(optional) saves/ui2.json';
            ui.onchange = () => { act.uiFile = ui.value.trim(); };
            ur.appendChild(makeLbl('UI File')); ur.appendChild(ui); card.appendChild(ur);
          }

          // playMusic fields
          if (act.type === 'playMusic') {
            const mr = document.createElement('div'); mr.className = 'pp-row';
            const mi = document.createElement('input'); mi.className = 'pp-input';
            mi.value = act.musicSrc || ''; mi.placeholder = './audio/music.mp3';
            mi.onchange = () => { act.musicSrc = mi.value.trim(); };
            mr.appendChild(makeLbl('Music File')); mr.appendChild(mi); card.appendChild(mr);
            const lr = document.createElement('div'); lr.className = 'pp-row';
            const ls = document.createElement('select'); ls.className = 'pp-select';
            [['true','Loop'],['false','Play Once']].forEach(([v,t]) => { const o = document.createElement('option'); o.value = v; o.textContent = t; if (String(act.musicLoop !== false) === v) o.selected = true; ls.appendChild(o); });
            ls.onchange = () => { act.musicLoop = ls.value === 'true'; };
            lr.appendChild(makeLbl('Loop')); lr.appendChild(ls); card.appendChild(lr);
          }

          // setVolume fields
          if (act.type === 'setVolume') {
            const vr = document.createElement('div'); vr.className = 'pp-row';
            const vi = document.createElement('input'); vi.className = 'pp-input'; vi.type = 'range'; vi.min = 0; vi.max = 1; vi.step = 0.05;
            vi.value = act.volume !== undefined ? act.volume : 0.5;
            const vl = document.createElement('span'); vl.style.cssText = 'min-width:30px;text-align:right;font-size:0.6rem;color:#a0b0c0;'; vl.textContent = Math.round((act.volume !== undefined ? act.volume : 0.5) * 100) + '%';
            vi.oninput = () => { act.volume = parseFloat(vi.value); vl.textContent = Math.round(act.volume * 100) + '%'; };
            vr.appendChild(makeLbl('Volume')); vr.appendChild(vi); vr.appendChild(vl); card.appendChild(vr);
          }

          // loadURL fields
          if (act.type === 'loadURL') {
            const ur = document.createElement('div'); ur.className = 'pp-row';
            const ui = document.createElement('input'); ui.className = 'pp-input';
            ui.value = act.url || ''; ui.placeholder = 'https://example.com';
            ui.onchange = () => { act.url = ui.value.trim(); };
            ur.appendChild(makeLbl('URL')); ur.appendChild(ui); card.appendChild(ur);
            const tr = document.createElement('div'); tr.className = 'pp-row';
            const ts = document.createElement('select'); ts.className = 'pp-select';
            [['same','Same Tab'],['newTab','New Tab'],['newWindow','New Window'],['iframe','In-Game Overlay']].forEach(([v,t]) => { const o = document.createElement('option'); o.value = v; o.textContent = t; if (v === (act.urlTarget||'newTab')) o.selected = true; ts.appendChild(o); });
            ts.onchange = () => { act.urlTarget = ts.value; buildChain(); };
            tr.appendChild(makeLbl('Target')); tr.appendChild(ts); card.appendChild(tr);
            if (act.urlTarget === 'iframe') {
              const bir = document.createElement('div'); bir.className = 'pp-row';
              const bii = document.createElement('input'); bii.className = 'pp-input'; bii.value = act.iframeBackIcon || ''; bii.placeholder = 'back icon image';
              bii.onchange = () => { act.iframeBackIcon = bii.value.trim(); };
              bir.appendChild(makeLbl('Back Icon')); bir.appendChild(bii); card.appendChild(bir);
            }
          }

          // showHTML fields
          if (act.type === 'showHTML') {
            const mr = document.createElement('div'); mr.className = 'pp-row';
            const ms = document.createElement('select'); ms.className = 'pp-select';
            [['popup','UI Popup'],['3d','3D Billboard']].forEach(([v,t]) => { const o = document.createElement('option'); o.value = v; o.textContent = t; if (v === (act.htmlMode||'popup')) o.selected = true; ms.appendChild(o); });
            ms.onchange = () => { act.htmlMode = ms.value; buildChain(); };
            mr.appendChild(makeLbl('Display')); mr.appendChild(ms); card.appendChild(mr);
            const cr = document.createElement('div'); cr.className = 'pp-row';
            const ci = document.createElement('textarea'); ci.className = 'pp-input'; ci.style.cssText = 'min-height:50px;font-family:monospace;font-size:0.55rem;';
            ci.value = act.htmlContent || ''; ci.placeholder = '<p>Hello</p>';
            ci.onchange = () => { act.htmlContent = ci.value; };
            cr.appendChild(makeLbl('HTML')); cr.appendChild(ci); card.appendChild(cr);
          }

          // Delay from trigger (parallel) or from previous step (chain)
          if (i > 0) {
            const dR = document.createElement('div'); dR.className = 'pp-row';
            const dI = document.createElement('input'); dI.className = 'pp-input'; dI.type = 'number'; dI.min = 0; dI.step = 100;
            dI.value = act.delay || 0;
            dI.onchange = () => { act.delay = parseInt(dI.value) || 0; };
            const dLbl = el.events[modeKey] === 'chain' ? 'Delay ms (after prev)' : 'Delay ms (from start)';
            dR.appendChild(makeLbl(dLbl)); dR.appendChild(dI); card.appendChild(dR);
          }

          chainBox.appendChild(card);
        });
      }
      buildChain();

      const addBtn = document.createElement('button'); addBtn.className = 'pp-btn';
      addBtn.style.cssText = 'width:100%;margin-top:2px;background:rgba(245,158,11,0.06);border-color:var(--accent3);color:var(--accent3);font-size:0.55rem;padding:3px 6px;';
      addBtn.innerHTML = '<i class="bi bi-plus-lg"></i> Add';
      addBtn.onclick = () => { acts.push({ type:'show', targetId:'', delay:0 }); buildChain(); buildPropsPanel(el); };
      evtBody.appendChild(chainBox);
      evtBody.appendChild(addBtn);
      evtSec.appendChild(evtBody);
    });

    ppProps.appendChild(evtSec);
  }

  // ── Actions section ──
  const acts = makeSection('Actions', 'bi-lightning');
  const lockCheck = document.createElement('input');
  lockCheck.type = 'checkbox'; lockCheck.className = 'pp-check'; lockCheck.checked = el.locked;
  lockCheck.onchange = () => { el.locked = lockCheck.checked; renderElement(el); };
  acts.appendChild(makeRow('Locked', lockCheck));
  const visCheck = document.createElement('input');
  visCheck.type = 'checkbox'; visCheck.className = 'pp-check'; visCheck.checked = el.visible;
  visCheck.onchange = () => { el.visible = visCheck.checked; renderElement(el); rerenderChildren(el.id); buildLayerList(); };
  acts.appendChild(makeRow('Visible', visCheck));
  const disCheck = document.createElement('input');
  disCheck.type = 'checkbox'; disCheck.className = 'pp-check'; disCheck.checked = !!el.disabled;
  disCheck.onchange = () => { el.disabled = disCheck.checked; renderElement(el); rerenderChildren(el.id); buildLayerList(); };
  acts.appendChild(makeRow('Disabled', disCheck));
  const disHint = document.createElement('div');
  disHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
  disHint.textContent = 'Disabled = always hidden, ignores all show/hide actions.';
  acts.appendChild(disHint);

  // Show on viewports
  const showOnLabel = document.createElement('div');
  showOnLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
  showOnLabel.textContent = '● Show On Viewports';
  acts.appendChild(showOnLabel);
  const showHint = document.createElement('div');
  showHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
  showHint.textContent = 'None checked = show on all. Check specific viewports to limit.';
  acts.appendChild(showHint);
  if (!el.showOn) el.showOn = [];
  const vpOpts = [
    { key:'desktop', label:'Desktop', icon:'bi-display' },
    { key:'tablet', label:'Tablet', icon:'bi-tablet' },
    { key:'phone', label:'Phone', icon:'bi-phone' },
    { key:'phone-land', label:'Ph. Land', icon:'bi-phone-landscape' },
  ];
  const vpRow = document.createElement('div');
  vpRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;padding:2px 0 6px;';
  vpOpts.forEach(vp => {
    const lbl = document.createElement('label');
    lbl.style.cssText = 'display:flex;align-items:center;gap:3px;font-size:0.5rem;color:var(--text-secondary);cursor:pointer;padding:2px 6px;border:1px solid var(--border-color);border-radius:4px;background:' + (el.showOn.includes(vp.key) ? 'rgba(167,139,250,0.12)' : 'transparent');
    const cb = document.createElement('input');
    cb.type = 'checkbox'; cb.checked = el.showOn.includes(vp.key);
    cb.style.cssText = 'margin:0;width:12px;height:12px;';
    cb.onchange = () => {
      if (cb.checked) { if (!el.showOn.includes(vp.key)) el.showOn.push(vp.key); }
      else { el.showOn = el.showOn.filter(v => v !== vp.key); }
      buildPropsPanel(el);
    };
    lbl.appendChild(cb);
    const ico = document.createElement('i');
    ico.className = 'bi ' + vp.icon;
    lbl.appendChild(ico);
    lbl.appendChild(document.createTextNode(' ' + vp.label));
    vpRow.appendChild(lbl);
  });
  acts.appendChild(vpRow);

  // Control mode (for panels)
  if (el.type === 'panel') {
    const ctrlLabel = document.createElement('div');
    ctrlLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent);margin:8px 0 4px;text-transform:uppercase;';
    ctrlLabel.textContent = '● Control Mode';
    acts.appendChild(ctrlLabel);
    acts.appendChild(makeRow('Mode', makeSelect(el.controlMode || 'none', ['none','dpad','joystick'], v => { el.controlMode = v; buildPropsPanel(el); renderElement(el); })));
    if (el.controlMode === 'dpad' || el.controlMode === 'joystick') {
      acts.appendChild(makeRow('Ctrl BG', makeColorCombo(el.controlBg || '', v => { el.controlBg = v; renderElement(el); })));
      acts.appendChild(makeRow(el.controlMode === 'dpad' ? 'Arrow Color' : 'Knob Color', makeColorCombo(el.controlKnob || '', v => { el.controlKnob = v; renderElement(el); })));
    }
    const ctrlHint = document.createElement('div');
    ctrlHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.3;';
    ctrlHint.textContent = 'D-Pad: renders arrow buttons. Joystick: draggable knob. Both simulate WASD to control the player.';
    acts.appendChild(ctrlHint);
  }

  const btnRow = document.createElement('div');
  btnRow.style.cssText = 'display:flex;gap:4px;margin-top:8px;';
  const dupBtn = document.createElement('button');
  dupBtn.className = 'pp-btn'; dupBtn.style.cssText = 'flex:1;background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);';
  dupBtn.innerHTML = '<i class="bi bi-copy"></i> Duplicate';
  dupBtn.onclick = () => duplicateElement(el.id);
  btnRow.appendChild(dupBtn);
  const delBtn = document.createElement('button');
  delBtn.className = 'pp-btn'; delBtn.style.cssText = 'flex:1;background:rgba(255,77,106,0.08);border-color:var(--danger);color:var(--danger);';
  delBtn.innerHTML = '<i class="bi bi-trash3"></i> Remove';
  delBtn.onclick = () => removeElement(el.id);
  btnRow.appendChild(delBtn);
  acts.appendChild(btnRow);

  // Layer buttons
  const layerRow = document.createElement('div');
  layerRow.style.cssText = 'display:flex;gap:4px;margin-top:4px;';
  const fwdBtn = document.createElement('button');
  fwdBtn.className = 'pp-btn'; fwdBtn.style.cssText = 'flex:1;background:rgba(167,139,250,0.08);border-color:var(--accent2);color:var(--accent2);';
  fwdBtn.innerHTML = '<i class="bi bi-arrow-up"></i> Forward';
  fwdBtn.onclick = () => { el.zIndex = Math.min(el.zIndex + 1, 100); renderElement(el); buildPropsPanel(el); };
  layerRow.appendChild(fwdBtn);
  const bkBtn = document.createElement('button');
  bkBtn.className = 'pp-btn'; bkBtn.style.cssText = 'flex:1;background:rgba(167,139,250,0.08);border-color:var(--accent2);color:var(--accent2);';
  bkBtn.innerHTML = '<i class="bi bi-arrow-down"></i> Back';
  bkBtn.onclick = () => { el.zIndex = Math.max(el.zIndex - 1, 0); renderElement(el); buildPropsPanel(el); };
  layerRow.appendChild(bkBtn);
  acts.appendChild(layerRow);

  ppProps.appendChild(acts);

  applyPropsTab();
}

// ══════════════════════════════════════
//  RIGHT PANEL TABS (Style / Properties)
// ══════════════════════════════════════
function switchPropsTab(tab) {
  window._ppTab = tab;
  document.querySelectorAll('.pp-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
  document.querySelectorAll('.pp-tab-panel').forEach(p => p.classList.toggle('active', p.id === 'pp-panel-' + tab));
  ppContent.scrollTop = 0;
}

function applyPropsTab() {
  // Keep the user's chosen tab across selections; default to Properties
  switchPropsTab(window._ppTab || 'props');
}

document.querySelectorAll('.pp-tab').forEach(btn => {
  btn.addEventListener('click', () => switchPropsTab(btn.dataset.tab));
});

// ── Property helper builders ──
/** Local timestamp for filenames: YYYY-MM-DD_HHMMSS */
function fileTimestamp(d) {
  d = d || new Date();
  const p = n => String(n).padStart(2, '0');
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
    '_' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
}

function makeSection(title, icon) {
  const s = document.createElement('div');
  s.className = 'pp-section';
  s.innerHTML = `<h6><i class="bi ${icon}"></i> ${title}</h6>`;
  return s;
}

// ══════════════════════════════════════
//  CAROUSEL PROPERTIES (Owl Carousel 2)
// ══════════════════════════════════════
function makeCheck(val, onChange) {
  const c = document.createElement('input');
  c.type = 'checkbox'; c.className = 'pp-check'; c.checked = !!val;
  c.onchange = () => onChange(c.checked);
  return c;
}

function makeTextareaInput(val, onChange, placeholder, rows) {
  const t = document.createElement('textarea');
  t.className = 'pp-input';
  t.style.cssText = 'flex:1;min-height:' + ((rows || 3) * 16) + 'px;resize:vertical;font-family:monospace;font-size:0.55rem;line-height:1.4;';
  t.value = val || '';
  if (placeholder) t.placeholder = placeholder;
  t.onchange = () => onChange(t.value);
  return t;
}

const NAV_SHAPES = { circle:'50%', rounded:'8px', square:'0', pill:'999px' };
const DOT_SHAPES = { circle:'50%', rounded:'3px', square:'0', bar:'999px' };
function navShapeOf(el) {
  const r = el.carNavRadius || '50%';
  for (const [k, v] of Object.entries(NAV_SHAPES)) if (v === r) return k;
  return 'circle';
}
function dotShapeOf(el) {
  const r = el.carDotRadius || '50%';
  if (r === '999px' && (parseFloat(el.carDotWidth) || 7) > (parseFloat(el.carDotHeight) || 7)) return 'bar';
  for (const [k, v] of Object.entries(DOT_SHAPES)) if (v === r) return k;
  return 'circle';
}

/** Active-item props are stored per device; desktop is the base value. */
function carGet(el, prop) {
  if (deviceMode !== 'desktop' && el.carDeviceOverrides && el.carDeviceOverrides[deviceMode]) {
    const v = el.carDeviceOverrides[deviceMode][prop];
    if (v !== undefined && v !== null) return v;
  }
  return el[prop];
}

function carSet(el, prop, val) {
  if (deviceMode !== 'desktop') {
    if (!el.carDeviceOverrides) el.carDeviceOverrides = { tablet:{}, phone:{}, 'phone-land':{} };
    if (!el.carDeviceOverrides[deviceMode]) el.carDeviceOverrides[deviceMode] = {};
    el.carDeviceOverrides[deviceMode][prop] = val;
  } else {
    el[prop] = val;
  }
  renderElement(el);
}

function carHasOv(el, prop) {
  return deviceMode !== 'desktop' && el.carDeviceOverrides && el.carDeviceOverrides[deviceMode]
    && el.carDeviceOverrides[deviceMode][prop] !== undefined
    && el.carDeviceOverrides[deviceMode][prop] !== null;
}

/** Row with a purple dot + reset button when this device overrides the desktop value */
function carOvRow(el, label, prop, inputEl) {
  const row = makeRow(label, inputEl);
  if (deviceMode === 'desktop') return row;
  const lbl = row.querySelector('.pp-label');
  if (carHasOv(el, prop)) {
    if (lbl) {
      const dot = document.createElement('span');
      dot.style.cssText = 'display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--accent2);margin-left:4px;vertical-align:middle;';
      dot.title = 'Overridden for ' + deviceMode;
      lbl.appendChild(dot);
    }
    const rst = document.createElement('button');
    rst.className = 'car-ed-btn';
    rst.innerHTML = '<i class="bi bi-arrow-counterclockwise"></i>';
    rst.title = 'Use the desktop value on ' + deviceMode;
    rst.onclick = () => {
      delete el.carDeviceOverrides[deviceMode][prop];
      buildPropsPanel(el); renderElement(el);
    };
    row.appendChild(rst);
  }
  return row;
}

function buildHeroProps(el) {
  if (!Array.isArray(el.heroSlides)) el.heroSlides = [];
  const rerender = () => renderElement(el);
  const rebuild = () => { renderElement(el); buildPropsPanel(el); };

  // ══ PROPERTIES TAB ══
  // Backgrounds
  const bgSec = makeSection('Hero Backgrounds', 'bi-images');
  const bgHint = document.createElement('div');
  bgHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
  bgHint.textContent = 'Each background fades into the next. Give one its own title/subtitle to change the wording as it appears, or leave them blank to keep the hero text below.';
  bgSec.appendChild(bgHint);

  const list = document.createElement('div');
  const openSet = window._heroOpen || (window._heroOpen = new Set());
  el.heroSlides.forEach((sl, idx) => {
    if (!sl.id) sl.id = 'hs_' + Math.random().toString(36).slice(2, 8);
    const card = document.createElement('div');
    card.className = 'car-ed-item' + (openSet.has(sl.id) ? ' open' : '');
    const head = document.createElement('div');
    head.className = 'car-ed-head';
    const thumb = document.createElement(sl.image ? 'img' : 'div');
    thumb.className = 'car-ed-thumb';
    if (sl.image) thumb.src = sl.image; else thumb.innerHTML = '<i class="bi ' + (sl.video ? 'bi-film' : 'bi-image') + '"></i>';
    const nameEl = document.createElement('span');
    nameEl.className = 'car-ed-name';
    nameEl.textContent = sl.title || sl.text || ('Background ' + (idx + 1));
    head.appendChild(thumb); head.appendChild(nameEl);
    const mkBtn = (icon, cls, title, fn) => {
      const b = document.createElement('button');
      b.className = 'car-ed-btn ' + (cls || ''); b.title = title;
      b.innerHTML = '<i class="bi ' + icon + '"></i>';
      b.onclick = e => { e.stopPropagation(); fn(); };
      return b;
    };
    head.appendChild(mkBtn('bi-chevron-up', '', 'Move up', () => {
      if (idx > 0) { const a = el.heroSlides; [a[idx-1], a[idx]] = [a[idx], a[idx-1]]; rebuild(); }
    }));
    head.appendChild(mkBtn('bi-chevron-down', '', 'Move down', () => {
      const a = el.heroSlides;
      if (idx < a.length - 1) { [a[idx+1], a[idx]] = [a[idx], a[idx+1]]; rebuild(); }
    }));
    head.appendChild(mkBtn('bi-trash', 'del', 'Delete', () => { el.heroSlides.splice(idx, 1); rebuild(); }));
    head.onclick = () => {
      if (openSet.has(sl.id)) openSet.delete(sl.id); else openSet.add(sl.id);
      card.classList.toggle('open');
    };
    const body = document.createElement('div');
    body.className = 'car-ed-body';
    body.appendChild(makeRow('Image', makeImageInput(sl.image || '', v => { sl.image = v; rebuild(); })));
    body.appendChild(makeRow('Video', makeVideoInput(sl.video || '', v => { sl.video = v; rebuild(); })));
    body.appendChild(makeRow('Colour', makeColorCombo(sl.color || '', v => { sl.color = v; rerender(); })));
    body.appendChild(makeRow('Title', makeInput(sl.title || '', v => { sl.title = v; rebuild(); }, 'leave blank to keep hero title')));
    body.appendChild(makeRow('Subtitle', makeInput(sl.text || '', v => { sl.text = v; rebuild(); }, 'leave blank to keep hero subtitle')));
    card.appendChild(head); card.appendChild(body);
    list.appendChild(card);
  });
  bgSec.appendChild(list);
  const addBg = document.createElement('button');
  addBg.className = 'car-ed-add';
  addBg.innerHTML = '<i class="bi bi-plus-lg"></i> Add Background';
  addBg.onclick = () => {
    const id = 'hs_' + Math.random().toString(36).slice(2, 8);
    el.heroSlides.push({ id, image:'', video:'', color:'', title:'', text:'' });
    openSet.add(id); rebuild();
  };
  bgSec.appendChild(addBg);
  ppProps.appendChild(bgSec);

  // Text
  const txtSec = makeSection('Hero Text', 'bi-fonts');
  txtSec.appendChild(makeRow('Title', makeInput(el.heroTitle, v => { el.heroTitle = v; rerender(); }, 'Main heading')));
  txtSec.appendChild(makeRow('Subtitle', makeInput(el.heroSubtitle, v => { el.heroSubtitle = v; rerender(); }, 'Supporting line')));
  const txtHint = document.createElement('div');
  txtHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
  txtHint.textContent = 'Basic HTML works here (<br>, <b>, <span>). A background with its own title/subtitle overrides these while it is showing.';
  txtSec.appendChild(txtHint);
  ppProps.appendChild(txtSec);

  // Size + behaviour
  const laySec = makeSection('Hero Layout', 'bi-aspect-ratio');
  const fsBtn = document.createElement('button');
  fsBtn.className = 'car-ed-add';
  fsBtn.style.cssText += 'margin-bottom:6px;';
  fsBtn.innerHTML = '<i class="bi bi-arrows-fullscreen"></i> Make full screen';
  fsBtn.onclick = () => {
    el.x = 50; el.y = 50; el.w = 100; el.h = 100; el.anchor = 'center';
    if (el.responsive) el.overrides = { tablet:{}, phone:{}, 'phone-land':{} };
    rebuild();
  };
  laySec.appendChild(fsBtn);
  laySec.appendChild(makeRow('Align H', makeSelect(el.heroAlignH, ['left','center','right'], v => { el.heroAlignH = v; rerender(); })));
  laySec.appendChild(makeRow('Align V', makeSelect(el.heroAlignV, ['top','center','bottom'], v => { el.heroAlignV = v; rerender(); })));
  laySec.appendChild(makeRow('Padding', makeInput(el.heroPadding, v => { el.heroPadding = v; rerender(); }, '6% / 40px')));
  laySec.appendChild(makeRow('Text Width', makeInput(el.heroMaxWidth, v => { el.heroMaxWidth = v; rerender(); }, '70% / 640px')));
  laySec.appendChild(makeRow('Gap', makeInput(el.heroGap, v => { el.heroGap = v; rerender(); }, '10px')));
  ppProps.appendChild(laySec);

  const behSec = makeSection('Hero Behaviour', 'bi-arrow-repeat');
  behSec.appendChild(makeRow('Auto Play', makeCheck(el.heroAutoplay, v => { el.heroAutoplay = v; rebuild(); })));
  if (el.heroAutoplay) {
    behSec.appendChild(makeRow('Interval', makeNumInput(el.heroInterval, 800, 60000, 250, v => { el.heroInterval = Math.round(v); rerender(); })));
  }
  behSec.appendChild(makeRow('Transition', makeSelect(el.heroTransition, ['fade','zoom','slide-left','slide-right','slide-up'], v => { el.heroTransition = v; rerender(); })));
  behSec.appendChild(makeRow('Duration', makeNumInput(el.heroTransDuration, 0, 5000, 50, v => { el.heroTransDuration = Math.round(v); rerender(); })));
  behSec.appendChild(makeRow('Ken Burns', makeCheck(el.heroKenBurns, v => { el.heroKenBurns = v; rebuild(); })));
  if (el.heroKenBurns) {
    behSec.appendChild(makeRow('Zoom To', makeSlider(el.heroKenScale, 1, 1.6, 0.01, v => { el.heroKenScale = v; rerender(); })));
  }
  behSec.appendChild(makeRow('Start At', makeNumInput(el.heroStart, 0, 99, 1, v => { el.heroStart = Math.max(0, Math.round(v)); rerender(); })));
  behSec.appendChild(makeRow('On Change', makeInput(el.heroChangeFunc, v => { el.heroChangeFunc = v; rerender(); }, 'onHeroSlide')));
  const behHint = document.createElement('div');
  behHint.style.cssText = 'font-size:0.42rem;color:var(--accent3);padding:2px 0;line-height:1.35;';
  behHint.textContent = 'Backgrounds cycle in Preview and in the player — the canvas holds the first one so you can design against it.';
  behSec.appendChild(behHint);
  ppProps.appendChild(behSec);

  const kidHint = makeSection('Child Elements', 'bi-diagram-3');
  const kids = elements.filter(c => c.parentId === el.id);
  const kidText = document.createElement('div');
  kidText.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  kidText.textContent = kids.length
    ? kids.length + ' element' + (kids.length === 1 ? '' : 's') + ' sit inside this hero. Their position is a percentage of the hero, not the screen.'
    : 'Drop any element on the canvas, then set its Parent to this hero (in the element\'s Info section) to place it inside — buttons, logos, badges, anything.';
  kidHint.appendChild(kidText);
  ppProps.appendChild(kidHint);

  // ══ STYLE TAB ══
  const bgStyle = makeSection('Background Style', 'bi-image');
  bgStyle.appendChild(makeRow('Fit', makeSelect(el.heroBgFit, ['cover','contain','fill','none'], v => { el.heroBgFit = v; rerender(); })));
  bgStyle.appendChild(makeRow('Position', makeInput(el.heroBgPosition, v => { el.heroBgPosition = v; rerender(); }, 'center / top left')));
  bgStyle.appendChild(makeRow('Overlay', makeInput(el.heroOverlay, v => { el.heroOverlay = v; rerender(); }, 'linear-gradient(...) / #0008')));
  bgStyle.appendChild(makeRow('Overlay α', makeSlider(el.heroOverlayOpacity, 0, 1, 0.05, v => { el.heroOverlayOpacity = v; rerender(); })));
  bgStyle.appendChild(makeRow('Empty Bg', makeInput(el.heroEmptyBg, v => { el.heroEmptyBg = v; rerender(); }, 'used when a slide has no media')));
  ppStyle.appendChild(bgStyle);

  const FONT_UNITS = ['rem','px','em','%','vw','vh','pt'];
  const sizeRow = (label, sizeKey, unitKey) => {
    const wrap = document.createElement('div');
    wrap.style.cssText = 'display:flex;gap:4px;flex:1;align-items:center;';
    const num = document.createElement('input');
    num.type = 'number'; num.className = 'pp-input'; num.style.cssText = 'flex:1;min-width:0;';
    num.step = (el[unitKey] === 'px' || el[unitKey] === 'pt') ? '1' : '0.05';
    num.value = el[sizeKey];
    num.onchange = () => { el[sizeKey] = parseFloat(num.value) || 0; rerender(); };
    const us = document.createElement('select');
    us.className = 'pp-select'; us.style.cssText = 'flex:0 0 54px;';
    FONT_UNITS.forEach(u => {
      const o = document.createElement('option');
      o.value = u; o.textContent = u;
      if (u === (el[unitKey] || 'rem')) o.selected = true;
      us.appendChild(o);
    });
    us.onchange = () => { el[unitKey] = us.value; buildPropsPanel(el); rerender(); };
    wrap.appendChild(num); wrap.appendChild(us);
    return makeRow(label, wrap);
  };

  const tSec = makeSection('Title Style', 'bi-type-h1');
  tSec.appendChild(makeRow('Font', makeSelect(el.heroTitleFont, [
    "'Orbitron',sans-serif", "'Rajdhani',sans-serif", "'Courier New',monospace",
    "Georgia,serif", "'Arial',sans-serif", "'Trebuchet MS',sans-serif", "system-ui",
  ], v => { el.heroTitleFont = v; rerender(); })));
  tSec.appendChild(sizeRow('Size', 'heroTitleSize', 'heroTitleUnit'));
  tSec.appendChild(makeRow('Weight', makeSelect(el.heroTitleWeight, ['300','400','500','600','700','800','900'], v => { el.heroTitleWeight = v; rerender(); })));
  tSec.appendChild(makeRow('Colour', makeColorCombo(el.heroTitleColor, v => { el.heroTitleColor = v; rerender(); })));
  tSec.appendChild(makeRow('Line H', makeInput(el.heroTitleLineHeight, v => { el.heroTitleLineHeight = v; rerender(); }, '1.1')));
  tSec.appendChild(makeRow('Spacing', makeInput(el.heroTitleSpacing, v => { el.heroTitleSpacing = v; rerender(); }, 'normal / 2px')));
  tSec.appendChild(makeRow('Shadow', makeInput(el.heroTitleShadow, v => { el.heroTitleShadow = v; rerender(); }, '0 2px 18px #0008')));
  ppStyle.appendChild(tSec);

  const sSec = makeSection('Subtitle Style', 'bi-type');
  sSec.appendChild(makeRow('Font', makeSelect(el.heroSubFont, [
    "'Rajdhani',sans-serif", "'Orbitron',sans-serif", "'Courier New',monospace",
    "Georgia,serif", "'Arial',sans-serif", "'Trebuchet MS',sans-serif", "system-ui",
  ], v => { el.heroSubFont = v; rerender(); })));
  sSec.appendChild(sizeRow('Size', 'heroSubSize', 'heroSubUnit'));
  sSec.appendChild(makeRow('Weight', makeSelect(el.heroSubWeight, ['300','400','500','600','700'], v => { el.heroSubWeight = v; rerender(); })));
  sSec.appendChild(makeRow('Colour', makeColorCombo(el.heroSubColor, v => { el.heroSubColor = v; rerender(); })));
  sSec.appendChild(makeRow('Line H', makeInput(el.heroSubLineHeight, v => { el.heroSubLineHeight = v; rerender(); }, '1.5')));
  sSec.appendChild(makeRow('Shadow', makeInput(el.heroSubShadow, v => { el.heroSubShadow = v; rerender(); }, '0 1px 10px #0007')));
  ppStyle.appendChild(sSec);

  const navSec = makeSection('Hero Dots & Arrows', 'bi-three-dots');
  navSec.appendChild(makeRow('Dots', makeCheck(el.heroDots, v => { el.heroDots = v; rebuild(); })));
  if (el.heroDots) {
    navSec.appendChild(makeRow('Width', makeNumInput(el.heroDotWidth, 2, 80, 1, v => { el.heroDotWidth = Math.round(v); rerender(); })));
    navSec.appendChild(makeRow('Height', makeNumInput(el.heroDotHeight, 2, 80, 1, v => { el.heroDotHeight = Math.round(v); rerender(); })));
    navSec.appendChild(makeRow('Radius', makeInput(el.heroDotRadius, v => { el.heroDotRadius = v; rerender(); }, '50% / 2px')));
    navSec.appendChild(makeRow('Gap', makeNumInput(el.heroDotGap, 0, 60, 1, v => { el.heroDotGap = Math.round(v); rerender(); })));
    navSec.appendChild(makeRow('From Bottom', makeNumInput(el.heroDotBottom, -100, 400, 1, v => { el.heroDotBottom = Math.round(v); rerender(); })));
    navSec.appendChild(makeRow('Colour', makeColorCombo(el.heroDotColor, v => { el.heroDotColor = v; rerender(); })));
    navSec.appendChild(makeRow('Active Col', makeColorCombo(el.heroDotActiveColor, v => { el.heroDotActiveColor = v; rerender(); })));
    navSec.appendChild(makeRow('Active W', makeInput(el.heroDotActiveWidth, v => { el.heroDotActiveWidth = v; rerender(); }, 'same / 28')));
  }
  navSec.appendChild(makeRow('Arrows', makeCheck(el.heroArrows, v => { el.heroArrows = v; rebuild(); })));
  if (el.heroArrows) {
    navSec.appendChild(makeRow('Size', makeNumInput(el.heroArrowSize, 16, 120, 1, v => { el.heroArrowSize = Math.round(v); rerender(); })));
    navSec.appendChild(makeRow('Radius', makeInput(el.heroArrowRadius, v => { el.heroArrowRadius = v; rerender(); }, '50% / 8px')));
    navSec.appendChild(makeRow('Inset', makeNumInput(el.heroArrowInset, -100, 400, 1, v => { el.heroArrowInset = Math.round(v); rerender(); })));
    navSec.appendChild(makeRow('Bg', makeColorCombo(el.heroArrowBg, v => { el.heroArrowBg = v; rerender(); })));
    navSec.appendChild(makeRow('Icon Col', makeColorCombo(el.heroArrowColor, v => { el.heroArrowColor = v; rerender(); })));
    navSec.appendChild(makeRow('Icon Size', makeSlider(el.heroArrowIconSize, 0.3, 3, 0.05, v => { el.heroArrowIconSize = v; rerender(); })));
    navSec.appendChild(makeRow('Prev Icon', makeInput(el.heroPrevIcon, v => { el.heroPrevIcon = v; rerender(); }, 'bi-chevron-left')));
    navSec.appendChild(makeRow('Next Icon', makeInput(el.heroNextIcon, v => { el.heroNextIcon = v; rerender(); }, 'bi-chevron-right')));
  }
  ppStyle.appendChild(navSec);
}

function buildCarouselProps(el) {
  if (!Array.isArray(el.carouselItems)) el.carouselItems = [];
  const rerender = () => renderElement(el);
  const rebuild = () => { renderElement(el); buildPropsPanel(el); };

  // ── Items ──
  const itemsSec = makeSection('Carousel Items', 'bi-collection');
  const hint = document.createElement('div');
  hint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
  hint.textContent = 'Each item reuses the design below — fill in its image, title and text.';
  itemsSec.appendChild(hint);

  const list = document.createElement('div');
  const openSet = window._carOpenItems || (window._carOpenItems = new Set());

  el.carouselItems.forEach((item, idx) => {
    if (!item.id) item.id = 'ci_' + Math.random().toString(36).slice(2, 8);
    const card = document.createElement('div');
    card.className = 'car-ed-item' + (openSet.has(item.id) ? ' open' : '');

    const head = document.createElement('div');
    head.className = 'car-ed-head';
    const thumb = document.createElement(item.image ? 'img' : 'div');
    thumb.className = 'car-ed-thumb';
    if (item.image) thumb.src = item.image; else thumb.innerHTML = '<i class="bi bi-image"></i>';
    const nameEl = document.createElement('span');
    nameEl.className = 'car-ed-name';
    nameEl.textContent = item.title || item.text || ('Item ' + (idx + 1));
    const lay = item.layer || (item.model ? 'model' : (item.foreground ? 'image' : 'none'));
    if (lay !== 'none') {
      const lb = document.createElement('span');
      lb.style.cssText = 'font-size:0.4rem;color:var(--accent2);background:rgba(167,139,250,0.12);border-radius:6px;padding:1px 5px;margin-left:4px;';
      lb.textContent = lay === 'model' ? '3D' : 'IMG';
      lb.title = lay === 'model' ? 'A 3D model sits on top of this card' : 'An overlay image sits on top of this card';
      nameEl.appendChild(lb);
    }
    if (item.actions && item.actions.length) {
      const tb = document.createElement('span');
      tb.style.cssText = 'font-size:0.4rem;color:var(--accent);background:rgba(0,229,160,0.12);border-radius:6px;padding:1px 5px;';
      tb.textContent = item.actions.length + ' ⚡';
      tb.title = item.actions.length + ' trigger(s) on this item';
      nameEl.appendChild(tb);
    }
    head.appendChild(thumb);
    head.appendChild(nameEl);

    const mkBtn = (icon, cls, title, fn) => {
      const b = document.createElement('button');
      b.className = 'car-ed-btn ' + (cls || '');
      b.title = title;
      b.innerHTML = '<i class="bi ' + icon + '"></i>';
      b.onclick = e => { e.stopPropagation(); fn(); };
      return b;
    };
    head.appendChild(mkBtn('bi-chevron-up', '', 'Move up', () => {
      if (idx > 0) { const a = el.carouselItems; [a[idx-1], a[idx]] = [a[idx], a[idx-1]]; rebuild(); }
    }));
    head.appendChild(mkBtn('bi-chevron-down', '', 'Move down', () => {
      const a = el.carouselItems;
      if (idx < a.length - 1) { [a[idx+1], a[idx]] = [a[idx], a[idx+1]]; rebuild(); }
    }));
    head.appendChild(mkBtn('bi-files', '', 'Duplicate', () => {
      const copy = JSON.parse(JSON.stringify(el.carouselItems[idx]));
      copy.id = 'ci_' + Math.random().toString(36).slice(2, 8);
      el.carouselItems.splice(idx + 1, 0, copy); rebuild();
    }));
    head.appendChild(mkBtn('bi-trash', 'del', 'Delete', () => {
      el.carouselItems.splice(idx, 1); rebuild();
    }));
    head.onclick = () => {
      if (openSet.has(item.id)) openSet.delete(item.id); else openSet.add(item.id);
      card.classList.toggle('open');
    };

    const body = document.createElement('div');
    body.className = 'car-ed-body';
    body.appendChild(makeRow('Image', makeImageInput(item.image || '', v => { item.image = v; rebuild(); })));
    body.appendChild(makeRow('Title', makeInput(item.title || '', v => { item.title = v; rebuild(); }, 'Item title')));
    body.appendChild(makeRow('Text', makeInput(item.text || '', v => { item.text = v; rebuild(); }, 'Caption / description')));
    // What goes on top of the card image
    const curLayer = item.layer || (item.model ? 'model' : (item.foreground ? 'image' : 'none'));
    const layerLbl = document.createElement('div');
    layerLbl.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent);margin:8px 0 4px;text-transform:uppercase;';
    layerLbl.textContent = '● On Top Of The Image';
    body.appendChild(layerLbl);
    body.appendChild(makeRow('Layer', makeSelect(curLayer, ['none','model','image'], v => { item.layer = v; rebuild(); })));

    if (curLayer === 'model') {
      body.appendChild(makeRow('GLB File', makeInput(item.model || '', v => { item.model = v; rebuild(); }, './models/thing.glb')));
      const spinVal = (item.modelAutoRotate === undefined || item.modelAutoRotate === '') ? 'inherit' : (item.modelAutoRotate ? 'on' : 'off');
      body.appendChild(makeRow('Auto Rotate', makeSelect(spinVal, ['inherit','on','off'], v => {
        item.modelAutoRotate = v === 'inherit' ? '' : (v === 'on');
        rebuild();
      })));
      body.appendChild(makeRow('Scale', makeNumInput(item.modelScale !== undefined ? item.modelScale : 1, 0.05, 8, 0.05, v => { item.modelScale = v; rebuild(); })));
      body.appendChild(makeRow('Zoom (dist)', makeNumInput(item.modelDistance !== undefined ? item.modelDistance : (el.carModelDistance || 4), 0.5, 30, 0.1, v => { item.modelDistance = v; rebuild(); })));
      body.appendChild(makeRow('Rot X°', makeNumInput(item.modelRotX || 0, -360, 360, 5, v => { item.modelRotX = v; rebuild(); })));
      body.appendChild(makeRow('Rot Y°', makeNumInput(item.modelRotY || 0, -360, 360, 5, v => { item.modelRotY = v; rebuild(); })));
      body.appendChild(makeRow('Rot Z°', makeNumInput(item.modelRotZ || 0, -360, 360, 5, v => { item.modelRotZ = v; rebuild(); })));
      body.appendChild(makeRow('Shift X', makeNumInput(item.modelX || 0, -10, 10, 0.05, v => { item.modelX = v; rebuild(); })));
      body.appendChild(makeRow('Shift Y', makeNumInput(item.modelY || 0, -10, 10, 0.05, v => { item.modelY = v; rebuild(); })));
      body.appendChild(makeRow('Shift Z', makeNumInput(item.modelZ || 0, -10, 10, 0.05, v => { item.modelZ = v; rebuild(); })));
      body.appendChild(makeRow('Margin', makeInput(item.modelInset !== undefined ? item.modelInset : '', v => { item.modelInset = v; rebuild(); }, 'inherit / 12')));
      const zh = document.createElement('div');
      zh.style.cssText = 'font-size:0.4rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
      zh.textContent = 'Smaller Zoom = camera closer = model looks bigger. Auto Rotate: inherit follows the 3D Cards setting. Active/side poses override the rotations when set.';
      body.appendChild(zh);
    } else if (curLayer === 'image') {
      body.appendChild(makeRow('Overlay Img', makeImageInput(item.foreground || '', v => { item.foreground = v; rebuild(); })));
      body.appendChild(makeRow('Margin', makeInput(item.fgInset !== undefined ? item.fgInset : '', v => { item.fgInset = v; rebuild(); }, 'inherit / 12 / 8%')));
      body.appendChild(makeRow('Padding', makeInput(item.fgPadding !== undefined ? item.fgPadding : '', v => { item.fgPadding = v; rebuild(); }, 'inherit / 16')));
      body.appendChild(makeRow('Width', makeInput(item.fgWidth !== undefined ? item.fgWidth : '', v => { item.fgWidth = v; rebuild(); }, 'inherit / 70% / 180px')));
      body.appendChild(makeRow('Height', makeInput(item.fgHeight !== undefined ? item.fgHeight : '', v => { item.fgHeight = v; rebuild(); }, 'inherit / auto')));
      body.appendChild(makeRow('Fit', makeSelect(item.fgFit || 'inherit', ['inherit','contain','cover','stretch','none'], v => { item.fgFit = v === 'inherit' ? '' : v; rebuild(); })));
      const fh = document.createElement('div');
      fh.style.cssText = 'font-size:0.4rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
      fh.textContent = 'Blank = use the shared values in the Overlay Image section of the Style tab.';
      body.appendChild(fh);
    }
    body.appendChild(makeRow('Link', makeInput(item.link || '', v => { item.link = v; rerender(); }, 'https://... (used by Item Click)')));

    // ── Extra fields (feed linked elements: headings, paragraphs, big images…) ──
    if (!item.fields || typeof item.fields !== 'object') item.fields = {};
    const fldLabel = document.createElement('div');
    fldLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
    fldLabel.textContent = '● Extra Fields';
    body.appendChild(fldLabel);
    Object.keys(item.fields).forEach(key => {
      const r = document.createElement('div');
      r.className = 'pp-row';
      const kIn = document.createElement('input');
      kIn.className = 'pp-input'; kIn.value = key; kIn.style.cssText = 'flex:0 0 70px;font-size:0.5rem;';
      kIn.title = 'Field name';
      kIn.onchange = () => {
        const nk = kIn.value.trim();
        if (!nk || nk === key) { kIn.value = key; return; }
        item.fields[nk] = item.fields[key]; delete item.fields[key]; rebuild();
      };
      const vIn = makeInput(item.fields[key], v => { item.fields[key] = v; rerender(); }, 'value');
      const del = document.createElement('button');
      del.className = 'car-ed-btn del'; del.innerHTML = '<i class="bi bi-x"></i>'; del.title = 'Remove field';
      del.onclick = () => { delete item.fields[key]; rebuild(); };
      r.appendChild(kIn); r.appendChild(vIn); r.appendChild(del);
      body.appendChild(r);
    });
    const addFld = document.createElement('button');
    addFld.className = 'pp-btn';
    addFld.style.cssText = 'width:100%;margin-top:2px;background:rgba(167,139,250,0.06);border-color:var(--accent2);color:var(--accent2);font-size:0.5rem;padding:3px 6px;';
    addFld.innerHTML = '<i class="bi bi-plus-lg"></i> Add field';
    addFld.onclick = () => {
      let n = 1; while (item.fields['field' + n] !== undefined) n++;
      item.fields['field' + n] = ''; rebuild();
    };
    body.appendChild(addFld);

    // ── Triggers: what happens to other elements when this item is active ──
    if (!Array.isArray(item.actions)) item.actions = [];
    const trgLabel = document.createElement('div');
    trgLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent);margin:10px 0 4px;text-transform:uppercase;';
    trgLabel.textContent = '● When This Item Is Active';
    body.appendChild(trgLabel);

    const ACT_OPTS = [['setText','Set text'],['setHTML','Set HTML'],['setImage','Set image src'],
      ['setBackground','Set background'],['setVideo','Set video src'],['setColor','Set text colour'],
      ['show','Show'],['hide','Hide']];
    const FIELD_OPTS = [['','(type a value)'],['title','Item title'],['text','Item text'],
      ['image','Item image'],['link','Item link'],['index','Item number']];

    item.actions.forEach((act, ai) => {
      const aCard = document.createElement('div');
      aCard.style.cssText = 'border:1px solid var(--border-color);border-radius:6px;padding:5px 6px;margin-bottom:4px;background:rgba(0,229,160,0.03);';

      const aHead = document.createElement('div');
      aHead.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:3px;';
      const aNum = document.createElement('span');
      aNum.style.cssText = 'font-size:0.4rem;letter-spacing:1px;color:var(--accent);font-family:"Orbitron",sans-serif;';
      aNum.textContent = 'T' + (ai + 1);
      const aDel = document.createElement('button');
      aDel.className = 'car-ed-btn del'; aDel.style.marginLeft = 'auto';
      aDel.innerHTML = '<i class="bi bi-x"></i>'; aDel.title = 'Remove trigger';
      aDel.onclick = e => { e.stopPropagation(); item.actions.splice(ai, 1); rebuild(); };
      aHead.appendChild(aNum); aHead.appendChild(aDel);
      aCard.appendChild(aHead);

      // target element
      const tSel = document.createElement('select'); tSel.className = 'pp-select';
      const tNone = document.createElement('option');
      tNone.value = ''; tNone.textContent = '(choose element)';
      if (!act.targetId) tNone.selected = true;
      tSel.appendChild(tNone);
      elements.filter(o => o.id !== el.id).forEach(o => {
        const op = document.createElement('option');
        op.value = o.id; op.textContent = (o.name || o.type) + ' — ' + o.id;
        if (o.id === act.targetId) op.selected = true;
        tSel.appendChild(op);
      });
      tSel.onchange = () => { act.targetId = tSel.value; rerender(); };
      aCard.appendChild(makeRow('Element', tSel));

      // action
      const aSel = document.createElement('select'); aSel.className = 'pp-select';
      ACT_OPTS.forEach(([v, t]) => {
        const op = document.createElement('option');
        op.value = v; op.textContent = t;
        if (v === (act.action || 'setText')) op.selected = true;
        aSel.appendChild(op);
      });
      aSel.onchange = () => { act.action = aSel.value; rebuild(); };
      aCard.appendChild(makeRow('Do', aSel));

      // value (not needed for show/hide)
      if (act.action !== 'show' && act.action !== 'hide') {
        const fSel = document.createElement('select'); fSel.className = 'pp-select';
        FIELD_OPTS.concat(Object.keys(item.fields || {}).map(k => ['field:' + k, 'Field: ' + k]))
          .forEach(([v, t]) => {
            const op = document.createElement('option');
            op.value = v; op.textContent = t;
            if (v === (act.useField || '')) op.selected = true;
            fSel.appendChild(op);
          });
        fSel.onchange = () => { act.useField = fSel.value; rebuild(); };
        aCard.appendChild(makeRow('Value', fSel));
        if (!act.useField) {
          aCard.appendChild(makeRow('Text', makeInput(act.value || '', v => { act.value = v; rerender(); }, 'value to apply')));
        }
      }
      body.appendChild(aCard);
    });

    const addTrg = document.createElement('button');
    addTrg.className = 'pp-btn';
    addTrg.style.cssText = 'width:100%;margin-top:2px;background:rgba(0,229,160,0.06);border-color:var(--accent);color:var(--accent);font-size:0.5rem;padding:3px 6px;';
    addTrg.innerHTML = '<i class="bi bi-plus-lg"></i> Add trigger';
    addTrg.onclick = () => { item.actions.push({ targetId:'', action:'setText', useField:'title', value:'' }); rebuild(); };
    body.appendChild(addTrg);

    card.appendChild(head);
    card.appendChild(body);
    list.appendChild(card);
  });
  itemsSec.appendChild(list);

  const addBtn = document.createElement('button');
  addBtn.className = 'car-ed-add';
  addBtn.innerHTML = '<i class="bi bi-plus-lg"></i> Add Item';
  addBtn.onclick = () => {
    const id = 'ci_' + Math.random().toString(36).slice(2, 8);
    el.carouselItems.push({ id, image: '', title: 'Item ' + (el.carouselItems.length + 1), text: '', link: '' });
    openSet.add(id);
    rebuild();
  };
  itemsSec.appendChild(addBtn);
  ppProps.appendChild(itemsSec);

  // ── What each card shows ──
  const shown = makeSection('Item Content', 'bi-card-heading');
  const shownHint = document.createElement('div');
  shownHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
  shownHint.textContent = 'Applies to every card, including the active one.';
  shown.appendChild(shownHint);
  shown.appendChild(makeRow('Image', makeCheck(el.carShowImage, v => { el.carShowImage = v; rerender(); })));
  shown.appendChild(makeRow('Title', makeCheck(el.carShowTitle, v => { el.carShowTitle = v; rerender(); })));
  shown.appendChild(makeRow('Text', makeCheck(el.carShowText, v => { el.carShowText = v; rerender(); })));

  // Extra fields → show inside the card
  if (!Array.isArray(el.carShowFields)) el.carShowFields = [];
  const allKeys = new Set();
  el.carouselItems.forEach(it => { if (it.fields) Object.keys(it.fields).forEach(k => allKeys.add(k)); });
  const fldTitle = document.createElement('div');
  fldTitle.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
  fldTitle.textContent = '● Extra Fields In Card';
  shown.appendChild(fldTitle);
  if (allKeys.size === 0) {
    const none = document.createElement('div');
    none.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    none.textContent = 'Add fields to an item above, then tick them here to show them in every card.';
    shown.appendChild(none);
  } else {
    const chips = document.createElement('div');
    chips.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;padding:2px 0 4px;';
    allKeys.forEach(k => {
      const on = el.carShowFields.includes(k);
      const lbl = document.createElement('label');
      lbl.style.cssText = 'display:flex;align-items:center;gap:3px;font-size:0.5rem;color:var(--text-secondary);cursor:pointer;padding:2px 6px;border:1px solid var(--border-color);border-radius:4px;background:' + (on ? 'rgba(167,139,250,0.12)' : 'transparent');
      const cb = document.createElement('input');
      cb.type = 'checkbox'; cb.checked = on;
      cb.onchange = () => {
        if (cb.checked) { if (!el.carShowFields.includes(k)) el.carShowFields.push(k); }
        else el.carShowFields = el.carShowFields.filter(x => x !== k);
        rebuild();
      };
      lbl.appendChild(cb); lbl.appendChild(document.createTextNode(k));
      chips.appendChild(lbl);
    });
    shown.appendChild(chips);
    const tplNote = document.createElement('div');
    tplNote.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    tplNote.textContent = 'In a custom item HTML template you can also place them anywhere as {fieldName}.';
    shown.appendChild(tplNote);
  }
  ppProps.appendChild(shown);

  // ── Layout ──
  const lay = makeSection('Carousel Layout', 'bi-columns-gap');
  lay.appendChild(makeRow('Visible', makeNumInput(el.carItems, 1, 12, 1, v => { el.carItems = Math.max(1, Math.round(v)); rerender(); })));
  lay.appendChild(makeRow('On Tablet', makeNumInput(el.carItemsTablet, 1, 12, 1, v => { el.carItemsTablet = Math.max(1, Math.round(v)); rerender(); })));
  lay.appendChild(makeRow('On Phone', makeNumInput(el.carItemsPhone, 1, 12, 1, v => { el.carItemsPhone = Math.max(1, Math.round(v)); rerender(); })));
  lay.appendChild(makeRow('On Landscape', makeNumInput(el.carItemsPhoneLand === '' ? (parseInt(el.carItemsTablet) || 2) : el.carItemsPhoneLand, 1, 12, 1,
    v => { el.carItemsPhoneLand = Math.max(1, Math.round(v)); rerender(); })));
  const landHint = document.createElement('div');
  landHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  landHint.textContent = 'Landscape = phones held sideways (≤768px wide). Blank falls back to the tablet count.';
  lay.appendChild(landHint);
  lay.appendChild(makeRow('Gap (px)', makeNumInput(el.carMargin, 0, 100, 1, v => { el.carMargin = Math.round(v); rerender(); })));
  lay.appendChild(makeRow('Active Gap', makeNumInput(el.carSideGap, -200, 300, 2, v => { el.carSideGap = Math.round(v); rerender(); })));
  lay.appendChild(makeRow('Gap Stacks', makeCheck(el.carSideGapStacks, v => { el.carSideGapStacks = v; rerender(); })));
  const gapHint = document.createElement('div');
  gapHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  gapHint.textContent = 'Gap spaces every card evenly. Active Gap pushes the side cards further away from the active one — with Gap Stacks on, the second neighbour moves twice as far.';
  lay.appendChild(gapHint);
  lay.appendChild(makeRow('Stage Pad', makeNumInput(el.carStagePadding, 0, 200, 5, v => { el.carStagePadding = Math.round(v); rerender(); })));
  lay.appendChild(makeRow('Auto Width', makeCheck(el.carAutoWidth, v => { el.carAutoWidth = v; rerender(); })));
  lay.appendChild(makeRow('Start At', makeNumInput(el.carStart, 0, 99, 1, v => { el.carStart = Math.max(0, Math.round(v)); rerender(); })));
  ppProps.appendChild(lay);

  // ── Active item emphasis ──
  const ctr = makeSection('Active Item', 'bi-bullseye');
  const devBanner = document.createElement('div');
  devBanner.style.cssText = 'font-size:0.42rem;padding:4px 6px;margin-bottom:6px;border-radius:5px;line-height:1.35;' +
    (deviceMode === 'desktop'
      ? 'color:var(--text-muted);background:rgba(255,255,255,0.03);'
      : 'color:var(--accent2);background:rgba(167,139,250,0.1);border:1px solid rgba(167,139,250,0.3);');
  devBanner.textContent = deviceMode === 'desktop'
    ? 'Editing the desktop values. Switch device at the top to give tablet, phone or landscape their own active-item settings.'
    : 'Editing ' + deviceMode.toUpperCase() + ' — changes here only apply on ' + deviceMode + '. Values without a dot are inherited from desktop.';
  ctr.appendChild(devBanner);

  ctr.appendChild(makeRow('Center Mode', makeCheck(el.carCenter, v => { el.carCenter = v; rerender(); })));
  const perView = parseInt(deviceMode === 'phone' ? (el.carItemsPhone || 1)
    : deviceMode === 'tablet' ? (el.carItemsTablet || el.carItems)
    : deviceMode === 'phone-land' ? (el.carItemsPhoneLand || el.carItemsTablet || el.carItems)
    : el.carItems) || 3;
  const slotOpts = ['auto'];
  for (let i = 0; i < perView; i++) slotOpts.push(String(i));
  const slotVal = carGet(el, 'carActiveSlot');
  ctr.appendChild(carOvRow(el, 'Active Slot', 'carActiveSlot', makeSelect(
    (slotVal === '' || slotVal === undefined) ? 'auto' : String(slotVal),
    slotOpts,
    v => { carSet(el, 'carActiveSlot', v === 'auto' ? '' : parseInt(v)); buildPropsPanel(el); }
  )));
  const slotHint = document.createElement('div');
  slotHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  if (slotVal === '' || slotVal === undefined) {
    slotHint.textContent = 'auto = the active card sits dead centre. Pick a slot number for an off-balance layout (0 = leftmost).';
  } else {
    const left = parseInt(slotVal);
    const right = perView - left - 1;
    slotHint.textContent = 'Active card in slot ' + left + ' → ' + left + ' card' + (left === 1 ? '' : 's') +
      ' on the left, ' + right + ' on the right (' + perView + ' visible on ' + deviceMode + ').';
    slotHint.style.color = 'var(--accent)';
  }
  ctr.appendChild(slotHint);

  ctr.appendChild(carOvRow(el, 'Active Scale', 'carCenterScale', makeSlider(carGet(el,'carCenterScale'), 0.8, 2.5, 0.02, v => carSet(el,'carCenterScale',v))));
  ctr.appendChild(carOvRow(el, 'Active W', 'carCenterWidth', makeInput(carGet(el,'carCenterWidth'), v => carSet(el,'carCenterWidth',v), 'auto / 240px / 110%')));
  ctr.appendChild(carOvRow(el, 'Active H', 'carCenterHeight', makeInput(carGet(el,'carCenterHeight'), v => carSet(el,'carCenterHeight',v), 'auto / 220px')));
  ctr.appendChild(carOvRow(el, 'Offset X', 'carCenterOffsetX', makeNumInput(carGet(el,'carCenterOffsetX'), -300, 300, 1, v => carSet(el,'carCenterOffsetX',Math.round(v)))));
  ctr.appendChild(carOvRow(el, 'Offset Y', 'carCenterOffsetY', makeNumInput(carGet(el,'carCenterOffsetY'), -300, 300, 1, v => carSet(el,'carCenterOffsetY',Math.round(v)))));
  ctr.appendChild(carOvRow(el, 'Side Scale', 'carSideScale', makeSlider(carGet(el,'carSideScale'), 0.4, 1.2, 0.02, v => carSet(el,'carSideScale',v))));
  ctr.appendChild(carOvRow(el, 'Side Opacity', 'carSideOpacity', makeSlider(carGet(el,'carSideOpacity'), 0.1, 1, 0.05, v => carSet(el,'carSideOpacity',v))));
  ctr.appendChild(makeRow('Click→Center', makeCheck(el.carClickToCenter, v => { el.carClickToCenter = v; rerender(); })));
  ctr.appendChild(carOvRow(el, 'Space Above', 'carPadTop', makeNumInput(carGet(el,'carPadTop'), 0, 500, 2, v => carSet(el,'carPadTop',Math.round(v)))));
  ctr.appendChild(carOvRow(el, 'Space Below', 'carPadBottom', makeNumInput(carGet(el,'carPadBottom'), 0, 500, 2, v => carSet(el,'carPadBottom',Math.round(v)))));
  const padHint = document.createElement('div');
  padHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  padHint.textContent = 'Extra room inside the strip so a scaled or offset active card is never clipped — increase Space Below if its bottom edge is being cut.';
  ctr.appendChild(padHint);
  ctr.appendChild(makeRow('Active Layer', makeNumInput(el.carActiveZ, 1, 999, 1, v => { el.carActiveZ = Math.round(v); rerender(); })));
  const zHint = document.createElement('div');
  zHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  zHint.textContent = 'Stacking order of the active card against its neighbours (the element\'s own z-index only affects the whole carousel).';
  ctr.appendChild(zHint);

  const ovLabel = document.createElement('div');
  ovLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
  ovLabel.textContent = '● Active Card Box';
  ctr.appendChild(ovLabel);
  ctr.appendChild(carOvRow(el, 'Padding', 'carCenterPadding', makeInput(carGet(el,'carCenterPadding'), v => carSet(el,'carCenterPadding',v), 'inherit / 16px')));
  ctr.appendChild(carOvRow(el, 'Margin', 'carCenterMargin', makeInput(carGet(el,'carCenterMargin'), v => carSet(el,'carCenterMargin',v), 'inherit / 0 8px')));
  ctr.appendChild(carOvRow(el, 'Radius', 'carCenterRadius', makeInput(carGet(el,'carCenterRadius'), v => carSet(el,'carCenterRadius',v), 'inherit / 18px')));
  ctr.appendChild(makeRow('Bg', makeColorCombo(el.carCenterBg, v => { el.carCenterBg = v; rerender(); })));
  ctr.appendChild(makeRow('Border', makeInput(el.carCenterBorder, v => { el.carCenterBorder = v; rerender(); }, '2px solid #00e5a0')));
  ctr.appendChild(makeRow('Shadow', makeInput(el.carCenterShadow, v => { el.carCenterShadow = v; rerender(); }, '0 12px 34px #0009')));
  const ovHint = document.createElement('div');
  ovHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
  ovHint.textContent = 'Blank = use the shared Item Design values. Bg / Border / Shadow are the same on every device.';
  ctr.appendChild(ovHint);

  // Summary of which devices carry their own settings
  const devs = ['tablet','phone','phone-land'].filter(dv => {
    const o = (el.carDeviceOverrides || {})[dv] || {};
    return Object.keys(o).some(k => o[k] !== undefined && o[k] !== null);
  });
  if (devs.length) {
    const sum = document.createElement('div');
    sum.style.cssText = 'font-size:0.42rem;color:var(--accent2);padding:4px 0 0;line-height:1.35;';
    sum.textContent = 'Custom active-item settings saved for: ' + devs.join(', ') + '.';
    ctr.appendChild(sum);
  }
  ppStyle.appendChild(ctr);

  // ── Behaviour ──
  const beh = makeSection('Carousel Behaviour', 'bi-arrow-repeat');
  beh.appendChild(makeRow('Loop', makeCheck(el.carLoop, v => { el.carLoop = v; rerender(); })));
  beh.appendChild(makeRow('Autoplay', makeCheck(el.carAutoplay, v => { el.carAutoplay = v; rebuild(); })));
  if (el.carAutoplay) {
    beh.appendChild(makeRow('Interval', makeNumInput(el.carAutoplayTimeout, 500, 20000, 250, v => { el.carAutoplayTimeout = Math.round(v); rerender(); })));
    beh.appendChild(makeRow('Pause Hover', makeCheck(el.carAutoplayHoverPause, v => { el.carAutoplayHoverPause = v; rerender(); })));
  }
  beh.appendChild(makeRow('Speed (ms)', makeNumInput(el.carSpeed, 100, 3000, 50, v => { el.carSpeed = Math.round(v); rerender(); })));
  beh.appendChild(makeRow('Drag', makeCheck(el.carMouseDrag, v => { el.carMouseDrag = v; rerender(); })));
  const behHint = document.createElement('div');
  behHint.style.cssText = 'font-size:0.42rem;color:var(--accent3);padding:2px 0;line-height:1.35;';
  behHint.textContent = 'Autoplay and dragging only run in Preview / the player — the canvas stays still while you design.';
  beh.appendChild(behHint);
  ppProps.appendChild(beh);

  // ── Navigation ──
  const nav = makeSection('Arrows', 'bi-chevron-bar-right');
  nav.appendChild(makeRow('Show', makeCheck(el.carNav, v => { el.carNav = v; rerender(); })));
  nav.appendChild(makeRow('Size (px)', makeNumInput(el.carNavSize, 12, 96, 1, v => { el.carNavSize = Math.round(v); rerender(); })));
  nav.appendChild(makeRow('Shape', makeSelect(navShapeOf(el), ['circle','rounded','square','pill'], v => { el.carNavRadius = NAV_SHAPES[v]; rerender(); })));
  nav.appendChild(makeRow('Radius', makeInput(el.carNavRadius, v => { el.carNavRadius = v; rerender(); }, '50% / 8px')));
  nav.appendChild(makeRow('Icon Size', makeSlider(el.carNavIconSize, 0.3, 2.5, 0.05, v => { el.carNavIconSize = v; rerender(); })));
  nav.appendChild(makeRow('Prev Icon', makeInput(el.carNavPrevIcon, v => { el.carNavPrevIcon = v; rerender(); }, 'bi-chevron-left')));
  nav.appendChild(makeRow('Next Icon', makeInput(el.carNavNextIcon, v => { el.carNavNextIcon = v; rerender(); }, 'bi-chevron-right')));
  nav.appendChild(makeRow('Icon Col', makeColorCombo(el.carNavColor, v => { el.carNavColor = v; rerender(); })));
  nav.appendChild(makeRow('Bg', makeColorCombo(el.carNavBg, v => { el.carNavBg = v; rerender(); })));
  nav.appendChild(makeRow('Border', makeInput(el.carNavBorder, v => { el.carNavBorder = v; rerender(); }, 'none / 1px solid #fff3')));
  const posLabel = document.createElement('div');
  posLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
  posLabel.textContent = '● Placement' + (deviceMode === 'desktop' ? '' : ' — ' + deviceMode.toUpperCase());
  nav.appendChild(posLabel);
  nav.appendChild(carOvRow(el, 'Inset (px)', 'carNavInset', makeNumInput(carGet(el,'carNavInset'), -200, 300, 1,
    v => { carSet(el, 'carNavInset', Math.round(v)); if (deviceMode === 'desktop') el.carNavOffset = el.carNavInset; })));
  nav.appendChild(carOvRow(el, 'Both X', 'carNavOffsetX', makeNumInput(carGet(el,'carNavOffsetX'), -600, 600, 1, v => carSet(el,'carNavOffsetX',Math.round(v)))));
  nav.appendChild(carOvRow(el, 'Both Y', 'carNavOffsetY', makeNumInput(carGet(el,'carNavOffsetY'), -600, 600, 1, v => carSet(el,'carNavOffsetY',Math.round(v)))));
  const lrLabel = document.createElement('div');
  lrLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent);margin:8px 0 4px;text-transform:uppercase;';
  lrLabel.textContent = '◀ Left Arrow';
  nav.appendChild(lrLabel);
  nav.appendChild(carOvRow(el, 'Left X', 'carNavPrevX', makeNumInput(carGet(el,'carNavPrevX'), -600, 600, 1, v => carSet(el,'carNavPrevX',Math.round(v)))));
  nav.appendChild(carOvRow(el, 'Left Y', 'carNavPrevY', makeNumInput(carGet(el,'carNavPrevY'), -600, 600, 1, v => carSet(el,'carNavPrevY',Math.round(v)))));
  const rrLabel = document.createElement('div');
  rrLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent);margin:8px 0 4px;text-transform:uppercase;';
  rrLabel.textContent = 'Right Arrow ▶';
  nav.appendChild(rrLabel);
  nav.appendChild(carOvRow(el, 'Right X', 'carNavNextX', makeNumInput(carGet(el,'carNavNextX'), -600, 600, 1, v => carSet(el,'carNavNextX',Math.round(v)))));
  nav.appendChild(carOvRow(el, 'Right Y', 'carNavNextY', makeNumInput(carGet(el,'carNavNextY'), -600, 600, 1, v => carSet(el,'carNavNextY',Math.round(v)))));
  const navHint = document.createElement('div');
  navHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
  navHint.textContent = 'Icons use Bootstrap Icon names (bi-arrow-left, bi-caret-right-fill…). Inset pulls both arrows toward the middle (negative pushes them out), Both X/Y shifts the pair, and Left/Right X/Y move each arrow on its own. Placement is saved per device.';
  nav.appendChild(navHint);
  ppStyle.appendChild(nav);

  const dots = makeSection('Dots', 'bi-three-dots');
  dots.appendChild(makeRow('Show', makeCheck(el.carDots, v => { el.carDots = v; rerender(); })));
  dots.appendChild(makeRow('Position', makeSelect(el.carDotPosition || 'below', ['below','above','overlay'], v => { el.carDotPosition = v; buildPropsPanel(el); renderElement(el); })));
  if (el.carDotPosition === 'overlay') {
    dots.appendChild(carOvRow(el, 'From Bottom', 'carDotBottom', makeNumInput(carGet(el,'carDotBottom'), -200, 400, 1, v => carSet(el,'carDotBottom',Math.round(v)))));
  }
  const dotPosHint = document.createElement('div');
  dotPosHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  dotPosHint.textContent = 'overlay puts the dots on top of the cards — useful when the carousel fills the whole screen and dots would fall off the bottom.';
  dots.appendChild(dotPosHint);
  dots.appendChild(makeRow('Shape', makeSelect(dotShapeOf(el), ['circle','rounded','square','bar'], v => {
    el.carDotRadius = DOT_SHAPES[v];
    if (v === 'bar') { el.carDotWidth = 18; el.carDotHeight = 5; }
    rebuild();
  })));
  dots.appendChild(makeRow('Width', makeNumInput(el.carDotWidth, 2, 60, 1, v => { el.carDotWidth = Math.round(v); rerender(); })));
  dots.appendChild(makeRow('Height', makeNumInput(el.carDotHeight, 2, 60, 1, v => { el.carDotHeight = Math.round(v); rerender(); })));
  dots.appendChild(makeRow('Radius', makeInput(el.carDotRadius, v => { el.carDotRadius = v; rerender(); }, '50% / 3px')));
  dots.appendChild(makeRow('Gap', makeNumInput(el.carDotGap, 0, 40, 1, v => { el.carDotGap = Math.round(v); rerender(); })));
  const dotPosLabel = document.createElement('div');
  dotPosLabel.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
  dotPosLabel.textContent = '● Placement' + (deviceMode === 'desktop' ? '' : ' — ' + deviceMode.toUpperCase());
  dots.appendChild(dotPosLabel);
  dots.appendChild(carOvRow(el, 'Top Space', 'carDotMarginTop', makeNumInput(carGet(el,'carDotMarginTop'), -200, 400, 1, v => carSet(el,'carDotMarginTop',Math.round(v)))));
  dots.appendChild(carOvRow(el, 'Move X', 'carDotOffsetX', makeNumInput(carGet(el,'carDotOffsetX'), -600, 600, 1, v => carSet(el,'carDotOffsetX',Math.round(v)))));
  dots.appendChild(carOvRow(el, 'Move Y', 'carDotOffsetY', makeNumInput(carGet(el,'carDotOffsetY'), -600, 600, 1, v => carSet(el,'carDotOffsetY',Math.round(v)))));
  dots.appendChild(makeRow('Colour', makeColorCombo(el.carDotColor, v => { el.carDotColor = v; rerender(); })));
  dots.appendChild(makeRow('Active Col', makeColorCombo(el.carDotActiveColor, v => { el.carDotActiveColor = v; rerender(); })));
  dots.appendChild(makeRow('Active W', makeInput(el.carDotActiveWidth, v => { el.carDotActiveWidth = v; rerender(); }, 'same / 20')));
  dots.appendChild(makeRow('Active H', makeInput(el.carDotActiveHeight, v => { el.carDotActiveHeight = v; rerender(); }, 'same / 8')));
  const dotHint = document.createElement('div');
  dotHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
  dotHint.textContent = 'Top Space adds room above the row; Move X/Y nudges it anywhere from there. Placement is saved per device — switch device at the top to give phones their own. Give the active dot a wider width for the classic "pill" indicator.';
  dots.appendChild(dotHint);
  ppStyle.appendChild(dots);

  // ── Item design (replicated across all items) ──
  const des = makeSection('Item Design', 'bi-palette');
  des.appendChild(makeRow('Item Width', makeInput(el.carItemWidth, v => { el.carItemWidth = v; rerender(); }, 'auto / 180px / 90%')));
  des.appendChild(makeRow('Item Height', makeInput(el.carItemHeight, v => { el.carItemHeight = v; rerender(); }, 'auto / 200px')));
  const sizeHint = document.createElement('div');
  sizeHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  sizeHint.textContent = 'Leave blank to let "Visible" items decide the width. The active item can override both in the Active Item section.';
  des.appendChild(sizeHint);
  des.appendChild(makeRow('Card Bg', makeColorCombo(el.carItemBg, v => { el.carItemBg = v; rerender(); })));
  des.appendChild(makeRow('Card Border', makeInput(el.carItemBorder, v => { el.carItemBorder = v; rerender(); }, '1px solid #fff2')));
  des.appendChild(makeRow('Radius', makeInput(el.carItemRadius, v => { el.carItemRadius = v; rerender(); }, '12px')));
  des.appendChild(makeRow('Padding', makeInput(el.carItemPadding, v => { el.carItemPadding = v; rerender(); }, '10px')));
  des.appendChild(makeRow('Margin', makeInput(el.carItemMargin, v => { el.carItemMargin = v; rerender(); }, '0 / 4px 6px')));
  des.appendChild(makeRow('Shadow', makeInput(el.carItemShadow, v => { el.carItemShadow = v; rerender(); }, '0 6px 20px #0006')));
  des.appendChild(makeRow('Align', makeSelect(el.carItemAlign, ['center','left','right'], v => { el.carItemAlign = v; rerender(); })));
  des.appendChild(makeRow('Image Mode', makeSelect(el.carImgMode || 'inline', ['inline','cover'], v => { el.carImgMode = v; buildPropsPanel(el); rerender(); })));
  const imgModeHint = document.createElement('div');
  imgModeHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
  imgModeHint.textContent = el.carImgMode === 'cover'
    ? 'cover = the image fills the whole card and the title/text sit on top of it.'
    : 'inline = the image sits above the text at its own height. Switch to cover to fill the card.';
  des.appendChild(imgModeHint);
  if (el.carImgMode === 'cover') {
    des.appendChild(makeRow('Text At', makeSelect(el.carCoverAlign || 'bottom', ['top','center','bottom'], v => { el.carCoverAlign = v; rerender(); })));
    des.appendChild(makeRow('Img Position', makeInput(el.carImgPosition, v => { el.carImgPosition = v; rerender(); }, 'center / top / 50% 20%')));
  } else {
    des.appendChild(makeRow('Img Height', makeInput(el.carImgHeight, v => { el.carImgHeight = v; rerender(); }, '110px / auto')));
  }
  des.appendChild(makeRow('Img Fit', makeSelect(el.carImgFit, ['cover','contain','fill','none'], v => { el.carImgFit = v; rerender(); })));
  des.appendChild(makeRow('Img Radius', makeInput(el.carImgRadius, v => { el.carImgRadius = v; rerender(); }, '8px')));
  const CAR_UNITS = ['rem','px','em','%','vw','vh','pt'];
  const sizeRow = (label, sizeKey, unitKey) => {
    const wrap = document.createElement('div');
    wrap.style.cssText = 'display:flex;gap:4px;flex:1;align-items:center;';
    const num = document.createElement('input');
    num.type = 'number'; num.className = 'pp-input'; num.style.cssText = 'flex:1;min-width:0;';
    num.step = (el[unitKey] === 'px' || el[unitKey] === 'pt') ? '1' : '0.05';
    num.value = el[sizeKey];
    num.onchange = () => { el[sizeKey] = parseFloat(num.value) || 0; rerender(); };
    const us = document.createElement('select');
    us.className = 'pp-select'; us.style.cssText = 'flex:0 0 54px;';
    CAR_UNITS.forEach(u => {
      const o = document.createElement('option');
      o.value = u; o.textContent = u;
      if (u === (el[unitKey] || 'rem')) o.selected = true;
      us.appendChild(o);
    });
    us.onchange = () => { el[unitKey] = us.value; buildPropsPanel(el); rerender(); };
    wrap.appendChild(num); wrap.appendChild(us);
    return makeRow(label, wrap);
  };
  des.appendChild(sizeRow('Title Size', 'carTitleSize', 'carTitleUnit'));
  des.appendChild(makeRow('Title Col', makeColorCombo(el.carTitleColor, v => { el.carTitleColor = v; rerender(); })));
  des.appendChild(makeRow('Title Wt', makeSelect(el.carTitleWeight, ['400','500','600','700'], v => { el.carTitleWeight = v; rerender(); })));
  des.appendChild(sizeRow('Text Size', 'carTextSize', 'carTextUnit'));
  des.appendChild(makeRow('Text Col', makeColorCombo(el.carTextColor, v => { el.carTextColor = v; rerender(); })));
  des.appendChild(sizeRow('Field Size', 'carFieldSize', 'carFieldUnit'));
  des.appendChild(makeRow('Field Col', makeColorCombo(el.carFieldColor, v => { el.carFieldColor = v; rerender(); })));
  ppStyle.appendChild(des);

  // ── Overlay images on the cards ──
  if ((el.carouselItems || []).some(it => it.foreground && (it.layer || 'image') !== 'model')) {
    const fg = makeSection('Overlay Image', 'bi-images');
    const fgHint = document.createElement('div');
    fgHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
    fgHint.textContent = 'Applies to every item whose layer is an image. Each item can still override the margin.';
    fg.appendChild(fgHint);
    fg.appendChild(makeRow('Margin', makeInput(el.carFgInset, v => { el.carFgInset = v; rebuild(); }, '12 / 8%')));
    fg.appendChild(makeRow('Padding', makeInput(el.carFgPadding, v => { el.carFgPadding = v; rebuild(); }, '0 / 16 / 10px 24px')));
    const mpHint = document.createElement('div');
    mpHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    mpHint.textContent = 'Margin insets the overlay area from the card edges; Padding adds space inside that area (CSS shorthand works: "10px 24px").';
    fg.appendChild(mpHint);
    fg.appendChild(makeRow('Width', makeInput(el.carFgWidth, v => { el.carFgWidth = v; rebuild(); }, '100% / 70% / 180px')));
    fg.appendChild(makeRow('Height', makeInput(el.carFgHeight, v => { el.carFgHeight = v; rebuild(); }, '100% / auto / 120px')));
    fg.appendChild(makeRow('Fit', makeSelect(el.carFgFit === 'fill' ? 'stretch' : el.carFgFit, ['contain','cover','stretch','none'], v => { el.carFgFit = v; rebuild(); })));
    const fitHint = document.createElement('div');
    fitHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    fitHint.textContent = 'contain = fits inside, keeps proportions · cover = fills and crops · stretch = distorts to fill · none = original size.';
    fg.appendChild(fitHint);
    fg.appendChild(makeRow('Align H', makeSelect(el.carFgAlignH, ['left','center','right'], v => { el.carFgAlignH = v; rebuild(); })));
    fg.appendChild(makeRow('Align V', makeSelect(el.carFgAlignV, ['top','center','bottom'], v => { el.carFgAlignV = v; rebuild(); })));
    fg.appendChild(makeRow('Position', makeInput(el.carFgPosition, v => { el.carFgPosition = v; rebuild(); }, 'center / bottom')));
    fg.appendChild(makeRow('Radius', makeInput(el.carFgRadius, v => { el.carFgRadius = v; rebuild(); }, '0 / 10px')));
    fg.appendChild(makeRow('Shadow', makeInput(el.carFgShadow, v => { el.carFgShadow = v; rebuild(); }, '0 8px 16px #0006')));
    ppStyle.appendChild(fg);
  }

  // ── 3D model cards ──
  if ((el.carouselItems || []).some(it => it.model && (it.layer || 'model') !== 'image')) {
    const cm = makeSection('3D Cards', 'bi-box');
    const cmHint = document.createElement('div');
    cmHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
    cmHint.textContent = 'Applies to every item that has a 3D model instead of an image.';
    cm.appendChild(cmHint);
    cm.appendChild(makeRow('Canvas W', makeInput(el.carModelWidth, v => { el.carModelWidth = v; rebuild(); }, '100% / 240px')));
    cm.appendChild(makeRow('Canvas H', makeInput(el.carModelHeight, v => { el.carModelHeight = v; rebuild(); }, 'defaults to the image height')));
    cm.appendChild(makeRow('Inset', makeNumInput(el.carModelInset, -80, 120, 1, v => { el.carModelInset = Math.round(v); rebuild(); })));
    const sizeHint3d = document.createElement('div');
    sizeHint3d.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    sizeHint3d.textContent = 'The canvas sits on top of the item image and matches its box, so adding a model never resizes the card. Inset shrinks (or with a negative value, expands) the canvas inside that box.';
    cm.appendChild(sizeHint3d);
    cm.appendChild(makeRow('Background', makeColorCombo(el.carModelBg, v => { el.carModelBg = v; rebuild(); })));
    cm.appendChild(makeRow('Auto Rotate', makeCheck(el.carModelAutoRotate, v => { el.carModelAutoRotate = v; rebuild(); })));
    cm.appendChild(makeRow('Spin Speed', makeSlider(el.carModelRotateSpeed, 0.1, 12, 0.1, v => { el.carModelRotateSpeed = v; rebuild(); })));
    cm.appendChild(makeRow('Interactive', makeCheck(el.carModelControls, v => { el.carModelControls = v; buildPropsPanel(el); rebuild(); })));
    if (el.carModelControls) {
      const ih = document.createElement('div');
      ih.style.cssText = 'font-size:0.42rem;color:var(--accent3);padding:0 0 4px;line-height:1.35;';
      ih.textContent = 'Dragging a card now turns its model instead of sliding the carousel — use the arrows, dots or a drag outside the model area to change slides.';
      cm.appendChild(ih);
      cm.appendChild(makeRow('Drag Rotate', makeCheck(el.carModelDragRotate, v => { el.carModelDragRotate = v; rebuild(); })));
      cm.appendChild(makeRow('Allow Zoom', makeCheck(el.carModelZoom, v => { el.carModelZoom = v; rebuild(); })));
      cm.appendChild(makeRow('Allow Pan', makeCheck(el.carModelPan, v => { el.carModelPan = v; rebuild(); })));
      cm.appendChild(makeRow('Drag Speed', makeSlider(el.carModelDragSpeed, 0.1, 4, 0.05, v => { el.carModelDragSpeed = v; rebuild(); })));
      cm.appendChild(makeRow('Zoom Speed', makeSlider(el.carModelZoomSpeed, 0.1, 4, 0.05, v => { el.carModelZoomSpeed = v; rebuild(); })));
      cm.appendChild(makeRow('Smoothing', makeCheck(el.carModelDamping, v => { el.carModelDamping = v; rebuild(); })));
      cm.appendChild(makeRow('Pause Spin', makeCheck(el.carModelStopSpinOnInteract, v => { el.carModelStopSpinOnInteract = v; buildPropsPanel(el); rebuild(); })));
      if (el.carModelStopSpinOnInteract) {
        cm.appendChild(makeRow('Resume After', makeNumInput(el.carModelResumeSpin, 0, 60000, 250, v => { el.carModelResumeSpin = Math.round(v); rebuild(); })));
      }
      const climLbl = document.createElement('div');
      climLbl.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
      climLbl.textContent = '● Limits (blank = free)';
      cm.appendChild(climLbl);
      const clim = (label, key, ph) => {
        const inp = document.createElement('input');
        inp.type = 'number'; inp.className = 'pp-input'; inp.style.cssText = 'flex:1;min-width:0;';
        inp.value = el[key] === '' || el[key] === undefined ? '' : el[key];
        inp.placeholder = ph;
        inp.onchange = () => { el[key] = inp.value === '' ? '' : parseFloat(inp.value); rebuild(); };
        return makeRow(label, inp);
      };
      cm.appendChild(clim('Min Zoom', 'carModelMinDistance', 'e.g. 2'));
      cm.appendChild(clim('Max Zoom', 'carModelMaxDistance', 'e.g. 10'));
      cm.appendChild(clim('Min Tilt°', 'carModelMinPolar', '0 = top'));
      cm.appendChild(clim('Max Tilt°', 'carModelMaxPolar', '180 = bottom'));
    }
    cm.appendChild(makeRow('Distance', makeSlider(el.carModelDistance, 0.5, 20, 0.1, v => { el.carModelDistance = v; rebuild(); })));
    cm.appendChild(makeRow('FOV', makeSlider(el.carModelFov, 10, 110, 1, v => { el.carModelFov = v; rebuild(); })));
    cm.appendChild(makeRow('Scale', makeSlider(el.carModelScale, 0.05, 5, 0.05, v => { el.carModelScale = v; rebuild(); })));
    cm.appendChild(makeRow('Lighting', makeSelect(el.carModelLighting, ['studio','soft','dramatic'], v => { el.carModelLighting = v; rebuild(); })));
    cm.appendChild(makeRow('Intensity', makeSlider(el.carModelLightIntensity, 0, 3, 0.05, v => { el.carModelLightIntensity = v; rebuild(); })));
    cm.appendChild(makeRow('Auto Rotate', makeCheck(el.carModelAutoRotate, v => { el.carModelAutoRotate = v; rebuild(); })));
    cm.appendChild(makeRow('Spin Active Only', makeCheck(el.carModelSpinActiveOnly, v => { el.carModelSpinActiveOnly = v; rebuild(); })));
    const spinHint = document.createElement('div');
    spinHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 4px;line-height:1.35;';
    spinHint.textContent = 'This is the default; each item can override it with its own Auto Rotate (inherit / on / off). "Spin Active Only" additionally limits spinning to the centred card.';
    cm.appendChild(spinHint);

    // Base pose for every card
    const baseLbl = document.createElement('div');
    baseLbl.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);margin:8px 0 4px;text-transform:uppercase;';
    baseLbl.textContent = '● Base Pose (all cards)';
    cm.appendChild(baseLbl);
    cm.appendChild(makeRow('Rot X°', makeNumInput(el.carModelRotX, -360, 360, 5, v => { el.carModelRotX = Math.round(v); rebuild(); })));
    cm.appendChild(makeRow('Rot Y°', makeNumInput(el.carModelRotY, -360, 360, 5, v => { el.carModelRotY = Math.round(v); rebuild(); })));
    cm.appendChild(makeRow('Rot Z°', makeNumInput(el.carModelRotZ, -360, 360, 5, v => { el.carModelRotZ = Math.round(v); rebuild(); })));
    cm.appendChild(makeRow('Shift X', makeNumInput(el.carModelX, -10, 10, 0.05, v => { el.carModelX = v; rebuild(); })));
    cm.appendChild(makeRow('Shift Y', makeNumInput(el.carModelY, -10, 10, 0.05, v => { el.carModelY = v; rebuild(); })));
    cm.appendChild(makeRow('Shift Z', makeNumInput(el.carModelZ, -10, 10, 0.05, v => { el.carModelZ = v; rebuild(); })));
    cm.appendChild(makeRow('Cam Height', makeNumInput(el.carModelCamHeight, -10, 10, 0.05, v => { el.carModelCamHeight = v; rebuild(); })));

    const poseRow = (label, key, min, max, step, ph) => {
      const inp = document.createElement('input');
      inp.type = 'number'; inp.className = 'pp-input'; inp.style.cssText = 'flex:1;min-width:0;';
      inp.min = min; inp.max = max; inp.step = step;
      inp.value = el[key] === '' || el[key] === undefined ? '' : el[key];
      inp.placeholder = ph;
      inp.onchange = () => { el[key] = inp.value === '' ? '' : parseFloat(inp.value); rerender(); };
      return makeRow(label, inp);
    };

    const actLbl = document.createElement('div');
    actLbl.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent);margin:8px 0 4px;text-transform:uppercase;';
    actLbl.textContent = '● Active Card Pose';
    cm.appendChild(actLbl);
    cm.appendChild(poseRow('Rot X°', 'carModelActiveRotX', -360, 360, 5, 'inherit'));
    cm.appendChild(poseRow('Rot Y°', 'carModelActiveRotY', -360, 360, 5, 'inherit'));
    cm.appendChild(poseRow('Rot Z°', 'carModelActiveRotZ', -360, 360, 5, 'inherit'));
    cm.appendChild(poseRow('Scale', 'carModelActiveScale', 0.05, 8, 0.05, 'inherit'));
    cm.appendChild(poseRow('Zoom (dist)', 'carModelActiveDistance', 0.5, 30, 0.1, 'inherit'));

    const sideLbl = document.createElement('div');
    sideLbl.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent3);margin:8px 0 4px;text-transform:uppercase;';
    sideLbl.textContent = '● Side Cards Pose';
    cm.appendChild(sideLbl);
    cm.appendChild(poseRow('Rot X°', 'carModelSideRotX', -360, 360, 5, 'inherit'));
    cm.appendChild(poseRow('Rot Y°', 'carModelSideRotY', -360, 360, 5, 'inherit'));
    cm.appendChild(poseRow('Rot Z°', 'carModelSideRotZ', -360, 360, 5, 'inherit'));
    cm.appendChild(poseRow('Scale', 'carModelSideScale', 0.05, 8, 0.05, 'inherit'));
    cm.appendChild(poseRow('Zoom (dist)', 'carModelSideDistance', 0.5, 30, 0.1, 'inherit'));
    const poseHint = document.createElement('div');
    poseHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
    poseHint.textContent = 'Leave blank to inherit. Set them to turn the model as a card becomes active — e.g. side cards at -30° and the active one at 0°. Applied every time the slide changes.';
    cm.appendChild(poseHint);
    ppStyle.appendChild(cm);
  }

  // ── Linked elements: react to the active item changing ──
  if (!Array.isArray(el.carLinks)) el.carLinks = [];
  const lnk = makeSection('On Active Item Change', 'bi-broadcast-pin');
  const lnkHint = document.createElement('div');
  lnkHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
  lnkHint.textContent = 'Same mapping for every slide — e.g. a heading always takes the active slide\'s title. For per-slide behaviour (different text, or showing/hiding components), use the triggers inside each item above.';
  lnk.appendChild(lnkHint);

  // Field options: built-ins + every custom field key used by any item
  const fieldOpts = [['title','Title'],['text','Text'],['image','Image'],['link','Link'],['index','Index (1-based)']];
  const customKeys = new Set();
  el.carouselItems.forEach(it => { if (it.fields) Object.keys(it.fields).forEach(k => customKeys.add(k)); });
  customKeys.forEach(k => fieldOpts.push(['field:' + k, 'Field: ' + k]));

  const actionOpts = [['setText','Set text'],['setHTML','Set HTML'],['setImage','Set image src'],
                      ['setBackground','Set background image'],['setVideo','Set video src'],['show','Show'],['hide','Hide']];

  el.carLinks.forEach((link, li) => {
    const card = document.createElement('div');
    card.style.cssText = 'border:1px solid var(--border-color);border-radius:8px;padding:6px 8px;margin-bottom:6px;background:rgba(255,255,255,0.02);';

    const head = document.createElement('div');
    head.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:4px;';
    const num = document.createElement('span');
    num.style.cssText = 'font-family:"Orbitron",sans-serif;font-size:0.4rem;letter-spacing:1px;color:var(--accent2);';
    num.textContent = 'LINK ' + (li + 1);
    const del = document.createElement('button');
    del.className = 'car-ed-btn del'; del.style.marginLeft = 'auto';
    del.innerHTML = '<i class="bi bi-trash"></i>'; del.title = 'Remove link';
    del.onclick = () => { el.carLinks.splice(li, 1); rebuild(); };
    head.appendChild(num); head.appendChild(del);
    card.appendChild(head);

    // Target element
    const tRow = document.createElement('div'); tRow.className = 'pp-row';
    const tSel = document.createElement('select'); tSel.className = 'pp-select';
    const none = document.createElement('option');
    none.value = ''; none.textContent = '(choose element)';
    if (!link.targetId) none.selected = true;
    tSel.appendChild(none);
    elements.filter(o => o.id !== el.id).forEach(o => {
      const opt = document.createElement('option');
      opt.value = o.id; opt.textContent = (o.name || o.type) + ' — ' + o.id;
      if (o.id === link.targetId) opt.selected = true;
      tSel.appendChild(opt);
    });
    tSel.onchange = () => { link.targetId = tSel.value; rerender(); };
    tRow.appendChild(makeLbl('Element')); tRow.appendChild(tSel); card.appendChild(tRow);

    // Source field
    const fRow = document.createElement('div'); fRow.className = 'pp-row';
    const fSel = document.createElement('select'); fSel.className = 'pp-select';
    fieldOpts.forEach(([v, t]) => {
      const o = document.createElement('option');
      o.value = v; o.textContent = t;
      if (v === (link.field || 'title')) o.selected = true;
      fSel.appendChild(o);
    });
    fSel.onchange = () => { link.field = fSel.value; rerender(); };
    fRow.appendChild(makeLbl('Use')); fRow.appendChild(fSel); card.appendChild(fRow);

    // Action
    const aRow = document.createElement('div'); aRow.className = 'pp-row';
    const aSel = document.createElement('select'); aSel.className = 'pp-select';
    actionOpts.forEach(([v, t]) => {
      const o = document.createElement('option');
      o.value = v; o.textContent = t;
      if (v === (link.action || 'setText')) o.selected = true;
      aSel.appendChild(o);
    });
    aSel.onchange = () => { link.action = aSel.value; rerender(); };
    aRow.appendChild(makeLbl('Action')); aRow.appendChild(aSel); card.appendChild(aRow);

    lnk.appendChild(card);
  });

  const addLink = document.createElement('button');
  addLink.className = 'car-ed-add';
  addLink.innerHTML = '<i class="bi bi-plus-lg"></i> Add Linked Element';
  addLink.onclick = () => { el.carLinks.push({ targetId:'', field:'title', action:'setText' }); rebuild(); };
  lnk.appendChild(addLink);

  lnk.appendChild(makeRow('Reset Show/Hide', makeCheck(el.carResetVisibility, v => { el.carResetVisibility = v; rerender(); })));
  const resetHint = document.createElement('div');
  resetHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:0 0 6px;line-height:1.35;';
  resetHint.textContent = 'On: any element shown/hidden by an item trigger returns to its designed state before the next slide applies its own triggers — so a component can appear on one slide only.';
  lnk.appendChild(resetHint);
  lnk.appendChild(makeRow('On Change', makeInput(el.carChangeFunc, v => { el.carChangeFunc = v; rerender(); }, 'onSlideChange')));
  const fnHint = document.createElement('div');
  fnHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
  fnHint.textContent = 'Optional function called as fn(item, index, element) whenever the active slide changes.';
  lnk.appendChild(fnHint);
  ppProps.appendChild(lnk);

  // ── Item click + custom template ──
  const adv = makeSection('Item Click & Template', 'bi-code-slash');
  adv.appendChild(makeRow('On Click', makeSelect(el.carItemClick, ['none','link','function'], v => { el.carItemClick = v; rebuild(); })));
  if (el.carItemClick === 'link') {
    adv.appendChild(makeRow('Target', makeSelect(el.carLinkTarget, ['newTab','same','iframe'], v => { el.carLinkTarget = v; rerender(); })));
  } else if (el.carItemClick === 'function') {
    adv.appendChild(makeRow('Function', makeInput(el.carItemFunc, v => { el.carItemFunc = v; rerender(); }, 'onCarouselItem')));
    const fh = document.createElement('div');
    fh.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
    fh.textContent = 'Called as fn(item, index, element) — define it in Settings → Custom Game Functions.';
    adv.appendChild(fh);
  }
  const tplRow = document.createElement('div');
  tplRow.className = 'pp-row';
  tplRow.style.cssText = 'flex-direction:column;align-items:stretch;gap:4px;';
  const tplLbl = document.createElement('span');
  tplLbl.className = 'pp-label'; tplLbl.textContent = 'Custom item HTML';
  tplRow.appendChild(tplLbl);
  tplRow.appendChild(makeTextareaInput(el.carItemTemplate, v => { el.carItemTemplate = v; rerender(); },
    '<img src="{image}"><h4>{title}</h4><p>{text}</p>', 4));
  adv.appendChild(tplRow);
  const tplHint = document.createElement('div');
  tplHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:2px 0;line-height:1.35;';
  tplHint.textContent = 'Leave empty for the built-in layout. Placeholders: {image} {title} {text} {link}';
  adv.appendChild(tplHint);
  ppProps.appendChild(adv);
}
function makeRow(label, inputEl) {
  const r = document.createElement('div');
  r.className = 'pp-row';
  const l = document.createElement('span');
  l.className = 'pp-label'; l.textContent = label;
  r.appendChild(l);
  r.appendChild(inputEl);
  return r;
}
function makeInput(val, onChange, placeholder) {
  const inp = document.createElement('input');
  inp.className = 'pp-input'; inp.value = val || '';
  if (placeholder) inp.placeholder = placeholder;
  inp.onchange = () => onChange(inp.value);
  return inp;
}
function makeReadonly(val) {
  const inp = document.createElement('input');
  inp.className = 'pp-input'; inp.value = val; inp.readOnly = true;
  inp.style.opacity = '0.6';
  return inp;
}
function makeNumInput(val, min, max, step, onChange, placeholder) {
  const inp = document.createElement('input');
  inp.className = 'pp-input'; inp.type = 'number';
  inp.value = val; inp.min = min; inp.max = max; inp.step = step;
  if (placeholder) inp.placeholder = placeholder;
  inp.oninput = () => onChange(parseFloat(inp.value) || 0);
  return inp;
}
function makeSlider(val, min, max, step, onChange) {
  const wrap = document.createElement('div');
  wrap.style.cssText = 'display:flex;align-items:center;gap:4px;flex:1;';
  const sl = document.createElement('input');
  sl.className = 'pp-slider'; sl.type = 'range';
  sl.min = min; sl.max = max; sl.step = step; sl.value = val;
  const vSpan = document.createElement('span');
  vSpan.className = 'pp-val'; vSpan.textContent = parseFloat(val).toFixed(step < 1 ? 2 : 0);
  sl.oninput = () => { vSpan.textContent = parseFloat(sl.value).toFixed(step < 1 ? 2 : 0); onChange(parseFloat(sl.value)); };
  wrap.appendChild(sl); wrap.appendChild(vSpan);
  return wrap;
}
function makeSelect(val, options, onChange) {
  const sel = document.createElement('select');
  sel.className = 'pp-select';
  options.forEach(o => {
    const opt = document.createElement('option');
    opt.value = o; opt.textContent = o;
    if (o === val) opt.selected = true;
    sel.appendChild(opt);
  });
  sel.onchange = () => onChange(sel.value);
  return sel;
}
function makeColor(val, onChange) {
  const inp = document.createElement('input');
  inp.type = 'color'; inp.className = 'pp-color'; inp.value = val || '#ffffff';
  inp.oninput = () => onChange(inp.value);
  return inp;
}
function makeLbl(text) {
  const l = document.createElement('span');
  l.className = 'pp-label'; l.textContent = text;
  return l;
}
function makeColorCombo(val, onChange) {
  const wrap = document.createElement('div');
  wrap.className = 'color-combo';
  // Extract hex color from value for the picker
  const hexMatch = (val || '').match(/#[0-9a-fA-F]{3,8}/);
  const colorInp = document.createElement('input');
  colorInp.type = 'color'; colorInp.className = 'pp-color';
  colorInp.value = hexMatch ? hexMatch[0] : '#000000';
  const textInp = document.createElement('input');
  textInp.className = 'pp-input'; textInp.value = val || '';
  colorInp.oninput = () => { textInp.value = colorInp.value; onChange(colorInp.value); };
  textInp.onchange = () => { onChange(textInp.value); try { colorInp.value = textInp.value; } catch(e) {} };
  wrap.appendChild(colorInp); wrap.appendChild(textInp);
  return wrap;
}
function makeImageInput(val, onChange, compact) {
  const wrap = document.createElement('div');
  wrap.style.cssText = 'display:flex;gap:3px;flex:1;align-items:center;';
  const textInp = document.createElement('input');
  textInp.className = 'pp-input'; textInp.value = (val || '').startsWith('data:') ? '(base64 image)' : (val || '');
  textInp.placeholder = 'URL or upload...';
  textInp.style.cssText = 'flex:1;min-width:0;';
  textInp.onchange = () => { onChange(textInp.value); };
  // Preview thumbnail
  const thumb = document.createElement('div');
  thumb.style.cssText = 'width:22px;height:22px;border-radius:3px;border:1px solid var(--border-color);background-size:cover;background-position:center;flex-shrink:0;cursor:pointer;';
  if (val) thumb.style.backgroundImage = `url(${val})`;
  thumb.title = 'Click to preview';
  // Upload button
  const upBtn = document.createElement('label');
  upBtn.style.cssText = 'display:flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:3px;background:rgba(167,139,250,0.1);border:1px solid var(--accent2);color:var(--accent2);cursor:pointer;flex-shrink:0;font-size:0.55rem;';
  upBtn.innerHTML = '<i class="bi bi-upload"></i>';
  upBtn.title = 'Upload image';
  const fileInp = document.createElement('input');
  fileInp.type = 'file'; fileInp.accept = 'image/*'; fileInp.style.display = 'none';
  fileInp.onchange = () => {
    const file = fileInp.files[0];
    if (!file) return;
    if (file.size > 5 * 1024 * 1024) { showToast('Image too large (max 5MB)', 'error'); return; }
    const reader = new FileReader();
    reader.onload = ev => {
      const b64 = ev.target.result;
      textInp.value = '(base64 image)';
      thumb.style.backgroundImage = `url(${b64})`;
      onChange(b64);
      showToast(`Image loaded (${(file.size/1024).toFixed(0)}KB)`);
    };
    reader.readAsDataURL(file);
    fileInp.value = '';
  };
  upBtn.appendChild(fileInp);
  // Clear button
  const clrBtn = document.createElement('div');
  clrBtn.style.cssText = 'width:22px;height:22px;border-radius:3px;background:rgba(255,77,106,0.08);border:1px solid var(--danger);color:var(--danger);cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:0.45rem;';
  clrBtn.innerHTML = '<i class="bi bi-x"></i>';
  clrBtn.title = 'Clear image';
  clrBtn.onclick = () => { textInp.value = ''; thumb.style.backgroundImage = ''; onChange(''); };
  wrap.appendChild(thumb); wrap.appendChild(textInp); wrap.appendChild(upBtn); wrap.appendChild(clrBtn);
  return wrap;
}
function makeVideoInput(val, onChange) {
  const wrap = document.createElement('div');
  wrap.style.cssText = 'display:flex;gap:3px;flex:1;align-items:center;';
  const textInp = document.createElement('input');
  textInp.className = 'pp-input';
  textInp.value = (val || '').startsWith('data:') ? '(embedded video)' : (val || '');
  textInp.placeholder = './videos/intro.mp4 or URL';
  textInp.style.cssText = 'flex:1;min-width:0;';
  textInp.onchange = () => { if (textInp.value !== '(embedded video)') onChange(textInp.value.trim()); };
  // Upload button (embeds as base64 — heavy, prefer paths)
  const upBtn = document.createElement('label');
  upBtn.style.cssText = 'display:flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:3px;background:rgba(167,139,250,0.1);border:1px solid var(--accent2);color:var(--accent2);cursor:pointer;flex-shrink:0;font-size:0.55rem;';
  upBtn.innerHTML = '<i class="bi bi-upload"></i>';
  upBtn.title = 'Embed video file (base64, max 25MB — inflates JSON, prefer a file path)';
  const fileInp = document.createElement('input');
  fileInp.type = 'file'; fileInp.accept = 'video/mp4,video/webm,video/ogg'; fileInp.style.display = 'none';
  fileInp.onchange = () => {
    const file = fileInp.files[0];
    if (!file) return;
    if (file.size > 25 * 1024 * 1024) { showToast('Video too large (max 25MB embed) — use a file path instead', 'error'); return; }
    const reader = new FileReader();
    reader.onload = ev => {
      textInp.value = '(embedded video)';
      onChange(ev.target.result);
      showToast(`Video embedded (${(file.size/1024/1024).toFixed(1)}MB) — file paths keep JSON smaller`);
    };
    reader.readAsDataURL(file);
    fileInp.value = '';
  };
  upBtn.appendChild(fileInp);
  // Clear button
  const clrBtn = document.createElement('div');
  clrBtn.style.cssText = 'width:22px;height:22px;border-radius:3px;background:rgba(255,77,106,0.08);border:1px solid var(--danger);color:var(--danger);cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:0.45rem;';
  clrBtn.innerHTML = '<i class="bi bi-x"></i>';
  clrBtn.title = 'Clear video';
  clrBtn.onclick = () => { textInp.value = ''; onChange(''); };
  wrap.appendChild(textInp); wrap.appendChild(upBtn); wrap.appendChild(clrBtn);
  return wrap;
}
// Override-aware row: shows purple dot if this property has a device override
function makeOvRow(el, label, prop, inputEl) {
  const r = makeRow(label, inputEl);
  if (hasOverride(el, prop)) r.classList.add('has-override');
  return r;
}

// ══════════════════════════════════════
//  LAYER LIST
// ══════════════════════════════════════
function buildLayerList() {
  const container = document.getElementById('layer-items');
  container.innerHTML = '';
  const typeIcons = { text:'bi-fonts', button:'bi-hand-index', image:'bi-image', icon:'bi-emoji-smile', panel:'bi-square', progressbar:'bi-battery-half', video:'bi-film', carousel:'bi-collection-play', hero:'bi-image-alt', iframe:'bi-window', model3d:'bi-box' };
  const collapsed = window._layerCollapsed || (window._layerCollapsed = new Set());

  function renderLayerItem(el, depth) {
    const children = elements.filter(e => e.parentId === el.id);
    const hasChildren = children.length > 0;
    const isCollapsed = collapsed.has(el.id);

    const div = document.createElement('div');
    div.className = 'layer-item' + (el.id === selectedId ? ' selected' : '') + (depth > 0 ? ' child' : '');
    if (depth > 0) div.style.paddingLeft = (12 + depth * 16) + 'px';
    div.draggable = true;
    div.dataset.layerId = el.id;

    const collapseIcon = hasChildren
      ? `<i class="bi ${isCollapsed ? 'bi-caret-right-fill' : 'bi-caret-down-fill'} li-collapse" style="font-size:0.5rem;cursor:pointer;color:var(--text-muted);min-width:10px;"></i>`
      : '<span style="min-width:10px;"></span>';

    div.innerHTML = `
      ${collapseIcon}
      <i class="bi ${typeIcons[el.type] || 'bi-square'} li-icon"></i>
      <span class="li-name">${el.name || el.id}</span>
      ${el.locked ? '<i class="bi bi-lock-fill" style="font-size:0.45rem;color:var(--accent3);"></i>' : ''}
      ${el.disabled ? '<i class="bi bi-slash-circle" style="font-size:0.45rem;color:var(--text-muted);"></i>' : ''}
      <button class="li-vis" title="${el.visible ? 'Hide' : 'Show'}" style="color:${el.visible ? 'var(--text-primary)' : 'var(--danger)'}"><i class="bi ${el.visible ? 'bi-eye-fill' : 'bi-eye-slash-fill'}"></i></button>
    `;

    // Collapse toggle
    const colBtn = div.querySelector('.li-collapse');
    if (colBtn) {
      colBtn.onclick = (ev) => {
        ev.stopPropagation();
        if (isCollapsed) collapsed.delete(el.id); else collapsed.add(el.id);
        buildLayerList();
      };
    }

    div.querySelector('.li-name').onclick = () => selectElement(el.id);
    div.querySelector('.li-vis').onclick = (ev) => { ev.stopPropagation(); el.visible = !el.visible; renderElement(el); rerenderChildren(el.id); buildLayerList(); };

    // Drag & drop reorder
    div.addEventListener('dragstart', ev => {
      ev.dataTransfer.setData('text/plain', el.id);
      ev.dataTransfer.effectAllowed = 'move';
      div.style.opacity = '0.4';
    });
    div.addEventListener('dragend', () => { div.style.opacity = '1'; });
    div.addEventListener('dragover', ev => {
      ev.preventDefault(); ev.dataTransfer.dropEffect = 'move';
      div.style.borderTop = '2px solid var(--accent)';
    });
    div.addEventListener('dragleave', () => { div.style.borderTop = ''; });
    div.addEventListener('drop', ev => {
      ev.preventDefault(); div.style.borderTop = '';
      const srcId = ev.dataTransfer.getData('text/plain');
      if (srcId === el.id) return;
      const srcIdx = elements.findIndex(e => e.id === srcId);
      const tgtIdx = elements.findIndex(e => e.id === el.id);
      if (srcIdx < 0 || tgtIdx < 0) return;
      const [moved] = elements.splice(srcIdx, 1);
      elements.splice(tgtIdx, 0, moved);
      // Update z-indices to match order
      elements.forEach((e, i) => { e.zIndex = 10 + i; });
      renderAllElements();
      buildLayerList();
    });

    container.appendChild(div);

    // Render children if not collapsed
    if (hasChildren && !isCollapsed) {
      children.forEach(c => renderLayerItem(c, depth + 1));
    }
  }

  const topLevel = [...elements].filter(e => !e.parentId).reverse();
  topLevel.forEach(el => renderLayerItem(el, 0));

  // Hierarchy tab badge + empty state
  const countEl = document.getElementById('hierarchy-count');
  if (countEl) countEl.textContent = elements.length;
  const listEl = document.getElementById('layer-list');
  if (listEl) listEl.classList.toggle('empty', elements.length === 0);
}

// ══════════════════════════════════════
//  LEFT PANEL TABS (Tools / Hierarchy)
// ══════════════════════════════════════
function switchPaletteTab(tab) {
  document.querySelectorAll('.pal-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
  document.querySelectorAll('.pal-tab-panel').forEach(p => p.classList.toggle('active', p.id === 'pal-panel-' + tab));
}
document.querySelectorAll('.pal-tab').forEach(btn => {
  btn.addEventListener('click', () => switchPaletteTab(btn.dataset.tab));
});

// ══════════════════════════════════════
//  CANVAS INTERACTION
// ══════════════════════════════════════
let dragStartX, dragStartY, dragElStartX, dragElStartY, resizeDir, resizeStartW, resizeStartH;

function onElementPointerDown(e, elId) {
  if (previewMode) return;
  e.stopPropagation();
  const el = elements.find(e => e.id === elId);
  if (!el) return;

  // Locked elements: can't select, drag, or resize
  if (el.locked) {
    // If this is a child of a panel, try to select the parent instead
    if (el.parentId) {
      const parent = elements.find(p => p.id === el.parentId);
      if (parent && !parent.locked) { selectElement(parent.id); return; }
    }
    return;
  }

  // If clicking a child element and the parent panel is currently selected,
  // check if the click target is actually the parent's inner area (not the child's content)
  const clickedDom = e.target.closest('.ui-el');
  if (clickedDom) {
    const clickedId = clickedDom.dataset.id;
    if (clickedId !== elId) {
      // Click was on a nested element; the event came from an ancestor handler
      return;
    }
  }

  // If Alt+Click on a child, select its parent panel instead
  if (e.altKey && el.parentId) {
    const parent = elements.find(p => p.id === el.parentId);
    if (parent) { selectElement(parent.id); return; }
  }

  selectElement(elId);

  // Check if resize handle
  const handle = e.target.closest('.resize-handle');
  if (handle && !el.locked) {
    isResizing = true;
    resizeDir = handle.dataset.handle;
    const rect = viewport.getBoundingClientRect();
    dragStartX = e.clientX;
    dragStartY = e.clientY;
    resizeStartW = getVal(el,'w');
    resizeStartH = getVal(el,'h');
    dragElStartX = getVal(el,'x');
    dragElStartY = getVal(el,'y');
    document.addEventListener('pointermove', onResizeMove);
    document.addEventListener('pointerup', onResizeUp);
    return;
  }

  if (el.locked) return;

  // Start drag
  isDragging = true;
  const rect = viewport.getBoundingClientRect();
  dragStartX = e.clientX;
  dragStartY = e.clientY;
  dragElStartX = getVal(el,'x');
  dragElStartY = getVal(el,'y');
  document.addEventListener('pointermove', onDragMove);
  document.addEventListener('pointerup', onDragUp);
}

function onDragMove(e) {
  if (!isDragging || !selectedId) return;
  const el = elements.find(e => e.id === selectedId);
  if (!el) return;
  const rect = viewport.getBoundingClientRect();
  const dx = ((e.clientX - dragStartX) / rect.width) * 100;
  const dy = ((e.clientY - dragStartY) / rect.height) * 100;
  let nx = dragElStartX + dx;
  let ny = dragElStartY + dy;
  if (snapEnabled) { nx = Math.round(nx / snapSize) * snapSize; ny = Math.round(ny / snapSize) * snapSize; }
  nx = Math.max(0, Math.min(100, nx));
  ny = Math.max(0, Math.min(100, ny));
  setVal(el, 'x', nx);
  setVal(el, 'y', ny);
}

function onDragUp() {
  isDragging = false;
  document.removeEventListener('pointermove', onDragMove);
  document.removeEventListener('pointerup', onDragUp);
  if (selectedId) {
    const el = elements.find(e => e.id === selectedId);
    if (el) buildPropsPanel(el);
  }
}

function onResizeMove(e) {
  if (!isResizing || !selectedId) return;
  const el = elements.find(e => e.id === selectedId);
  if (!el) return;
  const rect = viewport.getBoundingClientRect();
  const dx = ((e.clientX - dragStartX) / rect.width) * 100;
  const dy = ((e.clientY - dragStartY) / rect.height) * 100;

  let nw = getVal(el,'w'), nh = getVal(el,'h'), nx = getVal(el,'x'), ny = getVal(el,'y');
  if (resizeDir.includes('r')) nw = Math.max(1, resizeStartW + dx);
  if (resizeDir.includes('l')) { nw = Math.max(1, resizeStartW - dx); nx = dragElStartX + dx; }
  if (resizeDir.includes('b')) nh = Math.max(1, resizeStartH + dy);
  if (resizeDir.includes('t')) { nh = Math.max(1, resizeStartH - dy); ny = dragElStartY + dy; }

  if (snapEnabled) {
    nw = Math.round(nw / snapSize) * snapSize;
    nh = Math.round(nh / snapSize) * snapSize;
  }
  setVal(el,'w',nw); setVal(el,'h',nh);
  if (resizeDir.includes('l')) setVal(el,'x',nx);
  if (resizeDir.includes('t')) setVal(el,'y',ny);
}

function onResizeUp() {
  isResizing = false;
  document.removeEventListener('pointermove', onResizeMove);
  document.removeEventListener('pointerup', onResizeUp);
  if (selectedId) {
    const el = elements.find(e => e.id === selectedId);
    if (el) buildPropsPanel(el);
  }
}

// ── Canvas drop from palette ──
viewport.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
viewport.addEventListener('drop', e => {
  e.preventDefault();
  if (!dragData || dragData.source !== 'palette') return;
  const template = findTemplate(dragData.itemId);
  if (!template) return;
  const rect = viewport.getBoundingClientRect();
  const x = ((e.clientX - rect.left) / rect.width) * 100;
  const y = ((e.clientY - rect.top) / rect.height) * 100;
  createElement(template, x, y);
  showToast(`Added ${template.name}`);
  dragData = null;
});

// ── Canvas click to deselect ──
viewport.addEventListener('pointerdown', e => {
  if (e.target === viewport) deselectAll();
});

// ══════════════════════════════════════
//  VIEWPORT SIZING
// ══════════════════════════════════════
function resizeViewport() {
  const size = DEVICE_SIZES[deviceMode];
  const area = canvasArea.getBoundingClientRect();
  const maxW = area.width - 40;
  const maxH = area.height - 40;
  const scale = Math.min(maxW / size.w, maxH / size.h, 1.5);
  // The canvas is always the design resolution and is *scaled* to fit, so a 110px
  // image is always 110 design pixels. Sizing the canvas itself (the old behaviour)
  // silently changed every px/rem proportion whenever the window changed, which is
  // why exports never matched the design.
  viewport.style.width = size.w + 'px';
  viewport.style.height = size.h + 'px';
  viewport.style.transform = 'scale(' + scale + ')';
  viewport.style.transformOrigin = 'center center';
  window.__uiDesignScale = scale;
  if (bgImage) viewport.style.backgroundImage = `url(${bgImage})`;
  else viewport.style.backgroundImage = 'none';
  viewport.style.backgroundSize = 'cover';
  viewport.style.backgroundPosition = 'center';
}

// ── Device tabs ──
document.querySelectorAll('.device-tab').forEach(btn => {
  btn.onclick = () => {
    document.querySelectorAll('.device-tab').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    deviceMode = btn.dataset.device;
    resizeViewport();
    renderAllElements();
    // Reapply preview styles if in preview mode
    if (previewMode) {
      viewport.querySelectorAll('.ui-el').forEach(d => {
        d.style.outline = 'none';
        const evts = d._elEvents || {};
        const hasActions = Object.values(evts).some(a => a && a.length > 0);
        d.style.cursor = hasActions ? 'pointer' : 'default';
      });
    }
    // Rebuild props panel if element is selected
    if (selectedId) {
      const el = elements.find(e => e.id === selectedId);
      if (el) buildPropsPanel(el);
    }
  };
});
window.addEventListener('resize', resizeViewport);

// ══════════════════════════════════════
//  TOOLBAR ACTIONS
// ══════════════════════════════════════
// Grid toggle
document.getElementById('btn-grid').onclick = function() {
  viewport.classList.toggle('show-grid');
  this.classList.toggle('active');
};
document.getElementById('btn-grid').classList.add('active');

// Guides toggle
document.getElementById('btn-guides').onclick = function() {
  viewport.classList.toggle('show-guides');
  this.classList.toggle('active');
};
document.getElementById('btn-guides').classList.add('active');

// Snap toggle
document.getElementById('btn-snap').onclick = function() {
  snapEnabled = !snapEnabled;
  this.classList.toggle('active');
  showToast(snapEnabled ? 'Snap ON' : 'Snap OFF');
};
document.getElementById('btn-snap').classList.add('active');

// Background image
document.getElementById('btn-bg-image').onclick = () => {
  // Create a mini popup for BG image
  let overlay = document.getElementById('bg-img-overlay');
  if (overlay) { overlay.remove(); return; }
  overlay = document.createElement('div');
  overlay.id = 'bg-img-overlay';
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:flex;align-items:center;justify-content:center;';
  const box = document.createElement('div');
  box.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border-color);border-radius:10px;padding:16px;width:360px;max-width:90vw;';
  box.innerHTML = `<h6 style="font-family:'Orbitron',sans-serif;font-size:0.5rem;color:var(--accent);margin:0 0 10px;letter-spacing:1px;">BACKGROUND IMAGE</h6>`;
  const urlInp = document.createElement('input');
  urlInp.className = 'pp-input'; urlInp.placeholder = 'Paste image URL...';
  urlInp.value = bgImage.startsWith('data:') ? '' : (bgImage || '');
  urlInp.style.cssText = 'width:100%;margin-bottom:8px;';
  box.appendChild(urlInp);
  // Preview
  const preview = document.createElement('div');
  preview.style.cssText = 'width:100%;height:80px;border-radius:6px;border:1px solid var(--border-color);background-size:cover;background-position:center;margin-bottom:8px;';
  if (bgImage) preview.style.backgroundImage = `url(${bgImage})`;
  box.appendChild(preview);
  // Buttons row
  const btnRow = document.createElement('div');
  btnRow.style.cssText = 'display:flex;gap:6px;';
  // Upload button
  const upLabel = document.createElement('label');
  upLabel.className = 'pp-btn';
  upLabel.style.cssText = 'flex:1;display:flex;align-items:center;justify-content:center;gap:4px;background:rgba(167,139,250,0.08);border-color:var(--accent2);color:var(--accent2);cursor:pointer;';
  upLabel.innerHTML = '<i class="bi bi-upload"></i> Upload';
  const fileInp = document.createElement('input');
  fileInp.type = 'file'; fileInp.accept = 'image/*'; fileInp.style.display = 'none';
  fileInp.onchange = () => {
    const file = fileInp.files[0]; if (!file) return;
    if (file.size > 5*1024*1024) { showToast('Max 5MB', 'error'); return; }
    const reader = new FileReader();
    reader.onload = ev => {
      bgImage = ev.target.result;
      preview.style.backgroundImage = `url(${bgImage})`;
      urlInp.value = '(base64 image)';
      resizeViewport();
      showToast(`BG image loaded (${(file.size/1024).toFixed(0)}KB)`);
    };
    reader.readAsDataURL(file);
  };
  upLabel.appendChild(fileInp);
  // Apply URL button
  const applyBtn = document.createElement('button');
  applyBtn.className = 'pp-btn';
  applyBtn.style.cssText = 'flex:1;background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);';
  applyBtn.innerHTML = '<i class="bi bi-check-lg"></i> Apply URL';
  applyBtn.onclick = () => { bgImage = urlInp.value === '(base64 image)' ? bgImage : urlInp.value; resizeViewport(); preview.style.backgroundImage = bgImage ? `url(${bgImage})` : ''; };
  // Clear
  const clrBtn = document.createElement('button');
  clrBtn.className = 'pp-btn';
  clrBtn.style.cssText = 'background:rgba(255,77,106,0.08);border-color:var(--danger);color:var(--danger);';
  clrBtn.innerHTML = '<i class="bi bi-x"></i>';
  clrBtn.onclick = () => { bgImage = ''; urlInp.value = ''; preview.style.backgroundImage = ''; resizeViewport(); };
  // Close
  const closeBtn = document.createElement('button');
  closeBtn.className = 'pp-btn';
  closeBtn.style.cssText = 'background:rgba(255,255,255,0.04);border-color:var(--border-color);color:var(--text-secondary);';
  closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
  closeBtn.onclick = () => overlay.remove();
  btnRow.appendChild(upLabel); btnRow.appendChild(applyBtn); btnRow.appendChild(clrBtn); btnRow.appendChild(closeBtn);
  box.appendChild(btnRow);
  overlay.appendChild(box);
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
  document.body.appendChild(overlay);
};

// Preview mode
document.getElementById('btn-preview').onclick = function() {
  previewMode = !previewMode;
  this.classList.toggle('active');
  document.getElementById('palette-items').style.display = previewMode ? 'none' : '';
  // In preview, the Tools tab is unusable — jump to Hierarchy and back
  const toolsTab = document.querySelector('.pal-tab[data-tab="tools"]');
  if (toolsTab) {
    toolsTab.style.opacity = previewMode ? '0.35' : '';
    toolsTab.style.pointerEvents = previewMode ? 'none' : '';
  }
  switchPaletteTab(previewMode ? 'hierarchy' : 'tools');
  if (previewMode) {
    deselectAll();
    // Full re-render with clean DOM first
    renderAllElements();
    // THEN inject custom code so UI elements exist for top-level statements
    customCode = document.getElementById('set-custom-code')?.value || customCode;
    if (customCode) {
      let scriptEl = document.getElementById('preview-custom-code');
      if (scriptEl) scriptEl.remove();
      scriptEl = document.createElement('script');
      scriptEl.id = 'preview-custom-code';
      scriptEl.textContent = customCode + '\nif (typeof onReady === "function") onReady();';
      document.body.appendChild(scriptEl);
    }
  } else {
    // Remove injected script
    const s = document.getElementById('preview-custom-code');
    if (s) s.remove();
    renderAllElements();
  }
  viewport.querySelectorAll('.ui-el').forEach(d => {
    d.style.outline = previewMode ? 'none' : '';
    // Show pointer on elements with actions in preview
    const evts = d._elEvents || {};
    const hasActions = Object.values(evts).some(a => a && a.length > 0);
    d.style.cursor = previewMode ? (hasActions ? 'pointer' : 'default') : '';
  });
  showToast(previewMode ? 'Preview Mode — click to test actions' : 'Edit Mode');
};

// UI Name
document.getElementById('ui-name').onchange = function() { uiName = this.value; };

// Props close
document.getElementById('pp-close').onclick = deselectAll;

// ══════════════════════════════════════
//  SAVE / LOAD
// ══════════════════════════════════════
document.getElementById('btn-save').onclick = () => {
  customCode = document.getElementById('set-custom-code').value || customCode;
  if (blocklyWorkspace && blocklyCurrentFunc) saveCurrentBlocklyFunc();
  const data = {
    formatVersion: 2,
    type: 'game-ui',
    name: uiName,
    bgImage,
    customCode,
    blocklyFunctions: blocklyFuncData,
    customBlocks: customBlocks,
    elements: elements.map(el => ({ ...el })),
    // Design resolution the layout was built against — runtimes scale to match
    scaleMode: projectScaleMode,
    designSizes: DEVICE_SIZES,
    responsive: {
      breakpoints: { mobile: 768, tablet: 1024 }
    }
  };

  // Show save dialog
  let ov = document.getElementById('save-dialog-overlay');
  if (ov) ov.remove();
  ov = document.createElement('div');
  ov.id = 'save-dialog-overlay';
  ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:500;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(3px);';

  const box = document.createElement('div');
  box.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border-color);border-radius:12px;padding:0;width:480px;max-width:95vw;overflow:hidden;';

  // Header
  const hdr = document.createElement('div');
  hdr.style.cssText = 'padding:14px 18px;border-bottom:1px solid var(--border-color);display:flex;align-items:center;gap:8px;';
  hdr.innerHTML = `<i class="bi bi-download" style="color:var(--accent);font-size:1rem;"></i><span style="font-family:'Orbitron',sans-serif;font-size:0.52rem;color:var(--accent);letter-spacing:1px;flex:1;">SAVE PROJECT</span>`;
  const closeBtn = document.createElement('div');
  closeBtn.style.cssText = 'width:24px;height:24px;border-radius:4px;background:rgba(255,77,106,0.08);border:1px solid var(--danger);color:var(--danger);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:0.45rem;';
  closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
  closeBtn.onclick = () => ov.remove();
  hdr.appendChild(closeBtn);
  box.appendChild(hdr);

  const body = document.createElement('div');
  body.style.cssText = 'padding:18px;display:flex;flex-direction:column;gap:10px;';

  // Option 1: Download JSON
  const opt1 = document.createElement('div');
  opt1.style.cssText = 'padding:14px 16px;background:rgba(0,229,160,0.04);border:1px solid rgba(0,229,160,0.2);border-radius:8px;cursor:pointer;transition:all 0.15s;';
  opt1.innerHTML = `<div style="display:flex;align-items:center;gap:10px;">
    <i class="bi bi-filetype-json" style="font-size:1.4rem;color:var(--accent);"></i>
    <div style="flex:1;">
      <div style="font-size:0.52rem;font-weight:600;color:var(--text-primary);margin-bottom:2px;">Download Project JSON</div>
      <div style="font-size:0.42rem;color:var(--text-muted);">Save as .ui.json — re-open in UI Builder to continue editing</div>
    </div>
    <i class="bi bi-chevron-right" style="color:var(--text-muted);"></i>
  </div>`;
  opt1.onmouseenter = () => opt1.style.borderColor = 'var(--accent)';
  opt1.onmouseleave = () => opt1.style.borderColor = 'rgba(0,229,160,0.2)';
  opt1.onclick = () => {
    const filename = uiName.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() || 'game_ui';
    const stamp = fileTimestamp();
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
    a.download = `${filename}_${stamp}.ui.json`;
    a.click();
    URL.revokeObjectURL(a.href);
    ov.remove();
    showToast(`Saved "${uiName}" as ${filename}_${stamp}.ui.json`);
  };
  body.appendChild(opt1);

  // Option 2: Export HTML
  const opt2 = document.createElement('div');
  opt2.style.cssText = 'padding:14px 16px;background:rgba(167,139,250,0.04);border:1px solid rgba(167,139,250,0.2);border-radius:8px;cursor:pointer;transition:all 0.15s;';
  opt2.innerHTML = `<div style="display:flex;align-items:center;gap:10px;">
    <i class="bi bi-filetype-html" style="font-size:1.4rem;color:var(--accent2);"></i>
    <div style="flex:1;">
      <div style="font-size:0.52rem;font-weight:600;color:var(--text-primary);margin-bottom:2px;">Export Standalone HTML</div>
      <div style="font-size:0.42rem;color:var(--text-muted);">Complete page with all UI, CSS, JS — ready to use anywhere</div>
    </div>
    <i class="bi bi-chevron-right" style="color:var(--text-muted);"></i>
  </div>`;
  opt2.onmouseenter = () => opt2.style.borderColor = 'var(--accent2)';
  opt2.onmouseleave = () => opt2.style.borderColor = 'rgba(167,139,250,0.2)';
  opt2.onclick = () => {
    ov.remove();
    showExportHTMLDialog(data);
  };
  body.appendChild(opt2);

  box.appendChild(body);
  ov.appendChild(box);
  ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); });
  document.body.appendChild(ov);
};

// ── Export HTML Dialog ──
function showExportHTMLDialog(data) {
  let ov = document.createElement('div');
  ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:500;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(3px);';

  const box = document.createElement('div');
  box.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border-color);border-radius:12px;padding:0;width:500px;max-width:95vw;overflow:hidden;';

  const hdr = document.createElement('div');
  hdr.style.cssText = 'padding:14px 18px;border-bottom:1px solid var(--border-color);display:flex;align-items:center;gap:8px;';
  hdr.innerHTML = `<i class="bi bi-filetype-html" style="color:var(--accent2);font-size:1rem;"></i><span style="font-family:'Orbitron',sans-serif;font-size:0.52rem;color:var(--accent2);letter-spacing:1px;flex:1;">EXPORT HTML</span>`;
  const closeBtn = document.createElement('div');
  closeBtn.style.cssText = 'width:24px;height:24px;border-radius:4px;background:rgba(255,77,106,0.08);border:1px solid var(--danger);color:var(--danger);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:0.45rem;';
  closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
  closeBtn.onclick = () => ov.remove();
  hdr.appendChild(closeBtn);
  box.appendChild(hdr);

  const body = document.createElement('div');
  body.style.cssText = 'padding:18px;';

  function lbl(t) { const s = document.createElement('div'); s.style.cssText = 'font-size:0.42rem;color:var(--text-muted);margin-bottom:4px;margin-top:10px;'; s.textContent = t; return s; }

  // Page Title
  body.appendChild(lbl('Page Title'));
  const titleInp = document.createElement('input'); titleInp.className = 'pp-input';
  titleInp.value = data.name || 'My UI'; titleInp.style.width = '100%';
  body.appendChild(titleInp);

  // Background
  body.appendChild(lbl('Page Background'));
  const bgInp = document.createElement('input'); bgInp.className = 'pp-input';
  bgInp.value = '#000000'; bgInp.placeholder = '#000000 or gradient CSS'; bgInp.style.width = '100%';
  body.appendChild(bgInp);

  // Canvas
  body.appendChild(lbl('3D Canvas'));
  const canvasChk = document.createElement('div');
  canvasChk.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:4px;';
  const chk = document.createElement('input'); chk.type = 'checkbox'; chk.id = 'exp-canvas-chk'; chk.checked = false;
  const chkLbl = document.createElement('label'); chkLbl.htmlFor = 'exp-canvas-chk'; chkLbl.style.cssText = 'font-size:0.45rem;color:var(--text-secondary);cursor:pointer;'; chkLbl.textContent = 'Include canvas container for 3D engine (Three.js / Babylon.js)';
  canvasChk.appendChild(chk); canvasChk.appendChild(chkLbl);
  body.appendChild(canvasChk);

  // Scaling mode — how the design maps onto the visitor's screen
  body.appendChild(lbl('Screen Fitting'));
  const scaleSel = document.createElement('select');
  scaleSel.className = 'pp-select'; scaleSel.style.width = '100%';
  [['fit','Keep design proportions (scale to fit) — matches the builder'],
   ['fill','Fill the screen (scale to cover, edges may crop)'],
   ['stretch','Stretch to window (old behaviour — px sizes do not scale)']].forEach(([v,t]) => {
    const o = document.createElement('option');
    o.value = v; o.textContent = t;
    if (v === projectScaleMode) o.selected = true;
    scaleSel.appendChild(o);
  });
  scaleSel.onchange = () => { projectScaleMode = scaleSel.value; };
  body.appendChild(scaleSel);
  const scaleNote = document.createElement('div');
  scaleNote.style.cssText = 'font-size:0.42rem;color:var(--text-muted);padding:4px 0 0;line-height:1.35;';
  scaleNote.textContent = 'The canvas designs at ' + DEVICE_SIZES.desktop.w + '×' + DEVICE_SIZES.desktop.h +
    ' (desktop). "Scale to fit" reproduces it exactly on any screen; the other modes let font and image sizes drift.';
  body.appendChild(scaleNote);

  // Game level JSON path
  body.appendChild(lbl('Game Level JSON (optional — loads 3D scene at runtime)'));
  const glInp = document.createElement('input'); glInp.className = 'pp-input';
  glInp.value = ''; glInp.placeholder = './levels/level1.json'; glInp.style.width = '100%';
  body.appendChild(glInp);

  // Base64 embed option
  body.appendChild(lbl('Asset Embedding'));
  const b64Chk = document.createElement('div');
  b64Chk.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:4px;';
  const chk2 = document.createElement('input'); chk2.type = 'checkbox'; chk2.id = 'exp-b64-chk'; chk2.checked = false;
  const chk2Lbl = document.createElement('label'); chk2Lbl.htmlFor = 'exp-b64-chk'; chk2Lbl.style.cssText = 'font-size:0.45rem;color:var(--text-secondary);cursor:pointer;'; chk2Lbl.textContent = 'Embed all images as base64 (fully self-contained, larger file size)';
  b64Chk.appendChild(chk2); b64Chk.appendChild(chk2Lbl);
  body.appendChild(b64Chk);

  const vidChk = document.createElement('div');
  vidChk.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:4px;';
  const chk3 = document.createElement('input'); chk3.type = 'checkbox'; chk3.id = 'exp-vid-chk';
  const chromaEls = (data.elements || []).filter(e => e.type === 'video' && e.chromaKey && e.src && !String(e.src).startsWith('data:'));
  chk3.checked = chromaEls.length > 0;
  const chk3Lbl = document.createElement('label'); chk3Lbl.htmlFor = 'exp-vid-chk';
  chk3Lbl.style.cssText = 'font-size:0.45rem;color:var(--text-secondary);cursor:pointer;';
  chk3Lbl.textContent = 'Embed chroma-keyed videos as base64 (required for transparency)';
  vidChk.appendChild(chk3); vidChk.appendChild(chk3Lbl);
  body.appendChild(vidChk);
  // Collect every 3D model the project references (elements + carousel cards)
  const modelRefs = [];
  (data.elements || []).forEach(e => {
    if (e.type === 'model3d' && e.src && !String(e.src).startsWith('data:')) modelRefs.push({ get: () => e.src, set: v => { e.src = v; } });
    (e.carouselItems || []).forEach(it => {
      if (it.model && !String(it.model).startsWith('data:')) modelRefs.push({ get: () => it.model, set: v => { it.model = v; } });
    });
    (e.heroSlides || []).forEach(sl => {
      if (sl.model && !String(sl.model).startsWith('data:')) modelRefs.push({ get: () => sl.model, set: v => { sl.model = v; } });
    });
  });

  const glbChk = document.createElement('div');
  glbChk.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:4px;';
  const chk4 = document.createElement('input'); chk4.type = 'checkbox'; chk4.id = 'exp-glb-chk';
  chk4.checked = modelRefs.length > 0;
  const chk4Lbl = document.createElement('label'); chk4Lbl.htmlFor = 'exp-glb-chk';
  chk4Lbl.style.cssText = 'font-size:0.45rem;color:var(--text-secondary);cursor:pointer;';
  chk4Lbl.textContent = 'Embed 3D models (.glb) as base64 — needed to open the file directly';
  glbChk.appendChild(chk4); glbChk.appendChild(chk4Lbl);
  body.appendChild(glbChk);
  if (modelRefs.length) {
    const gw = document.createElement('div');
    gw.style.cssText = 'font-size:0.4rem;color:var(--accent3);line-height:1.35;padding:2px 0 0;';
    gw.textContent = '⚠ ' + modelRefs.length + ' 3D model' + (modelRefs.length === 1 ? '' : 's') +
      ' referenced. A browser refuses to fetch .glb files from a file:// page, so an export opened straight from disk ' +
      'shows "could not load". Embedding them (or serving the folder over http) fixes it.';
    body.appendChild(gw);
  }

  if (chromaEls.length) {
    const warn = document.createElement('div');
    warn.style.cssText = 'font-size:0.4rem;color:var(--accent3);line-height:1.35;padding:2px 0 0;';
    warn.textContent = '⚠ ' + chromaEls.length + ' chroma-keyed video' + (chromaEls.length === 1 ? '' : 's') +
      ' found. Keying reads the video frames, which a browser refuses to do for a file:// or cross-origin source — ' +
      'so an exported page opened straight from disk shows the raw video. Embedding it keeps the transparency working everywhere.';
    body.appendChild(warn);
  }

  // Status text
  const statusEl = document.createElement('div');
  statusEl.style.cssText = 'font-size:0.4rem;color:var(--text-muted);min-height:14px;margin-top:4px;';
  body.appendChild(statusEl);

  // Export button
  const expBtn = document.createElement('button'); expBtn.className = 'pp-btn';
  expBtn.style.cssText = 'width:100%;margin-top:12px;padding:10px;background:rgba(167,139,250,0.08);border-color:var(--accent2);color:var(--accent2);font-size:0.5rem;';
  expBtn.innerHTML = '<i class="bi bi-download"></i> Export HTML';
  expBtn.onclick = async () => {
    const embedB64 = chk2.checked;
    const embedVideos = chk3.checked;
    let exportData = JSON.parse(JSON.stringify(data));

    if (chk4.checked && modelRefs.length) {
      expBtn.disabled = true;
      // exportData is a deep copy, so re-collect against it
      const refs = [];
      (exportData.elements || []).forEach(e => {
        if (e.type === 'model3d' && e.src && !String(e.src).startsWith('data:')) refs.push({ get: () => e.src, set: v => { e.src = v; } });
        (e.carouselItems || []).forEach(it => {
          if (it.model && !String(it.model).startsWith('data:')) refs.push({ get: () => it.model, set: v => { it.model = v; } });
        });
        (e.heroSlides || []).forEach(sl => {
          if (sl.model && !String(sl.model).startsWith('data:')) refs.push({ get: () => sl.model, set: v => { sl.model = v; } });
        });
      });
      const cache = {};
      for (const ref of refs) {
        const url = ref.get();
        try {
          if (cache[url]) { ref.set(cache[url]); continue; }
          statusEl.textContent = 'Embedding model: ' + url + '…';
          const resp = await fetch(url);
          if (!resp.ok) throw new Error(resp.statusText);
          const blob = await resp.blob();
          const dataUrl = await new Promise((res, rej) => {
            const r = new FileReader();
            r.onload = () => res(r.result);
            r.onerror = rej;
            r.readAsDataURL(blob);
          });
          cache[url] = dataUrl;
          ref.set(dataUrl);
          statusEl.textContent = 'Embedded ' + (blob.size / 1048576).toFixed(1) + 'MB model';
        } catch(err) {
          statusEl.textContent = 'Could not embed ' + url + ' (' + err.message + ')';
          statusEl.style.color = '#ff6b6b';
        }
      }
      expBtn.disabled = false;
    }

    if (embedVideos) {
      expBtn.disabled = true;
      const vids = (exportData.elements || []).filter(e => e.type === 'video' && e.chromaKey && e.src && !String(e.src).startsWith('data:'));
      for (const ve of vids) {
        try {
          statusEl.textContent = 'Embedding video: ' + ve.src + '…';
          const resp = await fetch(ve.src);
          if (!resp.ok) throw new Error(resp.statusText);
          const blob = await resp.blob();
          ve.src = await new Promise((res, rej) => {
            const r = new FileReader();
            r.onload = () => res(r.result);
            r.onerror = rej;
            r.readAsDataURL(blob);
          });
          statusEl.textContent = 'Embedded ' + (blob.size / 1048576).toFixed(1) + 'MB video';
        } catch(err) {
          statusEl.textContent = 'Could not embed ' + ve.src + ' (' + err.message + ') — chroma key may not work from file://';
          statusEl.style.color = '#ff6b6b';
        }
      }
      expBtn.disabled = false;
    }

    if (embedB64) {
      expBtn.disabled = true;
      expBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Converting images...';
      statusEl.textContent = 'Converting images to base64...';
      try {
        exportData = await embedAllImagesAsBase64(exportData, (msg) => { statusEl.textContent = msg; });
        statusEl.textContent = 'Conversion complete!';
      } catch(e) {
        console.warn('Base64 embed error:', e);
        statusEl.textContent = 'Some images could not be converted — exported with original URLs';
      }
      expBtn.disabled = false;
      expBtn.innerHTML = '<i class="bi bi-download"></i> Export HTML';
    }

    exportData.scaleMode = projectScaleMode;
    exportData.designSizes = DEVICE_SIZES;
    const html = generateStandaloneHTML(exportData, {
      title: titleInp.value,
      background: bgInp.value,
      includeCanvas: chk.checked,
      gameLevelJson: glInp.value.trim()
    });
    const filename = (data.name || 'export').replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
    a.download = `${filename}_${fileTimestamp()}.html`;
    a.click();
    URL.revokeObjectURL(a.href);
    ov.remove();
    showToast(`Exported "${titleInp.value}" as HTML`);
  };
  body.appendChild(expBtn);

  box.appendChild(body);
  ov.appendChild(box);
  ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); });
  document.body.appendChild(ov);
}

// ══════════════════════════════════════
//  BASE64 IMAGE EMBEDDER
// ══════════════════════════════════════
async function embedAllImagesAsBase64(data, onStatus) {
  const cache = {}; // url → dataURI

  async function toBase64(url) {
    if (!url || url.startsWith('data:')) return url; // already base64
    if (cache[url]) return cache[url];
    try {
      const resp = await fetch(url);
      if (!resp.ok) throw new Error(resp.status);
      const blob = await resp.blob();
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => { cache[url] = reader.result; resolve(reader.result); };
        reader.onerror = reject;
        reader.readAsDataURL(blob);
      });
    } catch(e) {
      console.warn('Could not convert to base64:', url, e);
      return url; // return original on failure
    }
  }

  // Collect all image URLs
  const urls = new Set();
  if (data.bgImage) urls.add(data.bgImage);
  (data.elements || []).forEach(el => {
    if (el.src && el.type !== 'video') urls.add(el.src); // video files are skipped (too heavy) — keep as paths
    if (el.hoverSrc) urls.add(el.hoverSrc);
    if (el.activeSrc) urls.add(el.activeSrc);
    if (el.videoPoster) urls.add(el.videoPoster);
    (el.carouselItems || []).forEach(it => { if (it.image) urls.add(it.image); });
    (el.heroSlides || []).forEach(sl => { if (sl.image) urls.add(sl.image); });
  });

  // Filter out already-base64 ones
  const toConvert = [...urls].filter(u => u && !u.startsWith('data:'));
  if (toConvert.length === 0) return data;

  // Convert all in parallel
  let done = 0;
  await Promise.all(toConvert.map(async (url) => {
    await toBase64(url);
    done++;
    if (onStatus) onStatus(`Converting images... ${done}/${toConvert.length}`);
  }));

  // Replace URLs in data
  if (data.bgImage && cache[data.bgImage]) data.bgImage = cache[data.bgImage];
  (data.elements || []).forEach(el => {
    if (el.src && cache[el.src]) el.src = cache[el.src];
    if (el.hoverSrc && cache[el.hoverSrc]) el.hoverSrc = cache[el.hoverSrc];
    if (el.activeSrc && cache[el.activeSrc]) el.activeSrc = cache[el.activeSrc];
    if (el.videoPoster && cache[el.videoPoster]) el.videoPoster = cache[el.videoPoster];
    (el.carouselItems || []).forEach(it => { if (it.image && cache[it.image]) it.image = cache[it.image]; });
    (el.heroSlides || []).forEach(sl => { if (sl.image && cache[sl.image]) sl.image = cache[sl.image]; });
  });

  return data;
}

// ══════════════════════════════════════
//  STANDALONE HTML GENERATOR
// ══════════════════════════════════════
function generateStandaloneHTML(data, opts) {
  const esc = s => s.replace(/</g,'\\x3c').replace(/>/g,'\\x3e');
  const elems = data.elements || [];
  const customJS = data.customCode || '';
  const hasCarousel = elems.some(e => e.type === 'carousel');

  // Collect all unique font families
  const fonts = new Set(['Rajdhani','Orbitron']);
  elems.forEach(el => {
    if (el.fontFamily) {
      const match = el.fontFamily.match(/['"]([^'"]+)['"]/);
      if (match) fonts.add(match[1]);
    }
  });
  const fontImport = [...fonts].map(f => `family=${f.replace(/ /g,'+')}`).join('&');

  // Generate override CSS
  function genOverrideCss(ov, elId, fontUnit) {
    let css = '';
    if (!ov || typeof ov !== 'object') return css;
    if (ov.visible === false) css += `  #gui-${elId} { display:none !important; }\n`;
    if (ov.x !== undefined) css += `  #gui-${elId} { left:${ov.x}% !important; }\n`;
    if (ov.y !== undefined) css += `  #gui-${elId} { top:${ov.y}% !important; }\n`;
    if (ov.w !== undefined) css += `  #gui-${elId} { width:${ov.w > 0 ? ov.w + '%' : 'auto'} !important; }\n`;
    if (ov.h !== undefined) css += `  #gui-${elId} { height:${ov.h > 0 ? ov.h + '%' : 'auto'} !important; }\n`;
    if (ov.anchor) css += `  #gui-${elId} { transform:${uiAnchorTransform(ov.anchor)} !important; }\n`;
    ['fontSize','fontWeight','padding','background','borderRadius','border','color','textAlign','boxShadow','letterSpacing'].forEach(prop => {
      if (ov[prop] !== undefined) {
        const val = prop === 'fontSize' ? ov[prop] + (ov.fontUnit || fontUnit || 'rem') : ov[prop];
        css += `  #gui-${elId} .gui-inner { ${prop.replace(/[A-Z]/g,c=>'-'+c.toLowerCase())}:${val} !important; }\n`;
      }
    });
    if (ov.opacity !== undefined) css += `  #gui-${elId} { opacity:${ov.opacity} !important; }\n`;
    return css;
  }

  const deviceQueries = {
    tablet: '@media (max-width:1024px) and (min-width:481px)',
    phone: '@media (max-width:480px) and (orientation:portrait)',
    'phone-land': '@media (max-width:768px) and (orientation:landscape)',
  };
  const showOnQueries = {
    desktop: '@media (min-width:1025px)',
    tablet: '@media (max-width:1024px) and (min-width:481px)',
    phone: '@media (max-width:480px) and (orientation:portrait)',
    'phone-land': '@media (max-width:768px) and (orientation:landscape)',
  };

  let responsiveCss = '';
  elems.forEach(el => {
    if (el.overrides) {
      for (const [dev, query] of Object.entries(deviceQueries)) {
        const css = genOverrideCss(el.overrides[dev], el.id, el.fontUnit);
        if (css) responsiveCss += `${query} {\n${css}}\n`;
      }
    }
    if (el.mobileHidden) responsiveCss += `@media (max-width:768px) { #gui-${el.id} { display:none !important; } }\n`;
    if (el.showOn && el.showOn.length > 0) {
      for (const [dev, query] of Object.entries(showOnQueries)) {
        if (!el.showOn.includes(dev)) {
          responsiveCss += `${query} { #gui-${el.id} { display:none !important; } }\n`;
        }
      }
    }
  });

  return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>${opts.title || 'My UI'}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?${fontImport}&display=swap" rel="stylesheet">
${hasCarousel ? `<link href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.theme.default.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"><` + `/script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"><` + `/script>` : ''}
<style>
/* ══════════════════════════════════════
   CONFIG — Edit these to customize
   ══════════════════════════════════════ */
:root {
  --page-bg: ${opts.background || '#000000'};
  --game-level-json: '${opts.gameLevelJson || ''}'; /* Path to game level JSON (for reference) */
}

* { margin:0; padding:0; box-sizing:border-box; }
body {
  overflow:hidden;
  background: var(--page-bg);
  font-family:'Rajdhani',sans-serif;
  color:#e0e8f0;
  width:100vw; height:100vh;
  position:relative;
}
${opts.includeCanvas ? `
/* ── 3D Canvas Container ── */
#canvas-container {
  position:fixed; inset:0;
  z-index:1;
}
#canvas-container canvas {
  width:100%; height:100%; display:block;
}
` : ''}
/* ── UI Overlay ── */
#game-ui-overlay {
  position:fixed; top:0; left:0; width:100%; height:100%;
  pointer-events:none; z-index:100; overflow:hidden;
}
.gui-el {
  position:absolute; pointer-events:auto;
  transition: opacity 0.2s;
}
.gui-el.gui-hidden { display:none; }
.gui-progressbar-track {
  width:100%; overflow:hidden; position:relative;
}
.gui-progressbar-fill {
  height:100%; transition:width 0.3s ease;
}
.gui-progressbar-label {
  position:absolute; left:6px; top:50%; transform:translateY(-50%);
  font-family:'Rajdhani',sans-serif; font-size:0.55rem; font-weight:700;
  color:rgba(255,255,255,0.8); text-shadow:0 1px 2px rgba(0,0,0,0.5); z-index:1;
}
@media (max-width:768px) {
  .gui-el.gui-mobile-hidden { display:none !important; }
}
${responsiveCss}
</style>
</head>
<body>
${opts.includeCanvas ? `
<!-- ═══ 3D Canvas ═══ -->
<div id="canvas-container">
  <!-- Your 3D engine renders here (Three.js / Babylon.js / PlayCanvas) -->
</div>
` : ''}
<!-- ═══ UI Overlay ═══ -->
<div id="game-ui-overlay"></div>

<script>
// ══════════════════════════════════════
// CONFIG
// ══════════════════════════════════════
const CONFIG = {
  gameLevelJson: '${opts.gameLevelJson || ''}',  // Set path to load a 3D game level
  includeCanvas: ${opts.includeCanvas ? 'true' : 'false'},
};

// ══════════════════════════════════════
// UI DATA (embedded from UI Builder)
// ══════════════════════════════════════
const UI_DATA = ${JSON.stringify(data, null, 2)};

// ══════════════════════════════════════
// GAME API BRIDGE (for UI ↔ Game communication)
// ══════════════════════════════════════
const gameUIState = {};
const gameUIElements = {};
let gameUIData = null;

window._gameGlobals = window._gameGlobals || {};
const GameAPI = {
  globals: window._gameGlobals,
  setGlobal(name, value){ window._gameGlobals[name] = value; },
  getGlobal(name){ return window._gameGlobals[name]; },
  setUIValue(key, val) {
    gameUIState[key] = val;
    updateGameUI();
  },
  getUIValue(key) { return gameUIState[key]; },
  setUIVisible(elId, visible) {
    const entry = gameUIElements[elId];
    if (!entry) return;
    if (visible) { entry.dom.classList.remove('gui-hidden'); entry.data.visible = true; }
    else { entry.dom.classList.add('gui-hidden'); entry.data.visible = false; }
  },
  setUIContent(elId, text) {
    const entry = gameUIElements[elId];
    if (!entry) return;
    entry.data.content = text;
    const inner = entry.dom.querySelector('.gui-inner');
    if (inner) inner.textContent = resolveBindingText(text, entry.data.binding);
  },
  setUIStyle(elId, prop, val) {
    const entry = gameUIElements[elId];
    if (!entry) return;
    const inner = entry.dom.querySelector('.gui-inner');
    if (inner) inner.style[prop] = val;
  },
  // ── Video control ──
  getUIVideo(elId) { const e = gameUIElements[elId]; return e ? e.dom.querySelector('video') : null; },
  playVideo(elId) { const v = this.getUIVideo(elId); if (v) v.play().catch(function(){}); },
  pauseVideo(elId) { const v = this.getUIVideo(elId); if (v) v.pause(); },
  toggleVideo(elId) { const v = this.getUIVideo(elId); if (v) { v.paused ? v.play().catch(function(){}) : v.pause(); } },
  stopVideo(elId) { const v = this.getUIVideo(elId); if (v) { v.pause(); try { v.currentTime = 0; } catch(e) {} } },
  restartVideo(elId) { const v = this.getUIVideo(elId); if (v) { try { v.currentTime = 0; } catch(e) {} v.play().catch(function(){}); } },
  setVideoSrc(elId, src) { const v = this.getUIVideo(elId); if (v) { v.src = src; v.load(); } },
  // ── Carousel control ──
  carouselNext(elId) { const e = gameUIElements[elId]; if (e) controlUICarousel(e.dom, 'next'); },
  carouselPrev(elId) { const e = gameUIElements[elId]; if (e) controlUICarousel(e.dom, 'prev'); },
  carouselGoTo(elId, i) { const e = gameUIElements[elId]; if (e) controlUICarousel(e.dom, 'goTo', i); },
  carouselPlay(elId) { const e = gameUIElements[elId]; if (e) controlUICarousel(e.dom, 'play'); },
  carouselStop(elId) { const e = gameUIElements[elId]; if (e) controlUICarousel(e.dom, 'stop'); },
  carouselRefresh(elId) { const e = gameUIElements[elId]; if (e) controlUICarousel(e.dom, 'refresh'); },
  // ── Hero section ──
  heroNext(elId) { const e = gameUIElements[elId]; if (e) controlUIHero(e.dom, 'next'); },
  heroPrev(elId) { const e = gameUIElements[elId]; if (e) controlUIHero(e.dom, 'prev'); },
  heroGoTo(elId, i) { const e = gameUIElements[elId]; if (e) controlUIHero(e.dom, 'goTo', i); },
  heroPlay(elId) { const e = gameUIElements[elId]; if (e) controlUIHero(e.dom, 'play'); },
  heroStop(elId) { const e = gameUIElements[elId]; if (e) controlUIHero(e.dom, 'stop'); },
  // ── Embedded pages ──
  reloadFrame(elId) { const e = gameUIElements[elId]; if (e) controlUIFrame(e.dom, 'reload'); },
  setFrameSrc(elId, url) { const e = gameUIElements[elId]; if (e) controlUIFrame(e.dom, 'setSrc', url); },
  getFrame(elId) { const e = gameUIElements[elId]; return e ? e.dom.querySelector('iframe') : null; },
  // ── 3D models ──
  spinModel(elId, on) { const e = gameUIElements[elId]; if (e) controlUIModel(e.dom, on === false ? 'stop' : 'spin'); },
  resetModel(elId) { const e = gameUIElements[elId]; if (e) controlUIModel(e.dom, 'reset'); },
  setModelSrc(elId, url) { const e = gameUIElements[elId]; if (e) controlUIModel(e.dom, 'setModel', url); },
  getModelApi(elId) {
    const e = gameUIElements[elId];
    const host = e ? e.dom.querySelector('.ui-model') : null;
    return host ? host._modelApi : null;
  },
  setHeroSlides(elId, slides) {
    const e = gameUIElements[elId];
    if (!e) return;
    e.data.heroSlides = slides || [];
    const inner = e.dom.querySelector('.gui-inner');
    if (inner) renderUIHero(inner, e.data, true);
  },
  // ── Animation sequence ──
  playAnimation(elId, stage) {
    const e = gameUIElements[elId];
    if (!e) return;
    const inner = e.dom.querySelector('.gui-inner');
    if (inner) runUIAnimSequence(inner, e.data, stage || 'all');
  },
  stopAnimation(elId) { this.playAnimation(elId, 'stop'); },
  setCarouselItems(elId, items) {
    const e = gameUIElements[elId];
    if (!e) return;
    e.data.carouselItems = items || [];
    const inner = e.dom.querySelector('.gui-inner');
    if (inner) renderUICarousel(inner, e.data, true);
  },
  playAudio(url) {
    try { new Audio(url).play().catch(e => console.warn(e)); } catch(e) {}
  },
  showToast(msg, dur) {
    const t = document.createElement('div');
    t.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#fff;padding:8px 20px;border-radius:8px;font-size:0.8rem;z-index:9999;pointer-events:none;transition:opacity 0.3s;';
    t.textContent = msg;
    document.body.appendChild(t);
    setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 300); }, dur || 2500);
  },
  // Keyboard simulation for mobile controls
  pressKey(k) { if (window.playerKeys) window.playerKeys[k] = true; },
  releaseKey(k) { if (window.playerKeys) window.playerKeys[k] = false; },
  simulateKey(k, dur) { GameAPI.pressKey(k); setTimeout(() => GameAPI.releaseKey(k), dur || 100); },
  releaseAllKeys() { if (window.playerKeys) Object.keys(window.playerKeys).forEach(k => window.playerKeys[k] = false); },
};
window.GameAPI = GameAPI;
window.playerKeys = {
    _globals: {},
    setGlobal(name, value) { this._globals[name] = value; },
    getGlobal(name) { return this._globals[name]; }, w:false, a:false, s:false, d:false, shift:false };

// ══════════════════════════════════════
// CUSTOM CODE (from UI Builder Code tab)
// ══════════════════════════════════════
${customJS}

// ══════════════════════════════════════
// UI RENDERER
// ══════════════════════════════════════
function resolveBindingText(content, binding) {
  if (!content) return '';
  return content.replace(/\\{(\\w+)\\}/g, (match, key) => {
    return gameUIState[key] !== undefined ? gameUIState[key] : match;
  });
}

/* ── Multi-stage element animation (entrance → attention → exit) ── */
function uiAnimHasSequence(el) {
  return !!(el.animEnterClass || el.animAttnClass || el.animExitClass || el.animateClass);
}

function clearUIAnim(inner) {
  if (!inner) return;
  if (inner._animTimers) inner._animTimers.forEach(function(t) { window.clearTimeout(t); });
  inner._animTimers = [];
  Array.prototype.slice.call(inner.classList).forEach(function(c) {
    if (c.indexOf('animate__') === 0) inner.classList.remove(c);
  });
  inner.style.removeProperty('--animate-duration');
  inner.style.removeProperty('animation-delay');
  inner.style.removeProperty('animation-duration');
  inner.style.removeProperty('animation-iteration-count');
  inner.style.removeProperty('opacity');
  inner.style.removeProperty('pointer-events');
}

/** Plays one animate.css class. \`done\` is not called for infinite repeats. */
function playUIAnimStage(inner, cls, durationMs, repeat, done) {
  if (!inner) return;
  if (!cls) { if (done) done(); return; }
  Array.prototype.slice.call(inner.classList).forEach(function(c) {
    if (c.indexOf('animate__') === 0) inner.classList.remove(c);
  });
  void inner.offsetWidth; // reflow so the animation can restart
  var dur = parseInt(durationMs);
  if (!dur || dur < 0) dur = 800;
  var infinite = (String(repeat) === 'infinite');
  var rep = infinite ? 'infinite' : (parseInt(repeat) || 1);
  inner.style.setProperty('--animate-duration', (dur / 1000) + 's');
  inner.style.animationDuration = (dur / 1000) + 's';
  inner.style.animationIterationCount = rep;
  inner.classList.add('animate__animated', cls.indexOf('animate__') === 0 ? cls : 'animate__' + cls);
  if (infinite || !done) return;
  var t = window.setTimeout(done, dur * rep + 30);
  (inner._animTimers = inner._animTimers || []).push(t);
}

/**
 * Runs the element's animation sequence.
 * stage: 'all' (default) | 'entrance' | 'attention' | 'exit' | 'stop'
 */
function runUIAnimSequence(inner, el, stage) {
  if (!inner || !el) return;
  stage = stage || 'all';
  clearUIAnim(inner);
  if (stage === 'stop') return;
  inner._animTimers = inner._animTimers || [];

  // Legacy single-animation projects behave like an entrance
  var enterCls = el.animEnterClass || (!el.animAttnClass && !el.animExitClass ? el.animateClass : '') || '';
  var enterDur = el.animEnterDuration !== undefined && el.animEnterDuration !== ''
    ? el.animEnterDuration
    : (parseFloat(el.animateDuration) ? parseFloat(el.animateDuration) * 1000 : 800);
  var enterDelay = el.animEnterDelay !== undefined && el.animEnterDelay !== ''
    ? el.animEnterDelay
    : (parseFloat(el.animateDelay) ? parseFloat(el.animateDelay) * 1000 : 0);
  var enterRep = el.animEnterRepeat || (el.animateRepeat && !el.animEnterClass ? el.animateRepeat : 1);

  function wait(ms, fn) {
    var t = window.setTimeout(fn, Math.max(0, parseInt(ms) || 0));
    inner._animTimers.push(t);
    return t;
  }

  function doExit() {
    if (!el.animExitClass) return;
    wait(el.animExitDelay, function() {
      playUIAnimStage(inner, el.animExitClass, el.animExitDuration, el.animExitRepeat || 1, function() {
        if (el.animExitHide !== false) {
          inner.style.opacity = '0';
          inner.style.pointerEvents = 'none';
        }
      });
    });
  }

  function doAttention(next) {
    if (!el.animAttnClass) { if (next) next(); return; }
    wait(el.animAttnDelay, function() {
      var infinite = String(el.animAttnRepeat) === 'infinite';
      playUIAnimStage(inner, el.animAttnClass, el.animAttnDuration, el.animAttnRepeat || 1, infinite ? null : next);
      if (infinite && next) next(); // exit is timed from when the loop starts
    });
  }

  // While an entrance is waiting out its delay, keep the element invisible —
  // otherwise it pops in first and only then animates.
  function armEntrance(then) {
    var d = Math.max(0, parseInt(enterDelay) || 0);
    if (enterCls && d > 0) inner.style.opacity = '0';
    wait(d, function() {
      inner.style.removeProperty('opacity');
      then();
    });
  }

  if (stage === 'entrance') { armEntrance(function() { playUIAnimStage(inner, enterCls, enterDur, enterRep, null); }); return; }
  if (stage === 'attention') { doAttention(null); return; }
  if (stage === 'exit') { doExit(); return; }

  // Full sequence
  if (enterCls) {
    armEntrance(function() {
      playUIAnimStage(inner, enterCls, enterDur, enterRep, function() { doAttention(doExit); });
    });
  } else {
    doAttention(doExit);
  }
}

/* ── Carousel runtime (Owl Carousel 2) ── */
function escAttr(s) { return String(s == null ? '' : s).replace(/"/g, '&quot;'); }

/** Overlay image: an absolutely placed box (margin + padding) holding a sized image */
function carFgHTML(el, item, boxStyle, isCover) {
  var pad = item.fgPadding !== undefined && item.fgPadding !== '' ? item.fgPadding : el.carFgPadding;
  var w = item.fgWidth !== undefined && item.fgWidth !== '' ? item.fgWidth : el.carFgWidth;
  var hgt = item.fgHeight !== undefined && item.fgHeight !== '' ? item.fgHeight : el.carFgHeight;
  var fit = item.fgFit || el.carFgFit || 'contain';
  if (fit === 'stretch') fit = 'fill';
  var imgStyle = 'width:' + cssLen(w, '100%') + ';height:' + cssLen(hgt, '100%') +
    ';object-fit:' + fit + ';';
  return '<div class="car-fg-box' + (isCover ? ' car-fg-cover' : '') + '" style="' + boxStyle +
    'padding:' + cssLen(pad, '0px') + ';">' +
    '<img class="car-fg" src="' + escAttr(item.foreground) + '" style="' + imgStyle + '" draggable="false"></div>';
}

function carouselItemInnerHTML(el, item) {
  if (el.carItemTemplate) {
    var out = el.carItemTemplate
      .replace(/\\{image\\}/g, item.image || '')
      .replace(/\\{title\\}/g, item.title || '')
      .replace(/\\{text\\}/g, item.text || '')
      .replace(/\\{link\\}/g, item.link || '');
    // Any custom field can be used as {fieldName}
    if (item.fields) {
      Object.keys(item.fields).forEach(function(k) {
        out = out.split('{' + k + '}').join(item.fields[k] == null ? '' : item.fields[k]);
      });
    }
    return out;
  }
  var h = '';
  var cover = el.carImgMode === 'cover';
  // What sits on top of the card image: a 3D model, another image, or nothing
  var layer = item.layer || (item.model ? 'model' : (item.foreground ? 'image' : 'none'));
  var hasModel = layer === 'model' && item.model;
  var hasFg = layer === 'image' && item.foreground;
  var fgInset = item.fgInset !== undefined && item.fgInset !== '' ? item.fgInset : el.carFgInset;
  var fgStyle = 'inset:' + cssLen(fgInset, '0px') + ';';

  if (cover) {
    // The image fills the whole card and everything else stacks on top of it
    if (el.carShowImage !== false && item.image) {
      h += '<img class="car-bgimg" src="' + escAttr(item.image) + '" alt="' + escAttr(item.title) + '" draggable="false">';
    }
    if (hasModel) h += '<div class="car-model car-model-cover" data-model="' + escAttr(item.model) + '"></div>';
    else if (hasFg) h += carFgHTML(el, item, fgStyle, true);
  } else if (hasModel || hasFg) {
    // Background image keeps its normal size; the model / overlay image is layered over it,
    // so a card with either is exactly the same size as one without.
    h += '<div class="car-media">';
    if (el.carShowImage !== false && item.image) {
      h += '<img class="car-img" src="' + escAttr(item.image) + '" alt="' + escAttr(item.title) + '" draggable="false">';
    }
    if (hasModel) h += '<div class="car-model" data-model="' + escAttr(item.model) + '"></div>';
    else h += carFgHTML(el, item, fgStyle, false);
    h += '</div>';
  } else if (el.carShowImage !== false && item.image) {
    h += '<img class="car-img" src="' + escAttr(item.image) + '" alt="' + escAttr(item.title) + '" draggable="false">';
  }
  if (el.carShowTitle !== false && item.title) h += '<div class="car-title">' + item.title + '</div>';
  if (el.carShowText !== false && item.text) h += '<div class="car-text">' + item.text + '</div>';
  // Extra fields marked "show in card"
  (el.carShowFields || []).forEach(function(key) {
    var v = item.fields ? item.fields[key] : undefined;
    if (v !== undefined && v !== '') h += '<div class="car-field car-field-' + escAttr(key).replace(/[^\\w-]/g, '') + '">' + v + '</div>';
  });
  return h;
}

/** Active-item settings can differ per device; these are the keys that support it */
var CAR_DEVICE_PROPS = ['carActiveSlot','carCenterScale','carSideScale','carSideOpacity',
  'carCenterOffsetX','carCenterOffsetY','carCenterWidth','carCenterHeight',
  'carCenterPadding','carCenterMargin','carCenterRadius',
  // breathing room around the strip, per-device
  'carPadTop','carPadBottom',
  // arrow + dot placement is per-device too
  'carNavInset','carNavOffsetX','carNavOffsetY',
  'carNavPrevX','carNavPrevY','carNavNextX','carNavNextY',
  'carDotOffsetX','carDotOffsetY','carDotMarginTop','carDotBottom'];

/** el merged with its overrides for \`device\` ('tablet' | 'phone' | 'phone-land') */
function carMergedFor(el, device) {
  var ov = (device && el.carDeviceOverrides) ? el.carDeviceOverrides[device] : null;
  if (!ov) return el;
  var m = {}, k;
  for (k in el) m[k] = el[k];
  CAR_DEVICE_PROPS.forEach(function(prop) {
    if (ov[prop] !== undefined && ov[prop] !== null && ov[prop] !== '') m[prop] = ov[prop];
    else if (prop === 'carActiveSlot' && ov[prop] === '') m[prop] = '';
  });
  return m;
}

function carHasDeviceOverride(el, device) {
  var ov = (el.carDeviceOverrides || {})[device];
  if (!ov) return false;
  return CAR_DEVICE_PROPS.some(function(p) { return ov[p] !== undefined && ov[p] !== null; });
}

/** Explicit active-slot mode: the active card sits in a chosen visible slot
 *  instead of being geometrically centred (e.g. 1 card left, 2 cards right). */
function carActiveSlotOf(el, perView) {
  var v = el.carActiveSlot;
  if (v === '' || v === undefined || v === null || v === 'auto') return null;
  var n = parseInt(v);
  if (isNaN(n)) return null;
  return Math.max(0, Math.min((parseInt(perView) || 3) - 1, n));
}

function carUsesSlotMode(el) {
  return carActiveSlotOf(el, el.carItems) !== null;
}

/** Media queries matching the builder's device modes */
var CAR_DEVICE_QUERIES = {
  tablet: ['@media (max-width:1024px) and (min-width:481px)'],
  phone: ['@media (max-width:480px) and (orientation:portrait)'],
  'phone-land': ['@media (orientation:landscape) and (max-height:500px)', '@media (max-width:768px) and (orientation:landscape)']
};

/** The active-item + stage-shift rules for one configuration (base or a device override) */
function carActiveRules(el, cfg, itemCount) {
  var s = '.car-' + el.id;
  var cScale = cfg.carCenterScale !== undefined ? cfg.carCenterScale : 1;
  var sScale = cfg.carSideScale !== undefined ? cfg.carSideScale : 1;
  var sOpac = cfg.carSideOpacity !== undefined ? cfg.carSideOpacity : 1;
  var cx = parseFloat(cfg.carCenterOffsetX) || 0;
  var cy = parseFloat(cfg.carCenterOffsetY) || 0;
  var grow = Math.max(0, (cScale - 1) * 60);
  var padTop = Math.round(grow + Math.max(0, -cy) + (parseFloat(cfg.carPadTop) || 0));
  var padBottom = Math.round(grow + Math.max(0, cy) + (parseFloat(cfg.carPadBottom) || 0));
  var centerSizeCss = '';
  if (cfg.carCenterWidth) centerSizeCss += 'width:' + cssLen(cfg.carCenterWidth) + '; margin-left:auto; margin-right:auto;';
  if (cfg.carCenterHeight) centerSizeCss += 'height:' + cssLen(cfg.carCenterHeight) + ';';
  var centerSel = s + ' .owl-item.center .car-item, ' + s + ' .owl-item.car-active .car-item, ' + s + ' .car-fallback .car-item.center';
  var out = [
    s + ' .owl-stage-outer { padding:' + padTop + 'px 0 ' + padBottom + 'px; }',
    s + ' .car-item { transform:translateX(var(--car-shift, 0px)) scale(' + sScale + '); opacity:' + sOpac + '; }',
    centerSel + ' { transform:translate(' + cx + 'px, ' + cy + 'px) scale(' + cScale + '); opacity:1; ' + centerSizeCss +
      (cfg.carCenterPadding ? ' padding:' + cfg.carCenterPadding + ';' : '') +
      (cfg.carCenterMargin ? ' margin:' + cfg.carCenterMargin + ';' : '') +
      (cfg.carCenterRadius ? ' border-radius:' + cfg.carCenterRadius + ';' : '') +
      (cfg.carCenterBg ? ' background:' + cfg.carCenterBg + ';' : '') +
      (cfg.carCenterBorder ? ' border:' + cfg.carCenterBorder + ';' : '') +
      (cfg.carCenterShadow ? ' box-shadow:' + cfg.carCenterShadow + ';' : '') + ' }'
  ];
  // Off-balance layout: push the whole strip right by whole slots
  var c = Math.max(1, parseInt(itemCount) || 3);
  var slot = carActiveSlotOf(cfg, c);
  if (slot !== null) out.push(s + ' .owl-stage { margin-left:' + ((slot / c) * 100).toFixed(4) + '%; }');
  else out.push(s + ' .owl-stage { margin-left:0; }');
  return out.join('\\n');
}

/** Arrow + dot placement for one configuration (base or a device override) */
function carPositionRules(el, cfg) {
  var s = '.car-' + el.id;
  var num = function(v) { return parseFloat(v) || 0; };
  var inset = num(cfg.carNavInset !== undefined ? cfg.carNavInset : cfg.carNavOffset);
  var prevT = 'translate(' + num(cfg.carNavPrevX) + 'px, ' + num(cfg.carNavPrevY) + 'px)';
  var nextT = 'translate(' + num(cfg.carNavNextX) + 'px, ' + num(cfg.carNavNextY) + 'px)';
  var dotT = 'translate(' + num(cfg.carDotOffsetX) + 'px, ' + num(cfg.carDotOffsetY) + 'px)';
  var topSpace = (cfg.carDotMarginTop === '' || cfg.carDotMarginTop === undefined) ? 6 : num(cfg.carDotMarginTop);
  var out = [
    s + ' .owl-nav { position:absolute; top:calc(50% + ' + num(cfg.carNavOffsetY) + 'px); left:' + num(cfg.carNavOffsetX) +
      'px; width:100%; margin:0 !important; transform:translateY(-50%); display:flex; justify-content:space-between; pointer-events:none; box-sizing:border-box; }',
    s + ' .owl-nav button.owl-prev { margin:0 ' + inset + 'px !important; transform:' + prevT + '; }',
    s + ' .owl-nav button.owl-next { margin:0 ' + inset + 'px !important; transform:' + nextT + '; }',
    // keep the nudge when hovering, or the button would jump back to its default spot
    s + ' .owl-nav button.owl-prev:hover { transform:' + prevT + ' scale(1.12); }',
    s + ' .owl-nav button.owl-next:hover { transform:' + nextT + ' scale(1.12); }'
  ];
  if (cfg.carDotPosition === 'overlay') {
    out.push(s + ' { position:relative; }');
    out.push(s + ' .owl-dots { position:absolute; left:0; right:0; bottom:' +
      ((cfg.carDotBottom === '' || cfg.carDotBottom === undefined) ? 8 : num(cfg.carDotBottom)) +
      'px; z-index:3; margin:0 !important; transform:' + dotT + '; }');
  } else {
    if (cfg.carDotPosition === 'above') {
      out.push(s + ' .owl-dots { order:-1; }');
      out.push(s + ' .car-wrap { display:flex; flex-direction:column; }');
    }
    // !important because owl.theme.default.css has a more specific rule
    // (\`.owl-theme .owl-nav.disabled + .owl-dots { margin-top:10px }\`) that would
    // otherwise pin the spacing and make "Top space" do nothing.
    out.push(s + ' .owl-dots { margin-top:' + topSpace + 'px !important; position:static; transform:' + dotT + '; }');
  }
  return out.join('\\n');
}

/** "160" → "160px"; leaves "160px", "50%", "auto", "" alone */
function cssLen(v, fallback) {
  if (v === undefined || v === null || v === '') return fallback;
  var str = String(v).trim();
  return /^-?\\d*\\.?\\d+$/.test(str) ? str + 'px' : str;
}

function buildCarouselCSS(el) {
  var s = '.car-' + el.id;
  // The builder previews one device at a time and can't rely on media queries,
  // so it asks for that device's values to be baked into the base rules.
  var previewDevice = (typeof window !== 'undefined') ? window.__uiCarouselDevice : null;
  var base = previewDevice ? carMergedFor(el, previewDevice) : el;
  var per = parseInt(el.carItems) || 3;
  var gap = parseInt(el.carMargin) || 0;
  // Item box sizing
  var sizeCss = '';
  if (el.carItemWidth) sizeCss += 'width:' + cssLen(el.carItemWidth) + '; margin-left:auto; margin-right:auto;';
  if (el.carItemHeight) sizeCss += 'height:' + cssLen(el.carItemHeight) + ';';

  // Active-item rules: base first, then a block per device that overrides them
  var basePer = previewDevice
    ? (previewDevice === 'phone' ? (parseInt(el.carItemsPhone) || 1)
      : previewDevice === 'tablet' ? (parseInt(el.carItemsTablet) || per)
      : (parseInt(el.carItemsPhoneLand) || parseInt(el.carItemsTablet) || per))
    : per;
  var activeCss = [carActiveRules(el, base, basePer), carPositionRules(el, base)];
  if (!previewDevice) {
    var counts = {
      tablet: parseInt(el.carItemsTablet) || per,
      phone: parseInt(el.carItemsPhone) || 1,
      'phone-land': parseInt(el.carItemsPhoneLand) || parseInt(el.carItemsTablet) || per
    };
    Object.keys(CAR_DEVICE_QUERIES).forEach(function(dev) {
      var cfg = carMergedFor(el, dev);
      if (cfg === el && counts[dev] === per) return;   // nothing device-specific
      // (cfg !== el means this device carries at least one override)
      var rules = carActiveRules(el, cfg, counts[dev]) + '\\n' + carPositionRules(el, cfg);
      CAR_DEVICE_QUERIES[dev].forEach(function(q) { activeCss.push(q + ' {\\n' + rules + '\\n}'); });
    });
  }
  var slotCss = activeCss.join('\\n');
  return [
    s + ' { width:100%; }',
    // Owl ships \`.owl-carousel { display:none }\` and only reveals it once it has
    // initialised — if anything goes wrong the whole carousel vanishes. Keep it shown.
    s + ' .owl-carousel { display:block; }',
    s + ' .owl-carousel .owl-item { float:left; }',
    s + ' .owl-stage { display:flex; align-items:center; }',
    // z-index only applies to positioned elements — without this the *next* card in
    // DOM order paints over the enlarged active one.
    s + ' .owl-item { display:flex; align-items:center; position:relative; z-index:1; }',
    s + ' .owl-item.center, ' + s + ' .owl-item.car-active { z-index:' + (parseInt(el.carActiveZ) || 10) + '; }',
    s + ' .car-item { position:relative; }',
    s + ' .car-fallback .car-item.center { z-index:' + (parseInt(el.carActiveZ) || 10) + '; }',
    s + ' .car-item { background:' + (el.carItemBg || 'transparent') + '; border:' + (el.carItemBorder || 'none') +
      '; border-radius:' + (el.carItemRadius || '0') + '; padding:' + (el.carItemPadding || '0') +
      '; margin:' + (el.carItemMargin || '0') +
      '; box-shadow:' + (el.carItemShadow || 'none') + '; text-align:' + (el.carItemAlign || 'center') +
      '; overflow:hidden; box-sizing:border-box; ' + sizeCss +
      ' transition:transform 0.35s ease, opacity 0.35s ease; }',
    s + ' .car-media { position:relative; width:' + cssLen(el.carModelWidth, '100%') + '; height:' +
      cssLen(el.carModelHeight, cssLen(el.carImgHeight, '140px')) + '; margin:0 auto; }',
    // The canvas covers the media box, so adding a model never resizes the card
    s + ' .car-media .car-model { position:absolute; inset:' + (parseFloat(el.carModelInset) || 0) + 'px; }',
    s + ' .car-media .car-fg-box { position:absolute; }',
    s + ' .car-media .car-img { position:absolute; inset:0; width:100%; height:100%; }',
    s + ' .car-model { border-radius:' + (el.carImgRadius || '0') + '; overflow:hidden; z-index:1; }',
    // Full-bleed image mode: the picture covers the card, content sits above it
    s + ' .car-bgimg { position:absolute; inset:0; width:100%; height:100%; object-fit:' +
      (el.carImgFit || 'cover') + '; object-position:' + (el.carImgPosition || 'center') +
      '; border-radius:inherit; z-index:0; display:block; }',
    s + ' .car-model-cover { position:absolute; inset:' + (parseFloat(el.carModelInset) || 0) + 'px; z-index:1; }',
    // Overlay image: sits above the card image, its inset acts as a margin from the edges
    // Overlay image: the box handles margin/padding/alignment, the image its own size + fit
    s + ' .car-fg-box { position:absolute; inset:' + cssLen(el.carFgInset, '0px') + '; padding:' + cssLen(el.carFgPadding, '0px') +
      '; box-sizing:border-box; display:flex; z-index:1; pointer-events:none;' +
      ' align-items:' + (el.carFgAlignV === 'top' ? 'flex-start' : el.carFgAlignV === 'bottom' ? 'flex-end' : 'center') +
      '; justify-content:' + (el.carFgAlignH === 'left' ? 'flex-start' : el.carFgAlignH === 'right' ? 'flex-end' : 'center') + '; }',
    s + ' .car-fg { max-width:100%; max-height:100%; object-position:' + (el.carFgPosition || 'center') +
      '; border-radius:' + (el.carFgRadius || '0') + '; display:block; ' +
      (el.carFgShadow ? 'filter:drop-shadow(' + el.carFgShadow + ');' : '') + ' }',
    s + ' .car-item > .car-title, ' + s + ' .car-item > .car-text, ' + s + ' .car-item > .car-field { position:relative; z-index:2; }',
    (el.carImgMode === 'cover' ? s + ' .car-item { padding:' + (el.carItemPadding || '0') + '; display:flex; flex-direction:column; justify-content:' +
      (el.carCoverAlign === 'top' ? 'flex-start' : el.carCoverAlign === 'center' ? 'center' : 'flex-end') + '; }' : ''),
    s + ' .car-img { width:100%; height:' + cssLen(el.carImgHeight, 'auto') + '; object-fit:' + (el.carImgFit || 'cover') +
      '; border-radius:' + (el.carImgRadius || '0') + '; display:block; }',
    s + ' .car-title { font-size:' + (el.carTitleSize !== undefined ? el.carTitleSize : 0.85) + (el.carTitleUnit || 'rem') + '; color:' + (el.carTitleColor || '#fff') +
      '; font-weight:' + (el.carTitleWeight || '700') + '; margin-top:6px; line-height:1.3; }',
    s + ' .car-text { font-size:' + (el.carTextSize !== undefined ? el.carTextSize : 0.65) + (el.carTextUnit || 'rem') + '; color:' + (el.carTextColor || '#a0b0c0') +
      '; margin-top:3px; line-height:1.4; }',
    s + ' .car-field { font-size:' + (el.carFieldSize !== undefined ? el.carFieldSize : 0.6) + (el.carFieldUnit || 'rem') + '; color:' + (el.carFieldColor || '#8a93a8') +
      '; margin-top:2px; line-height:1.4; }',
    s + ' .owl-nav button.owl-prev, ' + s + ' .owl-nav button.owl-next { pointer-events:auto; color:' + (el.carNavColor || '#fff') +
      ' !important; background:' + (el.carNavBg || 'rgba(0,0,0,0.4)') + ' !important; width:' + (parseFloat(el.carNavSize) || 28) +
      'px; height:' + (parseFloat(el.carNavSize) || 28) + 'px; border-radius:' + (el.carNavRadius || '50%') +
      '; border:' + (el.carNavBorder || 'none') + ' !important; display:flex; align-items:center; justify-content:center; font-size:' +
      (el.carNavIconSize !== undefined ? el.carNavIconSize : 0.8) + 'rem !important; transition:transform 0.15s, background 0.15s; }',
    // Owl adds \`disabled\` to the nav/dots wrapper whenever every slide already fits on
    // screen, and its core CSS hides \`.disabled\` outright — which silently removes the
    // arrows and dots as soon as "Visible" reaches the number of slides. With loop on
    // they still work, so keep them shown when the user asked for them.
    (el.carNav !== false ? s + ' .owl-nav.disabled { display:flex !important; }' : ''),
    (el.carDots !== false ? s + ' .owl-dots.disabled { display:flex !important; }' : ''),
    s + ' .owl-dots { display:flex; justify-content:center; align-items:center; }',
    s + ' .owl-dots .owl-dot span { background:' + (el.carDotColor || 'rgba(255,255,255,0.25)') + ' !important; width:' +
      (parseFloat(el.carDotWidth) || 7) + 'px; height:' + (parseFloat(el.carDotHeight) || 7) + 'px; border-radius:' +
      (el.carDotRadius || '50%') + ' !important; margin:0 ' + ((parseFloat(el.carDotGap) || 6) / 2) + 'px !important; transition:all 0.2s; }',
    s + ' .owl-dots .owl-dot.active span { background:' + (el.carDotActiveColor || '#00e5a0') + ' !important;' +
      (el.carDotActiveWidth ? ' width:' + parseFloat(el.carDotActiveWidth) + 'px !important;' : '') +
      (el.carDotActiveHeight ? ' height:' + parseFloat(el.carDotActiveHeight) + 'px !important;' : '') + ' }',
    s + ' .car-fallback { display:flex; gap:' + gap + 'px; align-items:center; justify-content:center; width:100%; overflow:hidden; }',
    s + ' .car-fallback .car-item { flex:0 0 calc((100% - ' + (gap * (per - 1)) + 'px) / ' + per + '); }',
    (el.carItemClick && el.carItemClick !== 'none') ? s + ' .car-item { cursor:pointer; }' : '',
    slotCss
  ].join('\\n');
}

function applyCarouselCSS(el) {
  var st = document.getElementById('car-style-' + el.id);
  if (!st) { st = document.createElement('style'); st.id = 'car-style-' + el.id; document.head.appendChild(st); }
  st.textContent = buildCarouselCSS(el);
}

function owlAvailable() {
  return typeof window.jQuery !== 'undefined' && window.jQuery.fn && typeof window.jQuery.fn.owlCarousel === 'function';
}

/** Visible item count for the current viewport — mirrors the builder's device modes.
 *  window.__uiCarouselForceItems lets the builder pin a count for its device preview. */
function carItemsForViewport(el) {
  // The builder previews devices by resizing its canvas, not the window, so it installs
  // a resolver that answers from the selected device instead of window.innerWidth.
  if (typeof window !== 'undefined' && typeof window.__uiCarouselItemsResolver === 'function') {
    try {
      var r = parseInt(window.__uiCarouselItemsResolver(el));
      if (r > 0) return r;
    } catch(e) {}
  }
  var forced = (typeof window !== 'undefined') ? window.__uiCarouselForceItems : null;
  if (forced) return Math.max(1, parseInt(forced) || 1);
  var desktop = Math.max(1, parseInt(el.carItems) || 3);
  var tablet = Math.max(1, parseInt(el.carItemsTablet) || desktop);
  var phone = Math.max(1, parseInt(el.carItemsPhone) || 1);
  var land = el.carItemsPhoneLand ? Math.max(1, parseInt(el.carItemsPhoneLand)) : tablet;
  var w = (typeof window !== 'undefined' && window.innerWidth) ? window.innerWidth : 1280;
  var h = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800;
  var landscape = w > h;
  // Phones held sideways are wide but short (844×390, 932×430…), so height matters
  // more than width for spotting them.
  if (landscape && (w <= 768 || (h <= 500 && w <= 1200))) return land;
  if (w <= 480) return phone;
  if (w <= 1024) return tablet;
  return desktop;
}

function owlOptionsFor(el, live) {
  var per = carItemsForViewport(el);
  return {
    items: per,
    margin: parseInt(el.carMargin) || 0,
    loop: el.carLoop !== false,
    center: carUsesSlotMode(el) ? false : !!el.carCenter,
    autoWidth: !!el.carAutoWidth,
    stagePadding: parseInt(el.carStagePadding) || 0,
    nav: el.carNav !== false,
    dots: el.carDots !== false,
    dotsEach: 1,          // one dot per item (Owl otherwise draws one dot per "page",
                          // which gives a single dot when every item is already visible)
    navText: ['<i class="bi ' + (el.carNavPrevIcon || 'bi-chevron-left') + '"></i>', '<i class="bi ' + (el.carNavNextIcon || 'bi-chevron-right') + '"></i>'],
    autoplay: !!(live && el.carAutoplay),
    autoplayTimeout: parseInt(el.carAutoplayTimeout) || 4000,
    autoplayHoverPause: el.carAutoplayHoverPause !== false,
    smartSpeed: parseInt(el.carSpeed) || 450,
    mouseDrag: live ? el.carMouseDrag !== false : false,
    touchDrag: live ? el.carMouseDrag !== false : false,
    pullDrag: false,
    rewind: el.carLoop === false,
    startPosition: parseInt(el.carStart) || 0,
    // One bucket only: the count is resolved by carItemsForViewport(), which understands
    // portrait/landscape. Owl's own breakpoints can't express orientation.
    responsive: { 0: { items: per } }
  };
}

/* ── Linked elements: update other UI elements when the active item changes ── */
function carouselTargetDom(id) {
  if (!id) return null;
  try { if (typeof gameUIElements !== 'undefined' && gameUIElements && gameUIElements[id]) return gameUIElements[id].dom; } catch(e) {}
  try { if (typeof viewport !== 'undefined' && viewport) return viewport.querySelector('[data-id="' + id + '"]'); } catch(e) {}
  return document.getElementById('gui-' + id);
}

function carouselFieldValue(item, field, index) {
  if (!item) return '';
  if (field === 'index') return String(index + 1);
  if (field && field.indexOf('field:') === 0) {
    var key = field.slice(6);
    return (item.fields && item.fields[key] !== undefined) ? item.fields[key] : '';
  }
  return item[field || 'title'] !== undefined ? item[field || 'title'] : '';
}

/** Tags the current .owl-item with \`car-active\` (Owl only adds \`center\` in centre mode) */
function markCarouselActive(el, wrap, $t) {
  if (!wrap) return;
  var nodes = wrap.querySelectorAll('.owl-item');
  if (!nodes.length) return;
  var target = wrap.querySelector('.owl-item.center');
  if (!target) {
    var pos = -1;
    try {
      var inst = $t && $t.data ? $t.data('owl.carousel') : null;
      if (inst) pos = inst.current();
    } catch(e) {}
    if (pos >= 0 && nodes[pos]) target = nodes[pos];
  }
  Array.prototype.forEach.call(nodes, function(n) { n.classList.remove('car-active'); });
  if (target) target.classList.add('car-active');
  applyCarouselSpread(el, wrap, nodes, target);
}

/** Pushes the side cards away from the active one and poses their 3D models */
function applyCarouselSpread(el, wrap, nodes, activeNode) {
  nodes = nodes || wrap.querySelectorAll('.owl-item');
  if (!nodes.length) return;
  var activeIdx = -1;
  Array.prototype.forEach.call(nodes, function(n, i) { if (n === activeNode) activeIdx = i; });
  var gap = parseFloat(el.carSideGap) || 0;
  var stack = el.carSideGapStacks !== false;   // 2nd neighbour moves twice as far
  Array.prototype.forEach.call(nodes, function(n, i) {
    var card = n.querySelector('.car-item');
    if (!card) return;
    var shift = 0;
    if (activeIdx >= 0 && i !== activeIdx && gap) {
      var dist = i - activeIdx;
      var steps = stack ? Math.abs(dist) : 1;
      shift = (dist < 0 ? -1 : 1) * gap * steps;
    }
    card.style.setProperty('--car-shift', shift + 'px');

    // Per-card 3D pose: the active card can sit differently from the side ones
    var host = n.querySelector('.car-model .ui-model');
    if (host && host._modelApi && host._modelApi.setPose) {
      var isActive = (i === activeIdx);
      var item = null;
      var ci = card.dataset ? parseInt(card.dataset.index) : NaN;
      if (!isNaN(ci)) item = (el.carouselItems || [])[ci];
      var pose = {
        rotX: pickNum(isActive ? el.carModelActiveRotX : el.carModelSideRotX, item && item.modelRotX, el.carModelRotX, 0),
        rotY: pickNum(isActive ? el.carModelActiveRotY : el.carModelSideRotY, item && item.modelRotY, el.carModelRotY, 0),
        rotZ: pickNum(isActive ? el.carModelActiveRotZ : el.carModelSideRotZ, item && item.modelRotZ, el.carModelRotZ, 0),
        scale: pickNum(isActive ? el.carModelActiveScale : el.carModelSideScale, item && item.modelScale, el.carModelScale, 1),
        distance: pickNum(isActive ? el.carModelActiveDistance : el.carModelSideDistance, item && item.modelDistance, el.carModelDistance, 4)
      };
      host._modelApi.setPose(pose);
      var spins = carItemSpins(el, item, false);
      host._modelApi.setSpin(el.carModelSpinActiveOnly ? (isActive && spins) : spins);
    }
  });
}

/** Whether an item's model should spin — per item, falling back to the carousel */
function carItemSpins(el, item, atCreation) {
  var v = item ? item.modelAutoRotate : undefined;
  var on = (v === undefined || v === null || v === '') ? (el.carModelAutoRotate !== false) : (v === true || v === 'true');
  // "spin active only" is applied per card once we know which one is active
  if (atCreation && el.carModelSpinActiveOnly) return false;
  return on;
}

/** First value that is actually set */
function pickNum() {
  for (var i = 0; i < arguments.length; i++) {
    var v = arguments[i];
    if (v !== undefined && v !== null && v !== '' && !isNaN(parseFloat(v))) return parseFloat(v);
  }
  return 0;
}

function carouselActiveIndex(wrap) {
  if (!wrap) return 0;
  var node = wrap.querySelector('.owl-item.center .car-item')
    || wrap.querySelector('.owl-item.car-active .car-item')
    || wrap.querySelector('.owl-item.active .car-item')
    || wrap.querySelector('.car-item.center')
    || wrap.querySelector('.car-item');
  if (!node) return 0;
  var i = parseInt(node.dataset.index);
  return isNaN(i) ? 0 : i;
}

/** Base (designed) visibility of another element, so per-item show/hide can be reset */
function carouselTargetBaseVisible(id) {
  try { if (typeof gameUIElements !== 'undefined' && gameUIElements && gameUIElements[id]) return gameUIElements[id].data.visible !== false; } catch(e) {}
  try {
    if (typeof elements !== 'undefined' && elements && elements.find) {
      var m = elements.find(function(o) { return o.id === id; });
      if (m) return m.visible !== false;
    }
  } catch(e) {}
  return true;
}

function carouselSetTargetVisible(dom, show) {
  if (!dom) return;
  if (dom.classList.contains('gui-el')) {
    dom.classList.toggle('gui-hidden', !show);
    if (show) dom.style.removeProperty('display');
  } else {
    dom.style.display = show ? '' : 'none';
    dom.classList.toggle('el-hidden', !show);
  }
}

/** Applies one action (shared by carousel links and per-item triggers) */
function applyCarouselAction(dom, action, value) {
  if (!dom) return;
  var inner = dom.querySelector('.gui-inner') || dom.querySelector('.ui-el-inner') || dom.querySelector('.el-inner') || dom.firstElementChild || dom;
  switch (action || 'setText') {
    case 'setText': inner.textContent = value; break;
    case 'setHTML': inner.innerHTML = value; break;
    case 'setImage': {
      var img = inner.querySelector('img');
      if (img) img.src = value;
      else {
        inner.style.backgroundImage = value ? 'url(' + value + ')' : 'none';
        inner.style.backgroundSize = inner.style.backgroundSize || 'cover';
        inner.style.backgroundPosition = inner.style.backgroundPosition || 'center';
      }
      break;
    }
    case 'setBackground':
      inner.style.backgroundImage = value ? 'url(' + value + ')' : 'none';
      inner.style.backgroundSize = 'cover';
      inner.style.backgroundPosition = 'center';
      break;
    case 'setVideo': {
      var vid = inner.querySelector('video');
      if (vid && value) { vid.src = value; vid.load(); }
      break;
    }
    case 'setColor': inner.style.color = value; break;
    case 'show': carouselSetTargetVisible(dom, true); break;
    case 'hide': carouselSetTargetVisible(dom, false); break;
  }
}

/** Every element touched by any item's show/hide actions — reset before each slide */
function carouselManagedVisibility(el) {
  var ids = {};
  (el.carouselItems || []).forEach(function(it) {
    (it.actions || []).forEach(function(a) {
      if (a && a.targetId && (a.action === 'show' || a.action === 'hide')) ids[a.targetId] = true;
    });
  });
  return Object.keys(ids);
}

function applyCarouselLinks(el, index) {
  var items = el.carouselItems || [];
  if (!items.length) return;
  if (index < 0 || index >= items.length) index = 0;
  var item = items[index];
  // 1. Reset anything a slide controls, so show/hide is per-slide rather than sticky
  if (el.carResetVisibility !== false) {
    carouselManagedVisibility(el).forEach(function(id) {
      carouselSetTargetVisible(carouselTargetDom(id), carouselTargetBaseVisible(id));
    });
  }

  // 2. Carousel-wide links (same mapping for every slide)
  (el.carLinks || []).forEach(function(link) {
    if (!link || !link.targetId) return;
    applyCarouselAction(carouselTargetDom(link.targetId), link.action, carouselFieldValue(item, link.field, index));
  });

  // 3. This slide's own triggers
  (item.actions || []).forEach(function(a) {
    if (!a || !a.targetId) return;
    var val = a.value;
    if (a.useField) val = carouselFieldValue(item, a.useField, index);
    applyCarouselAction(carouselTargetDom(a.targetId), a.action, val);
  });
  if (el.carChangeFunc && typeof window[el.carChangeFunc] === 'function') {
    try { window[el.carChangeFunc](item, index, el); } catch(err) { console.warn('[Carousel] change function error:', err); }
  }
}

/** Current real item index — asks Owl first (clone-safe), then falls back to the DOM */
function carouselCurrentIndex(el, wrap, $t) {
  var n = (el.carouselItems || []).length;
  if (n) {
    try {
      var inst = $t && $t.data ? $t.data('owl.carousel') : null;
      if (inst && typeof inst.relative === 'function') {
        var rel = inst.relative(inst.current());
        if (!isNaN(rel)) return ((rel % n) + n) % n;
      }
    } catch(e) {}
  }
  return carouselActiveIndex(wrap);
}

function syncCarouselLinks(el, wrap, $t) {
  applyCarouselLinks(el, carouselCurrentIndex(el, wrap, $t));
}

function renderUICarousel(container, el, live) {
  var items = el.carouselItems || [];
  if (typeof disposeUIModel === 'function') {
    Array.prototype.forEach.call(container.querySelectorAll('.car-model'), function(m) { disposeUIModel(m); });
  }
  container.innerHTML = '';
  applyCarouselCSS(el);
  if (items.length === 0) return;

  var wrap = document.createElement('div');
  wrap.className = 'car-wrap car-' + el.id;
  wrap.style.cssText = 'width:100%;';

  var ready = owlAvailable();
  var track = document.createElement('div');
  track.className = ready ? 'owl-carousel owl-theme' : 'car-fallback';

  items.forEach(function(it, idx) {
    var box = document.createElement('div');
    box.className = 'car-item';
    box.dataset.index = idx;
    box.innerHTML = carouselItemInnerHTML(el, it);
    if (!ready) {
      var per = parseInt(el.carItems) || 3;
      var slotIdx = carUsesSlotMode(el) ? carActiveSlotOf(el, per) : (el.carCenter ? Math.floor(Math.min(items.length, per) / 2) : -1);
      if (slotIdx >= 0 && idx === slotIdx) box.classList.add('center');
    }
    var modelBox = box.querySelector('.car-model');
    if (modelBox && typeof renderUIModel === 'function') {
      renderUIModel(modelBox, {
        id: el.id + '_m' + idx,
        src: it.model,
        borderRadius: el.carImgRadius,
        modelBg: it.modelBg || el.carModelBg || 'transparent',
        modelStubBg: (el.carShowImage !== false && it.image) ? 'transparent' : null,
        modelAutoRotate: carItemSpins(el, it, true),
        modelRotateSpeed: el.carModelRotateSpeed,
        modelControls: el.carModelControls,
        modelDragRotate: el.carModelDragRotate,
        modelZoom: el.carModelZoom,
        modelPan: el.carModelPan,
        modelDragSpeed: el.carModelDragSpeed,
        modelZoomSpeed: el.carModelZoomSpeed,
        modelMinDistance: el.carModelMinDistance,
        modelMaxDistance: el.carModelMaxDistance,
        modelMinPolar: el.carModelMinPolar,
        modelMaxPolar: el.carModelMaxPolar,
        modelDamping: el.carModelDamping,
        modelStopSpinOnInteract: el.carModelStopSpinOnInteract,
        modelResumeSpin: el.carModelResumeSpin,
        modelDistance: it.modelDistance || el.carModelDistance,
        modelFov: el.carModelFov,
        modelLighting: el.carModelLighting,
        modelLightIntensity: el.carModelLightIntensity,
        modelScale: it.modelScale !== undefined ? it.modelScale : el.carModelScale,
        modelRotX: it.modelRotX !== undefined ? it.modelRotX : el.carModelRotX,
        modelRotY: it.modelRotY !== undefined ? it.modelRotY : el.carModelRotY,
        modelRotZ: it.modelRotZ !== undefined ? it.modelRotZ : el.carModelRotZ,
        modelY: it.modelY !== undefined ? it.modelY : el.carModelY,
        modelX: it.modelX !== undefined ? it.modelX : el.carModelX,
        modelZ: it.modelZ !== undefined ? it.modelZ : el.carModelZ,
        modelInset: it.modelInset,
        onModelReady: function() { markCarouselActive(el, wrap, $track); },
        modelCamHeight: el.carModelCamHeight,
        modelAutoFit: true
      }, live);
    }
    if (live && el.carItemClick && el.carItemClick !== 'none') {
      box.addEventListener('click', function() {
        if (el.carItemClick === 'link' && it.link) {
          var t = el.carLinkTarget || 'newTab';
          if (t === 'same') window.location.href = it.link;
          else if (t === 'iframe' && typeof showIframeOverlay === 'function') showIframeOverlay(it.link);
          else window.open(it.link, '_blank');
        } else if (el.carItemClick === 'function' && el.carItemFunc && typeof window[el.carItemFunc] === 'function') {
          try { window[el.carItemFunc](it, idx, el); } catch(err) { console.warn('[Carousel]', err); }
        }
      });
    }
    track.appendChild(box);
  });

  wrap.appendChild(track);
  container.appendChild(wrap);

  var $track = null;
  if (ready) {
    try {
      var $t = window.jQuery(track);
      $track = $t;
      $t.on('initialized.owl.carousel changed.owl.carousel translated.owl.carousel', function(e) {
        // Prefer Owl's own index (clone-safe), fall back to reading the DOM
        var idx = null;
        try {
          if (e && e.item && e.relatedTarget && typeof e.relatedTarget.relative === 'function') {
            idx = e.relatedTarget.relative(e.item.index);
          }
        } catch(err) {}
        markCarouselActive(el, wrap, $t);
        var n = items.length;
        if (idx !== null && !isNaN(idx) && n) applyCarouselLinks(el, ((idx % n) + n) % n);
        else syncCarouselLinks(el, wrap, $t);
        // Re-read once the slide has settled
        window.setTimeout(function() {
          markCarouselActive(el, wrap, $t);
          syncCarouselLinks(el, wrap, $t);
        }, (parseInt(el.carSpeed) || 450) + 40);
      });
      // Clicking a side item brings it to the centre (feels natural with center mode)
      if ((el.carCenter || carUsesSlotMode(el)) && live && el.carClickToCenter !== false) {
        $t.on('click', '.owl-item:not(.center):not(.car-active) .car-item', function(e) {
          if (el.carModelControls && e.target && e.target.closest && e.target.closest('.car-model')) return;
          var i = parseInt(this.dataset.index);
          if (!isNaN(i)) $t.trigger('to.owl.carousel', [i, parseInt(el.carSpeed) || 450]);
        });
      }
      $t.owlCarousel(owlOptionsFor(el, live));
    } catch(err) {
      console.warn('[Carousel] init failed:', err);
      track.classList.add('owl-loaded');     // show the cards rather than nothing
    }
  }
  window.setTimeout(function() {
    markCarouselActive(el, wrap, $track);
    syncCarouselLinks(el, wrap, $track);
  }, 30);

  // Keep Owl in step with size / orientation changes. Owl caches item widths at init,
  // so without this a resized (or rotated) carousel shows the wrong number of items.
  if (ready && $track) {
    var lastPer = owlOptionsFor(el, live).items;
    var reflowTimer = null;
    var reflow = function() {
      window.clearTimeout(reflowTimer);
      reflowTimer = window.setTimeout(function() {
        if (!document.body.contains(track)) return; // element was re-rendered
        var per = carItemsForViewport(el);
        if (per !== lastPer) {
          lastPer = per;
          try {
            $track.trigger('destroy.owl.carousel');
          } catch(e) { console.warn('[Carousel] destroy failed:', e); }
          try {
            $track.addClass('owl-carousel owl-theme');
            $track.owlCarousel(owlOptionsFor(el, live));
          } catch(e) {
            console.warn('[Carousel] re-init failed:', e);
            $track.addClass('owl-loaded');   // fall back to a plain visible row
          }
        } else {
          try { $track.trigger('refresh.owl.carousel'); } catch(e) {}
        }
        markCarouselActive(el, wrap, $track);
        syncCarouselLinks(el, wrap, $track);
      }, 120);
    };
    window.addEventListener('resize', reflow);
    window.addEventListener('orientationchange', reflow);
    if (typeof ResizeObserver !== 'undefined') {
      try { new ResizeObserver(reflow).observe(wrap); } catch(e) {}
    }
  }
}

function controlUICarousel(dom, cmd, index) {
  if (!owlAvailable() || !dom) return;
  var $t = window.jQuery(dom).find('.owl-carousel');
  if (!$t.length) return;
  switch (cmd || 'next') {
    case 'next': $t.trigger('next.owl.carousel'); break;
    case 'prev': $t.trigger('prev.owl.carousel'); break;
    case 'goTo': $t.trigger('to.owl.carousel', [parseInt(index) || 0, 300]); break;
    case 'play': $t.trigger('play.owl.autoplay'); break;
    case 'stop': $t.trigger('stop.owl.autoplay'); break;
    case 'refresh': $t.trigger('refresh.owl.carousel'); break;
  }
}

/* ── Design stage: reproduce the builder's canvas exactly ───────────────────
   The builder lays out on a fixed canvas (960×540 on desktop) and scales it to
   fit. Positions are percentages, but font sizes, image heights, paddings and
   radii are px/rem — so rendering the same UI into a raw browser window of a
   different size changes every one of those proportions. Runtimes therefore
   size the overlay to the design resolution and scale it, exactly like the
   builder does. */
var UI_DESIGN_SIZES = {
  desktop: { w: 960, h: 540 },
  tablet: { w: 512, h: 683 },
  phone: { w: 320, h: 568 },
  'phone-land': { w: 568, h: 320 }
};

/** Anchor → translate offsets (the element's own position is a % of the stage) */
function uiAnchorTranslate(anchor) {
  switch (anchor || 'center') {
    case 'top-left': return { tx: '0', ty: '0' };
    case 'top-center': return { tx: '-50%', ty: '0' };
    case 'top-right': return { tx: '-100%', ty: '0' };
    case 'center-left': return { tx: '0', ty: '-50%' };
    case 'center-right': return { tx: '-100%', ty: '-50%' };
    case 'bottom-left': return { tx: '0', ty: '-100%' };
    case 'bottom-center': return { tx: '-50%', ty: '-100%' };
    case 'bottom-right': return { tx: '-100%', ty: '-100%' };
    default: return { tx: '-50%', ty: '-50%' };
  }
}

/** The transform an element carries: anchor offset plus a hover/active scale hook */
function uiAnchorTransform(anchor) {
  var a = uiAnchorTranslate(anchor);
  return 'translate(' + a.tx + ', ' + a.ty + ') scale(var(--gui-scale, 1))';
}

function uiDeviceBucket() {
  var w = (typeof window !== 'undefined' && window.innerWidth) ? window.innerWidth : 1280;
  var h = (typeof window !== 'undefined' && window.innerHeight) ? window.innerHeight : 800;
  var landscape = w > h;
  if (landscape && (w <= 768 || (h <= 500 && w <= 1200))) return 'phone-land';
  if (w <= 480) return 'phone';
  if (w <= 1024) return 'tablet';
  return 'desktop';
}

function uiDesignSize(uiData) {
  var sizes = (uiData && uiData.designSizes) || UI_DESIGN_SIZES;
  return sizes[uiDeviceBucket()] || sizes.desktop || UI_DESIGN_SIZES.desktop;
}

function applyUIStage(uiData, overlayId) {
  var overlay = document.getElementById(overlayId || 'game-ui-overlay');
  if (!overlay) return;
  var mode = (uiData && uiData.scaleMode) || 'fit';

  if (mode === 'stretch') {   // legacy: fill the window, px sizes unscaled
    overlay.style.width = '100%';
    overlay.style.height = '100%';
    overlay.style.left = '0';
    overlay.style.top = '0';
    overlay.style.transform = '';
    overlay.style.transformOrigin = '';
    return;
  }

  var size = uiDesignSize(uiData);
  var w = window.innerWidth, h = window.innerHeight;
  var scale = mode === 'fill'
    ? Math.max(w / size.w, h / size.h)
    : Math.min(w / size.w, h / size.h);

  overlay.style.width = size.w + 'px';
  overlay.style.height = size.h + 'px';
  overlay.style.left = '50%';
  overlay.style.top = '50%';
  overlay.style.transformOrigin = 'center center';
  overlay.style.transform = 'translate(-50%, -50%) scale(' + scale + ')';
}

/** Re-apply on resize / rotate; safe to call repeatedly. */
function watchUIStage(getUIData, overlayId) {
  if (window.__uiStageWatch) window.removeEventListener('resize', window.__uiStageWatch);
  var timer = null;
  window.__uiStageWatch = function() {
    window.clearTimeout(timer);
    timer = window.setTimeout(function() { applyUIStage(getUIData(), overlayId); }, 60);
  };
  window.addEventListener('resize', window.__uiStageWatch);
  window.addEventListener('orientationchange', window.__uiStageWatch);
}

/* ── 3D model viewer (Three.js + GLB) ──────────────────────────────────────
   Loads Three.js on demand the first time a model element is rendered, so a
   project without one pays nothing. */
var UI_THREE_VERSION = '0.160.0';
var UI_THREE_CDN = 'https://cdn.jsdelivr.net/npm/three@' + UI_THREE_VERSION;
var UI_THREE_ESM = 'https://esm.sh/three@' + UI_THREE_VERSION;
var _uiThreePromise = null;
var _uiThreeError = '';

/**
 * Three's add-ons (GLTFLoader, OrbitControls) are published as ES modules that
 * \`import ... from 'three'\` — a bare specifier the browser can only resolve through
 * an import map. Loading them straight from a CDN therefore fails with
 * "Failed to resolve module specifier". So: make sure an import map exists first,
 * and keep a self-resolving CDN as a backup.
 */
function ensureThreeImportMap() {
  if (typeof document === 'undefined') return false;
  var existing = document.querySelector('script[type="importmap"]');
  if (existing) {
    // A host page (game-level) already maps "three" — use whatever it points at
    return true;
  }
  try {
    var map = document.createElement('script');
    map.type = 'importmap';
    map.textContent = JSON.stringify({
      imports: {
        'three': UI_THREE_CDN + '/build/three.module.js',
        'three/': UI_THREE_CDN + '/',
        'three/addons/': UI_THREE_CDN + '/examples/jsm/'
      }
    });
    document.head.appendChild(map);
    return true;
  } catch(e) {
    return false;
  }
}

function loadUIThree() {
  if (typeof window !== 'undefined' && window.__uiThreeStub) return Promise.resolve(window.__uiThreeStub);
  if (_uiThreePromise) return _uiThreePromise;

  var viaImportMap = function() {
    ensureThreeImportMap();
    return Promise.all([
      import(/* webpackIgnore: true */ 'three'),
      import(/* webpackIgnore: true */ 'three/addons/loaders/GLTFLoader.js'),
      import(/* webpackIgnore: true */ 'three/addons/controls/OrbitControls.js')
    ]);
  };
  // esm.sh rewrites the add-ons' internal "three" imports, so it needs no map
  var viaEsmSh = function() {
    return Promise.all([
      import(/* webpackIgnore: true */ UI_THREE_ESM),
      import(/* webpackIgnore: true */ UI_THREE_ESM + '/examples/jsm/loaders/GLTFLoader.js'),
      import(/* webpackIgnore: true */ UI_THREE_ESM + '/examples/jsm/controls/OrbitControls.js')
    ]);
  };

  _uiThreePromise = viaImportMap()
    .catch(function(err) {
      console.warn('[Model3D] import-map load failed (' + (err && err.message) + '), retrying via esm.sh');
      return viaEsmSh();
    })
    .then(function(m) {
      return { THREE: m[0], GLTFLoader: m[1].GLTFLoader, OrbitControls: m[2].OrbitControls };
    })
    .catch(function(err) {
      _uiThreeError = (err && err.message) || 'unknown error';
      console.warn('[Model3D] Three.js could not be loaded:', _uiThreeError,
        '\\nIf you are offline, host three.module.js yourself and add an import map to the page.');
      return null;
    });
  return _uiThreePromise;
}

function disposeUIModel(container) {
  if (!container) return;
  var host = container.querySelector('.ui-model');
  if (host && host._modelCleanup) { try { host._modelCleanup(); } catch(e) {} }
}

function modelPlaceholder(el, message, spinner) {
  return '<div class="ui-model-stub" style="width:100%;height:100%;min-height:60px;display:flex;flex-direction:column;' +
    'gap:6px;align-items:center;justify-content:center;background:' +
    (el.modelStubBg ? el.modelStubBg : (el.modelBg && el.modelBg !== 'transparent' ? el.modelBg : 'rgba(0,0,0,0.3)')) +
    ';border-radius:' + (el.borderRadius || '8px') + ';color:#c9d2e0;text-align:center;padding:8px;box-sizing:border-box;">' +
    '<i class="bi ' + (spinner ? 'bi-arrow-repeat' : 'bi-box') + '" style="font-size:1.5rem;opacity:0.65;' +
    (spinner ? 'animation:uiModelSpin 1.1s linear infinite;' : '') + '"></i>' +
    '<span style="font-size:0.5rem;opacity:0.75;">' + message + '</span></div>';
}

function renderUIModel(container, el, live) {
  disposeUIModel(container);
  container.innerHTML = '';

  if (!document.getElementById('ui-model-spin-css')) {
    var st = document.createElement('style');
    st.id = 'ui-model-spin-css';
    st.textContent = '@keyframes uiModelSpin { to { transform:rotate(360deg); } }';
    document.head.appendChild(st);
  }

  if (!el.src) {
    container.innerHTML = modelPlaceholder(el, 'Set a .glb / .gltf path', false);
    return null;
  }

  var host = document.createElement('div');
  host.className = 'ui-model';
  host.style.cssText = 'position:relative;width:100%;height:100%;min-height:40px;overflow:hidden;border-radius:' +
    (el.borderRadius || '0') + ';' + (el.modelBg && el.modelBg !== 'transparent' ? 'background:' + el.modelBg + ';' : '');
  var loading = document.createElement('div');
  loading.className = 'ui-model-loading';
  loading.style.cssText = 'position:absolute;inset:0;';
  loading.innerHTML = modelPlaceholder(el, 'Loading 3D model…', true);
  host.appendChild(loading);
  container.appendChild(host);

  var cancelled = false;
  host._modelEl = el;
  host._modelCleanup = function() { cancelled = true; };

  loadUIThree().then(function(libs) {
    if (cancelled || !host.isConnected) return;
    if (!libs || !libs.THREE) {
      host.innerHTML = modelPlaceholder(el, '3D viewer unavailable' + (_uiThreeError ? ' — ' + _uiThreeError : ''), false);
      return;
    }
    var THREE = libs.THREE;
    var w = host.clientWidth || 300, h = host.clientHeight || 200;

    var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: false });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    renderer.setSize(w, h, false);
    renderer.domElement.style.cssText = 'width:100%;height:100%;display:block;';
    if (renderer.outputColorSpace !== undefined && THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;
    if (el.modelShadows && renderer.shadowMap) { renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; }

    var scene = new THREE.Scene();
    if (el.modelBg && el.modelBg !== 'transparent') {
      try { scene.background = new THREE.Color(el.modelBg); } catch(e) {}
    } else {
      renderer.setClearColor(0x000000, 0);
    }

    var camera = new THREE.PerspectiveCamera(parseFloat(el.modelFov) || 45, w / h, 0.01, 2000);

    // Lighting presets
    var preset = el.modelLighting || 'studio';
    var intensity = el.modelLightIntensity !== undefined ? parseFloat(el.modelLightIntensity) : 1;
    var amb = new THREE.AmbientLight(0xffffff, (preset === 'dramatic' ? 0.25 : preset === 'soft' ? 0.9 : 0.55) * intensity);
    scene.add(amb);
    var key = new THREE.DirectionalLight(0xffffff, (preset === 'dramatic' ? 2.2 : preset === 'soft' ? 0.8 : 1.4) * intensity);
    key.position.set(3, 5, 4);
    if (el.modelShadows) { key.castShadow = true; }
    scene.add(key);
    if (preset !== 'dramatic') {
      var fill = new THREE.DirectionalLight(0xffffff, 0.6 * intensity);
      fill.position.set(-4, 2, -3);
      scene.add(fill);
    }
    if (el.modelHemi !== false && THREE.HemisphereLight) {
      scene.add(new THREE.HemisphereLight(0xffffff, 0x223344, 0.4 * intensity));
    }

    var root = new THREE.Group();
    scene.add(root);

    var controls = null;
    if (el.modelControls && libs.OrbitControls) {
      controls = new libs.OrbitControls(camera, renderer.domElement);
      controls.enableDamping = el.modelDamping !== false;
      controls.dampingFactor = parseFloat(el.modelDampingFactor) || 0.08;
      controls.enableRotate = el.modelDragRotate !== false;
      controls.enableZoom = el.modelZoom !== false;
      controls.enablePan = !!el.modelPan;
      controls.rotateSpeed = parseFloat(el.modelDragSpeed) || 1;
      controls.zoomSpeed = parseFloat(el.modelZoomSpeed) || 1;
      controls.autoRotate = !!(live && el.modelAutoRotate);
      controls.autoRotateSpeed = parseFloat(el.modelRotateSpeed) || 2;
      if (el.modelMinDistance !== undefined && el.modelMinDistance !== '') controls.minDistance = parseFloat(el.modelMinDistance);
      if (el.modelMaxDistance !== undefined && el.modelMaxDistance !== '') controls.maxDistance = parseFloat(el.modelMaxDistance);
      if (el.modelMinPolar !== undefined && el.modelMinPolar !== '') controls.minPolarAngle = parseFloat(el.modelMinPolar) * Math.PI / 180;
      if (el.modelMaxPolar !== undefined && el.modelMaxPolar !== '') controls.maxPolarAngle = parseFloat(el.modelMaxPolar) * Math.PI / 180;

      // Touch gestures: one finger turns, two fingers zoom (and pan when allowed)
      if (THREE.TOUCH) {
        controls.touches = {
          ONE: el.modelDragRotate === false ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE,
          TWO: el.modelPan ? THREE.TOUCH.DOLLY_PAN : THREE.TOUCH.DOLLY_ROTATE
        };
      }
      renderer.domElement.style.touchAction = 'none';
      renderer.domElement.style.cursor = 'grab';

      // Don't let a carousel (or any parent) steal the gesture
      ['pointerdown', 'mousedown', 'touchstart', 'wheel', 'click'].forEach(function(evt) {
        renderer.domElement.addEventListener(evt, function(e) { e.stopPropagation(); }, { passive: evt === 'wheel' ? false : true });
      });

      // Spinning pauses while the user is dragging, and can come back afterwards
      controls.addEventListener('start', function() {
        renderer.domElement.style.cursor = 'grabbing';
        if (el.modelStopSpinOnInteract !== false) {
          controls.autoRotate = false;
          if (host._modelState) host._modelState.spinning = false;
        }
        if (host._resumeTimer) { window.clearTimeout(host._resumeTimer); host._resumeTimer = null; }
      });
      controls.addEventListener('end', function() {
        renderer.domElement.style.cursor = 'grab';
        var delay = parseFloat(el.modelResumeSpin);
        if (el.modelStopSpinOnInteract !== false && el.modelAutoRotate && delay > 0) {
          host._resumeTimer = window.setTimeout(function() {
            controls.autoRotate = true;
            if (host._modelState) host._modelState.spinning = true;
          }, delay);
        }
      });
    }

    function frameModel(obj) {
      var box = new THREE.Box3().setFromObject(obj);
      if (!isFinite(box.min.x)) return;
      var size = box.getSize(new THREE.Vector3());
      var center = box.getCenter(new THREE.Vector3());
      var maxDim = Math.max(size.x, size.y, size.z) || 1;
      if (el.modelAutoFit !== false) {
        obj.position.sub(center);                       // sit on the origin
        var target = 1.6;
        obj.scale.multiplyScalar(target / maxDim);
      }
      var dist = (parseFloat(el.modelDistance) || 4);
      camera.position.set(0, parseFloat(el.modelCamHeight) || 0.4, dist);
      camera.lookAt(0, parseFloat(el.modelTargetY) || 0, 0);
      if (controls) { controls.target.set(0, parseFloat(el.modelTargetY) || 0, 0); controls.update(); }
    }

    function applyTransform(obj) {
      obj.position.x += parseFloat(el.modelX) || 0;
      obj.position.y += parseFloat(el.modelY) || 0;
      obj.position.z += parseFloat(el.modelZ) || 0;
      var sc = parseFloat(el.modelScale);
      if (!sc && sc !== 0) sc = 1;
      obj.scale.multiplyScalar(sc);
      obj.rotation.set(
        (parseFloat(el.modelRotX) || 0) * Math.PI / 180,
        (parseFloat(el.modelRotY) || 0) * Math.PI / 180,
        (parseFloat(el.modelRotZ) || 0) * Math.PI / 180
      );
    }

    var mixer = null, clock = new THREE.Clock();
    var loader = new libs.GLTFLoader();
    loader.load(el.src, function(gltf) {
      if (cancelled || !host.isConnected) return;
      var obj = gltf.scene || gltf.scenes[0];
      if (el.modelShadows) {
        obj.traverse(function(n) { if (n.isMesh) { n.castShadow = true; n.receiveShadow = true; } });
      }
      frameModel(obj);
      host._modelBaseScale = obj.scale.x;   // scale after auto-fit, before user scale
      applyTransform(obj);
      root.add(obj);
      host._modelObject = obj;
      if (gltf.animations && gltf.animations.length && el.modelPlayClips !== false) {
        mixer = new THREE.AnimationMixer(obj);
        gltf.animations.forEach(function(clip) { mixer.clipAction(clip).play(); });
      }
      if (loading.parentNode) loading.remove();
      // Let the host (e.g. a carousel) re-apply its pose now the model exists
      if (typeof el.onModelReady === 'function') {
        try { el.onModelReady(host); } catch(e) { console.warn('[Model3D] onModelReady:', e); }
      }
    }, undefined, function(err) {
      var isFile = (location.protocol === 'file:');
      console.warn('[Model3D] failed to load', el.src, err && err.message,
        isFile ? '\\nThe page was opened from disk (file://) — browsers refuse to fetch .glb files that way. ' +
                 'Tick "Embed 3D models" when exporting, or serve the folder over http.' : '');
      host.innerHTML = modelPlaceholder(el,
        isFile ? 'Cannot load models from file:// — embed them on export or serve over http'
               : 'Could not load ' + el.src, false);
    });

    host.appendChild(renderer.domElement);

    // Keep the drawing buffer in step with the element
    var ro = null;
    function resize() {
      var nw = host.clientWidth || w, nh = host.clientHeight || h;
      if (!nw || !nh) return;
      camera.aspect = nw / nh;
      camera.updateProjectionMatrix();
      renderer.setSize(nw, nh, false);
    }
    if (typeof ResizeObserver !== 'undefined') {
      try { ro = new ResizeObserver(resize); ro.observe(host); } catch(e) {}
    }
    window.addEventListener('resize', resize);

    var raf = null, spinning = !!(live && el.modelAutoRotate && !controls);
    host._modelState = { spinning: spinning };
    function tick() {
      if (cancelled || !host.isConnected) return;
      raf = requestAnimationFrame(tick);
      var dt = clock.getDelta();
      if (mixer) mixer.update(dt);
      if (host._modelState.spinning && host._modelObject) {
        host._modelObject.rotation.y += (parseFloat(el.modelRotateSpeed) || 2) * 0.25 * dt;
      }
      if (controls) controls.update();
      renderer.render(scene, camera);
    }
    tick();

    host._modelCleanup = function() {
      cancelled = true;
      if (raf) cancelAnimationFrame(raf);
      window.removeEventListener('resize', resize);
      if (ro) { try { ro.disconnect(); } catch(e) {} }
      if (controls) { try { controls.dispose(); } catch(e) {} }
      try {
        scene.traverse(function(n) {
          if (n.geometry) n.geometry.dispose();
          if (n.material) {
            var mats = Array.isArray(n.material) ? n.material : [n.material];
            mats.forEach(function(m) {
              Object.keys(m).forEach(function(k) { if (m[k] && m[k].isTexture) m[k].dispose(); });
              m.dispose();
            });
          }
        });
        renderer.dispose();
      } catch(e) {}
    };
    host._modelApi = {
      renderer: renderer, scene: scene, camera: camera, root: root, controls: controls,
      setSpin: function(on) {
        host._modelState.spinning = !!on && !controls;
        if (controls) controls.autoRotate = !!on;
      },
      setPose: function(pose) {
        if (!pose) return;
        var obj = host._modelObject;
        if (obj) {
          if (pose.rotX !== undefined || pose.rotY !== undefined || pose.rotZ !== undefined) {
            obj.rotation.set(
              (pose.rotX || 0) * Math.PI / 180,
              (pose.rotY || 0) * Math.PI / 180,
              (pose.rotZ || 0) * Math.PI / 180
            );
          }
          if (pose.scale !== undefined && host._modelBaseScale) {
            var sc = host._modelBaseScale * pose.scale;
            obj.scale.set(sc, sc, sc);
          }
        }
        if (pose.distance !== undefined && camera) {
          camera.position.z = pose.distance;
          if (controls) controls.update();
        }
      },
      controlsFor: function() { return controls; },
      reset: function() {
        if (host._modelObject) {
          host._modelObject.rotation.set(
            (parseFloat(el.modelRotX) || 0) * Math.PI / 180,
            (parseFloat(el.modelRotY) || 0) * Math.PI / 180,
            (parseFloat(el.modelRotZ) || 0) * Math.PI / 180
          );
        }
        if (controls) controls.reset();
      }
    };
  });

  return host;
}

function controlUIModel(dom, cmd, value) {
  var host = dom ? dom.querySelector('.ui-model') : null;
  if (!host) return;
  var api = host._modelApi;
  switch (cmd || 'toggleSpin') {
    case 'spin': if (api) api.setSpin(true); break;
    case 'stop': if (api) api.setSpin(false); break;
    case 'toggleSpin': if (api) api.setSpin(!(host._modelState && host._modelState.spinning)); break;
    case 'reset': if (api) api.reset(); break;
    case 'setModel':
      if (value && host.parentNode) {
        var el = host._modelEl || {};
        var next = {};
        for (var k in el) next[k] = el[k];
        next.src = value;
        renderUIModel(host.parentNode, next, true);
      }
      break;
  }
}

/* ── Iframe / embed element ── */
function uiEmbedURL(src) {
  var u = String(src || '').trim();
  if (!u) return '';
  // Friendly conversions so a pasted share link just works
  var yt = u.match(/(?:youtube\\.com\\/(?:watch\\?v=|live\\/|shorts\\/)|youtu\\.be\\/)([\\w-]{6,})/i);
  if (yt) return 'https://www.youtube.com/embed/' + yt[1];
  var vim = u.match(/vimeo\\.com\\/(?:video\\/)?(\\d+)/i);
  if (vim) return 'https://player.vimeo.com/video/' + vim[1];
  var gmap = u.match(/google\\.[a-z.]+\\/maps\\/place\\/([^/?]+)/i);
  if (gmap && u.indexOf('/embed') === -1) {
    return 'https://www.google.com/maps?q=' + encodeURIComponent(decodeURIComponent(gmap[1])) + '&output=embed';
  }
  return u;
}

function buildUIFrame(el) {
  var frame = document.createElement('iframe');
  frame.className = 'ui-frame';
  frame.src = uiEmbedURL(el.src);
  frame.style.cssText = 'width:100%;height:100%;display:block;border:' + (el.frameBorder || 'none') +
    ';border-radius:' + (el.borderRadius || '0') + ';background:' + (el.frameBg || 'transparent') + ';';
  if (el.frameTitle) frame.title = el.frameTitle;
  frame.setAttribute('allow', el.frameAllow || 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
  if (el.frameAllowFullscreen !== false) frame.setAttribute('allowfullscreen', '');
  if (el.frameLazy) frame.setAttribute('loading', 'lazy');
  if (el.frameSandbox) frame.setAttribute('sandbox', el.frameSandboxRules || 'allow-scripts allow-same-origin allow-popups allow-forms');
  if (el.frameScroll === false) frame.setAttribute('scrolling', 'no');
  return frame;
}

function renderUIFrame(container, el, live) {
  container.innerHTML = '';
  if (!el.src) {
    container.innerHTML = '<div style="width:100%;height:100%;min-height:60px;display:flex;flex-direction:column;gap:4px;' +
      'align-items:center;justify-content:center;background:rgba(0,0,0,0.25);border:1px dashed rgba(255,255,255,0.2);' +
      'border-radius:' + (el.borderRadius || '8px') + ';"><i class="bi bi-window" style="font-size:1.5rem;opacity:0.5;"></i>' +
      '<span style="font-size:0.5rem;opacity:0.6;">Set a page URL</span></div>';
    return null;
  }
  // While designing, a placeholder avoids loading (and autoplaying) the real page
  if (!live && el.framePreviewOnly !== false) {
    var box = document.createElement('div');
    box.className = 'ui-frame-stub';
    box.style.cssText = 'width:100%;height:100%;display:flex;flex-direction:column;gap:6px;align-items:center;justify-content:center;' +
      'background:' + (el.frameBg || 'rgba(0,0,0,0.35)') + ';border:' + (el.frameBorder || '1px solid rgba(255,255,255,0.12)') +
      ';border-radius:' + (el.borderRadius || '8px') + ';color:#cfd6e4;text-align:center;padding:8px;box-sizing:border-box;overflow:hidden;';
    box.innerHTML = '<i class="bi bi-window-stack" style="font-size:1.4rem;opacity:0.7;"></i>' +
      '<div style="font-size:0.5rem;font-weight:700;letter-spacing:1px;">EMBEDDED PAGE</div>' +
      '<div style="font-size:0.45rem;opacity:0.7;word-break:break-all;max-width:90%;">' + escAttr(uiEmbedURL(el.src)) + '</div>' +
      '<div style="font-size:0.4rem;opacity:0.5;">loads in Preview and in the player</div>';
    container.appendChild(box);
    return null;
  }
  var frame = buildUIFrame(el);
  container.appendChild(frame);
  return frame;
}

function controlUIFrame(dom, cmd, value) {
  var frame = dom ? dom.querySelector('iframe') : null;
  if (!frame) return;
  switch (cmd || 'reload') {
    case 'reload': frame.src = frame.src; break;
    case 'setSrc': if (value) frame.src = uiEmbedURL(value); break;
    case 'blank': frame.src = 'about:blank'; break;
  }
}

/* ── Hero section (full-bleed backgrounds that cross-fade) ── */
function heroSlidesOf(el) {
  var list = el.heroSlides || [];
  return list.length ? list : [{ id: 'hs_empty', image: '', title: '', text: '' }];
}

function buildHeroCSS(el) {
  var s = '.hero-' + el.id;
  var dur = parseInt(el.heroTransDuration);
  if (!dur && dur !== 0) dur = 900;
  var trans = el.heroTransition || 'fade';
  var pad = el.heroPadding || '6%';
  var alignH = el.heroAlignH || 'center';
  var alignV = el.heroAlignV || 'center';
  var justify = alignV === 'top' ? 'flex-start' : alignV === 'bottom' ? 'flex-end' : 'center';
  var items = alignH === 'left' ? 'flex-start' : alignH === 'right' ? 'flex-end' : 'center';
  var textAlign = alignH === 'left' ? 'left' : alignH === 'right' ? 'right' : 'center';

  // How an inactive slide sits before/after it becomes the active one
  var restState, activeState;
  if (trans === 'slide-left') { restState = 'transform:translateX(100%);'; activeState = 'transform:translateX(0);'; }
  else if (trans === 'slide-right') { restState = 'transform:translateX(-100%);'; activeState = 'transform:translateX(0);'; }
  else if (trans === 'slide-up') { restState = 'transform:translateY(100%);'; activeState = 'transform:translateY(0);'; }
  else if (trans === 'zoom') { restState = 'opacity:0; transform:scale(1.15);'; activeState = 'opacity:1; transform:scale(1);'; }
  else { restState = 'opacity:0;'; activeState = 'opacity:1;'; }

  var out = [
    s + ' { position:relative; width:100%; height:100%; overflow:hidden; }',
    s + ' .hero-bgs { position:absolute; inset:0; overflow:hidden; }',
    s + ' .hero-bg { position:absolute; inset:0; background-size:' + (el.heroBgFit || 'cover') +
      '; background-position:' + (el.heroBgPosition || 'center') + '; background-repeat:no-repeat; ' +
      restState + ' transition:opacity ' + dur + 'ms ease, transform ' + dur + 'ms ease; }',
    s + ' .hero-bg.active { ' + activeState + ' z-index:1; }',
    s + ' .hero-bg video { width:100%; height:100%; object-fit:' + (el.heroBgFit || 'cover') + '; display:block; }',
    s + ' .hero-overlay { position:absolute; inset:0; z-index:2; pointer-events:none; background:' +
      (el.heroOverlay || 'none') + '; opacity:' + (el.heroOverlayOpacity !== undefined ? el.heroOverlayOpacity : 1) + '; }',
    s + ' .hero-content { position:absolute; inset:0; z-index:3; display:flex; flex-direction:column; box-sizing:border-box;' +
      ' justify-content:' + justify + '; align-items:' + items + '; text-align:' + textAlign +
      '; padding:' + pad + '; gap:' + (el.heroGap || '10px') + '; pointer-events:none; }',
    s + ' .hero-content > * { pointer-events:auto; max-width:' + (el.heroMaxWidth || '100%') + '; }',
    s + ' .hero-title { font-family:' + (el.heroTitleFont || el.fontFamily || "'Orbitron',sans-serif") +
      '; font-size:' + (el.heroTitleSize !== undefined ? el.heroTitleSize : 2.4) + (el.heroTitleUnit || 'rem') +
      '; font-weight:' + (el.heroTitleWeight || '700') + '; color:' + (el.heroTitleColor || '#ffffff') +
      '; line-height:' + (el.heroTitleLineHeight || '1.1') + '; letter-spacing:' + (el.heroTitleSpacing || 'normal') +
      '; text-shadow:' + (el.heroTitleShadow || 'none') + '; margin:0; }',
    s + ' .hero-sub { font-family:' + (el.heroSubFont || el.fontFamily || "'Rajdhani',sans-serif") +
      '; font-size:' + (el.heroSubSize !== undefined ? el.heroSubSize : 1) + (el.heroSubUnit || 'rem') +
      '; font-weight:' + (el.heroSubWeight || '400') + '; color:' + (el.heroSubColor || '#d0d8e4') +
      '; line-height:' + (el.heroSubLineHeight || '1.5') + '; text-shadow:' + (el.heroSubShadow || 'none') + '; margin:0; }',
    s + ' .hero-children { position:absolute; inset:0; z-index:4; pointer-events:none; }',
    s + ' .hero-dots { position:absolute; z-index:5; left:0; right:0; bottom:' + (parseFloat(el.heroDotBottom) || 18) +
      'px; display:flex; justify-content:center; align-items:center; gap:' + (parseFloat(el.heroDotGap) || 8) + 'px; }',
    s + ' .hero-dot { width:' + (parseFloat(el.heroDotWidth) || 10) + 'px; height:' + (parseFloat(el.heroDotHeight) || 10) +
      'px; border-radius:' + (el.heroDotRadius || '50%') + '; background:' + (el.heroDotColor || 'rgba(255,255,255,0.35)') +
      '; cursor:pointer; transition:all 0.25s; border:none; padding:0; }',
    s + ' .hero-dot.active { background:' + (el.heroDotActiveColor || '#ffffff') + ';' +
      (el.heroDotActiveWidth ? ' width:' + parseFloat(el.heroDotActiveWidth) + 'px;' : '') + ' }',
    s + ' .hero-arrow { position:absolute; z-index:5; top:50%; transform:translateY(-50%); width:' +
      (parseFloat(el.heroArrowSize) || 36) + 'px; height:' + (parseFloat(el.heroArrowSize) || 36) +
      'px; border-radius:' + (el.heroArrowRadius || '50%') + '; background:' + (el.heroArrowBg || 'rgba(0,0,0,0.35)') +
      '; color:' + (el.heroArrowColor || '#fff') + '; border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:' +
      (el.heroArrowIconSize !== undefined ? el.heroArrowIconSize : 1) + 'rem; }',
    s + ' .hero-arrow.prev { left:' + (parseFloat(el.heroArrowInset) || 12) + 'px; }',
    s + ' .hero-arrow.next { right:' + (parseFloat(el.heroArrowInset) || 12) + 'px; }'
  ];
  if (el.heroKenBurns) {
    out.push('@keyframes heroKen-' + el.id + ' { from { transform:scale(1); } to { transform:scale(' +
      (el.heroKenScale || 1.12) + '); } }');
    out.push(s + ' .hero-bg.active { animation:heroKen-' + el.id + ' ' +
      ((parseInt(el.heroInterval) || 6000) + dur) + 'ms ease-out forwards; }');
  }
  return out.join('\\n');
}

function applyHeroCSS(el) {
  var st = document.getElementById('hero-style-' + el.id);
  if (!st) { st = document.createElement('style'); st.id = 'hero-style-' + el.id; document.head.appendChild(st); }
  st.textContent = buildHeroCSS(el);
}

function heroShowSlide(wrap, el, index, live) {
  var slides = heroSlidesOf(el);
  var n = slides.length;
  if (!n) return;
  index = ((index % n) + n) % n;
  wrap._heroIndex = index;
  var bgs = wrap.querySelectorAll('.hero-bg');
  Array.prototype.forEach.call(bgs, function(b, i) { b.classList.toggle('active', i === index); });
  var dots = wrap.querySelectorAll('.hero-dot');
  Array.prototype.forEach.call(dots, function(d, i) { d.classList.toggle('active', i === index); });

  // Per-slide headings fall back to the hero's own text
  var slide = slides[index] || {};
  var t = wrap.querySelector('.hero-title');
  var sub = wrap.querySelector('.hero-sub');
  if (t) {
    var title = slide.title !== undefined && slide.title !== '' ? slide.title : (el.heroTitle || '');
    t.innerHTML = title;
    t.style.display = title ? '' : 'none';
  }
  if (sub) {
    var text = slide.text !== undefined && slide.text !== '' ? slide.text : (el.heroSubtitle || '');
    sub.innerHTML = text;
    sub.style.display = text ? '' : 'none';
  }
  // Videos only play while their slide is on screen
  Array.prototype.forEach.call(bgs, function(b, i) {
    var v = b.querySelector('video');
    if (!v) return;
    if (i === index && live) { v.play().catch(function(){}); }
    else { v.pause(); }
  });
  if (live && el.heroChangeFunc && typeof window[el.heroChangeFunc] === 'function') {
    try { window[el.heroChangeFunc](slide, index, el); } catch(e) { console.warn('[Hero]', e); }
  }
}

function heroStopAuto(wrap) {
  if (wrap && wrap._heroTimer) { window.clearInterval(wrap._heroTimer); wrap._heroTimer = null; }
}

function heroStartAuto(wrap, el, live) {
  heroStopAuto(wrap);
  if (!live || !el.heroAutoplay) return;
  var slides = heroSlidesOf(el);
  if (slides.length < 2) return;
  wrap._heroTimer = window.setInterval(function() {
    if (!document.body.contains(wrap)) { heroStopAuto(wrap); return; }
    heroShowSlide(wrap, el, (wrap._heroIndex || 0) + 1, live);
  }, Math.max(600, parseInt(el.heroInterval) || 6000));
}

function renderUIHero(container, el, live) {
  var prev = container.querySelector('.hero-wrap');
  if (prev) heroStopAuto(prev);
  container.innerHTML = '';
  applyHeroCSS(el);

  var wrap = document.createElement('div');
  wrap.className = 'hero-wrap hero-' + el.id;

  var slides = heroSlidesOf(el);
  var bgs = document.createElement('div');
  bgs.className = 'hero-bgs';
  slides.forEach(function(sl, i) {
    var b = document.createElement('div');
    b.className = 'hero-bg';
    if (sl.video) {
      var v = document.createElement('video');
      v.src = sl.video; v.muted = true; v.loop = true; v.playsInline = true; v.preload = 'auto';
      b.appendChild(v);
    } else if (sl.image) {
      b.style.backgroundImage = 'url(' + sl.image + ')';
    } else {
      b.style.background = sl.color || (el.heroEmptyBg || 'linear-gradient(135deg,#1b2030,#0b0e17)');
    }
    bgs.appendChild(b);
  });
  wrap.appendChild(bgs);

  var ov = document.createElement('div');
  ov.className = 'hero-overlay';
  wrap.appendChild(ov);

  var content = document.createElement('div');
  content.className = 'hero-content';
  var title = document.createElement('div');
  title.className = 'hero-title';
  var sub = document.createElement('div');
  sub.className = 'hero-sub';
  content.appendChild(title);
  content.appendChild(sub);
  wrap.appendChild(content);

  // Anything parented onto the hero lands here
  var kids = document.createElement('div');
  kids.className = 'hero-children gui-children ui-el-children';
  wrap.appendChild(kids);

  if (el.heroArrows && slides.length > 1) {
    ['prev', 'next'].forEach(function(dir) {
      var b = document.createElement('button');
      b.className = 'hero-arrow ' + dir;
      b.innerHTML = '<i class="bi ' + (dir === 'prev'
        ? (el.heroPrevIcon || 'bi-chevron-left')
        : (el.heroNextIcon || 'bi-chevron-right')) + '"></i>';
      b.addEventListener('click', function(e) {
        e.stopPropagation();
        heroShowSlide(wrap, el, (wrap._heroIndex || 0) + (dir === 'next' ? 1 : -1), live);
        heroStartAuto(wrap, el, live);
      });
      wrap.appendChild(b);
    });
  }

  if (el.heroDots && slides.length > 1) {
    var dotBox = document.createElement('div');
    dotBox.className = 'hero-dots';
    slides.forEach(function(sl, i) {
      var dt = document.createElement('button');
      dt.className = 'hero-dot';
      dt.addEventListener('click', function(e) {
        e.stopPropagation();
        heroShowSlide(wrap, el, i, live);
        heroStartAuto(wrap, el, live);
      });
      dotBox.appendChild(dt);
    });
    wrap.appendChild(dotBox);
  }

  wrap._heroEl = el;
  wrap._heroLive = live;
  container.appendChild(wrap);
  heroShowSlide(wrap, el, parseInt(el.heroStart) || 0, live);
  heroStartAuto(wrap, el, live);
  return wrap;
}

function controlUIHero(dom, cmd, index) {
  var wrap = dom ? dom.querySelector('.hero-wrap') : null;
  if (!wrap) return;
  var el = wrap._heroEl;
  if (!el) return;
  switch (cmd || 'next') {
    case 'next': heroShowSlide(wrap, el, (wrap._heroIndex || 0) + 1, true); break;
    case 'prev': heroShowSlide(wrap, el, (wrap._heroIndex || 0) - 1, true); break;
    case 'goTo': heroShowSlide(wrap, el, parseInt(index) || 0, true); break;
    case 'play': heroStartAuto(wrap, el, true); break;
    case 'stop': heroStopAuto(wrap); break;
  }
}

/* ── Video chroma key (colour → transparency) ── */
function uiHexToRgb(hex) {
  var h = String(hex || '#00ff00').trim();
  if (h.charAt(0) === '#') h = h.slice(1);
  if (h.length === 3) h = h.charAt(0) + h.charAt(0) + h.charAt(1) + h.charAt(1) + h.charAt(2) + h.charAt(2);
  var n = parseInt(h, 16);
  if (isNaN(n)) return { r: 0, g: 255, b: 0 };
  return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}

function stopUIVideoChroma(container) {
  if (!container) return;
  var old = container.querySelector('canvas.ui-chroma');
  if (old) { old._stop = true; old.remove(); }
}

/**
 * Replaces the <video> with a canvas whose keyed colour is made transparent.
 * Falls back to the plain video if the source taints the canvas (cross-origin).
 */
function setupUIVideoChroma(container, vid, el) {
  stopUIVideoChroma(container);
  if (!container || !vid) return;
  vid.style.removeProperty('display');
  if (!el.chromaKey) return;

  var canvas = document.createElement('canvas');
  canvas.className = 'ui-chroma';
  canvas.style.cssText = vid.style.cssText;
  canvas.style.background = 'transparent';
  var ctx = null;
  try { ctx = canvas.getContext('2d', { willReadFrequently: true }); } catch(e) {}
  if (!ctx) return;

  vid.style.display = 'none';
  container.appendChild(canvas);

  var key = uiHexToRgb(el.chromaColor || '#00ff00');
  var sim = el.chromaSimilarity !== undefined ? parseFloat(el.chromaSimilarity) : 0.4;
  var smooth = el.chromaSmoothness !== undefined ? parseFloat(el.chromaSmoothness) : 0.08;
  var spill = el.chromaSpill !== undefined ? parseFloat(el.chromaSpill) : 0;
  var lastTime = -1, lastReady = -1;

  function draw() {
    if (canvas._stop || !canvas.isConnected) return;
    var w = vid.videoWidth, h = vid.videoHeight;
    // Only re-key when there is a new frame (a paused video stays cheap)
    if (w && h && (vid.currentTime !== lastTime || vid.readyState !== lastReady)) {
      lastTime = vid.currentTime; lastReady = vid.readyState;
      if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; }
      try {
        ctx.clearRect(0, 0, w, h);
        ctx.drawImage(vid, 0, 0, w, h);
        var img = ctx.getImageData(0, 0, w, h), d = img.data;
        var range = Math.max(0.0001, smooth);
        for (var i = 0; i < d.length; i += 4) {
          var dr = d[i] - key.r, dg = d[i + 1] - key.g, db = d[i + 2] - key.b;
          var dist = Math.sqrt(dr * dr + dg * dg + db * db) / 441.6729;
          if (dist < sim) {
            d[i + 3] = 0;
          } else if (dist < sim + range) {
            var a = (dist - sim) / range;
            d[i + 3] = d[i + 3] * a;
            if (spill > 0) {   // pull the key colour out of the soft edge
              d[i] = d[i] + (d[i] - key.r) * spill * (1 - a);
              d[i + 1] = d[i + 1] + (d[i + 1] - key.g) * spill * (1 - a);
              d[i + 2] = d[i + 2] + (d[i + 2] - key.b) * spill * (1 - a);
            }
          }
        }
        ctx.putImageData(img, 0, 0);
      } catch(err) {
        // Tainted canvas: the browser refuses to read frames from a cross-origin or
        // file:// video. Try once with CORS, then fall back to the untouched video.
        if (!vid._chromaRetried && /^https?:/i.test(vid.src)) {
          vid._chromaRetried = true;
          var wasTime = vid.currentTime, wasPlaying = !vid.paused;
          vid.crossOrigin = 'anonymous';
          vid.load();
          vid.addEventListener('loadeddata', function once() {
            vid.removeEventListener('loadeddata', once);
            try { vid.currentTime = wasTime; } catch(e) {}
            if (wasPlaying) vid.play().catch(function(){});
          });
          lastTime = -1; lastReady = -1;
          if (typeof requestAnimationFrame === 'function') requestAnimationFrame(draw);
          return;
        }
        console.warn('[Video] chroma key unavailable for this source (' + (err && err.message) + '). ' +
          'Frames cannot be read from a file:// or cross-origin video — embed the video in the export, ' +
          'or serve it from the same origin with CORS enabled.');
        canvas._stop = true;
        canvas.remove();
        vid.style.removeProperty('display');
        return;
      }
    }
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(draw);
    else window.setTimeout(draw, 40);
  }
  if (typeof requestAnimationFrame === 'function') requestAnimationFrame(draw);
  else window.setTimeout(draw, 40);
}

function buildUIVideo(el) {
  const vid = document.createElement('video');
  vid.className = 'gui-video';
  vid.src = el.src;
  vid.playsInline = true;
  vid.preload = 'auto';
  if (el.videoPoster) vid.poster = el.videoPoster;
  vid.muted = el.muted !== false;
  vid.volume = Math.max(0, Math.min(1, el.volume !== undefined ? el.volume : 1));
  vid.loop = el.loopMode === 'loop';
  if (el.controls) vid.controls = true;
  var vfit = el.videoFit || 'contain';
  var vcss = 'display:block;border-radius:' + (el.borderRadius || 0) + ';';
  if (vfit === 'fit-width') vcss += 'width:100%;height:auto;';
  else if (vfit === 'fit-height') vcss += 'height:100%;width:auto;';
  else vcss += 'width:100%;height:100%;object-fit:' + (vfit === 'stretch' ? 'fill' : vfit) + ';';
  vid.style.cssText = vcss;
  if (el.loopMode === 'count') {
    var vPlays = 1;
    vid.addEventListener('ended', function() {
      if (vPlays < (parseInt(el.loopCount) || 1)) { vPlays++; vid.currentTime = 0; vid.play().catch(function(){}); }
    });
  }
  if ((el.playMode || 'auto') === 'auto') {
    vid.autoplay = true;
    var pr = vid.play(); if (pr && pr.catch) pr.catch(function(){});
  } else if (el.playMode === 'click' && !el.controls) {
    vid.style.cursor = 'pointer';
    vid.addEventListener('click', function() { if (vid.paused) vid.play().catch(function(){}); else vid.pause(); });
  }
  return vid;
}

function controlUIVideo(dom, cmd) {
  const vid = dom ? dom.querySelector('video') : null;
  if (!vid) return;
  switch (cmd || 'play') {
    case 'play': vid.play().catch(function(){}); break;
    case 'pause': vid.pause(); break;
    case 'toggle': vid.paused ? vid.play().catch(function(){}) : vid.pause(); break;
    case 'stop': vid.pause(); try { vid.currentTime = 0; } catch(e) {} break;
    case 'restart': try { vid.currentTime = 0; } catch(e) {} vid.play().catch(function(){}); break;
  }
}

function createUIElement(el) {
  const dom = document.createElement('div');
  dom.className = 'gui-el';
  dom.id = 'gui-' + el.id;

  const anchor = el.anchor || 'center';

  dom.style.left = el.x + '%';
  dom.style.top = el.y + '%';
  dom.style.transform = uiAnchorTransform(anchor);
  dom.style.width = el.w > 0 ? el.w + '%' : 'auto';
  dom.style.height = el.h > 0 ? el.h + '%' : 'auto';
  dom.style.zIndex = el.zIndex || 10;
  dom.style.opacity = el.opacity !== undefined ? el.opacity : 1;
  if (!el.visible) dom.classList.add('gui-hidden');

  const inner = document.createElement('div');
  inner.className = 'gui-inner';
  inner.style.width = '100%'; inner.style.height = '100%';

  const styleProps = ['fontFamily','fontSize','fontWeight','color','background','borderRadius',
    'padding','border','textAlign','textShadow','boxShadow','letterSpacing','lineHeight',
    'textTransform','backdropFilter','objectFit','aspectRatio','minWidth','minHeight',
    'cursor','display','justifyContent','alignItems','borderTop','overflow'];
  styleProps.forEach(prop => {
    if (el[prop] && el[prop] !== '0' && el[prop] !== '' && el[prop] !== 'none' && el[prop] !== 'normal') {
      if (prop === 'fontSize') inner.style[prop] = el[prop] + (el.fontUnit || 'rem');
      else inner.style[prop] = el[prop];
    }
  });
  inner.style.overflow = (el.type === 'panel' || el.type === 'carousel') ? 'visible' : 'hidden';
  if (el.type === 'hero') inner.style.position = 'relative';
  if (el.type === 'panel') inner.style.position = 'relative';

  // Custom CSS
  if (el.customCSS) {
    try {
      el.customCSS.split(';').filter(p => p.includes(':')).forEach(p => {
        const [prop, ...rest] = p.split(':');
        const cssProp = prop.trim();
        const val = rest.join(':').trim();
        if (cssProp && val) {
          const camel = cssProp.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
          inner.style[camel] = val;
        }
      });
    } catch(e) {}
  }

  // Animation sequence (entrance → attention → exit); legacy animateClass is treated as an entrance
  if (uiAnimHasSequence(el) && el.animTrigger !== 'manual') {
    window.setTimeout(function() { runUIAnimSequence(inner, el, 'all'); }, 0);
  }

  // Type-specific content
  if (el.type === 'text' || el.type === 'button') {
    inner.textContent = resolveBindingText(el.content, el.binding);
    if (el.type === 'button') inner.style.cursor = el.cursor || 'pointer';
  } else if (el.type === 'image') {
    if (el.src && el.objectFit === 'background') {
      inner.style.backgroundImage = 'url(' + el.src + ')';
      inner.style.backgroundSize = el.bgSize || 'cover';
      inner.style.backgroundPosition = el.bgPosition || 'center';
      inner.style.backgroundRepeat = el.bgRepeat || 'no-repeat';
    } else if (el.src) {
      const img = document.createElement('img');
      img.src = el.src;
      img.style.cssText = 'width:100%;height:100%;object-fit:' + (el.objectFit||'contain') + ';border-radius:' + (el.borderRadius||0) + ';display:block;';
      img.draggable = false;
      inner.appendChild(img);
    }
  } else if (el.type === 'icon') {
    const i = document.createElement('i');
    i.className = 'bi ' + (el.iconClass || 'bi-circle');
    i.style.fontSize = (el.fontSize || 2) + (el.fontUnit || 'rem');
    i.style.color = el.color || '#ffffff';
    if (el.textShadow) i.style.textShadow = el.textShadow;
    inner.appendChild(i);
  } else if (el.type === 'video') {
    if (el.src) {
      const vid = buildUIVideo(el);
      if (el.videoFit === 'fit-width' || el.videoFit === 'fit-height') {
        inner.style.display = 'flex'; inner.style.alignItems = 'center'; inner.style.justifyContent = 'center';
      }
      if (el.videoBlend && el.videoBlend !== 'none') vid.style.mixBlendMode = el.videoBlend;
      inner.appendChild(vid);
      if (el.chromaKey) setupUIVideoChroma(inner, vid, el);
    }
  } else if (el.type === 'carousel') {
    inner.style.display = 'flex'; inner.style.alignItems = 'center'; inner.style.overflow = 'visible';
    renderUICarousel(inner, el, true);
  } else if (el.type === 'hero') {
    inner.style.overflow = 'hidden'; inner.style.position = 'relative';
    renderUIHero(inner, el, true);
  } else if (el.type === 'iframe') {
    renderUIFrame(inner, el, true);
  } else if (el.type === 'model3d') {
    inner.style.overflow = 'hidden';
    renderUIModel(inner, el, true);
  } else if (el.type === 'progressbar') {
    const pct = Math.max(0, Math.min(100, el.barValue || 0));
    const track = document.createElement('div');
    track.className = 'gui-progressbar-track';
    track.style.height = el.barHeight || '12px';
    track.style.background = el.barBgColor || 'rgba(255,255,255,0.08)';
    track.style.borderRadius = el.borderRadius || '6px';
    if (el.showLabel && el.labelText) {
      const label = document.createElement('span');
      label.className = 'gui-progressbar-label';
      label.textContent = el.labelText;
      track.appendChild(label);
    }
    const fill = document.createElement('div');
    fill.className = 'gui-progressbar-fill';
    fill.style.width = pct + '%';
    fill.style.background = el.barColor || '#00e5a0';
    fill.style.borderRadius = el.borderRadius || '6px';
    fill.dataset.binding = el.binding || '';
    track.appendChild(fill);
    inner.appendChild(track);
  } else if (el.type === 'panel') {
    const childBox = document.createElement('div');
    childBox.className = 'gui-children';
    childBox.style.cssText = 'position:absolute;inset:0;pointer-events:none;';
    inner.appendChild(childBox);

    // D-Pad
    if (el.controlMode === 'dpad') {
      const cBg = el.controlBg || 'rgba(255,255,255,0.08)';
      const cArrow = el.controlKnob || 'rgba(255,255,255,0.7)';
      const cActive = el.controlKnob || 'rgba(0,229,160,0.7)';
      const dpad = document.createElement('div');
      dpad.className = 'gui-dpad';
      dpad.style.cssText = 'position:absolute;inset:5%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;user-select:none;touch-action:none;z-index:5;';
      const btnStyle = 'display:flex;align-items:center;justify-content:center;width:36%;aspect-ratio:1;border-radius:8px;background:' + cBg + ';border:1px solid rgba(255,255,255,0.15);color:' + cArrow + ';font-size:1.2rem;cursor:pointer;user-select:none;-webkit-user-select:none;touch-action:none;transition:background 0.1s;';
      const midRow = document.createElement('div');
      midRow.style.cssText = 'display:flex;gap:2px;width:100%;justify-content:center;';
      function makeDir(key, icon, parent) {
        const btn = document.createElement('div');
        btn.style.cssText = btnStyle;
        btn.innerHTML = '<i class="bi ' + icon + '"></i>';
        const press = () => { if (window.playerKeys) window.playerKeys[key] = true; btn.style.background = cActive; };
        const release = () => { if (window.playerKeys) window.playerKeys[key] = false; btn.style.background = cBg; };
        btn.addEventListener('pointerdown', e => { e.preventDefault(); e.stopPropagation(); press(); });
        btn.addEventListener('pointerup', e => { e.stopPropagation(); release(); });
        btn.addEventListener('pointerleave', release);
        btn.addEventListener('pointercancel', release);
        parent.appendChild(btn);
      }
      makeDir('w', 'bi-caret-up-fill', dpad);
      makeDir('a', 'bi-caret-left-fill', midRow);
      makeDir('d', 'bi-caret-right-fill', midRow);
      dpad.appendChild(midRow);
      makeDir('s', 'bi-caret-down-fill', dpad);
      inner.appendChild(dpad);
    }

    // Joystick
    if (el.controlMode === 'joystick') {
      const cBg = el.controlBg || 'rgba(255,255,255,0.04)';
      const cKnob = el.controlKnob || 'rgba(0,229,160,0.3)';
      const cKnobActive = el.controlKnob || 'rgba(0,229,160,0.5)';
      inner.style.position = 'relative'; inner.style.touchAction = 'none'; inner.style.userSelect = 'none';
      const base = document.createElement('div');
      base.style.cssText = 'position:absolute;inset:10%;border-radius:50%;background:' + cBg + ';border:1px solid rgba(255,255,255,0.1);';
      const knob = document.createElement('div');
      knob.style.cssText = 'position:absolute;width:40%;height:40%;border-radius:50%;background:' + cKnob + ';border:2px solid ' + cKnobActive + ';left:50%;top:50%;transform:translate(-50%,-50%);cursor:grab;touch-action:none;transition:background 0.1s;';
      base.appendChild(knob); inner.appendChild(base);
      let dragging = false, baseRect = null;
      const deadzone = 0.2;
      knob.addEventListener('pointerdown', e => { e.preventDefault(); e.stopPropagation(); dragging = true; baseRect = base.getBoundingClientRect(); knob.setPointerCapture(e.pointerId); knob.style.background = cKnobActive; });
      const onMove = e => {
        if (!dragging || !baseRect) return;
        const cx = baseRect.left + baseRect.width/2, cy = baseRect.top + baseRect.height/2, maxR = baseRect.width/2;
        let dx = (e.clientX-cx)/maxR, dy = (e.clientY-cy)/maxR;
        const dist = Math.sqrt(dx*dx+dy*dy);
        if (dist>1) { dx/=dist; dy/=dist; }
        knob.style.left = (50+dx*40)+'%'; knob.style.top = (50+dy*40)+'%';
        if (window.playerKeys) { window.playerKeys.w=dy<-deadzone; window.playerKeys.s=dy>deadzone; window.playerKeys.a=dx<-deadzone; window.playerKeys.d=dx>deadzone; window.playerKeys.shift=dist>0.85; }
      };
      const onRelease = () => { if (!dragging) return; dragging=false; knob.style.left='50%'; knob.style.top='50%'; knob.style.background=cKnob; if(window.playerKeys){window.playerKeys.w=false;window.playerKeys.s=false;window.playerKeys.a=false;window.playerKeys.d=false;window.playerKeys.shift=false;}};
      window.addEventListener('pointermove', onMove);
      window.addEventListener('pointerup', onRelease);
      window.addEventListener('pointercancel', onRelease);
    }
  }

  dom.appendChild(inner);
  dom.style.pointerEvents = 'auto';

  // Event-based action chains
  const evts = el.events || {};
  if (!el.events && el.actions) evts.click = el.actions;
  if (el.onClick && (!evts.click || evts.click.length === 0)) {
    evts.click = [{ type:'callFunction', funcName:el.onClick, delay:0 }];
  }
  const hasClick = evts.click && evts.click.length > 0;
  const hasDbl = evts.dblclick && evts.dblclick.length > 0;
  const hasDown = evts.mousedown && evts.mousedown.length > 0;
  const hasUp = evts.mouseup && evts.mouseup.length > 0;
  const hasHoverEvt = evts.hover && evts.hover.length > 0;
  if (hasClick||hasDbl||hasDown||hasUp||hasHoverEvt) inner.style.cursor = inner.style.cursor || 'pointer';
  if (hasClick) dom.addEventListener('click', () => executeActions(evts.click, el, evts.clickMode));
  if (hasDbl) dom.addEventListener('dblclick', () => executeActions(evts.dblclick, el, evts.dblclickMode));
  if (hasDown) dom.addEventListener('mousedown', () => executeActions(evts.mousedown, el, evts.mousedownMode));
  if (hasUp) dom.addEventListener('mouseup', () => executeActions(evts.mouseup, el, evts.mouseupMode));
  if (hasHoverEvt) dom.addEventListener('mouseenter', () => executeActions(evts.hover, el, evts.hoverMode));

  // Hover/Active states
  const origBg = el.background || '', origColor = el.color || '';
  const hasHover = el.hoverBackground||el.hoverColor||el.hoverScale||el.hoverSrc||el.hoverBoxShadow;
  const hasActive = el.activeBackground||el.activeColor||el.activeScale||el.activeSrc||el.activeBoxShadow;
  const defaultBtnHover = (el.type==='button' && !hasHover);
  inner.style.transition = 'all 0.15s ease';
  inner.addEventListener('mouseenter', () => {
    if (hasHover) {
      if (el.hoverBackground) inner.style.background = el.hoverBackground;
      if (el.hoverColor) inner.style.color = el.hoverColor;
      if (el.hoverScale) dom.style.setProperty('--gui-scale', el.hoverScale);
      if (el.hoverBoxShadow) inner.style.boxShadow = el.hoverBoxShadow;
      if (el.hoverSrc && el.type==='image') { const img=inner.querySelector('img'); if(img)img.src=el.hoverSrc; }
    } else if (defaultBtnHover) inner.style.filter = 'brightness(1.15)';
  });
  inner.addEventListener('mouseleave', () => {
    if (hasHover) {
      inner.style.background=origBg; inner.style.color=origColor; inner.style.boxShadow=el.boxShadow||'';
      dom.style.removeProperty('--gui-scale');
      if (el.hoverSrc && el.type==='image') { const img=inner.querySelector('img'); if(img)img.src=el.src; }
    } else if (defaultBtnHover) inner.style.filter = '';
  });
  if (hasActive) {
    inner.addEventListener('mousedown', () => {
      if (el.activeBackground) inner.style.background=el.activeBackground;
      if (el.activeColor) inner.style.color=el.activeColor;
      if (el.activeScale) dom.style.setProperty('--gui-scale', el.activeScale);
      if (el.activeBoxShadow) inner.style.boxShadow=el.activeBoxShadow;
      if (el.activeSrc && el.type==='image') { const img=inner.querySelector('img'); if(img)img.src=el.activeSrc; }
    });
    inner.addEventListener('mouseup', () => {
      inner.style.background = hasHover&&el.hoverBackground?el.hoverBackground:origBg;
      inner.style.color = hasHover&&el.hoverColor?el.hoverColor:origColor;
      inner.style.boxShadow = (hasHover&&el.hoverBoxShadow)?el.hoverBoxShadow:(el.boxShadow||'');
      dom.style.removeProperty('--gui-scale');
    });
  }

  return dom;
}

function executeActions(actions, sourceEl, mode) {
  const isChain = mode === 'chain';
  let cumDelay = 0;
  actions.forEach((act, i) => {
    if (isChain) {
      cumDelay += (i > 0 ? (act.delay || 0) : 0);
    }
    const delay = isChain ? cumDelay : (act.delay || 0);
    setTimeout(() => {
      const targetId = act.targetId || sourceEl.id;
      const entry = gameUIElements[targetId];
      if (!entry && !['callFunction','addAnimation','removeAnimation','playSound','loadScene','playMusic','stopMusic','setVolume','loadURL','showHTML'].includes(act.type)) return;

      switch (act.type) {
        case 'show':
          if (entry) { entry.dom.classList.remove('gui-hidden'); entry.data.visible = true; }
          break;
        case 'hide':
          if (entry) { entry.dom.classList.add('gui-hidden'); entry.data.visible = false; }
          break;
        case 'toggle':
          if (entry) { const h = entry.dom.classList.toggle('gui-hidden'); entry.data.visible = !h; }
          break;
        case 'setText':
          if (entry) {
            entry.data.content = act.value || '';
            const inner = entry.dom.querySelector('.gui-inner');
            if (inner) inner.textContent = resolveBindingText(act.value || '', entry.data.binding);
          }
          break;
        case 'setProgress':
          if (entry) { const f = entry.dom.querySelector('.gui-progressbar-fill'); if(f) f.style.width = Math.max(0,Math.min(100,act.value||0))+'%'; }
          break;
        case 'videoControl':
          if (entry) controlUIVideo(entry.dom, act.videoCmd);
          break;
        case 'carouselControl':
          if (entry) controlUICarousel(entry.dom, act.carCmd, act.carIndex);
          break;
        case 'heroControl':
          if (entry) controlUIHero(entry.dom, act.heroCmd, act.heroIndex);
          break;
        case 'frameControl':
          if (entry) controlUIFrame(entry.dom, act.frameCmd, act.frameValue);
          break;
        case 'modelControl':
          if (entry) controlUIModel(entry.dom, act.modelCmd, act.modelValue);
          break;
        case 'playAnimation':
          if (entry) {
            const aInner = entry.dom.querySelector('.gui-inner');
            if (aInner) runUIAnimSequence(aInner, entry.data, act.animStage || 'all');
          }
          break;
        case 'playSound':
          if (act.soundUrl) { try { const a=new Audio(act.soundUrl); a.volume=act.volume!==undefined?act.volume:1; a.play().catch(e=>{}); } catch(e){} }
          break;
        case 'callFunction':
          if (act.funcName && typeof window[act.funcName] === 'function') {
            let extraArgs = [];
            if (act.funcArgs) { try { extraArgs = Function('return [' + act.funcArgs + ']')(); } catch(e){} }
            window[act.funcName](...extraArgs, sourceEl, entry ? entry.data : null);
          }
          break;
        case 'addAnimation':
          if (entry && act.animClass) {
            const aInner = entry.dom.querySelector('.gui-inner');
            if (aInner) {
              [...aInner.classList].filter(c=>c.startsWith('animate__')&&c!=='animate__animated').forEach(c=>aInner.classList.remove(c));
              aInner.classList.remove('animate__animated');
              void aInner.offsetWidth;
              aInner.classList.add('animate__animated', act.animClass);
              if (act.animDuration) aInner.style.setProperty('--animate-duration', act.animDuration);
              else aInner.style.removeProperty('--animate-duration');
              if (act.animDelay) aInner.style.animationDelay = act.animDelay;
              else aInner.style.animationDelay = '';
              if (act.animRepeat) aInner.style.animationIterationCount = act.animRepeat;
              else aInner.style.animationIterationCount = '';
            }
          }
          break;
        case 'removeAnimation':
          if (entry) {
            const rInner = entry.dom.querySelector('.gui-inner');
            if (rInner) {
              if (act.animClass) {
                rInner.classList.remove(act.animClass);
                const has = [...rInner.classList].some(c=>c.startsWith('animate__')&&c!=='animate__animated');
                if (!has) rInner.classList.remove('animate__animated');
              } else {
                [...rInner.classList].filter(c=>c.startsWith('animate__')).forEach(c=>rInner.classList.remove(c));
              }
              rInner.style.removeProperty('--animate-duration');
              rInner.style.animationDelay = '';
              rInner.style.animationIterationCount = '';
            }
          }
          break;
        case 'loadScene':
          if (act.levelFile && window.GameAPI && window.GameAPI.loadScene) {
            const params = {};
            if (act.sceneParams) try { Object.assign(params, typeof act.sceneParams==='string'?JSON.parse(act.sceneParams):act.sceneParams); } catch(e){}
            window.GameAPI.loadScene(act.levelFile, act.uiFile||null, params);
          }
          break;
        case 'playMusic':
          if (act.musicSrc && window.GameAPI && window.GameAPI.playMusic) window.GameAPI.playMusic(act.musicSrc, act.musicLoop!==false);
          break;
        case 'stopMusic':
          if (window.GameAPI && window.GameAPI.stopMusic) window.GameAPI.stopMusic();
          break;
        case 'setVolume':
          if (window.GameAPI && window.GameAPI.setMusicVolume) window.GameAPI.setMusicVolume(act.volume!==undefined?act.volume:0.5);
          break;
        case 'loadURL':
          if (act.url) {
            var tgt = act.urlTarget || 'newTab';
            if (tgt==='same') window.location.href=act.url;
            else if (tgt==='newTab') window.open(act.url,'_blank');
            else if (tgt==='newWindow') window.open(act.url,'_blank','width=800,height=600');
            else if (tgt==='iframe' && typeof showIframeOverlay==='function') showIframeOverlay(act.url,act.iframeBackIcon,act.iframeCloseSound);
          }
          break;
        case 'showHTML':
          if (act.htmlContent) {
            if (act.htmlMode==='3d' && typeof show3DHTMLBillboard==='function') show3DHTMLBillboard(null,act.htmlContent,{bgColor:act.htmlBgColor,textColor:act.htmlTextColor});
            else if (typeof showHTMLPopup==='function') showHTMLPopup(act.htmlContent,{bgColor:act.htmlBgColor,textColor:act.htmlTextColor});
          }
          break;
      }
    }, delay);
  });
}

function updateGameUI() {
  if (!gameUIData) return;
  for (const [id, entry] of Object.entries(gameUIElements)) {
    const el = entry.data;
    if (el.binding && (el.type === 'text' || el.type === 'button')) {
      const inner = entry.dom.querySelector('.gui-inner');
      if (inner) inner.textContent = resolveBindingText(el.content, el.binding);
    }
    if (el.type === 'progressbar' && el.binding) {
      const fill = entry.dom.querySelector('.gui-progressbar-fill');
      if (fill) {
        const val = gameUIState[el.binding] !== undefined ? gameUIState[el.binding] : el.barValue;
        fill.style.width = Math.max(0, Math.min(100, val || 0)) + '%';
      }
    }
  }
}

// ══════════════════════════════════════
// INIT — Build UI from data
// ══════════════════════════════════════
(function init() {
  gameUIData = UI_DATA;
  const overlay = document.getElementById('game-ui-overlay');
  const parents = (UI_DATA.elements || []).filter(e => !e.parentId && !e.disabled);
  const children = (UI_DATA.elements || []).filter(e => e.parentId && !e.disabled);

  parents.forEach(el => {
    const dom = createUIElement(el);
    overlay.appendChild(dom);
    gameUIElements[el.id] = { dom, data: el };
  });
  children.forEach(el => {
    const parentEntry = gameUIElements[el.parentId];
    const container = parentEntry ? (parentEntry.dom.querySelector('.gui-children') || parentEntry.dom.querySelector('.gui-inner .gui-children') || parentEntry.dom) : overlay;
    const dom = createUIElement(el);
    container.appendChild(dom);
    dom.style.pointerEvents = 'auto';
    gameUIElements[el.id] = { dom, data: el };
  });

  // Background image
  if (UI_DATA.bgImage) {
    const bg = document.createElement('div');
    bg.style.cssText = 'position:fixed;inset:0;z-index:0;background-image:url(' + UI_DATA.bgImage + ');background-size:cover;background-position:center;';
    document.body.insertBefore(bg, document.body.firstChild);
  }

  // Load game level if configured
  if (CONFIG.gameLevelJson) {
    fetch(CONFIG.gameLevelJson)
      .then(r => r.json())
      .then(levelData => {
        console.log('[GameLevel] Loaded:', CONFIG.gameLevelJson, levelData);
        // Initialize your 3D engine with levelData here
        window.dispatchEvent(new CustomEvent('gamelevel-loaded', { detail: levelData }));
      })
      .catch(e => console.warn('[GameLevel] Could not load:', e));
  }

  applyUIStage(UI_DATA);
  watchUIStage(function() { return UI_DATA; });

  console.log('[UI] Initialized ' + Object.keys(gameUIElements).length + ' elements');

  // Auto-call onReady() if defined in custom code
  if (typeof onReady === 'function') onReady();
})();
<` + `/script>
</body>
</html>`;
}

document.getElementById('btn-load').onclick = () => document.getElementById('file-load').click();
document.getElementById('file-load').onchange = e => {
  const file = e.target.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = ev => {
    try {
      const data = JSON.parse(ev.target.result);
      loadUIData(data);
      showToast(`Loaded "${data.name || 'UI'}"`);
    } catch(err) {
      showToast('Invalid file: ' + err.message, 'error');
    }
  };
  reader.readAsText(file);
  e.target.value = '';
};

function loadUIData(data) {
  if (data && data.scaleMode) projectScaleMode = data.scaleMode;
  elements = data.elements || [];
  // Migrate & normalize
  elements.forEach(el => {
    if (!el.parentId) el.parentId = null;
    // Migrate old actions[] → events.click[]
    if (el.actions && !el.events) {
      el.events = { click: el.actions, hover:[], mousedown:[], mouseup:[], dblclick:[] };
      delete el.actions;
    }
    if (!el.events) el.events = { click:[], hover:[], mousedown:[], mouseup:[], dblclick:[] };
    ['click','hover','mousedown','mouseup','dblclick'].forEach(k => { if (!el.events[k]) el.events[k] = []; });
    // Ensure overrides structure
    if (!el.overrides) el.overrides = { tablet:{}, phone:{}, 'phone-land':{} };
    ['tablet','phone','phone-land'].forEach(d => { if (!el.overrides[d]) el.overrides[d] = {}; });
    // Ensure showOn and controlMode fields
    if (!el.showOn) el.showOn = [];
    if (!el.controlMode) el.controlMode = 'none';
    // Animation: migrate the old single animation into the entrance stage
    if (el.animateClass && !el.animEnterClass && !el.animAttnClass && !el.animExitClass) {
      el.animEnterClass = el.animateClass;
      el.animEnterDelay = Math.round((parseFloat(el.animateDelay) || 0) * 1000);
      el.animEnterDuration = Math.round((parseFloat(el.animateDuration) || 0.8) * 1000);
      el.animEnterRepeat = el.animateRepeat || '1';
      el.animateClass = '';
    }
    if (!el.fontUnit) el.fontUnit = 'rem';
    if (!el.animTrigger) el.animTrigger = 'load';
    const animDefs = { animEnterClass:'', animEnterDelay:0, animEnterDuration:800, animEnterRepeat:'1',
      animAttnClass:'', animAttnDelay:400, animAttnDuration:1000, animAttnRepeat:'1',
      animExitClass:'', animExitDelay:1000, animExitDuration:800, animExitRepeat:'1', animExitHide:true };
    for (const [k, v] of Object.entries(animDefs)) if (el[k] === undefined) el[k] = v;

    // 3D model defaults
    if (el.type === 'model3d') {
      const md = { modelBg:'transparent', modelAutoRotate:true, modelRotateSpeed:2, modelControls:false,
        modelZoom:true, modelPan:false, modelAutoFit:true, modelDistance:4, modelFov:45,
        modelDragRotate:true, modelDragSpeed:1, modelZoomSpeed:1, modelDamping:true,
        modelStopSpinOnInteract:true, modelResumeSpin:0,
        modelMinDistance:'', modelMaxDistance:'', modelMinPolar:'', modelMaxPolar:'',
        modelCamHeight:0.4, modelTargetY:0, modelX:0, modelY:0, modelZ:0, modelScale:1,
        modelRotX:0, modelRotY:0, modelRotZ:0, modelLighting:'studio', modelLightIntensity:1,
        modelShadows:false, modelHemi:true, modelPlayClips:true, modelLiveOnCanvas:true };
      for (const [k, v] of Object.entries(md)) if (el[k] === undefined) el[k] = v;
    }

    // Hero element defaults
    if (el.type === 'hero') {
      if (!Array.isArray(el.heroSlides)) el.heroSlides = [];
      el.heroSlides.forEach(sl => { if (!sl.id) sl.id = 'hs_' + Math.random().toString(36).slice(2, 8); });
      const hd = {
        heroTitle:'', heroSubtitle:'', heroAutoplay:true, heroInterval:6000,
        heroTransition:'fade', heroTransDuration:900, heroKenBurns:false, heroKenScale:1.12, heroStart:0,
        heroBgFit:'cover', heroBgPosition:'center', heroEmptyBg:'linear-gradient(135deg,#1b2030,#0b0e17)',
        heroOverlay:'linear-gradient(180deg,rgba(0,0,0,0.15),rgba(0,0,0,0.55))', heroOverlayOpacity:1,
        heroAlignH:'center', heroAlignV:'center', heroPadding:'6%', heroGap:'10px', heroMaxWidth:'70%',
        heroTitleFont:"'Orbitron',sans-serif", heroTitleSize:2.4, heroTitleUnit:'rem', heroTitleWeight:'700',
        heroTitleColor:'#ffffff', heroTitleLineHeight:'1.1', heroTitleSpacing:'normal',
        heroTitleShadow:'0 2px 18px rgba(0,0,0,0.5)',
        heroSubFont:"'Rajdhani',sans-serif", heroSubSize:1, heroSubUnit:'rem', heroSubWeight:'400',
        heroSubColor:'#d5dce8', heroSubLineHeight:'1.5', heroSubShadow:'0 1px 10px rgba(0,0,0,0.45)',
        heroDots:true, heroDotWidth:10, heroDotHeight:10, heroDotRadius:'50%', heroDotGap:8,
        heroDotBottom:18, heroDotColor:'rgba(255,255,255,0.35)', heroDotActiveColor:'#ffffff', heroDotActiveWidth:'',
        heroArrows:false, heroArrowSize:36, heroArrowRadius:'50%', heroArrowBg:'rgba(0,0,0,0.35)',
        heroArrowColor:'#ffffff', heroArrowIconSize:1, heroArrowInset:12,
        heroPrevIcon:'bi-chevron-left', heroNextIcon:'bi-chevron-right', heroChangeFunc:'',
      };
      for (const [k, v] of Object.entries(hd)) if (el[k] === undefined) el[k] = v;
    }

    // Carousel element defaults
    if (el.type === 'carousel') {
      if (!Array.isArray(el.carouselItems)) el.carouselItems = [];
      el.carouselItems.forEach(it => {
        if (!it.id) it.id = 'ci_' + Math.random().toString(36).slice(2, 8);
        if (!it.layer && it.model) it.layer = 'model';
        else if (!it.layer && it.foreground) it.layer = 'image';
        if (!Array.isArray(it.actions)) it.actions = [];
        if (!it.fields || typeof it.fields !== 'object') it.fields = {};
      });
      if (!el.carDeviceOverrides) el.carDeviceOverrides = { tablet:{}, phone:{}, 'phone-land':{} };
      ['tablet','phone','phone-land'].forEach(dv => { if (!el.carDeviceOverrides[dv]) el.carDeviceOverrides[dv] = {}; });
      if (el.carNavInset === undefined) el.carNavInset = el.carNavOffset !== undefined ? el.carNavOffset : 0;
      const cd = {
        carItems: 3, carItemsTablet: 2, carItemsPhone: 1, carMargin: 12, carStagePadding: 0,
        carAutoWidth: false, carLoop: true, carCenter: true, carAutoplay: false,
        carAutoplayTimeout: 4000, carAutoplayHoverPause: true, carSpeed: 450, carMouseDrag: true,
        carNav: true, carDots: true, carStart: 0,
        carCenterScale: 1.18, carSideScale: 0.88, carSideOpacity: 0.55,
        carCenterOffsetX: 0, carCenterOffsetY: 0, carCenterWidth: '', carCenterHeight: '',
        carItemWidth: '', carItemHeight: '', carClickToCenter: true, carLinks: [], carChangeFunc: '',
        carActiveSlot: '', carItemsPhoneLand: '', carDotPosition: 'below', carDotBottom: 8, carResetVisibility: true,
        carActiveZ: 10, carPadTop: 0, carPadBottom: 0,
        carNavOffsetX: 0, carNavOffsetY: 0, carDotOffsetX: 0, carDotOffsetY: 0,
        carNavPrevX: 0, carNavPrevY: 0, carNavNextX: 0, carNavNextY: 0,
        carItemMargin: '0', carCenterPadding: '', carCenterMargin: '', carCenterRadius: '',
        carCenterBg: '', carCenterBorder: '', carCenterShadow: '',
        carShowFields: [], carFieldSize: 0.6, carFieldColor: '#8a93a8',
        carModelHeight: '', carModelBg: 'transparent', carModelAutoRotate: true, carModelRotateSpeed: 2,
        carModelControls: false, carModelDistance: 4, carModelFov: 45, carModelLighting: 'studio',
        carModelDragRotate: true, carModelZoom: false, carModelPan: false,
        carModelDragSpeed: 1, carModelZoomSpeed: 1, carModelDamping: true,
        carModelStopSpinOnInteract: true, carModelResumeSpin: 0,
        carModelMinDistance: '', carModelMaxDistance: '', carModelMinPolar: '', carModelMaxPolar: '',
        carModelLightIntensity: 1, carModelScale: 1,
        carModelWidth: '', carModelInset: 0, carModelRotX: 0, carModelRotY: 0, carModelRotZ: 0,
        carModelY: 0, carModelCamHeight: 0.4, carModelSpinActiveOnly: false,
        carModelActiveRotX: '', carModelActiveRotY: '', carModelActiveRotZ: '',
        carModelActiveScale: '', carModelActiveDistance: '',
        carModelSideRotX: '', carModelSideRotY: '', carModelSideRotZ: '',
        carModelSideScale: '', carModelSideDistance: '',
        carSideGap: 0, carSideGapStacks: true,
        carTitleUnit: 'rem', carTextUnit: 'rem', carFieldUnit: 'rem',
        carNavSize: 28, carNavRadius: '50%', carNavIconSize: 0.8, carNavBorder: 'none', carNavOffset: 0,
        carNavPrevIcon: 'bi-chevron-left', carNavNextIcon: 'bi-chevron-right',
        carDotWidth: 7, carDotHeight: 7, carDotRadius: '50%', carDotGap: 6,
        carDotActiveWidth: '', carDotActiveHeight: '', carDotMarginTop: 6,
        carItemBg: 'rgba(255,255,255,0.05)', carItemBorder: '1px solid rgba(255,255,255,0.1)',
        carItemRadius: '12px', carItemPadding: '10px', carItemShadow: '0 6px 20px rgba(0,0,0,0.35)',
        carItemAlign: 'center', carShowImage: true, carShowTitle: true, carShowText: true,
        carImgHeight: '110px', carImgFit: 'cover', carImgRadius: '8px',
        carImgMode: 'inline', carImgPosition: 'center', carCoverAlign: 'bottom',
        carFgInset: 12, carFgFit: 'contain', carFgPosition: 'center', carFgRadius: '0', carFgShadow: '',
        carFgPadding: 0, carFgWidth: '100%', carFgHeight: '100%', carFgAlignH: 'center', carFgAlignV: 'center',
        carModelX: 0, carModelZ: 0,
        carTitleSize: 0.85, carTitleColor: '#ffffff', carTitleWeight: '700',
        carTextSize: 0.65, carTextColor: '#a0b0c0',
        carNavColor: '#ffffff', carNavBg: 'rgba(0,0,0,0.4)',
        carDotColor: 'rgba(255,255,255,0.25)', carDotActiveColor: '#00e5a0',
        carItemTemplate: '', carItemClick: 'none', carItemFunc: '', carLinkTarget: 'newTab',
      };
      for (const [k, v] of Object.entries(cd)) if (el[k] === undefined) el[k] = v;
    }
    // Video element defaults
    if (el.type === 'video') {
      if (!el.videoFit) el.videoFit = 'contain';
      if (!el.playMode) el.playMode = 'auto';
      if (!el.loopMode) el.loopMode = 'loop';
      if (el.loopCount === undefined) el.loopCount = 1;
      if (el.muted === undefined) el.muted = true;
      if (el.volume === undefined) el.volume = 1;
      if (el.videoPoster === undefined) el.videoPoster = '';
      if (el.chromaKey === undefined) el.chromaKey = false;
      if (el.chromaColor === undefined) el.chromaColor = '#00ff00';
      if (el.chromaSimilarity === undefined) el.chromaSimilarity = 0.4;
      if (el.chromaSmoothness === undefined) el.chromaSmoothness = 0.08;
      if (el.chromaSpill === undefined) el.chromaSpill = 0.1;
      if (el.videoBlend === undefined) el.videoBlend = 'none';
    }
    // Migrate old mobileX/Y/W/H/etc → overrides.phone
    if (el.mobileX || el.mobileY || el.mobileW || el.mobileH || el.mobileFontSize || el.mobilePadding || el.mobileBackground || el.mobileHidden) {
      const ph = el.overrides.phone;
      if (el.mobileX) ph.x = el.mobileX;
      if (el.mobileY) ph.y = el.mobileY;
      if (el.mobileW) ph.w = el.mobileW;
      if (el.mobileH) ph.h = el.mobileH;
      if (el.mobileFontSize) ph.fontSize = el.mobileFontSize;
      if (el.mobilePadding) ph.padding = el.mobilePadding;
      if (el.mobileBackground) ph.background = el.mobileBackground;
      if (el.mobileHidden) ph.visible = false;
      el.responsive = true;
      // Clean old fields
      delete el.mobileX; delete el.mobileY; delete el.mobileW; delete el.mobileH;
      delete el.mobileFontSize; delete el.mobilePadding; delete el.mobileBackground; delete el.mobileHidden;
    }
  });
  uiName = data.name || 'Game HUD';
  bgImage = data.bgImage || '';
  customCode = data.customCode || '';
  blocklyFuncData = data.blocklyFunctions || {};
  blocklyCurrentFunc = '';
  customBlocks = data.customBlocks || [];
  if (blocklyWorkspace) { blocklyWorkspace.clear(); refreshBlocklyFuncSelect(); registerCustomBlocks(); }
  document.getElementById('ui-name').value = uiName;
  // Recalc nextId
  nextId = 1;
  elements.forEach(el => {
    const m = el.id.match(/_(\d+)$/);
    if (m) nextId = Math.max(nextId, parseInt(m[1]) + 1);
  });
  deselectAll();
  renderAllElements();
  buildLayerList();
  resizeViewport();
}

// ── Drag & drop JSON file onto canvas ──
document.body.addEventListener('dragover', e => e.preventDefault());
document.body.addEventListener('drop', e => {
  e.preventDefault();
  const file = e.dataTransfer.files[0];
  if (file && file.name.endsWith('.json')) {
    const reader = new FileReader();
    reader.onload = ev => {
      try {
        const data = JSON.parse(ev.target.result);
        if (data.type === 'game-ui') { loadUIData(data); showToast(`Loaded "${data.name}"`); }
      } catch(err) { showToast('Invalid file', 'error'); }
    };
    reader.readAsText(file);
  }
});

// ══════════════════════════════════════
//  KEYBOARD SHORTCUTS
// ══════════════════════════════════════
document.addEventListener('keydown', e => {
  // Don't capture when typing in inputs
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;

  if (e.key === 'Delete' || e.key === 'Backspace') {
    if (selectedId) { removeElement(selectedId); e.preventDefault(); }
  }
  if (e.key === 'Escape') {
    deselectAll();
    if (previewMode) { document.getElementById('btn-preview').click(); }
  }
  if ((e.ctrlKey || e.metaKey) && e.key === 'd') {
    e.preventDefault();
    if (selectedId) duplicateElement(selectedId);
  }
  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    e.preventDefault();
    document.getElementById('btn-save').click();
  }
  // Copy
  if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
    if (selectedId) {
      const src = elements.find(el => el.id === selectedId);
      if (src) { clipboard = { ...JSON.parse(JSON.stringify(src)), _cut: false }; showToast('Copied'); }
      e.preventDefault();
    }
  }
  // Cut
  if ((e.ctrlKey || e.metaKey) && e.key === 'x') {
    if (selectedId) {
      const src = elements.find(el => el.id === selectedId);
      if (src) { clipboard = { ...JSON.parse(JSON.stringify(src)), _cut: true, _origId: src.id }; showToast('Cut'); }
      e.preventDefault();
    }
  }
  // Paste
  if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
    if (clipboard) {
      e.preventDefault();
      if (clipboard._cut) {
        // Remove original if still exists
        const origIdx = elements.findIndex(el => el.id === clipboard._origId);
        if (origIdx >= 0) {
          const origDom = viewport.querySelector(`[data-id="${clipboard._origId}"]`);
          if (origDom) origDom.remove();
          elements.splice(origIdx, 1);
        }
      }
      const copy = JSON.parse(JSON.stringify(clipboard));
      copy.id = 'el_' + (nextId++);
      copy.name = (copy.name || '').replace(/ copy$/, '') + ' copy';
      if (!clipboard._cut) { copy.x = Math.min(copy.x + 2, 98); copy.y = Math.min(copy.y + 2, 98); }
      delete copy._cut; delete copy._origId;
      copy.zIndex = 10 + elements.length;
      elements.push(copy);
      renderAllElements();
      buildLayerList();
      selectElement(copy.id);
      showToast('Pasted');
      if (clipboard._cut) clipboard = null; // clear cut buffer after paste
    }
  }
  // Arrow keys nudge
  if (selectedId && ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
    const el = elements.find(e => e.id === selectedId);
    if (el && !el.locked) {
      e.preventDefault();
      const step = e.shiftKey ? 5 : 1;
      if (e.key === 'ArrowLeft') setVal(el, 'x', Math.max(0, getVal(el,'x') - step));
      if (e.key === 'ArrowRight') setVal(el, 'x', Math.min(100, getVal(el,'x') + step));
      if (e.key === 'ArrowUp') setVal(el, 'y', Math.max(0, getVal(el,'y') - step));
      if (e.key === 'ArrowDown') setVal(el, 'y', Math.min(100, getVal(el,'y') + step));
      buildPropsPanel(el);
    }
  }
});

// ══════════════════════════════════════
//  SETTINGS
// ══════════════════════════════════════
document.getElementById('btn-settings').onclick = () => {
  document.getElementById('settings-overlay').classList.add('visible');
  document.getElementById('set-custom-code').value = customCode;
  document.getElementById('set-theme').checked = document.documentElement.getAttribute('data-theme') === 'light';
  document.getElementById('set-snap').checked = snapEnabled;
  document.getElementById('set-grid').checked = viewport.classList.contains('show-grid');
};
document.getElementById('set-close').onclick = () => {
  document.getElementById('settings-overlay').classList.remove('visible');
};
document.getElementById('settings-overlay').onclick = (e) => {
  if (e.target === document.getElementById('settings-overlay')) {
    document.getElementById('settings-overlay').classList.remove('visible');
  }
};
document.getElementById('set-theme').onchange = function() {
  document.documentElement.setAttribute('data-theme', this.checked ? 'light' : 'dark');
};
document.getElementById('set-snap').onchange = function() {
  snapEnabled = this.checked;
  document.getElementById('btn-snap').classList.toggle('active', snapEnabled);
};
document.getElementById('set-grid').onchange = function() {
  viewport.classList.toggle('show-grid', this.checked);
  document.getElementById('btn-grid').classList.toggle('active', this.checked);
};
document.getElementById('set-save').onclick = () => {
  // Sync: if code editor changed customCode, settings textarea might be stale
  const stc = document.getElementById('set-custom-code');
  if (stc && stc.value !== customCode) customCode = stc.value;
  else customCode = codeMain?.value || customCode;
  const config = {
    theme: document.documentElement.getAttribute('data-theme'),
    snap: snapEnabled,
    grid: viewport.classList.contains('show-grid'),
    guides: viewport.classList.contains('show-guides'),
  };
  try { localStorage.setItem('uibuilder-config', JSON.stringify(config)); } catch(e) {}
  showToast('Config saved');
  document.getElementById('settings-overlay').classList.remove('visible');
};
// Load saved config on start
try {
  const cfg = JSON.parse(localStorage.getItem('uibuilder-config') || '{}');
  if (cfg.theme) document.documentElement.setAttribute('data-theme', cfg.theme);
  if (cfg.snap === false) { snapEnabled = false; document.getElementById('btn-snap').classList.remove('active'); }
  if (cfg.grid === false) { viewport.classList.remove('show-grid'); document.getElementById('btn-grid').classList.remove('active'); }
  if (cfg.guides === false) { viewport.classList.remove('show-guides'); document.getElementById('btn-guides').classList.remove('active'); }
} catch(e) {}

// ══════════════════════════════════════
//  CODE EDITOR
// ══════════════════════════════════════
const codeOverlay = document.getElementById('code-overlay');
const codeMain = document.getElementById('code-main');
let blocklyWorkspace = null;
let blocklyInited = false;

// Tab switching
document.querySelectorAll('.code-tab').forEach(tab => {
  tab.onclick = () => {
    document.querySelectorAll('.code-tab').forEach(t => t.classList.remove('active'));
    document.querySelectorAll('.code-tab-content').forEach(c => c.classList.remove('active'));
    tab.classList.add('active');
    document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
    if (tab.dataset.tab === 'blocks' && !blocklyInited) initBlockly();
    if (tab.dataset.tab === 'blocks' && blocklyWorkspace) {
      Blockly.svgResize(blocklyWorkspace);
    }
  };
});

// Add Function button
// ── Function creation dialog ──
function showFuncDialog(callback, existingName, existingArgs) {
  let overlay = document.getElementById('func-dialog-overlay');
  if (overlay) overlay.remove();
  overlay = document.createElement('div');
  overlay.id = 'func-dialog-overlay';
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:400;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(3px);';
  const box = document.createElement('div');
  box.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border-color);border-radius:10px;padding:16px;width:380px;max-width:90vw;';

  let args = existingArgs ? [...existingArgs] : ['sourceEl', 'targetEl'];

  function render() {
    box.innerHTML = '';
    // Title
    const title = document.createElement('h6');
    title.style.cssText = "font-family:'Orbitron',sans-serif;font-size:0.5rem;color:var(--accent);margin:0 0 12px;letter-spacing:1px;";
    title.textContent = existingName ? 'EDIT FUNCTION' : 'NEW FUNCTION';
    box.appendChild(title);

    // Name
    const nameRow = document.createElement('div');
    nameRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:10px;';
    const nameLbl = document.createElement('span');
    nameLbl.style.cssText = 'font-size:0.5rem;color:var(--text-secondary);width:50px;';
    nameLbl.textContent = 'Name';
    const nameInp = document.createElement('input');
    nameInp.className = 'pp-input'; nameInp.id = 'fd-name';
    nameInp.value = existingName || ''; nameInp.placeholder = 'e.g. onAttack';
    nameInp.style.flex = '1';
    if (existingName) { nameInp.readOnly = true; nameInp.style.opacity = '0.6'; }
    nameInp.oninput = () => {
      const pv = box.querySelector('.fd-preview');
      if (pv) pv.textContent = `function ${nameInp.value||'myFunc'}(${args.filter(a=>a).join(', ')}) { ... }`;
    };
    nameRow.appendChild(nameLbl); nameRow.appendChild(nameInp);
    box.appendChild(nameRow);

    // Args header
    const argsHdr = document.createElement('div');
    argsHdr.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;';
    const argsLbl = document.createElement('span');
    argsLbl.style.cssText = "font-family:'Orbitron',sans-serif;font-size:0.4rem;color:var(--accent2);letter-spacing:1px;";
    argsLbl.textContent = 'ARGUMENTS';
    const addArgBtn = document.createElement('button');
    addArgBtn.className = 'pp-btn';
    addArgBtn.style.cssText = 'font-size:0.4rem;padding:2px 8px;background:rgba(167,139,250,0.08);border-color:var(--accent2);color:var(--accent2);';
    addArgBtn.innerHTML = '<i class="bi bi-plus"></i> Add';
    addArgBtn.onclick = () => { args.push(''); render(); };
    argsHdr.appendChild(argsLbl); argsHdr.appendChild(addArgBtn);
    box.appendChild(argsHdr);

    // Presets
    const presetRow = document.createElement('div');
    presetRow.style.cssText = 'display:flex;gap:4px;margin-bottom:8px;flex-wrap:wrap;';
    const presets = [
      { label:'No args', args:[] },
      { label:'sourceEl, targetEl', args:['sourceEl','targetEl'] },
      { label:'sourceEl', args:['sourceEl'] },
      { label:'event', args:['event'] },
    ];
    presets.forEach(p => {
      const btn = document.createElement('button');
      btn.className = 'pp-btn';
      btn.style.cssText = 'font-size:0.38rem;padding:2px 6px;background:rgba(255,255,255,0.03);border-color:var(--border-color);color:var(--text-muted);';
      btn.textContent = p.label || '(none)';
      btn.onclick = () => { args = [...p.args]; render(); };
      presetRow.appendChild(btn);
    });
    box.appendChild(presetRow);

    // Arg list
    if (args.length === 0) {
      const empty = document.createElement('div');
      empty.style.cssText = 'font-size:0.45rem;color:var(--text-muted);padding:4px 0 8px;font-style:italic;';
      empty.textContent = 'function ' + (document.getElementById('fd-name')?.value || 'name') + '() — no arguments';
      box.appendChild(empty);
    }
    args.forEach((arg, i) => {
      const row = document.createElement('div');
      row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px;';
      const num = document.createElement('span');
      num.style.cssText = 'font-size:0.4rem;color:var(--text-muted);width:16px;text-align:right;';
      num.textContent = (i+1) + '.';
      const inp = document.createElement('input');
      inp.className = 'pp-input'; inp.value = arg; inp.placeholder = 'argName';
      inp.style.flex = '1';
      inp.onchange = () => { args[i] = inp.value.replace(/[^a-zA-Z0-9_$]/g, ''); render(); };
      const delBtn = document.createElement('div');
      delBtn.style.cssText = 'width:20px;height:20px;border-radius:3px;background:rgba(255,77,106,0.08);border:1px solid var(--danger);color:var(--danger);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:0.4rem;flex-shrink:0;';
      delBtn.innerHTML = '<i class="bi bi-x"></i>';
      delBtn.onclick = () => { args.splice(i, 1); render(); };
      row.appendChild(num); row.appendChild(inp); row.appendChild(delBtn);
      box.appendChild(row);
    });

    // Preview
    const preview = document.createElement('div');
    const pName = document.getElementById('fd-name')?.value || existingName || 'myFunc';
    preview.className = 'fd-preview';
    preview.style.cssText = 'font-family:"Courier New",monospace;font-size:0.5rem;color:#9cdcfe;background:rgba(0,0,0,0.2);padding:6px 10px;border-radius:4px;margin:10px 0 12px;';
    preview.textContent = `function ${pName}(${args.filter(a=>a).join(', ')}) { ... }`;
    box.appendChild(preview);

    // Buttons
    const btnRow = document.createElement('div');
    btnRow.style.cssText = 'display:flex;gap:6px;';
    const okBtn = document.createElement('button');
    okBtn.className = 'pp-btn';
    okBtn.style.cssText = 'flex:1;background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);';
    okBtn.innerHTML = '<i class="bi bi-check-lg"></i> ' + (existingName ? 'Update' : 'Create');
    okBtn.onclick = () => {
      const name = document.getElementById('fd-name').value.trim();
      if (!name.match(/^[a-zA-Z_]\w*$/)) { showToast('Invalid function name', 'error'); return; }
      const cleanArgs = args.filter(a => a && a.match(/^[a-zA-Z_$]\w*$/));
      overlay.remove();
      callback(name, cleanArgs);
    };
    const cancelBtn = document.createElement('button');
    cancelBtn.className = 'pp-btn';
    cancelBtn.style.cssText = 'flex:1;background:rgba(255,255,255,0.04);border-color:var(--border-color);color:var(--text-secondary);';
    cancelBtn.innerHTML = 'Cancel';
    cancelBtn.onclick = () => overlay.remove();
    btnRow.appendChild(okBtn); btnRow.appendChild(cancelBtn);
    box.appendChild(btnRow);
  }

  render();
  overlay.appendChild(box);
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
  document.body.appendChild(overlay);
  // Focus name input
  setTimeout(() => { const n = document.getElementById('fd-name'); if (n && !existingName) n.focus(); }, 50);
}

document.getElementById('btn-add-func').onclick = () => {
  showFuncDialog((name, args) => {
    const argStr = args.join(', ');
    const template = `\nfunction ${name}(${argStr}) {\n  // Your code here\n}\n`;
    codeMain.value += template;
    codeMain.scrollTop = codeMain.scrollHeight;
    updateFuncSelect();
    document.getElementById('code-func-select').value = name;
    showToast(`Function "${name}" added`);
  });
};

document.getElementById('btn-edit-func').onclick = () => {
  const sel = document.getElementById('code-func-select');
  const fname = sel.value;
  if (!fname) { showToast('Select a function first', 'error'); return; }
  const m = codeMain.value.match(new RegExp(`function\\s+${fname}\\s*\\(([^)]*)\\)`));
  const curArgs = m ? m[1].split(',').map(a => a.trim()).filter(a => a) : [];
  showFuncDialog((name, newArgs) => {
    const oldSig = new RegExp(`function\\s+${fname}\\s*\\([^)]*\\)`);
    codeMain.value = codeMain.value.replace(oldSig, `function ${fname}(${newArgs.join(', ')})`);
    updateFuncSelect();
    showToast(`Updated "${fname}" arguments`);
  }, fname, curArgs);
};

document.getElementById('btn-del-func').onclick = () => {
  const sel = document.getElementById('code-func-select');
  const fname = sel.value;
  if (!fname) { showToast('Select a function first', 'error'); return; }
  if (!confirm(`Delete function "${fname}" from code?`)) return;
  const regex = new RegExp(`\\n*function\\s+${fname}\\s*\\([^)]*\\)\\s*\\{[\\s\\S]*?\\n\\}`, 'm');
  codeMain.value = codeMain.value.replace(regex, '');
  updateFuncSelect();
  showToast(`Deleted "${fname}"`);
};

document.getElementById('code-func-select').onchange = () => {
  const fname = document.getElementById('code-func-select').value;
  if (!fname) return;
  const idx = codeMain.value.indexOf('function ' + fname);
  if (idx >= 0) {
    codeMain.focus();
    codeMain.setSelectionRange(idx, idx);
    // Scroll to the function
    const lines = codeMain.value.substring(0, idx).split('\n').length;
    const lineH = parseFloat(getComputedStyle(codeMain).lineHeight) || 18;
    codeMain.scrollTop = Math.max(0, (lines - 2) * lineH);
  }
};

function updateFuncSelect() {
  const sel = document.getElementById('code-func-select');
  const cur = sel.value;
  sel.innerHTML = '<option value="">(select function)</option>';
  const matches = [...codeMain.value.matchAll(/function\s+(\w+)\s*\(([^)]*)\)/g)];
  matches.forEach(m => {
    const opt = document.createElement('option');
    opt.value = m[1];
    opt.textContent = `${m[1]}(${m[2]})`;
    sel.appendChild(opt);
  });
  // Restore selection if still exists
  if (matches.some(m => m[1] === cur)) sel.value = cur;
}

codeMain.addEventListener('input', () => { clearTimeout(codeMain._chipTimer); codeMain._chipTimer = setTimeout(updateFuncSelect, 500); });

document.getElementById('btn-code').onclick = () => {
  codeMain.value = customCode;
  updateFuncSelect();
  if (blocklyInited) refreshBlocklyFuncSelect();
  codeOverlay.classList.add('visible');
  if (blocklyWorkspace) Blockly.svgResize(blocklyWorkspace);
};
document.getElementById('code-close').onclick = () => {
  codeOverlay.classList.remove('visible');
};
document.getElementById('code-save').onclick = () => {
  if (blocklyWorkspace && blocklyCurrentFunc) saveCurrentBlocklyFunc();
  customCode = codeMain.value;
  const stc = document.getElementById('set-custom-code');
  if (stc) stc.value = customCode;
  codeOverlay.classList.remove('visible');
  showToast('Code saved');
};
document.getElementById('code-run').onclick = () => {
  if (blocklyWorkspace && blocklyCurrentFunc) saveCurrentBlocklyFunc();
  customCode = codeMain.value;
  let s = document.getElementById('preview-custom-code');
  if (s) s.remove();
  s = document.createElement('script');
  s.id = 'preview-custom-code';
  try {
    s.textContent = customCode;
    document.body.appendChild(s);
    showToast('Code injected — functions available');
  } catch(err) {
    showToast('Error: ' + err.message, 'error');
  }
};

// ── Blockly Setup ──
let blocklyFuncData = {}; // { funcName: { json: workspaceJson } }
let blocklyCurrentFunc = '';
let customBlocks = []; // user-defined blockly blocks

function initBlockly() {
  if (blocklyInited || typeof Blockly === 'undefined') {
    if (typeof Blockly === 'undefined') showToast('Blockly not loaded — check internet', 'error');
    return;
  }
  blocklyInited = true;

  const C_UI = 160, C_KEY = 210, C_OBJ = 260, C_QRY = 30, C_SCN = 330, C_TIME = 65, C_FUNC = 290;

  // ── Function Definition block (visual wrapper) ──
  const FLabel = Blockly.FieldLabelSerializable || Blockly.FieldLabel;
  Blockly.Blocks['gameapi_func_def'] = { init() {
    this.appendDummyInput('HEADER')
      .appendField('⚡ function')
      .appendField(new FLabel('myFunc'), 'NAME')
      .appendField('(')
      .appendField(new FLabel(''), 'ARGS')
      .appendField(')');
    this.appendStatementInput('BODY').appendField('do');
    this.setColour(C_FUNC);
    this.setDeletable(false);
    this.setTooltip('Function body — add blocks inside');
  }};

  // ── UI API blocks ──
  Blockly.Blocks['gameapi_set_value'] = { init() {
    this.appendValueInput('VAL').setCheck(null).appendField('set UI var').appendField(new Blockly.FieldTextInput('score'), 'VAR').appendField('to');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_UI);
  }};
  Blockly.Blocks['gameapi_get_value'] = { init() {
    this.appendDummyInput().appendField('get UI var').appendField(new Blockly.FieldTextInput('score'), 'VAR');
    this.setOutput(true); this.setColour(C_UI);
  }};
  Blockly.Blocks['gameapi_set_ui_visible'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('set element');
    this.appendDummyInput().appendField('visible').appendField(new Blockly.FieldDropdown([['true','true'],['false','false']]), 'VIS');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_UI);
  }};
  Blockly.Blocks['gameapi_set_content'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('set element');
    this.appendValueInput('TEXT').setCheck(null).appendField('text to');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_UI);
  }};
  Blockly.Blocks['gameapi_set_style'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('set element');
    this.appendDummyInput().appendField('style').appendField(new Blockly.FieldTextInput('color'), 'PROP').appendField('to');
    this.appendValueInput('VAL').setCheck(null);
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_UI);
  }};

  // ── Key Simulation blocks ──
  const keyOpts = [['W','w'],['A','a'],['S','s'],['D','d'],['Shift','shift'],['Space','space']];
  Blockly.Blocks['gameapi_press_key'] = { init() {
    this.appendDummyInput().appendField('press key').appendField(new Blockly.FieldDropdown(keyOpts), 'KEY');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_KEY);
  }};
  Blockly.Blocks['gameapi_release_key'] = { init() {
    this.appendDummyInput().appendField('release key').appendField(new Blockly.FieldDropdown(keyOpts), 'KEY');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_KEY);
  }};
  Blockly.Blocks['gameapi_simulate_key'] = { init() {
    this.appendDummyInput().appendField('tap key').appendField(new Blockly.FieldDropdown(keyOpts), 'KEY')
      .appendField('for').appendField(new Blockly.FieldNumber(200, 50, 5000), 'MS').appendField('ms');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_KEY);
  }};
  Blockly.Blocks['gameapi_release_all'] = { init() {
    this.appendDummyInput().appendField('release all keys');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_KEY);
  }};

  // ── 3D Object blocks ──
  Blockly.Blocks['gameapi_get_object'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('get object');
    this.setInputsInline(true);
    this.setOutput(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_get_objects'] = { init() {
    this.appendValueInput('CAT').setCheck(null).appendField('get all objects of type');
    this.setInputsInline(true);
    this.setOutput(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_set_position'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('move');
    this.appendValueInput('X').setCheck('Number').appendField('x');
    this.appendValueInput('Y').setCheck('Number').appendField('y');
    this.appendValueInput('Z').setCheck('Number').appendField('z');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_set_rotation'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('rotate');
    this.appendValueInput('RX').setCheck('Number').appendField('rx');
    this.appendValueInput('RY').setCheck('Number').appendField('ry');
    this.appendValueInput('RZ').setCheck('Number').appendField('rz');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_set_scale'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('scale');
    this.appendValueInput('SX').setCheck('Number').appendField('x');
    this.appendValueInput('SY').setCheck('Number').appendField('y');
    this.appendValueInput('SZ').setCheck('Number').appendField('z');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_set_animation'] = { init() {
    this.appendValueInput('CLIP').setCheck(null).appendField('play animation');
    this.appendValueInput('ID').setCheck(null).appendField('on');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_get_animations'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('get animations of');
    this.setInputsInline(true);
    this.setOutput(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_set_obj_visible'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('set 3D object');
    this.appendDummyInput().appendField('visible').appendField(new Blockly.FieldDropdown([['true','true'],['false','false']]), 'VIS');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_OBJ);
  }};
  Blockly.Blocks['gameapi_destroy'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('destroy object');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_OBJ);
  }};

  // ── Query blocks ──
  Blockly.Blocks['gameapi_get_position'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('position of');
    this.setInputsInline(true);
    this.setOutput(true); this.setColour(C_QRY);
  }};
  Blockly.Blocks['gameapi_get_distance'] = { init() {
    this.appendValueInput('ID1').setCheck(null).appendField('distance');
    this.appendValueInput('ID2').setCheck(null).appendField('to');
    this.setInputsInline(true);
    this.setOutput(true); this.setColour(C_QRY);
  }};
  Blockly.Blocks['gameapi_get_property'] = { init() {
    this.appendDummyInput().appendField('get property').appendField(new Blockly.FieldTextInput('loot'), 'PROP').appendField('of');
    this.appendValueInput('ID').setCheck(null);
    this.setInputsInline(true);
    this.setOutput(true); this.setColour(C_QRY);
  }};
  Blockly.Blocks['gameapi_set_property'] = { init() {
    this.appendDummyInput().appendField('set property').appendField(new Blockly.FieldTextInput('opened'), 'PROP').appendField('of');
    this.appendValueInput('ID').setCheck(null);
    this.appendValueInput('VAL').setCheck(null).appendField('to');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_QRY);
  }};

  // ── Scene blocks ──
  Blockly.Blocks['gameapi_switch_camera'] = { init() {
    this.appendValueInput('ID').setCheck(null).appendField('switch camera to');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_SCN);
  }};
  Blockly.Blocks['gameapi_show_dialog'] = { init() {
    this.appendValueInput('TEXT').setCheck(null).appendField('show dialog');
    this.appendValueInput('SPEAKER').setCheck(null).appendField('speaker');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_SCN);
  }};
  Blockly.Blocks['gameapi_toast'] = { init() {
    this.appendValueInput('MSG').setCheck(null).appendField('show toast');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_SCN);
  }};
  Blockly.Blocks['gameapi_play_audio'] = { init() {
    this.appendValueInput('URL').setCheck(null).appendField('play audio');
    this.setInputsInline(true);
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_SCN);
  }};
  Blockly.Blocks['gameapi_wait'] = { init() {
    this.appendDummyInput().appendField('wait').appendField(new Blockly.FieldNumber(500, 10, 10000), 'MS').appendField('ms then');
    this.appendStatementInput('DO');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_TIME);
  }};
  Blockly.Blocks['gameapi_log'] = { init() {
    this.appendValueInput('MSG').appendField('console.log');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_TIME);
  }};
  Blockly.Blocks['gameapi_alert'] = { init() {
    this.appendValueInput('MSG').appendField('alert');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_TIME);
  }};
  Blockly.Blocks['gameapi_js_expr'] = { init() {
    this.appendDummyInput().appendField('JS').appendField(new Blockly.FieldTextInput('expression'), 'CODE');
    this.setOutput(true); this.setColour(C_TIME);
    this.setTooltip('Raw JavaScript expression — returns a value');
  }};
  Blockly.Blocks['gameapi_js_stmt'] = { init() {
    this.appendDummyInput().appendField('run JS:').appendField(new Blockly.FieldTextInput('// code'), 'CODE');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(C_TIME);
    this.setTooltip('Raw JavaScript statement');
  }};
  // Global variable blocks
  Blockly.Blocks['gameapi_set_global'] = { init() {
    this.appendValueInput('VAL').appendField('set global').appendField(new Blockly.FieldTextInput('myVar'),'NAME').appendField('to');
    this.setPreviousStatement(true); this.setNextStatement(true); this.setColour(50);
    this.setTooltip('Set a global variable accessible from any function');
  }};
  Blockly.Blocks['gameapi_get_global'] = { init() {
    this.appendDummyInput().appendField('get global').appendField(new Blockly.FieldTextInput('myVar'),'NAME');
    this.setOutput(true); this.setColour(50);
    this.setTooltip('Get a global variable value');
  }};

  // ══ Code Generators ══
  const gen = javascript.javascriptGenerator;
  const O = javascript.Order;
  gen.forBlock['gameapi_func_def'] = function(block) {
    return gen.statementToCode(block, 'BODY');
  };
  gen.forBlock['gameapi_set_value'] = b => `GameAPI.setUIValue('${b.getFieldValue('VAR')}', ${gen.valueToCode(b,'VAL',O.NONE)||'0'});\n`;
  gen.forBlock['gameapi_get_value'] = b => [`GameAPI.getUIValue('${b.getFieldValue('VAR')}')`, O.FUNCTION_CALL];
  gen.forBlock['gameapi_set_ui_visible'] = b => `GameAPI.setUIVisible(${gen.valueToCode(b,'ID',O.NONE)||"'el_1'"}, ${b.getFieldValue('VIS')});\n`;
  gen.forBlock['gameapi_set_content'] = b => `GameAPI.setUIContent(${gen.valueToCode(b,'ID',O.NONE)||"'el_1'"}, ${gen.valueToCode(b,'TEXT',O.NONE)||"''"});\n`;
  gen.forBlock['gameapi_set_style'] = b => `GameAPI.setUIStyle(${gen.valueToCode(b,'ID',O.NONE)||"'el_1'"}, '${b.getFieldValue('PROP')}', ${gen.valueToCode(b,'VAL',O.NONE)||"''"});\n`;
  gen.forBlock['gameapi_press_key'] = b => `GameAPI.pressKey('${b.getFieldValue('KEY')}');\n`;
  gen.forBlock['gameapi_release_key'] = b => `GameAPI.releaseKey('${b.getFieldValue('KEY')}');\n`;
  gen.forBlock['gameapi_simulate_key'] = b => `GameAPI.simulateKey('${b.getFieldValue('KEY')}', ${b.getFieldValue('MS')});\n`;
  gen.forBlock['gameapi_release_all'] = () => 'GameAPI.releaseAllKeys();\n';
  gen.forBlock['gameapi_get_object'] = b => [`GameAPI.getObject(${gen.valueToCode(b,'ID',O.NONE)||"'npc_1'"})`, O.FUNCTION_CALL];
  gen.forBlock['gameapi_get_objects'] = b => [`GameAPI.getObjects(${gen.valueToCode(b,'CAT',O.NONE)||"'npc'"})`, O.FUNCTION_CALL];
  gen.forBlock['gameapi_set_position'] = b => `GameAPI.setPosition(${gen.valueToCode(b,'ID',O.NONE)||"'obj_1'"}, ${gen.valueToCode(b,'X',O.NONE)||'0'}, ${gen.valueToCode(b,'Y',O.NONE)||'0'}, ${gen.valueToCode(b,'Z',O.NONE)||'0'});\n`;
  gen.forBlock['gameapi_set_rotation'] = b => `GameAPI.setRotation(${gen.valueToCode(b,'ID',O.NONE)||"'obj_1'"}, ${gen.valueToCode(b,'RX',O.NONE)||'0'}, ${gen.valueToCode(b,'RY',O.NONE)||'0'}, ${gen.valueToCode(b,'RZ',O.NONE)||'0'});\n`;
  gen.forBlock['gameapi_set_scale'] = b => `GameAPI.setScale(${gen.valueToCode(b,'ID',O.NONE)||"'obj_1'"}, ${gen.valueToCode(b,'SX',O.NONE)||'1'}, ${gen.valueToCode(b,'SY',O.NONE)||'1'}, ${gen.valueToCode(b,'SZ',O.NONE)||'1'});\n`;
  gen.forBlock['gameapi_set_animation'] = b => `GameAPI.setAnimation(${gen.valueToCode(b,'ID',O.NONE)||"'npc_1'"}, ${gen.valueToCode(b,'CLIP',O.NONE)||"'Idle'"});\n`;
  gen.forBlock['gameapi_get_animations'] = b => [`GameAPI.getAnimations(${gen.valueToCode(b,'ID',O.NONE)||"'npc_1'"})`, O.FUNCTION_CALL];
  gen.forBlock['gameapi_set_obj_visible'] = b => `GameAPI.setVisible(${gen.valueToCode(b,'ID',O.NONE)||"'obj_1'"}, ${b.getFieldValue('VIS')});\n`;
  gen.forBlock['gameapi_destroy'] = b => `GameAPI.destroy(${gen.valueToCode(b,'ID',O.NONE)||"'obj_1'"});\n`;
  gen.forBlock['gameapi_get_position'] = b => [`GameAPI.getPosition(${gen.valueToCode(b,'ID',O.NONE)||"'player_1'"})`, O.FUNCTION_CALL];
  gen.forBlock['gameapi_get_distance'] = b => [`GameAPI.getDistance(${gen.valueToCode(b,'ID1',O.NONE)||"'player_1'"}, ${gen.valueToCode(b,'ID2',O.NONE)||"'npc_1'"})`, O.FUNCTION_CALL];
  gen.forBlock['gameapi_get_property'] = b => [`GameAPI.getProperty(${gen.valueToCode(b,'ID',O.NONE)||"'obj_1'"}, '${b.getFieldValue('PROP')}')`, O.FUNCTION_CALL];
  gen.forBlock['gameapi_set_property'] = b => `GameAPI.setProperty(${gen.valueToCode(b,'ID',O.NONE)||"'obj_1'"}, '${b.getFieldValue('PROP')}', ${gen.valueToCode(b,'VAL',O.NONE)||'null'});\n`;
  gen.forBlock['gameapi_switch_camera'] = b => `GameAPI.switchCamera(${gen.valueToCode(b,'ID',O.NONE)||"'camera_2'"});\n`;
  gen.forBlock['gameapi_show_dialog'] = b => `GameAPI.showDialog(${gen.valueToCode(b,'TEXT',O.NONE)||"'Hello!'"}, {speaker:${gen.valueToCode(b,'SPEAKER',O.NONE)||"'NPC'"}});\n`;
  gen.forBlock['gameapi_toast'] = b => `GameAPI.showToast(${gen.valueToCode(b,'MSG',O.NONE)||"''"});\n`;
  gen.forBlock['gameapi_play_audio'] = b => `GameAPI.playAudio(${gen.valueToCode(b,'URL',O.NONE)||"''"});\n`;
  gen.forBlock['gameapi_wait'] = b => `setTimeout(function() {\n${gen.statementToCode(b,'DO')}}, ${b.getFieldValue('MS')});\n`;
  gen.forBlock['gameapi_log'] = b => `console.log(${gen.valueToCode(b,'MSG',O.NONE)||"''"});\n`;
  gen.forBlock['gameapi_alert'] = b => `alert(${gen.valueToCode(b,'MSG',O.NONE)||"''"});\n`;
  gen.forBlock['gameapi_js_expr'] = b => [b.getFieldValue('CODE') || '0', O.ATOMIC];
  gen.forBlock['gameapi_js_stmt'] = b => (b.getFieldValue('CODE') || '') + ';\n';
  gen.forBlock['gameapi_set_global'] = b => `GameAPI.setGlobal("${b.getFieldValue('NAME')}", ${gen.valueToCode(b,'VAL',O.NONE)||'null'});\n`;
  gen.forBlock['gameapi_get_global'] = b => [`GameAPI.getGlobal("${b.getFieldValue('NAME')}")`, O.FUNCTION_CALL];

  // ══ Toolbox ══
  const toolbox = { kind:'categoryToolbox', contents:[
    { kind:'category', name:'UI', colour:C_UI, contents:[
      { kind:'block', type:'gameapi_set_value' },
      { kind:'block', type:'gameapi_get_value' },
      { kind:'block', type:'gameapi_set_ui_visible' },
      { kind:'block', type:'gameapi_set_content' },
      { kind:'block', type:'gameapi_set_style' },
    ]},
    { kind:'category', name:'Controls', colour:C_KEY, contents:[
      { kind:'block', type:'gameapi_press_key' },
      { kind:'block', type:'gameapi_release_key' },
      { kind:'block', type:'gameapi_simulate_key' },
      { kind:'block', type:'gameapi_release_all' },
    ]},
    { kind:'category', name:'3D Objects', colour:C_OBJ, contents:[
      { kind:'block', type:'gameapi_get_object' },
      { kind:'block', type:'gameapi_get_objects' },
      { kind:'block', type:'gameapi_set_position' },
      { kind:'block', type:'gameapi_set_rotation' },
      { kind:'block', type:'gameapi_set_scale' },
      { kind:'block', type:'gameapi_set_animation' },
      { kind:'block', type:'gameapi_get_animations' },
      { kind:'block', type:'gameapi_set_obj_visible' },
      { kind:'block', type:'gameapi_destroy' },
    ]},
    { kind:'category', name:'Queries', colour:C_QRY, contents:[
      { kind:'block', type:'gameapi_get_position' },
      { kind:'block', type:'gameapi_get_distance' },
      { kind:'block', type:'gameapi_get_property' },
      { kind:'block', type:'gameapi_set_property' },
    ]},
    { kind:'category', name:'Scene', colour:C_SCN, contents:[
      { kind:'block', type:'gameapi_switch_camera' },
      { kind:'block', type:'gameapi_show_dialog' },
      { kind:'block', type:'gameapi_toast' },
      { kind:'block', type:'gameapi_play_audio' },
    ]},
    { kind:'category', name:'Timing', colour:C_TIME, contents:[
      { kind:'block', type:'gameapi_wait' },
      { kind:'block', type:'gameapi_log' },
      { kind:'block', type:'gameapi_alert' },
      { kind:'block', type:'gameapi_js_expr' },
      { kind:'block', type:'gameapi_js_stmt' },
    ]},
    { kind:'sep' },
    { kind:'category', name:'Logic', categorystyle:'logic_category', contents:[
      { kind:'block', type:'controls_if' },
      { kind:'block', type:'logic_compare' },
      { kind:'block', type:'logic_operation' },
      { kind:'block', type:'logic_negate' },
      { kind:'block', type:'logic_boolean' },
    ]},
    { kind:'category', name:'Loops', categorystyle:'loop_category', contents:[
      { kind:'block', type:'controls_repeat_ext' },
      { kind:'block', type:'controls_whileUntil' },
      { kind:'block', type:'controls_for' },
    ]},
    { kind:'category', name:'Math', categorystyle:'math_category', contents:[
      { kind:'block', type:'math_number' },
      { kind:'block', type:'math_arithmetic' },
      { kind:'block', type:'math_random_int' },
      { kind:'block', type:'math_modulo' },
    ]},
    { kind:'category', name:'Text', categorystyle:'text_category', contents:[
      { kind:'block', type:'text' },
      { kind:'block', type:'text_join' },
      { kind:'block', type:'text_length' },
    ]},
    { kind:'category', name:'Variables', categorystyle:'variable_category', custom:'VARIABLE' },
  ]};

  blocklyWorkspace = Blockly.inject('blockly-area', {
    toolbox,
    theme: Blockly.Themes.Classic,
    grid: { spacing:20, length:3, colour:'#333', snap:true },
    zoom: { controls:true, wheel:true, startScale:0.9 },
    trashcan: true,
  });
  // Store base toolbox for custom block rebuilds
  blocklyWorkspace._baseToolbox = JSON.parse(JSON.stringify(toolbox));

  // Auto-preview generated code on change
  blocklyWorkspace.addChangeListener(() => {
    clearTimeout(blocklyWorkspace._genTimer);
    blocklyWorkspace._genTimer = setTimeout(previewBlocklyCode, 400);
  });

  // ── Function selector UI ──
  const funcSelect = document.getElementById('blockly-func-select');
  funcSelect.onchange = () => {
    saveCurrentBlocklyFunc();
    blocklyCurrentFunc = funcSelect.value;
    loadBlocklyFunc(blocklyCurrentFunc);
  };
  document.getElementById('blockly-add-func').onclick = () => {
    showFuncDialog((name, args) => {
      if (blocklyFuncData[name]) { showToast('Function already exists', 'error'); return; }
      saveCurrentBlocklyFunc();
      blocklyFuncData[name] = { json: null, args: args };
      blocklyCurrentFunc = name;
      blocklyWorkspace.clear();
      registerArgBlocks(args);
      placeFuncDefBlock(name, args);
      refreshBlocklyFuncSelect();
      funcSelect.value = name;
      showToast(`Function "${name}" created — add blocks inside!`);
    });
  };
  document.getElementById('blockly-gen-func').onclick = () => {
    if (!blocklyCurrentFunc) { showToast('Select or create a function first', 'error'); return; }
    saveCurrentBlocklyFunc();
    insertBlocklyFuncIntoCode(blocklyCurrentFunc);
    showToast(`Generated "${blocklyCurrentFunc}" → Code tab`);
  };
  document.getElementById('blockly-clear').onclick = () => {
    if (!blocklyCurrentFunc) return;
    if (!confirm('Clear workspace for "' + blocklyCurrentFunc + '"?')) return;
    blocklyWorkspace.clear();
    blocklyFuncData[blocklyCurrentFunc].json = null;
    document.getElementById('blockly-code-output').textContent = '// (empty)';
  };
  document.getElementById('blockly-del-func').onclick = () => {
    if (!blocklyCurrentFunc) return;
    if (!confirm('Delete function "' + blocklyCurrentFunc + '" from Blockly? (Code tab not affected)')) return;
    delete blocklyFuncData[blocklyCurrentFunc];
    blocklyCurrentFunc = '';
    blocklyWorkspace.clear();
    registerArgBlocks([]);
    refreshBlocklyFuncSelect();
    document.getElementById('blockly-code-output').textContent = '// (empty)';
  };
  document.getElementById('blockly-edit-args').onclick = () => {
    if (!blocklyCurrentFunc) { showToast('Select a function first', 'error'); return; }
    const fd = blocklyFuncData[blocklyCurrentFunc];
    const curArgs = (fd && fd.args) ? fd.args : ['sourceEl', 'targetEl'];
    showFuncDialog((name, newArgs) => {
      if (!blocklyFuncData[blocklyCurrentFunc]) blocklyFuncData[blocklyCurrentFunc] = {};
      blocklyFuncData[blocklyCurrentFunc].args = newArgs;
      updateFuncDefBlockLabel(blocklyCurrentFunc, newArgs);
      registerArgBlocks(newArgs);
      refreshBlocklyFuncSelect();
      previewBlocklyCode();
      showToast(`Updated "${blocklyCurrentFunc}" args → (${newArgs.join(', ')})`);
    }, blocklyCurrentFunc, curArgs);
  };

  refreshBlocklyFuncSelect();
  registerCustomBlocks();
  showToast('Blockly workspace ready');

  document.getElementById('blockly-build-block').onclick = () => showBlockManager();
}

function refreshBlocklyFuncSelect() {
  const sel = document.getElementById('blockly-func-select');
  if (!sel) return;
  const cur = sel.value;
  sel.innerHTML = '<option value="">(select function)</option>';
  Object.keys(blocklyFuncData).sort().forEach(name => {
    const fd = blocklyFuncData[name];
    const argStr = (fd && fd.args) ? fd.args.join(', ') : 'sourceEl, targetEl';
    const opt = document.createElement('option');
    opt.value = name; opt.textContent = `${name}(${argStr})`;
    sel.appendChild(opt);
  });
  sel.value = cur in blocklyFuncData ? cur : '';
}

function saveCurrentBlocklyFunc() {
  if (!blocklyCurrentFunc || !blocklyWorkspace) return;
  if (!blocklyFuncData[blocklyCurrentFunc]) blocklyFuncData[blocklyCurrentFunc] = {};
  try {
    blocklyFuncData[blocklyCurrentFunc].json = Blockly.serialization.workspaces.save(blocklyWorkspace);
  } catch(e) { console.warn('Blockly save error:', e); }
}

function placeFuncDefBlock(name, args) {
  if (!blocklyWorkspace) return;
  const block = blocklyWorkspace.newBlock('gameapi_func_def');
  block.setFieldValue(name, 'NAME');
  block.setFieldValue(args.join(', '), 'ARGS');
  block.initSvg();
  block.render();
  block.moveBy(20, 20);
}

function updateFuncDefBlockLabel(name, args) {
  if (!blocklyWorkspace) return;
  const blocks = blocklyWorkspace.getBlocksByType('gameapi_func_def');
  if (blocks.length > 0) {
    blocks[0].setFieldValue(name, 'NAME');
    blocks[0].setFieldValue(args.join(', '), 'ARGS');
  }
}

function loadBlocklyFunc(name) {
  if (!blocklyWorkspace) return;
  blocklyWorkspace.clear();
  if (!name || !blocklyFuncData[name]) {
    document.getElementById('blockly-code-output').textContent = name ? '// (empty — add blocks above)' : '// Select or create a function';
    registerArgBlocks([]); // clear arg blocks
    return;
  }
  const fd = blocklyFuncData[name];
  // Register arg blocks BEFORE loading workspace (so deserialized arg blocks resolve)
  registerArgBlocks(fd.args || ['sourceEl', 'targetEl']);
  if (fd.json) {
    try {
      Blockly.serialization.workspaces.load(fd.json, blocklyWorkspace);
    } catch(e) { console.warn('Blockly load error:', e); }
  }
  // Ensure func_def block exists
  const defBlocks = blocklyWorkspace.getBlocksByType('gameapi_func_def');
  if (defBlocks.length === 0) {
    placeFuncDefBlock(name, fd.args || ['sourceEl', 'targetEl']);
  } else {
    updateFuncDefBlockLabel(name, fd.args || ['sourceEl', 'targetEl']);
  }
  previewBlocklyCode();
}

function previewBlocklyCode() {
  if (!blocklyWorkspace) return;
  try {
    const code = javascript.javascriptGenerator.workspaceToCode(blocklyWorkspace);
    const out = document.getElementById('blockly-code-output');
    if (blocklyCurrentFunc) {
      const fd = blocklyFuncData[blocklyCurrentFunc];
      const argStr = (fd && fd.args) ? fd.args.join(', ') : 'sourceEl, targetEl';
      out.textContent = `function ${blocklyCurrentFunc}(${argStr}) {\n${code.split('\n').map(l => '  ' + l).join('\n')}\n}`;
    } else {
      out.textContent = code || '// (empty)';
    }
  } catch(e) { console.warn('Blockly gen error:', e); }
}

function insertBlocklyFuncIntoCode(funcName) {
  if (!blocklyWorkspace) return;
  const bodyCode = javascript.javascriptGenerator.workspaceToCode(blocklyWorkspace);
  const indented = bodyCode.split('\n').filter(l => l.trim()).map(l => '  ' + l).join('\n');
  const fd = blocklyFuncData[funcName];
  const argStr = (fd && fd.args) ? fd.args.join(', ') : 'sourceEl, targetEl';
  const fullFunc = `function ${funcName}(${argStr}) {\n${indented}\n}`;

  // Replace existing function or append
  const regex = new RegExp(`function\\s+${funcName}\\s*\\([^)]*\\)\\s*\\{[\\s\\S]*?\\n\\}`, 'm');
  if (regex.test(codeMain.value)) {
    codeMain.value = codeMain.value.replace(regex, fullFunc);
  } else {
    codeMain.value = codeMain.value.trimEnd() + '\n\n' + fullFunc + '\n';
  }
  updateFuncSelect();
}

// ── Custom Block Registration ──
const TOOLBOX_CATEGORIES = ['UI','Controls','3D Objects','Queries','Scene','Timing','Custom'];
const CAT_COLORS = {'UI':160,'Controls':210,'3D Objects':260,'Queries':30,'Scene':330,'Timing':65,'Custom':45};

function registerCustomBlocks() {
  if (!blocklyWorkspace || typeof Blockly === 'undefined') return;
  const gen = javascript.javascriptGenerator;
  const O = javascript.Order;
  customBlocks.forEach(cb => {
    Blockly.Blocks[cb.type] = { init() {
      let hasValueIn = false;
      cb.fields.forEach(f => {
        if (f.kind === 'label') {
          this.appendDummyInput().appendField(f.text);
        } else if (f.kind === 'text') {
          this.appendDummyInput().appendField(f.label || '').appendField(new Blockly.FieldTextInput(f.default || ''), f.name);
        } else if (f.kind === 'number') {
          this.appendDummyInput().appendField(f.label || '').appendField(new Blockly.FieldNumber(f.default||0, f.min, f.max), f.name);
        } else if (f.kind === 'dropdown') {
          const opts = (f.options || []).map(o => Array.isArray(o) ? o : [o, o]);
          if (opts.length > 0) this.appendDummyInput().appendField(f.label || '').appendField(new Blockly.FieldDropdown(opts), f.name);
        } else if (f.kind === 'checkbox') {
          this.appendDummyInput().appendField(f.label || '').appendField(new Blockly.FieldCheckbox(f.default ? 'TRUE' : 'FALSE'), f.name);
        } else if (f.kind === 'value') {
          this.appendValueInput(f.name).setCheck(f.check || null).appendField(f.label || '');
          hasValueIn = true;
        } else if (f.kind === 'statement') {
          this.appendStatementInput(f.name).appendField(f.label || '');
        }
      });
      if (hasValueIn) this.setInputsInline(true);
      if (cb.shape === 'value') this.setOutput(true);
      else { this.setPreviousStatement(true); this.setNextStatement(true); }
      this.setColour(cb.colour || 45);
      if (cb.tooltip) this.setTooltip(cb.tooltip);
    }};
    gen.forBlock[cb.type] = function(block) {
      let code = cb.codeTemplate || '';
      cb.fields.forEach(f => {
        if (f.kind === 'value') {
          const val = gen.valueToCode(block, f.name, O.NONE) || f.default || '0';
          code = code.replace(new RegExp(`\\{${f.name}\\}`, 'g'), val);
        } else if (f.kind === 'statement') {
          const stm = gen.statementToCode(block, f.name);
          code = code.replace(new RegExp(`\\{${f.name}\\}`, 'g'), stm);
        } else if (f.name) {
          const val = block.getFieldValue(f.name);
          code = code.replace(new RegExp(`\\{${f.name}\\}`, 'g'), val);
        }
      });
      if (cb.shape === 'value') return [code, O.FUNCTION_CALL];
      return code.endsWith('\n') ? code : code + '\n';
    };
  });
  updateToolboxWithCustom();
}

let currentArgBlockTypes = []; // track registered arg blocks for cleanup

function registerArgBlocks(args) {
  if (typeof Blockly === 'undefined') return;
  const gen = javascript.javascriptGenerator;
  const O = javascript.Order;

  // Unregister old arg blocks
  currentArgBlockTypes.forEach(t => {
    delete Blockly.Blocks[t];
    delete gen.forBlock[t];
  });
  currentArgBlockTypes = [];

  if (!args || !args.length) { updateToolboxWithCustom(); return; }

  // Register a value block for each argument
  args.forEach(argName => {
    const type = 'gameapi_arg_' + argName.replace(/\W/g, '_');
    currentArgBlockTypes.push(type);

    Blockly.Blocks[type] = { init() {
      this.appendDummyInput().appendField('⚡ ' + argName);
      this.setOutput(true);
      this.setColour(290); // purple like func_def
      this.setTooltip('Function argument: ' + argName);
      this.setMovable(true);
      this.setDeletable(true);
    }};

    gen.forBlock[type] = function() {
      return [argName, O.ATOMIC];
    };
  });

  updateToolboxWithCustom();
}

function updateToolboxWithCustom() {
  if (!blocklyWorkspace || !blocklyWorkspace._baseToolbox) return;
  const newToolbox = JSON.parse(JSON.stringify(blocklyWorkspace._baseToolbox));
  const contents = newToolbox.contents;

  // 1. Insert arg blocks category at the very top if we have args
  if (currentArgBlockTypes.length > 0) {
    const argBlocks = currentArgBlockTypes.map(t => ({ kind:'block', type: t }));
    contents.unshift({ kind:'category', name:'⚡ Args', colour: 290, contents: argBlocks });
  }

  // 2. Group custom blocks by category
  const grouped = {};
  customBlocks.forEach(cb => {
    const cat = cb.category || 'Custom';
    if (!grouped[cat]) grouped[cat] = [];
    grouped[cat].push({ kind:'block', type: cb.type });
  });

  // Add to existing categories or create new ones
  Object.entries(grouped).forEach(([catName, blocks]) => {
    const existing = contents.find(c => c.kind === 'category' && c.name === catName);
    if (existing) {
      existing.contents = existing.contents || [];
      existing.contents.push(...blocks);
    } else {
      const sepIdx = contents.findIndex(c => c.kind === 'sep');
      const insertAt = sepIdx >= 0 ? sepIdx : contents.length;
      contents.splice(insertAt, 0, { kind:'category', name:catName, colour: CAT_COLORS[catName] || 45, contents: blocks });
    }
  });

  try {
    blocklyWorkspace.updateToolbox(newToolbox);
  } catch(e) { console.warn('Toolbox update error:', e); }
}

// ── Block Manager (list, import/export, manage) ──
function showBlockManager() {
  let overlay = document.getElementById('bb-overlay');
  if (overlay) overlay.remove();
  overlay = document.createElement('div');
  overlay.id = 'bb-overlay';
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:500;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(3px);';

  const box = document.createElement('div');
  box.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border-color);border-radius:10px;padding:0;width:600px;max-width:95vw;max-height:90vh;display:flex;flex-direction:column;overflow:hidden;';

  function renderManager() {
    box.innerHTML = '';

    // Header bar
    const hdr = document.createElement('div');
    hdr.style.cssText = 'display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--border-color);flex-shrink:0;';
    const title = document.createElement('div');
    title.style.cssText = "font-family:'Orbitron',sans-serif;font-size:0.5rem;color:var(--accent);letter-spacing:1px;flex:1;";
    title.innerHTML = '<i class="bi bi-bricks"></i> CUSTOM BLOCK MANAGER';
    hdr.appendChild(title);

    // New block button
    const newBtn = document.createElement('button'); newBtn.className = 'pp-btn';
    newBtn.style.cssText = 'font-size:0.4rem;padding:4px 10px;background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);';
    newBtn.innerHTML = '<i class="bi bi-plus-lg"></i> New Block';
    newBtn.onclick = () => { overlay.remove(); showBlockEditor(); };
    hdr.appendChild(newBtn);

    // Import button
    const impBtn = document.createElement('button'); impBtn.className = 'pp-btn';
    impBtn.style.cssText = 'font-size:0.4rem;padding:4px 10px;background:rgba(167,139,250,0.08);border-color:var(--accent2);color:var(--accent2);';
    impBtn.innerHTML = '<i class="bi bi-upload"></i> Import';
    impBtn.onclick = () => {
      const inp = document.createElement('input'); inp.type = 'file'; inp.accept = '.json';
      inp.onchange = () => {
        const reader = new FileReader();
        reader.onload = e => {
          try {
            const imported = JSON.parse(e.target.result);
            const arr = Array.isArray(imported) ? imported : (imported.customBlocks || []);
            if (!arr.length) { showToast('No blocks found in file', 'error'); return; }
            let added = 0;
            arr.forEach(cb => {
              if (!cb.type || !cb.name) return;
              if (customBlocks.some(c => c.type === cb.type)) {
                cb.type += '_' + Date.now().toString(36).slice(-4);
              }
              customBlocks.push({ ...cb });
              added++;
            });
            registerCustomBlocks();
            renderManager();
            showToast(`Imported ${added} block(s)`);
          } catch(e) { showToast('Invalid JSON file', 'error'); }
        };
        reader.readAsText(inp.files[0]);
      };
      inp.click();
    };
    hdr.appendChild(impBtn);

    // Export All button
    const expBtn = document.createElement('button'); expBtn.className = 'pp-btn';
    expBtn.style.cssText = 'font-size:0.4rem;padding:4px 10px;background:rgba(56,189,248,0.08);border-color:#38bdf8;color:#38bdf8;';
    expBtn.innerHTML = '<i class="bi bi-download"></i> Export All';
    expBtn.onclick = () => {
      if (!customBlocks.length) { showToast('No custom blocks to export', 'error'); return; }
      const blob = new Blob([JSON.stringify(customBlocks, null, 2)], { type:'application/json' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob); a.download = `custom-blocks_${fileTimestamp()}.json`; a.click();
      URL.revokeObjectURL(a.href);
      showToast(`Exported ${customBlocks.length} block(s)`);
    };
    hdr.appendChild(expBtn);

    // Close button
    const closeBtn = document.createElement('div');
    closeBtn.style.cssText = 'width:24px;height:24px;border-radius:4px;background:rgba(255,77,106,0.08);border:1px solid var(--danger);color:var(--danger);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:0.45rem;';
    closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
    closeBtn.onclick = () => overlay.remove();
    hdr.appendChild(closeBtn);
    box.appendChild(hdr);

    // Block list area
    const listArea = document.createElement('div');
    listArea.style.cssText = 'flex:1;overflow-y:auto;padding:12px 16px;';

    if (customBlocks.length === 0) {
      const empty = document.createElement('div');
      empty.style.cssText = 'text-align:center;padding:40px 20px;color:var(--text-muted);';
      empty.innerHTML = '<div style="font-size:2rem;margin-bottom:8px;opacity:0.3;"><i class="bi bi-bricks"></i></div>' +
        '<div style="font-size:0.5rem;margin-bottom:4px;">No custom blocks yet</div>' +
        '<div style="font-size:0.42rem;">Click <b>New Block</b> to create one, or <b>Import</b> a JSON file</div>';
      listArea.appendChild(empty);
    } else {
      // Group by category
      const grouped = {};
      customBlocks.forEach((cb, idx) => {
        const cat = cb.category || 'Custom';
        if (!grouped[cat]) grouped[cat] = [];
        grouped[cat].push({ cb, idx });
      });

      Object.entries(grouped).sort((a,b) => a[0].localeCompare(b[0])).forEach(([catName, items]) => {
        // Category header
        const catHdr = document.createElement('div');
        catHdr.style.cssText = "display:flex;align-items:center;gap:6px;margin:8px 0 6px;";
        const catDot = document.createElement('div');
        const hue = CAT_COLORS[catName] || 45;
        catDot.style.cssText = `width:10px;height:10px;border-radius:50%;background:hsl(${hue},70%,50%);flex-shrink:0;`;
        const catLbl = document.createElement('div');
        catLbl.style.cssText = "font-family:'Orbitron',sans-serif;font-size:0.38rem;color:var(--text-muted);letter-spacing:1px;text-transform:uppercase;";
        catLbl.textContent = `${catName} (${items.length})`;
        catHdr.appendChild(catDot); catHdr.appendChild(catLbl);
        listArea.appendChild(catHdr);

        // Block cards in this category
        items.forEach(({ cb, idx }) => {
          const card = document.createElement('div');
          card.style.cssText = `display:flex;align-items:center;gap:8px;padding:8px 10px;margin-bottom:4px;background:rgba(0,0,0,0.15);border:1px solid var(--border-color);border-radius:6px;border-left:3px solid hsl(${cb.colour || 45},70%,50%);`;

          // Info column
          const info = document.createElement('div'); info.style.cssText = 'flex:1;min-width:0;';
          const nameRow = document.createElement('div'); nameRow.style.cssText = 'display:flex;align-items:center;gap:6px;';
          const nameSpan = document.createElement('span');
          nameSpan.style.cssText = 'font-size:0.48rem;color:var(--text-primary);font-weight:600;';
          nameSpan.textContent = cb.name;
          const shapeTag = document.createElement('span');
          shapeTag.style.cssText = `font-size:0.32rem;padding:1px 5px;border-radius:3px;background:rgba(255,255,255,0.05);color:var(--text-muted);border:1px solid var(--border-color);`;
          shapeTag.textContent = cb.shape === 'value' ? '◯ value' : '▮ statement';
          nameRow.appendChild(nameSpan); nameRow.appendChild(shapeTag);
          info.appendChild(nameRow);

          // Code template preview
          const codePreview = document.createElement('div');
          codePreview.style.cssText = "font-family:'Courier New',monospace;font-size:0.38rem;color:var(--text-muted);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:320px;";
          codePreview.textContent = (cb.codeTemplate || '').replace(/\n/g, ' ').substring(0, 60);
          info.appendChild(codePreview);

          // Fields summary
          const fieldSum = document.createElement('div');
          fieldSum.style.cssText = 'font-size:0.35rem;color:var(--text-muted);margin-top:2px;opacity:0.7;';
          const fieldNames = cb.fields.filter(f => f.name).map(f => `{${f.name}}`).join(' ');
          fieldSum.textContent = cb.fields.length + ' field' + (cb.fields.length !== 1 ? 's' : '') + (fieldNames ? ': ' + fieldNames : '');
          info.appendChild(fieldSum);

          card.appendChild(info);

          // Action buttons
          const acts = document.createElement('div'); acts.style.cssText = 'display:flex;gap:4px;flex-shrink:0;';

          const editBtn = mk('bi-pencil', 'Edit', 'var(--accent3)', 'rgba(245,158,11,0.08)', () => {
            overlay.remove(); showBlockEditor(cb);
          });
          const dupeBtn = mk('bi-copy', 'Duplicate', 'var(--accent2)', 'rgba(167,139,250,0.08)', () => {
            const dup = JSON.parse(JSON.stringify(cb));
            dup.name = cb.name + ' Copy';
            dup.type = cb.type + '_copy_' + Date.now().toString(36).slice(-4);
            customBlocks.push(dup);
            registerCustomBlocks();
            renderManager();
            showToast(`Duplicated "${cb.name}"`);
          });
          const expOneBtn = mk('bi-download', 'Export', '#38bdf8', 'rgba(56,189,248,0.08)', () => {
            const blob = new Blob([JSON.stringify([cb], null, 2)], { type:'application/json' });
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob); a.download = `block-${cb.type}_${fileTimestamp()}.json`; a.click();
            URL.revokeObjectURL(a.href);
          });
          const delBtn = mk('bi-trash3', 'Delete', 'var(--danger)', 'rgba(255,77,106,0.08)', () => {
            if (!confirm(`Delete "${cb.name}"?`)) return;
            customBlocks.splice(idx, 1);
            delete Blockly.Blocks[cb.type];
            updateToolboxWithCustom();
            renderManager();
            showToast(`Deleted "${cb.name}"`);
          });

          acts.appendChild(editBtn); acts.appendChild(dupeBtn); acts.appendChild(expOneBtn); acts.appendChild(delBtn);
          card.appendChild(acts);
          listArea.appendChild(card);
        });
      });
    }
    box.appendChild(listArea);

    // Footer with count
    const ftr = document.createElement('div');
    ftr.style.cssText = 'padding:8px 16px;border-top:1px solid var(--border-color);font-size:0.38rem;color:var(--text-muted);display:flex;align-items:center;gap:8px;flex-shrink:0;';
    ftr.textContent = `${customBlocks.length} custom block(s)`;
    if (customBlocks.length > 0) {
      const clearAll = document.createElement('span');
      clearAll.style.cssText = 'color:var(--danger);cursor:pointer;margin-left:auto;';
      clearAll.textContent = 'Delete All';
      clearAll.onclick = () => {
        if (!confirm(`Delete all ${customBlocks.length} custom blocks?`)) return;
        customBlocks.forEach(cb => delete Blockly.Blocks[cb.type]);
        customBlocks.length = 0;
        updateToolboxWithCustom();
        renderManager();
        showToast('All custom blocks deleted');
      };
      ftr.appendChild(clearAll);
    }
    box.appendChild(ftr);
  }

  function mk(icon, title, color, bg, onclick) {
    const btn = document.createElement('div');
    btn.style.cssText = `width:24px;height:24px;border-radius:4px;background:${bg};border:1px solid ${color};color:${color};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:0.4rem;`;
    btn.innerHTML = `<i class="bi ${icon}"></i>`;
    btn.title = title;
    btn.onclick = onclick;
    return btn;
  }

  renderManager();
  overlay.appendChild(box);
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
  document.body.appendChild(overlay);
}

// ── Block Editor (create / edit a single block) ──
function showBlockEditor(editBlock) {
  let overlay = document.getElementById('bb-overlay');
  if (overlay) overlay.remove();
  overlay = document.createElement('div');
  overlay.id = 'bb-overlay';
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:500;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(3px);';
  const box = document.createElement('div');
  box.style.cssText = 'background:var(--bg-panel);border:1px solid var(--border-color);border-radius:10px;padding:16px;width:540px;max-width:95vw;max-height:90vh;overflow-y:auto;';
  const state = editBlock ? { ...editBlock, fields: editBlock.fields.map(f => ({...f})) } : {
    type: '', name: '', category: 'Custom', colour: 45, shape: 'statement', tooltip: '',
    fields: [{ kind:'label', text:'my block', label:'', name:'', default:'' }],
    codeTemplate: "// your code;\n"
  };

  function bbLbl(t) { const l = document.createElement('span'); l.style.cssText = 'font-size:0.42rem;color:var(--text-muted);white-space:nowrap;'; l.textContent = t; return l; }
  function bbInp(v, fn, ph) { const i = document.createElement('input'); i.className = 'pp-input'; i.value = v||''; if(ph)i.placeholder=ph; i.onchange = () => fn(i.value); return i; }

  function render() {
    box.innerHTML = '';

    // Header with back button
    const hdr = document.createElement('div');
    hdr.style.cssText = 'display:flex;align-items:center;gap:8px;margin-bottom:10px;';
    const backBtn = document.createElement('div');
    backBtn.style.cssText = 'width:24px;height:24px;border-radius:4px;background:rgba(255,255,255,0.04);border:1px solid var(--border-color);color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:0.4rem;';
    backBtn.innerHTML = '<i class="bi bi-arrow-left"></i>';
    backBtn.title = 'Back to Manager';
    backBtn.onclick = () => { overlay.remove(); showBlockManager(); };
    hdr.appendChild(backBtn);
    const h = document.createElement('div');
    h.style.cssText = "font-family:'Orbitron',sans-serif;font-size:0.5rem;color:var(--accent);letter-spacing:1px;flex:1;";
    h.textContent = editBlock ? 'EDIT BLOCK' : 'NEW BLOCK';
    hdr.appendChild(h);
    box.appendChild(hdr);

    // Row: Name + Color
    const r1 = document.createElement('div'); r1.style.cssText = 'display:flex;gap:6px;margin-bottom:8px;align-items:center;';
    r1.appendChild(bbLbl('Name')); const ni = bbInp(state.name, v => state.name = v, 'e.g. Move NPC'); ni.style.flex = '1'; r1.appendChild(ni);
    r1.appendChild(bbLbl('Hue')); const ci = document.createElement('input'); ci.type = 'number'; ci.className = 'pp-input';
    ci.style.width = '50px'; ci.min = 0; ci.max = 360; ci.value = state.colour; ci.onchange = () => { state.colour = parseInt(ci.value)||45; }; r1.appendChild(ci);
    box.appendChild(r1);

    // Row: Category + Shape
    const r2 = document.createElement('div'); r2.style.cssText = 'display:flex;gap:6px;margin-bottom:8px;align-items:center;';
    r2.appendChild(bbLbl('Category'));
    const catSel = document.createElement('select'); catSel.className = 'pp-select'; catSel.style.flex = '1';
    // Include existing categories from customBlocks + defaults
    const allCats = new Set(TOOLBOX_CATEGORIES);
    customBlocks.forEach(cb => allCats.add(cb.category || 'Custom'));
    [...allCats].sort().forEach(c => { const o = document.createElement('option'); o.value = c; o.textContent = c; if (c === state.category) o.selected = true; catSel.appendChild(o); });
    catSel.onchange = () => state.category = catSel.value; r2.appendChild(catSel);
    // Custom category input
    r2.appendChild(bbLbl('or'));
    const newCatInp = bbInp('', v => { if (v.trim()) { state.category = v.trim(); render(); } }, 'New category');
    newCatInp.style.width = '80px'; r2.appendChild(newCatInp);
    r2.appendChild(bbLbl('Returns'));
    const shSel = document.createElement('select'); shSel.className = 'pp-select'; shSel.style.width = '100px';
    [['statement','Statement ▮'],['value','Value ◯']].forEach(([v,t]) => {
      const o = document.createElement('option'); o.value = v; o.textContent = t; if (v === state.shape) o.selected = true; shSel.appendChild(o);
    }); shSel.onchange = () => state.shape = shSel.value; r2.appendChild(shSel);
    box.appendChild(r2);

    // Tooltip
    const r3 = document.createElement('div'); r3.style.cssText = 'display:flex;gap:6px;margin-bottom:10px;align-items:center;';
    r3.appendChild(bbLbl('Tooltip')); const tti = bbInp(state.tooltip||'', v => state.tooltip = v, 'Description'); tti.style.flex = '1'; r3.appendChild(tti);
    box.appendChild(r3);

    // Fields header
    const fh = document.createElement('div'); fh.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;';
    const fl = document.createElement('span'); fl.style.cssText = "font-family:'Orbitron',sans-serif;font-size:0.4rem;color:var(--accent2);letter-spacing:1px;"; fl.textContent = 'FIELDS';
    const addBtn = document.createElement('button'); addBtn.className = 'pp-btn';
    addBtn.style.cssText = 'font-size:0.4rem;padding:2px 8px;background:rgba(167,139,250,0.08);border-color:var(--accent2);color:var(--accent2);';
    addBtn.innerHTML = '<i class="bi bi-plus"></i> Add Field';
    addBtn.onclick = () => { state.fields.push({ kind:'text', text:'', label:'', name:'F'+state.fields.length, default:'' }); render(); };
    fh.appendChild(fl); fh.appendChild(addBtn); box.appendChild(fh);

    // Each field
    state.fields.forEach((f, i) => {
      const card = document.createElement('div');
      card.style.cssText = 'background:rgba(0,0,0,0.15);border:1px solid var(--border-color);border-radius:6px;padding:6px 8px;margin-bottom:4px;';
      const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:4px;align-items:center;flex-wrap:wrap;';
      const kindSel = document.createElement('select'); kindSel.className = 'pp-select'; kindSel.style.width = '85px';
      ['label','text','number','dropdown','checkbox','value','statement'].forEach(k => {
        const o = document.createElement('option'); o.value = k; o.textContent = k; if (k === f.kind) o.selected = true; kindSel.appendChild(o);
      });
      kindSel.onchange = () => { f.kind = kindSel.value; if (!f.name && f.kind !== 'label') f.name = 'F'+i; render(); };
      row.appendChild(kindSel);

      if (f.kind === 'label') {
        const ti = bbInp(f.text||'', v => f.text = v, 'display text'); ti.style.flex = '1'; row.appendChild(ti);
      } else {
        const nameI = bbInp(f.name||'', v => f.name = v.replace(/\W/g,''), 'NAME');
        nameI.style.cssText = "width:60px;font-family:'Courier New',monospace;color:var(--accent);";
        nameI.title = 'Use {' + (f.name||'NAME') + '} in code template'; row.appendChild(nameI);
        if (f.kind === 'text') {
          const li = bbInp(f.label||'', v => f.label = v, 'label'); li.style.width = '60px'; row.appendChild(li);
          const di = bbInp(f.default||'', v => f.default = v, 'default'); di.style.flex = '1'; row.appendChild(di);
        } else if (f.kind === 'number') {
          const li = bbInp(f.label||'', v => f.label = v, 'lbl'); li.style.width = '40px'; row.appendChild(li);
          const di = bbInp(f.default!=null?String(f.default):'0', v => f.default = parseFloat(v)||0, '0'); di.style.width = '40px'; row.appendChild(di);
        } else if (f.kind === 'dropdown') {
          const li = bbInp(f.label||'', v => f.label = v, 'lbl'); li.style.width = '40px'; row.appendChild(li);
          const oi = bbInp((f.options||[]).map(o => Array.isArray(o)?o.join(':'):o).join(', '), v => {
            f.options = v.split(',').map(s=>s.trim()).filter(s=>s).map(s => { const p=s.split(':'); return p.length>1?[p[0].trim(),p[1].trim()]:[s.trim(),s.trim()]; });
          }, 'Label:val, Label:val'); oi.style.flex = '1'; row.appendChild(oi);
        } else if (f.kind === 'value' || f.kind === 'statement' || f.kind === 'checkbox') {
          const li = bbInp(f.label||'', v => f.label = v, 'label'); li.style.flex = '1'; row.appendChild(li);
        }
      }
      const moveUp = document.createElement('div'); moveUp.style.cssText = 'cursor:pointer;font-size:0.4rem;color:var(--text-muted);padding:2px;'; moveUp.innerHTML = '▲'; moveUp.onclick = () => { if (i>0) { [state.fields[i-1],state.fields[i]]=[state.fields[i],state.fields[i-1]]; render(); }};
      const moveDown = document.createElement('div'); moveDown.style.cssText = 'cursor:pointer;font-size:0.4rem;color:var(--text-muted);padding:2px;'; moveDown.innerHTML = '▼'; moveDown.onclick = () => { if (i<state.fields.length-1) { [state.fields[i],state.fields[i+1]]=[state.fields[i+1],state.fields[i]]; render(); }};
      const del = document.createElement('div'); del.style.cssText = 'width:18px;height:18px;border-radius:3px;background:rgba(255,77,106,0.08);border:1px solid var(--danger);color:var(--danger);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:0.38rem;flex-shrink:0;'; del.innerHTML = '<i class="bi bi-x"></i>'; del.onclick = () => { state.fields.splice(i,1); render(); };
      row.appendChild(moveUp); row.appendChild(moveDown); row.appendChild(del);
      card.appendChild(row); box.appendChild(card);
    });

    // Code template
    const ctH = document.createElement('div'); ctH.style.cssText = "font-family:'Orbitron',sans-serif;font-size:0.4rem;color:var(--accent2);letter-spacing:1px;margin:10px 0 4px;"; ctH.textContent = 'CODE TEMPLATE';
    box.appendChild(ctH);
    const ctHint = document.createElement('div'); ctHint.style.cssText = 'font-size:0.42rem;color:var(--text-muted);margin-bottom:4px;line-height:1.4;';
    const fieldNames = state.fields.filter(f => f.name).map(f => '{' + f.name + '}');
    ctHint.innerHTML = fieldNames.length
      ? 'Placeholders: ' + fieldNames.map(n => `<code style="color:var(--accent);background:rgba(0,229,160,0.08);padding:0 3px;border-radius:2px;">${n}</code>`).join(' ')
      : 'Add named fields above, then use <code style="color:var(--accent);">{FIELD_NAME}</code> in your code';
    box.appendChild(ctHint);
    const ctTa = document.createElement('textarea'); ctTa.className = 'pp-input';
    ctTa.style.cssText = 'width:100%;min-height:70px;resize:vertical;font-family:"Courier New",monospace;font-size:0.5rem;line-height:1.4;';
    ctTa.value = state.codeTemplate||''; ctTa.onchange = () => state.codeTemplate = ctTa.value;
    box.appendChild(ctTa);

    // Action buttons
    const btns = document.createElement('div'); btns.style.cssText = 'display:flex;gap:6px;margin-top:12px;';
    const saveBtn = document.createElement('button'); saveBtn.className = 'pp-btn';
    saveBtn.style.cssText = 'flex:1;background:rgba(0,229,160,0.08);border-color:var(--accent);color:var(--accent);padding:6px;font-size:0.45rem;';
    saveBtn.innerHTML = '<i class="bi bi-check-lg"></i> ' + (editBlock ? 'Update Block' : 'Create Block');
    saveBtn.onclick = () => {
      if (!state.name.trim()) { showToast('Name required', 'error'); return; }
      if (!state.type) state.type = 'custom_' + state.name.toLowerCase().replace(/[^a-z0-9]/g,'_').replace(/_+/g,'_');
      state.fields = state.fields.filter(f => f.kind === 'label' ? f.text : true);
      if (editBlock) {
        const idx = customBlocks.findIndex(c => c.type === editBlock.type);
        if (idx >= 0) customBlocks[idx] = { ...state };
      } else {
        if (customBlocks.some(c => c.type === state.type)) state.type += '_' + Date.now().toString(36).slice(-4);
        customBlocks.push({ ...state });
      }
      registerCustomBlocks();
      overlay.remove();
      showBlockManager();
      showToast(`Block "${state.name}" ${editBlock ? 'updated' : 'created'}`);
    };
    const cancelBtn = document.createElement('button'); cancelBtn.className = 'pp-btn';
    cancelBtn.style.cssText = 'flex:1;background:rgba(255,255,255,0.04);border-color:var(--border-color);color:var(--text-secondary);padding:6px;font-size:0.45rem;';
    cancelBtn.textContent = 'Back to Manager';
    cancelBtn.onclick = () => { overlay.remove(); showBlockManager(); };
    btns.appendChild(saveBtn); btns.appendChild(cancelBtn); box.appendChild(btns);
  }

  render();
  overlay.appendChild(box);
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
  document.body.appendChild(overlay);
}
codeMain.addEventListener('keydown', e => {
  if (e.key === 'Tab') {
    e.preventDefault();
    const start = codeMain.selectionStart;
    const end = codeMain.selectionEnd;
    codeMain.value = codeMain.value.substring(0, start) + '  ' + codeMain.value.substring(end);
    codeMain.selectionStart = codeMain.selectionEnd = start + 2;
  }
  // Ctrl+Enter to run
  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
    e.preventDefault();
    document.getElementById('code-run').click();
  }
});

// ══════════════════════════════════════
//  INIT
// ══════════════════════════════════════
canvasArea.classList.add('props-hidden');
loadConfig();

</script>
</body>
</html>