(() => {
  'use strict';

  const APP_VERSION = 'control-mapping-pro-v22c-mapping-mode-lock';
  const CLIP_TRANSITION_MODES = ['cut', 'fade', 'crossfade', 'dipblack', 'wipe', 'slide'];
  const CLIP_TRANSITION_LABELS = { cut: 'CUT', fade: 'FADE', crossfade: 'XFADE', dipblack: 'DIP', wipe: 'WIPE', slide: 'SLIDE' };
  const CLIP_TRANSITION_DEFAULTS = { cut: 0, fade: 500, crossfade: 700, dipblack: 700, wipe: 600, slide: 600 };
  const CUE_TRANSITION_MODES = [...CLIP_TRANSITION_MODES];
  const CUE_TRANSITION_LABELS = { ...CLIP_TRANSITION_LABELS };
  const CUE_TRANSITION_DEFAULTS = { ...CLIP_TRANSITION_DEFAULTS, fade: 700, crossfade: 700, dipblack: 700, wipe: 700, slide: 700 };
  const CLIP_TRANSITION_MIN_MS = 0;
  const CLIP_TRANSITION_MAX_MS = 2000;
  const MIN_LAYER_SIZE = 24;
  const UI_PREFS_KEY = 'ellos_rebuild_ui_prefs_v20';
  const LOCAL_PROJECTS_KEY = 'ellos_rebuild_local_projects_v10';
  const AUTOSAVE_KEY = 'ellos_rebuild_autosave_v22';
  const PDF_EXTENSIONS = new Set(['pdf']);
  const OUTPUTS = ['P1', 'P2', 'P3', 'P4', 'P5'];
  const DEFAULT_W = 1280;
  const DEFAULT_H = 720;
  const HANDLE_SIZE = 14;
  const PIN_HANDLE_SIZE = 16;
  const SNAP_GRID_SIZE = 10;
  const MEDIA_THUMB_W = 116;
  const MEDIA_THUMB_H = 66;
  const PROJECT_PREVIEW_W = 160;
  const PROJECT_PREVIEW_H = 90;

  function createDefaultCameraFx() {
    return {
      cameraFxMirrorX: false,
      cameraFxMono: false,
      cameraFxBoost: false,
      cameraFxInvert: false
    };
  }

  function createDefaultClipFx() {
    return {
      effectMirrorX: false,
      effectMono: false,
      effectBoost: false,
      fxBrightness: 100,
      fxContrast: 100,
      fxSaturation: 100,
      fxHue: 0,
      fxBlur: 0,
      edgeSoftness: 0,
      bgRemoveMode: 'off',
      bgRemoveColor: '#00ff00',
      bgRemoveThreshold: 70,
      bgRemoveSoftness: 25,
      mediaZoom: 100
    };
  }

  function createDefaultTextStyle() {
    return {
      textContent: 'NEW TEXT',
      textFontSize: 72,
      textColor: '#ffffff',
      textBgColor: '#000000',
      textBgOpacity: 0,
      textAlign: 'center',
      textBold: 700
    };
  }

  function applyDefaultTextStyle(target, partial = {}) {
    if (!target) return target;
    target.textContent = String(partial.textContent ?? 'NEW TEXT');
    target.textFontSize = clamp(Number(partial.textFontSize ?? 72), 16, 240);
    target.textColor = /^#[0-9a-fA-F]{6}$/.test(String(partial.textColor || '')) ? String(partial.textColor) : '#ffffff';
    target.textBgColor = /^#[0-9a-fA-F]{6}$/.test(String(partial.textBgColor || '')) ? String(partial.textBgColor) : '#000000';
    target.textBgOpacity = clamp(Number(partial.textBgOpacity ?? 0), 0, 100);
    target.textAlign = ['left', 'center', 'right'].includes(String(partial.textAlign || '')) ? String(partial.textAlign) : 'center';
    target.textBold = [400, 700].includes(Number(partial.textBold)) ? Number(partial.textBold) : 700;
    return target;
  }

  function applyDefaultCameraFx(target, partial = {}) {
    if (!target) return target;
    target.cameraFxMirrorX = !!partial.cameraFxMirrorX;
    target.cameraFxMono = !!partial.cameraFxMono;
    target.cameraFxBoost = !!partial.cameraFxBoost;
    target.cameraFxInvert = !!partial.cameraFxInvert;
    return target;
  }

  function applyDefaultClipFx(target, partial = {}) {
    if (!target) return target;
    target.effectMirrorX = !!partial.effectMirrorX;
    target.effectMono = !!partial.effectMono;
    target.effectBoost = !!partial.effectBoost;
    target.fxBrightness = clamp(Number(partial.fxBrightness ?? 100), 0, 200);
    target.fxContrast = clamp(Number(partial.fxContrast ?? 100), 0, 220);
    target.fxSaturation = clamp(Number(partial.fxSaturation ?? 100), 0, 300);
    target.fxHue = clamp(Number(partial.fxHue ?? 0), -180, 180);
    target.fxBlur = clamp(Number(partial.fxBlur ?? 0), 0, 20);
    target.edgeSoftness = clamp(Number(partial.edgeSoftness ?? 0), 0, 35);
    target.bgRemoveMode = ['off', 'green', 'blue', 'black', 'white', 'custom'].includes(partial.bgRemoveMode) ? partial.bgRemoveMode : 'off';
    target.bgRemoveColor = /^#[0-9a-fA-F]{6}$/.test(String(partial.bgRemoveColor || '')) ? String(partial.bgRemoveColor) : '#00ff00';
    target.bgRemoveThreshold = clamp(Number(partial.bgRemoveThreshold ?? 70), 0, 180);
    target.bgRemoveSoftness = clamp(Number(partial.bgRemoveSoftness ?? 25), 0, 140);
    target.mediaZoom = clamp(Number(partial.mediaZoom ?? 100), 100, 300);
    return target;
  }

  function getLayerFxTarget(layer, media = null) {
    const activeMedia = media || getLayerActiveMedia(layer);
    return activeMedia || layer || null;
  }

  function getLayerFxTargetLabel(layer, media = null) {
    const activeMedia = media || getLayerActiveMedia(layer);
    if (activeMedia) return activeMedia.name || activeMedia.fileName || 'active clip';
    return layer?.name || 'layer';
  }

  function isTextMedia(item) {
    return !!(item && item.type === 'text');
  }

  function isPdfMedia(item) {
    return !!(item && item.type === 'pdf');
  }

  function hexToRgba(hex, alpha = 1) {
    const safe = /^#[0-9a-fA-F]{6}$/.test(String(hex || '')) ? String(hex) : '#000000';
    const value = safe.slice(1);
    const r = parseInt(value.slice(0, 2), 16);
    const g = parseInt(value.slice(2, 4), 16);
    const b = parseInt(value.slice(4, 6), 16);
    return `rgba(${r}, ${g}, ${b}, ${clamp(Number(alpha || 0), 0, 1).toFixed(3)})`;
  }

  function makeCanvasElement(width, height) {
    const canvas = document.createElement('canvas');
    canvas.width = Math.max(1, Math.round(width || 1));
    canvas.height = Math.max(1, Math.round(height || 1));
    return canvas;
  }

  function renderTextMediaCanvas(record) {
    if (!record) return null;
    applyDefaultTextStyle(record, record);
    const lines = String(record.textContent || 'NEW TEXT').replace(/\r/g, '').split('\n');
    const fontSize = clamp(Number(record.textFontSize || 72), 16, 240);
    const paddingX = Math.max(18, Math.round(fontSize * 0.45));
    const paddingY = Math.max(14, Math.round(fontSize * 0.34));
    const measure = document.createElement('canvas').getContext('2d');
    measure.font = `${record.textBold || 700} ${fontSize}px Inter, Arial, sans-serif`;
    const maxLineWidth = Math.max(...lines.map((line) => measure.measureText(line || ' ').width), fontSize * 2);
    const lineHeight = Math.round(fontSize * 1.2);
    const width = Math.max(240, Math.min(1600, Math.ceil(maxLineWidth + paddingX * 2)));
    const height = Math.max(120, Math.min(1000, Math.ceil(lines.length * lineHeight + paddingY * 2)));
    const canvas = record.element instanceof HTMLCanvasElement ? record.element : makeCanvasElement(width, height);
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, width, height);
    if (Number(record.textBgOpacity || 0) > 0) {
      ctx.fillStyle = hexToRgba(record.textBgColor, Number(record.textBgOpacity || 0) / 100);
      ctx.fillRect(0, 0, width, height);
    }
    ctx.font = `${record.textBold || 700} ${fontSize}px Inter, Arial, sans-serif`;
    ctx.fillStyle = record.textColor || '#ffffff';
    ctx.textBaseline = 'top';
    ctx.textAlign = record.textAlign || 'center';
    let x = width / 2;
    if (record.textAlign === 'left') x = paddingX;
    else if (record.textAlign === 'right') x = width - paddingX;
    lines.forEach((line, idx) => {
      ctx.fillText(line || ' ', x, paddingY + idx * lineHeight);
    });
    record.element = canvas;
    record.ready = true;
    record.width = width;
    record.height = height;
    record.duration = 0;
    record.thumbUrl = buildThumbnailDataUrl(canvas, width, height);
    return canvas;
  }

  function renderPdfPlaceholder(record) {
    if (!record) return null;
    const canvas = record.element instanceof HTMLCanvasElement ? record.element : makeCanvasElement(900, 1200);
    canvas.width = 900;
    canvas.height = 1200;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#f6f7fb';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.strokeStyle = '#c7cfdf';
    ctx.lineWidth = 10;
    ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
    ctx.fillStyle = '#d92d4c';
    ctx.fillRect(70, 70, 190, 88);
    ctx.fillStyle = '#ffffff';
    ctx.font = '700 46px Inter, Arial, sans-serif';
    ctx.fillText('PDF', 112, 128);
    ctx.fillStyle = '#13253b';
    ctx.font = '700 44px Inter, Arial, sans-serif';
    const title = String(record.name || record.fileName || 'Document.pdf');
    const safeTitle = title.length > 30 ? title.slice(0, 27) + '...' : title;
    ctx.fillText(safeTitle, 70, 230);
    ctx.fillStyle = '#4d607a';
    ctx.font = '600 34px Inter, Arial, sans-serif';
    ctx.fillText('PDF intake lite', 70, 310);
    ctx.font = '500 28px Inter, Arial, sans-serif';
    const lines = [
      'Το αρχείο PDF μπήκε στη media library.',
      'Στο layer εμφανίζεται ασφαλές placeholder page.',
      'Double click στη library = άνοιγμα PDF σε νέο παράθυρο.'
    ];
    lines.forEach((line, idx) => ctx.fillText(line, 70, 410 + idx * 54));
    ctx.fillStyle = '#e8edf6';
    ctx.fillRect(70, 720, 760, 320);
    ctx.fillStyle = '#5b708f';
    for (let i = 0; i < 5; i += 1) {
      ctx.fillRect(110, 780 + i * 42, 610 - i * 25, 16);
    }
    record.element = canvas;
    record.ready = true;
    record.width = canvas.width;
    record.height = canvas.height;
    record.duration = 0;
    record.thumbUrl = buildThumbnailDataUrl(canvas, canvas.width, canvas.height);
    return canvas;
  }

  function createTextMediaRecord(partial = {}) {
    const existing = state.media.filter(isTextMedia).length;
    const record = {
      id: partial.id || uid('media'),
      type: 'text',
      name: partial.name || `✎ Text ${existing + 1}`,
      fileName: partial.fileName || `text:${existing + 1}`,
      url: '',
      element: makeCanvasElement(640, 220),
      ready: true,
      width: 640,
      height: 220,
      duration: 0,
      error: null,
      thumbUrl: ''
    };
    applyDefaultCameraFx(record);
    applyDefaultClipFx(record);
    applyDefaultTextStyle(record, partial);
    renderTextMediaCanvas(record);
    if (!record.element.parentNode) el.hiddenMediaHost.appendChild(record.element);
    return record;
  }

  function createPdfMediaRecord(fileOrSaved) {
    const name = fileOrSaved?.name || fileOrSaved?.fileName || 'Document.pdf';
    const id = fileOrSaved?.id || uid('media');
    const record = {
      id,
      type: 'pdf',
      name,
      fileName: fileOrSaved?.fileName || name,
      url: fileOrSaved && fileOrSaved.url ? fileOrSaved.url : ((typeof File !== 'undefined' && fileOrSaved instanceof File) ? URL.createObjectURL(fileOrSaved) : ''),
      element: makeCanvasElement(900, 1200),
      ready: true,
      width: 900,
      height: 1200,
      duration: 0,
      error: null,
      thumbUrl: ''
    };
    applyDefaultCameraFx(record);
    applyDefaultClipFx(record);
    renderPdfPlaceholder(record);
    if (!record.element.parentNode) el.hiddenMediaHost.appendChild(record.element);
    return record;
  }

  function addTextSource() {
    const raw = window.prompt('Γράψε το κείμενο που θέλεις να βάλεις σαν source:', 'ELLOS TEXT');
    if (raw === null) return;
    const value = String(raw || '').trim() || 'NEW TEXT';
    const record = createTextMediaRecord({ textContent: value, name: `✎ ${value.split('\n')[0].slice(0, 18) || 'Text'}` });
    state.media.push(record);
    state.selectedMediaId = record.id;
    const layer = getSelectedLayer();
    if (layer) {
      setLayerPrimaryMedia(layer, record.id);
      fitLayerToMedia('fit');
    }
    renderAll();
    setStatus(`Μπήκε νέο text source: ${record.name}.`);
  }

  function updateSelectedTextMedia(partial = {}) {
    const layer = getSelectedLayer();
    const media = getLayerActiveMedia(layer) || getMediaById(state.selectedMediaId);
    if (!isTextMedia(media)) return;
    applyDefaultTextStyle(media, { ...media, ...partial });
    renderTextMediaCanvas(media);
    renderAll();
  }

  const el = {
    mediaFileInput: document.getElementById('mediaFileInput'),
    projectLoadInput: document.getElementById('projectLoadInput'),
    appBankLoadInput: document.getElementById('appBankLoadInput'),
    relinkFileInput: document.getElementById('relinkFileInput'),
    layerFileInput: document.getElementById('layerFileInput'),
    addMediaBtn: document.getElementById('addMediaBtn'),
    addTextBtn: document.getElementById('addTextBtn'),
    addCameraBtn: document.getElementById('addCameraBtn'),
    addScreenBtn: document.getElementById('addScreenBtn'),
    addLayerBtn: document.getElementById('addLayerBtn'),
    duplicateLayerBtn: document.getElementById('duplicateLayerBtn'),
    deleteLayerBtn: document.getElementById('deleteLayerBtn'),
    playAllBtn: document.getElementById('playAllBtn'),
    pauseAllBtn: document.getElementById('pauseAllBtn'),
    undoBtn: document.getElementById('undoBtn'),
    redoBtn: document.getElementById('redoBtn'),
    saveProjectBtn: document.getElementById('saveProjectBtn'),
    saveLocalBtn: document.getElementById('saveLocalBtn'),
    loadProjectBtn: document.getElementById('loadProjectBtn'),
    restoreAutosaveBtn: document.getElementById('restoreAutosaveBtn'),
    relinkBtn: document.getElementById('relinkBtn'),
    projectName: document.getElementById('projectName'),
    mediaList: document.getElementById('mediaList'),
    layersList: document.getElementById('layersList'),
    mediaCount: document.getElementById('mediaCount'),
    mediaPanelSection: document.getElementById('mediaPanelSection'),
    stageWrap: document.getElementById('stageWrap'),
    stageViewport: document.getElementById('stageViewport'),
    zoomOutBtn: document.getElementById('zoomOutBtn'),
    zoomInBtn: document.getElementById('zoomInBtn'),
    zoomResetBtn: document.getElementById('zoomResetBtn'),
    stageZoomInput: document.getElementById('stageZoomInput'),
    stageZoomValue: document.getElementById('stageZoomValue'),
    mediaPanelToggle: document.getElementById('mediaPanelToggle'),
    layerCount: document.getElementById('layerCount'),
    stageCanvas: document.getElementById('stageCanvas'),
    stageViewSelect: document.getElementById('stageViewSelect'),
    canvasPresetSelect: document.getElementById('canvasPresetSelect'),
    snapToggleBtn: document.getElementById('snapToggleBtn'),
    guidesToggleBtn: document.getElementById('guidesToggleBtn'),
    transformStepBtns: [...document.querySelectorAll('[data-transform-step]')],
    statusText: document.getElementById('statusText'),
    missingMediaText: document.getElementById('missingMediaText'),
    inspectorEmpty: document.getElementById('inspectorEmpty'),
    inspectorForm: document.getElementById('inspectorForm'),
    layerNameInput: document.getElementById('layerNameInput'),
    layerOutputInput: document.getElementById('layerOutputInput'),
    layerMediaInput: document.getElementById('layerMediaInput'),
    layerXInput: document.getElementById('layerXInput'),
    layerYInput: document.getElementById('layerYInput'),
    layerWInput: document.getElementById('layerWInput'),
    layerHInput: document.getElementById('layerHInput'),
    layerOpacityInput: document.getElementById('layerOpacityInput'),
    layerRotationInput: document.getElementById('layerRotationInput'),
    layerZInput: document.getElementById('layerZInput'),
    layerVisibleInput: document.getElementById('layerVisibleInput'),
    layerLockInput: document.getElementById('layerLockInput'),
    layerLoopInput: document.getElementById('layerLoopInput'),
    layerBrightnessInput: document.getElementById('layerBrightnessInput'),
    layerContrastInput: document.getElementById('layerContrastInput'),
    layerSaturationInput: document.getElementById('layerSaturationInput'),
    layerHueInput: document.getElementById('layerHueInput'),
    layerBlurInput: document.getElementById('layerBlurInput'),
    layerEdgeSoftnessInput: document.getElementById('layerEdgeSoftnessInput'),
    layerBgRemoveModeInput: document.getElementById('layerBgRemoveModeInput'),
    layerBgRemoveColorInput: document.getElementById('layerBgRemoveColorInput'),
    layerBgThresholdInput: document.getElementById('layerBgThresholdInput'),
    layerBgSoftnessInput: document.getElementById('layerBgSoftnessInput'),
    layerBrightnessValue: document.getElementById('layerBrightnessValue'),
    layerContrastValue: document.getElementById('layerContrastValue'),
    layerSaturationValue: document.getElementById('layerSaturationValue'),
    layerHueValue: document.getElementById('layerHueValue'),
    layerBlurValue: document.getElementById('layerBlurValue'),
    layerEdgeSoftnessValue: document.getElementById('layerEdgeSoftnessValue'),
    layerBgThresholdValue: document.getElementById('layerBgThresholdValue'),
    layerBgSoftnessValue: document.getElementById('layerBgSoftnessValue'),
    videoZoomPanel: document.getElementById('videoZoomPanel'),
    layerMediaZoomInput: document.getElementById('layerMediaZoomInput'),
    layerMediaZoomValue: document.getElementById('layerMediaZoomValue'),
    resetVideoZoomBtn: document.getElementById('resetVideoZoomBtn'),
    resetFxBtn: document.getElementById('resetFxBtn'),
    cameraFxPanel: document.getElementById('cameraFxPanel'),
    fxTargetHint: document.getElementById('fxTargetHint'),
    cameraMirrorBtn: document.getElementById('cameraMirrorBtn'),
    cameraMonoBtn: document.getElementById('cameraMonoBtn'),
    cameraBoostBtn: document.getElementById('cameraBoostBtn'),
    cameraInvertBtn: document.getElementById('cameraInvertBtn'),
    resetCameraFxBtn: document.getElementById('resetCameraFxBtn'),
    textMediaPanel: document.getElementById('textMediaPanel'),
    textContentInput: document.getElementById('textContentInput'),
    textFontSizeInput: document.getElementById('textFontSizeInput'),
    textAlignInput: document.getElementById('textAlignInput'),
    textColorInput: document.getElementById('textColorInput'),
    textBgColorInput: document.getElementById('textBgColorInput'),
    textBgOpacityInput: document.getElementById('textBgOpacityInput'),
    textBgOpacityValue: document.getElementById('textBgOpacityValue'),
    textBoldInput: document.getElementById('textBoldInput'),
    resetTextBtn: document.getElementById('resetTextBtn'),
    bringFrontBtn: document.getElementById('bringFrontBtn'),
    sendBackBtn: document.getElementById('sendBackBtn'),
    fitMediaBtn: document.getElementById('fitMediaBtn'),
    fillMediaBtn: document.getElementById('fillMediaBtn'),
    resetTransformBtn: document.getElementById('resetTransformBtn'),
    quickOutputBtns: [...document.querySelectorAll('[data-quick-output]')],
    handleNudgeBtns: [...document.querySelectorAll('[data-handle-select]')],
    projectBankList: document.getElementById('projectBankList'),
    projectSearchInput: document.getElementById('projectSearchInput'),
    exportAppBankBtn: document.getElementById('exportAppBankBtn'),
    importAppBankBtn: document.getElementById('importAppBankBtn'),
    clearAutosaveBtn: document.getElementById('clearAutosaveBtn'),
    autosaveInfo: document.getElementById('autosaveInfo'),
    showProjectsBtn: document.getElementById('showProjectsBtn'),
    projectBankPanel: document.getElementById('projectBankPanel'),
    saveCueBtn: document.getElementById('saveCueBtn'),
    prevCueBtn: document.getElementById('prevCueBtn'),
    nextCueBtn: document.getElementById('nextCueBtn'),
    cueNameInput: document.getElementById('cueNameInput'),
    cueCount: document.getElementById('cueCount'),
    cueList: document.getElementById('cueList'),
    cueActionBtns: [...document.querySelectorAll('[data-cue-action]')],
    cueTransitionModeInput: document.getElementById('cueTransitionModeInput'),
    cueTransitionDurationInput: document.getElementById('cueTransitionDurationInput'),
    outputOpsList: document.getElementById('outputOpsList'),
    hiddenMediaHost: document.getElementById('hiddenMediaHost')
  };

  const stageCtx = el.stageCanvas.getContext('2d');

  const state = {
    width: DEFAULT_W,
    height: DEFAULT_H,
    media: [],
    layers: [],
    selectedLayerId: null,
    selectedMediaId: null,
    projectorWins: new Map(),
    playing: false,
    drag: null,
    status: 'Ready.',
    missingMediaNames: new Set(),
    lastTick: 0,
    activeHandleName: null,
    localProjects: [],
    cues: [],
    activeCueId: null,
    outputOps: Object.fromEntries(OUTPUTS.map((outputId) => [outputId, { blackout: false, frozen: false, freezeCanvas: null, testPattern: 'live' }])),
    cueTransitionMode: 'cut',
    cueTransitionMs: 700,
    cueTransitionActive: null,
    layerImportTargetId: null,
    mediaLibraryCollapsed: false,
    layerDetailsOpenId: null,
    stageFocusLayerId: null,
    snapEnabled: false,
    guidesEnabled: true,
    stageZoom: 1,
    autosaveMeta: null,
    autosaveTimer: null,
    persistenceReady: false,
    history: { undoStack: [], redoStack: [], lastSerialized: '', max: 60, isRestoring: false, timer: null },
    layerFxCache: new WeakMap()
  };

  function uid(prefix) {
    return `${prefix}_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;
  }

  function loadUiPrefs() {
    try {
      const raw = localStorage.getItem(UI_PREFS_KEY);
      if (!raw) return;
      const parsed = JSON.parse(raw);
      state.mediaLibraryCollapsed = !!parsed.mediaLibraryCollapsed;
      state.layerDetailsOpenId = typeof parsed.layerDetailsOpenId === 'string' ? parsed.layerDetailsOpenId : null;
      state.snapEnabled = !!parsed.snapEnabled;
      state.guidesEnabled = parsed.guidesEnabled !== false;
      state.stageZoom = sanitizeStageZoom(parsed.stageZoom ?? 1);
    } catch (_) {}
  }

  function saveUiPrefs() {
    try {
      localStorage.setItem(UI_PREFS_KEY, JSON.stringify({ mediaLibraryCollapsed: !!state.mediaLibraryCollapsed, layerDetailsOpenId: state.layerDetailsOpenId || '', snapEnabled: !!state.snapEnabled, guidesEnabled: !!state.guidesEnabled, stageZoom: Number(state.stageZoom || 1) }));
    } catch (_) {}
  }

  function formatDateTime(ts) {
    if (!ts) return '';
    try {
      return new Date(ts).toLocaleString('el-GR');
    } catch (_) {
      return '';
    }
  }

  function updateAutosaveInfo() {
    if (!el.autosaveInfo) return;
    if (state.autosaveMeta && state.autosaveMeta.savedAt) {
      const suffix = state.autosaveMeta.projectName ? ` - ${state.autosaveMeta.projectName}` : '';
      el.autosaveInfo.textContent = `Autosave: ${formatDateTime(state.autosaveMeta.savedAt)}${suffix}`;
    } else {
      el.autosaveInfo.textContent = 'Autosave: off';
    }
  }

  function loadAutosaveMeta() {
    try {
      const raw = localStorage.getItem(AUTOSAVE_KEY);
      if (!raw) return;
      const parsed = JSON.parse(raw);
      if (parsed && parsed.savedAt) {
        state.autosaveMeta = {
          savedAt: Number(parsed.savedAt) || Date.now(),
          projectName: String(parsed.projectName || '')
        };
      }
    } catch (_) {
      state.autosaveMeta = null;
    }
  }

  function persistAutosaveNow() {
    if (!state.persistenceReady) return;
    try {
      const payload = {
        savedAt: Date.now(),
        projectName: String(el.projectName?.value || 'Untitled Project'),
        data: projectToSerializable()
      };
      localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(payload));
      state.autosaveMeta = { savedAt: payload.savedAt, projectName: payload.projectName };
      updateAutosaveInfo();
    } catch (_) {}
  }

  function scheduleAutosave() {
    if (!state.persistenceReady) return;
    if (state.autosaveTimer) window.clearTimeout(state.autosaveTimer);
    state.autosaveTimer = window.setTimeout(() => {
      state.autosaveTimer = null;
      persistAutosaveNow();
    }, 900);
  }

  function restoreAutosave() {
    try {
      const raw = localStorage.getItem(AUTOSAVE_KEY);
      if (!raw) {
        setStatus('Δεν υπάρχει autosave ακόμα.');
        return;
      }
      const parsed = JSON.parse(raw);
      if (!parsed || !parsed.data) {
        setStatus('Το autosave είναι άδειο ή χαλασμένο.');
        return;
      }
      applyProjectData(parsed.data || {});
      if (el.projectName && parsed.projectName) el.projectName.value = String(parsed.projectName);
      state.autosaveMeta = { savedAt: Number(parsed.savedAt) || Date.now(), projectName: String(parsed.projectName || '') };
      updateAutosaveInfo();
      setStatus(`Έγινε restore από autosave: ${state.autosaveMeta.projectName || 'Untitled Project'}.`);
    } catch (_) {
      setStatus('Απέτυχε το restore του autosave.');
    }
  }

  function clearAutosave() {
    try {
      localStorage.removeItem(AUTOSAVE_KEY);
    } catch (_) {}
    state.autosaveMeta = null;
    updateAutosaveInfo();
    setStatus('Το autosave καθαρίστηκε.');
  }

  function exportAppSaveBundle() {
    const payload = {
      app: 'ELLOS CONTROL MAPPING PRO',
      version: APP_VERSION,
      exportedAt: Date.now(),
      autosave: (() => {
        try {
          const raw = localStorage.getItem(AUTOSAVE_KEY);
          return raw ? JSON.parse(raw) : null;
        } catch (_) {
          return null;
        }
      })(),
      projects: Array.isArray(state.localProjects) ? state.localProjects : []
    };
    const safeName = String(el.projectName?.value || 'ellos_control_mapping_pro').replace(/[^a-z0-9_\-]+/gi, '_');
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${safeName}_app_save_backup.json`;
    a.click();
    window.setTimeout(() => URL.revokeObjectURL(url), 800);
    setStatus('Έγινε export του app save backup.');
  }

  async function importAppSaveBundleFile(file) {
    if (!file) return;
    const text = await file.text();
    const payload = JSON.parse(text);
    const incoming = Array.isArray(payload?.projects) ? payload.projects : [];
    const map = new Map();
    [...(Array.isArray(state.localProjects) ? state.localProjects : []), ...incoming].forEach((entry) => {
      if (!entry || !entry.name || !entry.data) return;
      const key = String(entry.name).trim().toLowerCase();
      const normalized = {
        ...entry,
        id: String(entry.id || uid('proj')),
        name: String(entry.name || 'Untitled Project'),
        updatedAt: Number(entry.updatedAt || Date.now()),
        layers: Number(entry.layers || entry.data?.layers?.length || 0),
        previewDataUrl: typeof entry.previewDataUrl === 'string' ? entry.previewDataUrl : '',
        data: entry.data
      };
      const current = map.get(key);
      if (!current || normalized.updatedAt >= Number(current.updatedAt || 0)) map.set(key, normalized);
    });
    state.localProjects = [...map.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, 60);
    persistLocalProjectBank();
    if (payload?.autosave?.data) {
      try {
        localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(payload.autosave));
        state.autosaveMeta = { savedAt: Number(payload.autosave.savedAt || Date.now()), projectName: String(payload.autosave.projectName || '') };
      } catch (_) {}
    }
    renderProjectBank();
    updateAutosaveInfo();
    setStatus(`Έγινε import app save backup: ${incoming.length} project(s).`);
  }

  function sanitizeStageZoom(value) {
    return clamp(Number(value || 1), 0.5, 2);
  }

  function renderStageZoomUi() {
    const percent = Math.round(sanitizeStageZoom(state.stageZoom) * 100);
    if (el.stageZoomInput && document.activeElement !== el.stageZoomInput) el.stageZoomInput.value = String(percent);
    if (el.stageZoomValue) el.stageZoomValue.textContent = `${percent}%`;
    if (el.zoomResetBtn) el.zoomResetBtn.textContent = percent === 100 ? '100%' : 'Reset';
  }

  function applyStageZoom() {
    const zoom = sanitizeStageZoom(state.stageZoom);
    state.stageZoom = zoom;
    if (el.stageViewport) el.stageViewport.style.width = `${Math.round(zoom * 100)}%`;
    renderStageZoomUi();
  }

  function setStageZoom(nextZoom, options = {}) {
    const zoom = sanitizeStageZoom(nextZoom);
    if (!options.force && Math.abs(zoom - sanitizeStageZoom(state.stageZoom)) < 0.001) return;
    state.stageZoom = zoom;
    applyStageZoom();
    saveUiPrefs();
    if (options.render !== false) renderStage();
    if (!options.silent) setStatus(`Workspace zoom: ${Math.round(zoom * 100)}%`);
  }

  function adjustStageZoom(delta) {
    setStageZoom(sanitizeStageZoom(state.stageZoom) + delta);
  }

  function applyMediaPanelState() {
    const collapsed = !!state.mediaLibraryCollapsed;
    el.mediaPanelSection?.classList.toggle('collapsed', collapsed);
    if (el.mediaPanelToggle) {
      el.mediaPanelToggle.textContent = collapsed ? '📂' : '📚';
      el.mediaPanelToggle.title = collapsed ? 'Άνοιγμα Media Library' : 'Κλείσιμο Media Library';
      el.mediaPanelToggle.setAttribute('aria-pressed', collapsed ? 'true' : 'false');
    }
  }

  function setMediaLibraryCollapsed(collapsed) {
    state.mediaLibraryCollapsed = !!collapsed;
    applyMediaPanelState();
    saveUiPrefs();
    setStatus(collapsed ? 'Το Media Library μαζεύτηκε. Δούλεψε μόνο μέσα από τα layers.' : 'Το Media Library άνοιξε ξανά.');
  }

  function renderStageToolStates() {
    if (el.snapToggleBtn) el.snapToggleBtn.classList.toggle('active', !!state.snapEnabled);
    if (el.guidesToggleBtn) el.guidesToggleBtn.classList.toggle('active', !!state.guidesEnabled);
    renderStageZoomUi();
  }

  function setSnapEnabled(enabled) {
    state.snapEnabled = !!enabled;
    renderStageToolStates();
    saveUiPrefs();
    setStatus(state.snapEnabled ? 'Το Snap στο grid είναι ενεργό.' : 'Το Snap στο grid έκλεισε.');
  }

  function setGuidesEnabled(enabled) {
    state.guidesEnabled = !!enabled;
    renderStageToolStates();
    saveUiPrefs();
    renderStage();
    setStatus(state.guidesEnabled ? 'Τα stage guides είναι ορατά.' : 'Τα stage guides κρύφτηκαν.');
  }

  function snapValue(value, step = SNAP_GRID_SIZE) {
    return Math.round(Number(value || 0) / step) * step;
  }

  function snapLayerToGrid(layer, mode = 'move') {
    if (!layer || !state.snapEnabled) return;
    if (mode === 'move') {
      const bounds = getCornerBounds(layer);
      const dx = snapValue(bounds.minX) - bounds.minX;
      const dy = snapValue(bounds.minY) - bounds.minY;
      ensureLayerCorners(layer).forEach((pt) => { pt.x += dx; pt.y += dy; });
      syncLayerFrameFromCorners(layer, false);
      return;
    }
    if (isLayerFreeformMode(layer)) {
      ensureLayerCorners(layer).forEach((pt) => {
        pt.x = snapValue(pt.x);
        pt.y = snapValue(pt.y);
      });
      syncLayerFrameFromCorners(layer, true);
      return;
    }
    const bounds = getCornerBounds(layer);
    const nextMinX = snapValue(bounds.minX);
    const nextMinY = snapValue(bounds.minY);
    const nextMaxX = Math.max(nextMinX + MIN_LAYER_SIZE, snapValue(bounds.maxX));
    const nextMaxY = Math.max(nextMinY + MIN_LAYER_SIZE, snapValue(bounds.maxY));
    setLayerRect(layer, nextMinX, nextMinY, nextMaxX - nextMinX, nextMaxY - nextMinY, Number(layer.rotation || 0));
  }

  function drawStageGuides(ctx) {
    if (!state.guidesEnabled) return;
    ctx.save();
    ctx.strokeStyle = 'rgba(46,143,255,0.22)';
    ctx.lineWidth = 1;
    ctx.setLineDash([6, 6]);
    [state.width / 3, (state.width * 2) / 3].forEach((x) => { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, state.height); ctx.stroke(); });
    [state.height / 3, (state.height * 2) / 3].forEach((y) => { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(state.width, y); ctx.stroke(); });
    ctx.setLineDash([]);
    ctx.strokeStyle = 'rgba(16,185,129,0.35)';
    ctx.beginPath(); ctx.moveTo(state.width / 2, 0); ctx.lineTo(state.width / 2, state.height); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(0, state.height / 2); ctx.lineTo(state.width, state.height / 2); ctx.stroke();
    ctx.restore();
  }

  function getLayerMappingMode(layer) {
    const mode = layer?.mappingMode;
    return mode === 'pin6' || mode === 'pin8' ? mode : 'bbox';
  }

  function isLayerPin6Mode(layer) {
    return getLayerMappingMode(layer) === 'pin6';
  }

  function isLayerPin8Mode(layer) {
    return getLayerMappingMode(layer) === 'pin8';
  }

  function isLayerFreeformMode(layer) {
    return isLayerPin6Mode(layer) || isLayerPin8Mode(layer);
  }


  function sanitizeBlendValue(value) {
    return Math.max(0, Math.min(40, Number(value || 0)));
  }

  function normalizeBlend(blend = {}) {
    return {
      left: sanitizeBlendValue(blend.left),
      right: sanitizeBlendValue(blend.right),
      top: sanitizeBlendValue(blend.top),
      bottom: sanitizeBlendValue(blend.bottom)
    };
  }

  function makeDefaultOutputOps() {
    return Object.fromEntries(OUTPUTS.map((outputId) => [outputId, { blackout: false, frozen: false, freezeCanvas: null, testPattern: 'live', blend: normalizeBlend() }]));
  }

  function getOutputOps(outputId) {
    if (!state.outputOps || typeof state.outputOps !== 'object') state.outputOps = makeDefaultOutputOps();
    if (!state.outputOps[outputId]) state.outputOps[outputId] = { blackout: false, frozen: false, freezeCanvas: null, testPattern: 'live', blend: normalizeBlend() };
    state.outputOps[outputId].blend = normalizeBlend(state.outputOps[outputId].blend);
    return state.outputOps[outputId];
  }

  function serializeOutputOps() {
    const result = {};
    OUTPUTS.forEach((outputId) => {
      const ops = getOutputOps(outputId);
      result[outputId] = {
        blackout: !!ops.blackout,
        testPattern: typeof ops.testPattern === 'string' ? ops.testPattern : 'live',
        blend: normalizeBlend(ops.blend)
      };
    });
    return result;
  }

  function applyOutputOpsData(payload = {}) {
    state.outputOps = makeDefaultOutputOps();
    OUTPUTS.forEach((outputId) => {
      const saved = payload && typeof payload === 'object' ? payload[outputId] : null;
      if (!saved || typeof saved !== 'object') return;
      const ops = getOutputOps(outputId);
      ops.blackout = !!saved.blackout;
      ops.testPattern = ['live', 'grid', 'white', 'bars'].includes(saved.testPattern) ? saved.testPattern : 'live';
      ops.blend = normalizeBlend(saved.blend);
      ops.frozen = false;
      ops.freezeCanvas = null;
    });
  }


  function cloneCorners(corners = []) {
    return corners.map((pt) => ({ name: pt.name, x: Number(pt.x), y: Number(pt.y) }));
  }

  function rotatePoint(point, center, rad) {
    const dx = point.x - center.x;
    const dy = point.y - center.y;
    const cos = Math.cos(rad);
    const sin = Math.sin(rad);
    return {
      x: center.x + dx * cos - dy * sin,
      y: center.y + dx * sin + dy * cos
    };
  }

  function makeRectCorners(x, y, w, h, rotation = 0) {
    const points = [
      { name: 'tl', x, y },
      { name: 'tr', x: x + w, y },
      { name: 'br', x: x + w, y: y + h },
      { name: 'bl', x, y: y + h }
    ];
    if (!rotation) return points;
    const center = { x: x + w / 2, y: y + h / 2 };
    const rad = (Number(rotation || 0) * Math.PI) / 180;
    return points.map((pt) => ({ name: pt.name, ...rotatePoint(pt, center, rad) }));
  }

  function ensureLayerCorners(layer) {
    if (!layer) return [];
    if (!Array.isArray(layer.corners) || layer.corners.length !== 4) {
      layer.corners = makeRectCorners(layer.x || 0, layer.y || 0, layer.w || 1, layer.h || 1, layer.rotation || 0);
    }
    const order = ['tl', 'tr', 'br', 'bl'];
    layer.corners = order.map((name, index) => {
      const found = layer.corners.find((pt) => pt.name === name) || layer.corners[index] || { x: 0, y: 0 };
      return { name, x: Number(found.x), y: Number(found.y) };
    });
    const xs = layer.corners.map((pt) => pt.x);
    const ys = layer.corners.map((pt) => pt.y);
    layer.x = Math.min(...xs);
    layer.y = Math.min(...ys);
    layer.w = Math.max(1, Math.max(...xs) - layer.x);
    layer.h = Math.max(1, Math.max(...ys) - layer.y);
    return layer.corners;
  }

  function getCorner(layer, name) {
    return ensureLayerCorners(layer).find((pt) => pt.name === name) || null;
  }

  function getCornerBounds(layer) {
    const corners = ensureLayerCorners(layer);
    const xs = corners.map((pt) => pt.x);
    const ys = corners.map((pt) => pt.y);
    return {
      minX: Math.min(...xs),
      minY: Math.min(...ys),
      maxX: Math.max(...xs),
      maxY: Math.max(...ys)
    };
  }

  function syncLayerFrameFromCorners(layer, updateRotation = true) {
    const bounds = getCornerBounds(layer);
    layer.x = bounds.minX;
    layer.y = bounds.minY;
    layer.w = Math.max(1, bounds.maxX - bounds.minX);
    layer.h = Math.max(1, bounds.maxY - bounds.minY);
    if (updateRotation) {
      const tl = getCorner(layer, 'tl');
      const tr = getCorner(layer, 'tr');
      if (tl && tr) layer.rotation = (Math.atan2(tr.y - tl.y, tr.x - tl.x) * 180) / Math.PI;
    }
  }

  function resizeLayerFromHandle(layer, handleName, dx, dy, originCorners = null) {
    const sourceCorners = cloneCorners(originCorners || ensureLayerCorners(layer));
    const xs = sourceCorners.map((pt) => pt.x);
    const ys = sourceCorners.map((pt) => pt.y);
    const bounds = {
      minX: Math.min(...xs),
      minY: Math.min(...ys),
      maxX: Math.max(...xs),
      maxY: Math.max(...ys)
    };
    const next = { ...bounds };
    switch (handleName) {
      case 'tl':
        next.minX = Math.min(bounds.maxX - MIN_LAYER_SIZE, bounds.minX + dx);
        next.minY = Math.min(bounds.maxY - MIN_LAYER_SIZE, bounds.minY + dy);
        break;
      case 'tr':
        next.maxX = Math.max(bounds.minX + MIN_LAYER_SIZE, bounds.maxX + dx);
        next.minY = Math.min(bounds.maxY - MIN_LAYER_SIZE, bounds.minY + dy);
        break;
      case 'br':
        next.maxX = Math.max(bounds.minX + MIN_LAYER_SIZE, bounds.maxX + dx);
        next.maxY = Math.max(bounds.minY + MIN_LAYER_SIZE, bounds.maxY + dy);
        break;
      case 'bl':
        next.minX = Math.min(bounds.maxX - MIN_LAYER_SIZE, bounds.minX + dx);
        next.maxY = Math.max(bounds.minY + MIN_LAYER_SIZE, bounds.maxY + dy);
        break;
      default:
        return;
    }
    setLayerRect(layer, next.minX, next.minY, next.maxX - next.minX, next.maxY - next.minY, Number(layer.rotation || 0));
  }

  function moveHandlePoint(layer, handleName, dx, dy, originCorners = null) {
    const sourceCorners = cloneCorners(originCorners || ensureLayerCorners(layer));
    const map = Object.fromEntries(sourceCorners.map((pt) => [pt.name, { ...pt }]));
    const moveNames = (names) => names.forEach((name) => { if (map[name]) { map[name].x += dx; map[name].y += dy; } });
    switch (handleName) {
      case 'tl':
      case 'tr':
      case 'br':
      case 'bl':
        moveNames([handleName]);
        break;
      case 'ml':
        moveNames(['tl', 'bl']);
        break;
      case 'mr':
        moveNames(['tr', 'br']);
        break;
      case 'tm':
        moveNames(['tl', 'tr']);
        break;
      case 'bm':
        moveNames(['bl', 'br']);
        break;
      default:
        return;
    }
    layer.corners = ['tl', 'tr', 'br', 'bl'].map((name) => ({ name, x: map[name].x, y: map[name].y }));
    syncLayerFrameFromCorners(layer, true);
  }

  function setLayerRect(layer, x, y, w, h, rotation = 0) {
    layer.corners = makeRectCorners(x, y, Math.max(1, w), Math.max(1, h), rotation);
    syncLayerFrameFromCorners(layer, true);
  }

  function moveLayerBy(layer, dx, dy) {
    ensureLayerCorners(layer).forEach((pt) => {
      pt.x += dx;
      pt.y += dy;
    });
    syncLayerFrameFromCorners(layer, false);
  }

  function scaleLayerToBounds(layer, nextBounds) {
    const corners = ensureLayerCorners(layer);
    const prev = getCornerBounds(layer);
    const prevW = Math.max(1, prev.maxX - prev.minX);
    const prevH = Math.max(1, prev.maxY - prev.minY);
    const nextW = Math.max(1, nextBounds.maxX - nextBounds.minX);
    const nextH = Math.max(1, nextBounds.maxY - nextBounds.minY);
    corners.forEach((pt) => {
      const nx = (pt.x - prev.minX) / prevW;
      const ny = (pt.y - prev.minY) / prevH;
      pt.x = nextBounds.minX + nx * nextW;
      pt.y = nextBounds.minY + ny * nextH;
    });
    syncLayerFrameFromCorners(layer, false);
  }

  function rotateLayerBy(layer, deltaDeg) {
    const corners = ensureLayerCorners(layer);
    const center = getLayerCenter(layer);
    const rad = (deltaDeg * Math.PI) / 180;
    layer.corners = corners.map((pt) => ({ name: pt.name, ...rotatePoint(pt, center, rad) }));
    syncLayerFrameFromCorners(layer, true);
  }

  function quadLooksRect(layer) {
    const corners = ensureLayerCorners(layer);
    const [tl, tr, br, bl] = corners;
    const eps = 0.5;
    return Math.abs(tl.y - tr.y) < eps && Math.abs(bl.y - br.y) < eps && Math.abs(tl.x - bl.x) < eps && Math.abs(tr.x - br.x) < eps;
  }

  function quadPoint(layer, u, v) {
    const tl = getCorner(layer, 'tl');
    const tr = getCorner(layer, 'tr');
    const br = getCorner(layer, 'br');
    const bl = getCorner(layer, 'bl');
    const x = tl.x * (1 - u) * (1 - v) + tr.x * u * (1 - v) + br.x * u * v + bl.x * (1 - u) * v;
    const y = tl.y * (1 - u) * (1 - v) + tr.y * u * (1 - v) + br.y * u * v + bl.y * (1 - u) * v;
    return { x, y };
  }

  function expandTriangleForOverlap(points, overlap = 0.75) {
    const center = points.reduce((acc, pt) => ({ x: acc.x + pt.x / points.length, y: acc.y + pt.y / points.length }), { x: 0, y: 0 });
    return points.map((pt) => {
      const dx = pt.x - center.x;
      const dy = pt.y - center.y;
      const len = Math.hypot(dx, dy) || 1;
      return { x: pt.x + (dx / len) * overlap, y: pt.y + (dy / len) * overlap };
    });
  }

  function drawTriangleImage(ctx, image, sx0, sy0, sx1, sy1, sx2, sy2, dx0, dy0, dx1, dy1, dx2, dy2) {
    const expanded = expandTriangleForOverlap([
      { x: dx0, y: dy0 },
      { x: dx1, y: dy1 },
      { x: dx2, y: dy2 }
    ]);
    const [p0, p1, p2] = expanded;
    ctx.save();
    ctx.imageSmoothingEnabled = true;
    ctx.beginPath();
    ctx.moveTo(p0.x, p0.y);
    ctx.lineTo(p1.x, p1.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.closePath();
    ctx.clip();
    const denom = sx0 * (sy1 - sy2) + sx1 * (sy2 - sy0) + sx2 * (sy0 - sy1);
    if (Math.abs(denom) < 1e-6) {
      ctx.restore();
      return;
    }
    const m11 = (p0.x * (sy1 - sy2) + p1.x * (sy2 - sy0) + p2.x * (sy0 - sy1)) / denom;
    const m12 = (p0.y * (sy1 - sy2) + p1.y * (sy2 - sy0) + p2.y * (sy0 - sy1)) / denom;
    const m21 = (p0.x * (sx2 - sx1) + p1.x * (sx0 - sx2) + p2.x * (sx1 - sx0)) / denom;
    const m22 = (p0.y * (sx2 - sx1) + p1.y * (sx0 - sx2) + p2.y * (sx1 - sx0)) / denom;
    const dx = (p0.x * (sx1 * sy2 - sx2 * sy1) + p1.x * (sx2 * sy0 - sx0 * sy2) + p2.x * (sx0 * sy1 - sx1 * sy0)) / denom;
    const dy = (p0.y * (sx1 * sy2 - sx2 * sy1) + p1.y * (sx2 * sy0 - sx0 * sy2) + p2.y * (sx0 * sy1 - sx1 * sy0)) / denom;
    ctx.setTransform(m11, m12, m21, m22, dx, dy);
    ctx.drawImage(image, 0, 0);
    ctx.restore();
  }

  function drawMediaToQuad(ctx, layer, element, media = null) {
    ensureLayerCorners(layer);
    const flipX = shouldMirrorLayerMedia(layer, media || getLayerActiveMedia(layer));
    if (quadLooksRect(layer)) {
      const bounds = getCornerBounds(layer);
      ctx.save();
      drawElementInBounds(ctx, element, bounds, flipX, media || getLayerActiveMedia(layer));
      ctx.restore();
      return;
    }
    const bounds = getCornerBounds(layer);
    const cols = 3;
    const rows = 3;
    const sourceRect = getMediaSourceRect(element, media || getLayerActiveMedia(layer));
    const sw = sourceRect.sw;
    const sh = sourceRect.sh;
    const corners = ensureLayerCorners(layer);
    const srcX = (u) => (flipX ? sourceRect.sx + (1 - u) * sw : sourceRect.sx + u * sw);

    ctx.save();
    ctx.beginPath();
    corners.forEach((pt, index) => {
      if (!index) ctx.moveTo(pt.x, pt.y);
      else ctx.lineTo(pt.x, pt.y);
    });
    ctx.closePath();
    ctx.clip();
    drawElementInBounds(ctx, element, bounds, flipX, media || getLayerActiveMedia(layer));
    ctx.restore();

    const grid = [];
    for (let y = 0; y <= rows; y += 1) {
      const row = [];
      for (let x = 0; x <= cols; x += 1) {
        row.push(quadPoint(layer, x / cols, y / rows));
      }
      grid.push(row);
    }
    for (let y = 0; y < rows; y += 1) {
      for (let x = 0; x < cols; x += 1) {
        const p00 = grid[y][x];
        const p10 = grid[y][x + 1];
        const p01 = grid[y + 1][x];
        const p11 = grid[y + 1][x + 1];
        const u0 = x / cols; const v0 = y / rows;
        const u1 = (x + 1) / cols; const v1 = (y + 1) / rows;
        const sx0 = srcX(u0); const sy0 = sourceRect.sy + v0 * sh;
        const sx1 = srcX(u1); const sy1 = sy0;
        const sx2 = srcX(u0); const sy2 = sourceRect.sy + v1 * sh;
        const sx3 = srcX(u1); const sy3 = sourceRect.sy + v1 * sh;
        drawTriangleImage(ctx, element, sx0, sy0, sx1, sy1, sx2, sy2, p00.x, p00.y, p10.x, p10.y, p01.x, p01.y);
        drawTriangleImage(ctx, element, sx3, sy3, sx2, sy2, sx1, sy1, p11.x, p11.y, p01.x, p01.y, p10.x, p10.y);
      }
    }
  }

  function setStatus(text) {
    state.status = text;
    el.statusText.textContent = text;
  }

  function fitRectIntoBox(srcW, srcH, boxW, boxH) {
    const safeSrcW = Math.max(1, Number(srcW) || 1);
    const safeSrcH = Math.max(1, Number(srcH) || 1);
    const ratio = Math.min(boxW / safeSrcW, boxH / safeSrcH);
    const drawW = Math.max(1, Math.round(safeSrcW * ratio));
    const drawH = Math.max(1, Math.round(safeSrcH * ratio));
    return {
      drawW,
      drawH,
      drawX: Math.round((boxW - drawW) / 2),
      drawY: Math.round((boxH - drawH) / 2)
    };
  }

  function buildThumbnailDataUrl(element, srcW, srcH, boxW = MEDIA_THUMB_W, boxH = MEDIA_THUMB_H) {
    try {
      const canvas = document.createElement('canvas');
      canvas.width = boxW;
      canvas.height = boxH;
      const ctx = canvas.getContext('2d');
      ctx.fillStyle = '#08131e';
      ctx.fillRect(0, 0, boxW, boxH);
      const fit = fitRectIntoBox(srcW, srcH, boxW, boxH);
      ctx.drawImage(element, fit.drawX, fit.drawY, fit.drawW, fit.drawH);
      return canvas.toDataURL('image/jpeg', 0.82);
    } catch (_) {
      return '';
    }
  }

  function updateMediaThumbnail(record, dataUrl) {
    if (!record || !dataUrl) return;
    record.thumbUrl = dataUrl;
    renderMediaList();
    renderLayersList();
    renderLayerMonitorCanvases();
    renderCueList();
    renderProjectBank();
  }

  function generateVideoThumbnail(record) {
    if (!record || record.type !== 'video' || !record.url || record._thumbPending) return;
    record._thumbPending = true;
    const temp = document.createElement('video');
    temp.src = record.url;
    temp.muted = true;
    temp.playsInline = true;
    temp.preload = 'auto';
    temp.crossOrigin = 'anonymous';
    temp.style.display = 'none';
    el.hiddenMediaHost.appendChild(temp);

    const cleanup = () => {
      record._thumbPending = false;
      try { temp.pause(); } catch (_) {}
      if (temp.parentNode) temp.parentNode.removeChild(temp);
      temp.removeAttribute('src');
      try { temp.load(); } catch (_) {}
    };

    const finishWithCurrentFrame = () => {
      const dataUrl = buildThumbnailDataUrl(temp, temp.videoWidth || record.width || MEDIA_THUMB_W, temp.videoHeight || record.height || MEDIA_THUMB_H);
      if (dataUrl) updateMediaThumbnail(record, dataUrl);
      cleanup();
    };

    temp.addEventListener('error', cleanup, { once: true });
    temp.addEventListener('loadeddata', () => {
      const duration = Number(temp.duration) || 0;
      const targetTime = duration > 0.4 ? Math.min(Math.max(duration * 0.16, 0.2), 2.8, Math.max(0.2, duration - 0.1)) : 0;
      if (targetTime <= 0) {
        finishWithCurrentFrame();
        return;
      }
      const onSeeked = () => finishWithCurrentFrame();
      temp.addEventListener('seeked', onSeeked, { once: true });
      try {
        temp.currentTime = targetTime;
      } catch (_) {
        temp.removeEventListener('seeked', onSeeked);
        finishWithCurrentFrame();
      }
    }, { once: true });

    try { temp.load(); } catch (_) { cleanup(); }
  }

  function captureProjectPreview() {
    try {
      const preview = document.createElement('canvas');
      preview.width = PROJECT_PREVIEW_W;
      preview.height = PROJECT_PREVIEW_H;
      const ctx = preview.getContext('2d');
      ctx.fillStyle = '#08131e';
      ctx.fillRect(0, 0, preview.width, preview.height);
      const fit = fitRectIntoBox(state.width, state.height, preview.width, preview.height);
      ctx.drawImage(el.stageCanvas, 0, 0, state.width, state.height, fit.drawX, fit.drawY, fit.drawW, fit.drawH);
      return preview.toDataURL('image/jpeg', 0.8);
    } catch (_) {
      return '';
    }
  }

  function captureSceneCanvas(outputFilter = 'ALL') {
    const canvas = document.createElement('canvas');
    canvas.width = state.width;
    canvas.height = state.height;
    const ctx = canvas.getContext('2d');
    const prevActive = state.cueTransitionActive;
    state.cueTransitionActive = null;
    drawScene(ctx, outputFilter, false);
    state.cueTransitionActive = prevActive;
    return canvas;
  }

  function clearCueTransitionIfExpired() {
    const active = state.cueTransitionActive;
    if (!active) return false;
    const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
    if (now - active.startedAt >= active.durationMs) {
      state.cueTransitionActive = null;
      return true;
    }
    return false;
  }

  function sanitizeCueTransitionMode(value) {
    return CUE_TRANSITION_MODES.includes(value) ? value : 'cut';
  }

  function sanitizeCueTransitionMs(value, mode = state.cueTransitionMode || 'cut') {
    const num = Math.max(0, Number(value || 0));
    if (!Number.isFinite(num)) return 0;
    if (sanitizeCueTransitionMode(mode) === 'cut') return 0;
    return Math.min(CLIP_TRANSITION_MAX_MS, Math.round(num));
  }

  function getCueTransitionLabel(mode) {
    return CUE_TRANSITION_LABELS[sanitizeCueTransitionMode(mode)] || 'CUT';
  }

  function getCueTransitionSummary(mode, durationMs) {
    const safeMode = sanitizeCueTransitionMode(mode);
    const safeMs = sanitizeCueTransitionMs(durationMs, safeMode);
    return safeMode === 'cut' ? 'CUT' : `${getCueTransitionLabel(safeMode)} ${safeMs}ms`;
  }

  function captureCueFrames() {
    const frames = { ALL: captureSceneCanvas('ALL') };
    OUTPUTS.forEach((outputId) => {
      frames[outputId] = captureSceneCanvas(outputId);
    });
    return frames;
  }

  function startCueTransition(previousFrames, nextFrames, mode = state.cueTransitionMode || 'cut', durationMs = state.cueTransitionMs || 0) {
    const safeMode = sanitizeCueTransitionMode(mode);
    const safeMs = sanitizeCueTransitionMs(durationMs, safeMode);
    state.cueTransitionActive = null;
    if (safeMode === 'cut' || safeMs <= 0 || !previousFrames || !nextFrames) return;
    state.cueTransitionActive = {
      mode: safeMode,
      durationMs: safeMs,
      startedAt: (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(),
      previousFrames,
      nextFrames
    };
  }

  function drawCueTransitionFrame(ctx, outputFilter = 'ALL') {
    const active = state.cueTransitionActive;
    if (!active) return false;
    const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
    const progress = Math.min(1, (now - active.startedAt) / Math.max(1, active.durationMs || 1));
    if (progress >= 1) {
      state.cueTransitionActive = null;
      return false;
    }
    const previousFrame = active.previousFrames?.[outputFilter] || active.previousFrames?.ALL || null;
    const nextFrame = active.nextFrames?.[outputFilter] || active.nextFrames?.ALL || null;
    if (!previousFrame || !nextFrame) {
      state.cueTransitionActive = null;
      return false;
    }

    const mode = sanitizeCueTransitionMode(active.mode);
    ctx.clearRect(0, 0, state.width, state.height);

    if (mode === 'fade') {
      ctx.drawImage(nextFrame, 0, 0, state.width, state.height);
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, 1 - progress));
      ctx.drawImage(previousFrame, 0, 0, state.width, state.height);
      ctx.restore();
      return true;
    }

    if (mode === 'crossfade') {
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, 1 - progress));
      ctx.drawImage(previousFrame, 0, 0, state.width, state.height);
      ctx.restore();
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, progress));
      ctx.drawImage(nextFrame, 0, 0, state.width, state.height);
      ctx.restore();
      return true;
    }

    if (mode === 'dipblack') {
      ctx.fillStyle = '#000000';
      ctx.fillRect(0, 0, state.width, state.height);
      if (progress < 0.5) {
        ctx.save();
        ctx.globalAlpha = Math.max(0, Math.min(1, 1 - (progress / 0.5)));
        ctx.drawImage(previousFrame, 0, 0, state.width, state.height);
        ctx.restore();
      } else {
        ctx.save();
        ctx.globalAlpha = Math.max(0, Math.min(1, (progress - 0.5) / 0.5));
        ctx.drawImage(nextFrame, 0, 0, state.width, state.height);
        ctx.restore();
      }
      return true;
    }

    if (mode === 'wipe') {
      ctx.drawImage(previousFrame, 0, 0, state.width, state.height);
      const revealW = Math.max(0, Math.min(state.width, Math.round(state.width * progress)));
      if (revealW > 0) {
        ctx.save();
        ctx.beginPath();
        ctx.rect(0, 0, revealW, state.height);
        ctx.clip();
        ctx.drawImage(nextFrame, 0, 0, state.width, state.height);
        ctx.restore();
      }
      return true;
    }

    if (mode === 'slide') {
      const offset = Math.round(state.width * progress);
      ctx.save();
      ctx.drawImage(previousFrame, -offset, 0, state.width, state.height);
      ctx.drawImage(nextFrame, state.width - offset, 0, state.width, state.height);
      ctx.restore();
      return true;
    }

    ctx.drawImage(nextFrame, 0, 0, state.width, state.height);
    return true;
  }


  function sanitizeClipTransitionMode(value) {
    return CLIP_TRANSITION_MODES.includes(value) ? value : 'cut';
  }

  function sanitizeClipTransitionMs(value) {
    const num = Math.max(0, Number(value || 0));
    if (!Number.isFinite(num)) return 0;
    return Math.min(CLIP_TRANSITION_MAX_MS, Math.round(num));
  }

  function getLayerClipTransitionMode(layer) {
    return sanitizeClipTransitionMode(layer?.clipTransitionMode || 'cut');
  }

  function getLayerClipTransitionMs(layer) {
    return sanitizeClipTransitionMs(layer?.clipTransitionMs || 0);
  }

  function getLayerClipTransitionLabel(layer) {
    return CLIP_TRANSITION_LABELS[getLayerClipTransitionMode(layer)] || 'CUT';
  }

  function fillLayerQuad(ctx, layer, color = '#000000', alpha = 1) {
    const corners = ensureLayerCorners(layer);
    ctx.save();
    ctx.globalAlpha = Math.max(0, Math.min(1, alpha));
    ctx.fillStyle = color;
    ctx.beginPath();
    corners.forEach((pt, index) => {
      if (!index) ctx.moveTo(pt.x, pt.y);
      else ctx.lineTo(pt.x, pt.y);
    });
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  }

  function drawAxisAlignedLayerMedia(ctx, layer, element, options = {}) {
    const bounds = getCornerBounds(layer);
    const width = Math.max(1, bounds.maxX - bounds.minX);
    const height = Math.max(1, bounds.maxY - bounds.minY);
    const offsetX = Number(options.offsetX || 0);
    const offsetY = Number(options.offsetY || 0);
    const clip = options.clip || null;
    ctx.save();
    if (clip) {
      ctx.beginPath();
      ctx.rect(clip.x, clip.y, Math.max(0, clip.w), Math.max(0, clip.h));
      ctx.clip();
    }
    ctx.drawImage(element, bounds.minX + offsetX, bounds.minY + offsetY, width, height);
    ctx.restore();
  }

  function cycleLayerClipTransitionMode(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return;
    const current = getLayerClipTransitionMode(layer);
    const idx = CLIP_TRANSITION_MODES.indexOf(current);
    const next = CLIP_TRANSITION_MODES[(idx + 1) % CLIP_TRANSITION_MODES.length] || 'cut';
    layer.clipTransitionMode = next;
    if (next === 'cut') layer.clipTransitionMs = 0;
    else if (getLayerClipTransitionMs(layer) <= 0) layer.clipTransitionMs = CLIP_TRANSITION_DEFAULTS[next] || 600;
    layer.clipTransitionActive = null;
    renderAll();
    setStatus(`${layer.name}: clip transition ${getLayerClipTransitionLabel(layer)}.`);
  }

  function setLayerClipTransitionMs(layerId, value) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return;
    layer.clipTransitionMs = sanitizeClipTransitionMs(value);
    if (getLayerClipTransitionMode(layer) === 'cut' && layer.clipTransitionMs > 0) layer.clipTransitionMode = 'fade';
    layer.clipTransitionActive = null;
    renderAll();
    setStatus(`${layer.name}: διάρκεια transition ${layer.clipTransitionMs}ms.`);
  }

  function startLayerClipTransition(layer, previousMediaId, nextMediaId) {
    if (!layer) return false;
    layer.clipTransitionActive = null;
    const mode = getLayerClipTransitionMode(layer);
    const durationMs = getLayerClipTransitionMs(layer);
    if (mode === 'cut' || durationMs <= 0 || !previousMediaId || !nextMediaId || previousMediaId === nextMediaId) return false;
    const previousMedia = getMediaById(previousMediaId);
    const nextMedia = getMediaById(nextMediaId);
    if (!previousMedia?.element || !nextMedia?.element) return false;
    layer.clipTransitionActive = {
      mode,
      durationMs,
      startedAt: (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now(),
      previousMediaId,
      nextMediaId
    };
    return true;
  }

  function getLayerClipTransitionState(layer) {
    const active = layer?.clipTransitionActive;
    if (!active) return null;
    const now = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
    const progress = Math.min(1, (now - active.startedAt) / Math.max(1, active.durationMs || 1));
    if (progress >= 1) {
      layer.clipTransitionActive = null;
      return null;
    }
    return { ...active, progress };
  }

  function processLayerClipTransitions() {
    let changed = false;
    state.layers.forEach((layer) => {
      if (layer?.clipTransitionActive && !getLayerClipTransitionState(layer)) changed = true;
    });
    return changed;
  }

  function drawLayerClipTransition(ctx, layer) {
    const active = getLayerClipTransitionState(layer);
    if (!active) return false;
    const previousMedia = getMediaById(active.previousMediaId);
    const nextMedia = getMediaById(active.nextMediaId);
    if (!previousMedia?.element || !nextMedia?.element) {
      layer.clipTransitionActive = null;
      return false;
    }
    const progress = active.progress;
    const mode = sanitizeClipTransitionMode(active.mode);
    const rectLike = quadLooksRect(layer);
    if (mode === 'crossfade') {
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, 1 - progress));
      drawLayerMedia(ctx, layer, previousMedia.element, previousMedia);
      ctx.restore();
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, progress));
      drawLayerMedia(ctx, layer, nextMedia.element, nextMedia);
      ctx.restore();
      return true;
    }
    if (mode === 'fade' || mode === 'dipblack') {
      const dipWindow = mode === 'dipblack' ? 0.34 : 0.5;
      fillLayerQuad(ctx, layer, '#000000', 1);
      if (progress < dipWindow) {
        ctx.save();
        ctx.globalAlpha = Math.max(0, Math.min(1, 1 - progress / dipWindow));
        drawLayerMedia(ctx, layer, previousMedia.element, previousMedia);
        ctx.restore();
      }
      if (progress > 1 - dipWindow) {
        ctx.save();
        ctx.globalAlpha = Math.max(0, Math.min(1, (progress - (1 - dipWindow)) / dipWindow));
        drawLayerMedia(ctx, layer, nextMedia.element, nextMedia);
        ctx.restore();
      }
      return true;
    }
    if (mode == 'wipe' && rectLike) {
      const bounds = getCornerBounds(layer);
      const width = Math.max(1, bounds.maxX - bounds.minX);
      const height = Math.max(1, bounds.maxY - bounds.minY);
      drawAxisAlignedLayerMedia(ctx, layer, previousMedia.element);
      drawAxisAlignedLayerMedia(ctx, layer, nextMedia.element, { clip: { x: bounds.minX, y: bounds.minY, w: width * progress, h: height } });
      return true;
    }
    if (mode == 'slide' && rectLike) {
      const bounds = getCornerBounds(layer);
      const width = Math.max(1, bounds.maxX - bounds.minX);
      const height = Math.max(1, bounds.maxY - bounds.minY);
      ctx.save();
      ctx.beginPath();
      ctx.rect(bounds.minX, bounds.minY, width, height);
      ctx.clip();
      drawAxisAlignedLayerMedia(ctx, layer, previousMedia.element, { offsetX: -width * progress });
      drawAxisAlignedLayerMedia(ctx, layer, nextMedia.element, { offsetX: width * (1 - progress) });
      ctx.restore();
      return true;
    }
    ctx.save();
    ctx.globalAlpha = Math.max(0, Math.min(1, 1 - progress));
    drawLayerMedia(ctx, layer, previousMedia.element, previousMedia);
    ctx.restore();
    ctx.save();
    ctx.globalAlpha = Math.max(0, Math.min(1, progress));
    drawLayerMedia(ctx, layer, nextMedia.element, nextMedia);
    ctx.restore();
    return true;
  }

  function serializeLayer(layer) {
    const media = getLayerActiveMedia(layer);
    return {
      id: layer.id,
      name: layer.name,
      mediaId: layer.mediaId,
      pendingMediaFileName: layer.pendingMediaFileName || media?.fileName || '',
      output: layer.output,
      x: layer.x,
      y: layer.y,
      w: layer.w,
      h: layer.h,
      opacity: layer.opacity,
      rotation: layer.rotation || 0,
      visible: layer.visible,
      locked: layer.locked,
      loop: layer.loop,
      clipTransitionMode: getLayerClipTransitionMode(layer),
      clipTransitionMs: getLayerClipTransitionMs(layer),
      sequenceEnabled: !!layer.sequenceEnabled,
      playlistIndex: Number(layer.playlistIndex || 0),
      playlistMediaIds: [...(Array.isArray(layer.playlistMediaIds) ? layer.playlistMediaIds : [])],
      playlistFileNames: (Array.isArray(layer.playlistMediaIds) ? layer.playlistMediaIds : []).map((id) => getMediaById(id)?.fileName || ''),
      mappingMode: getLayerMappingMode(layer),
      effectMirrorX: !!layer.effectMirrorX,
      effectMono: !!layer.effectMono,
      effectBoost: !!layer.effectBoost,
      fxBrightness: clamp(Number(layer.fxBrightness ?? 100), 0, 200),
      fxContrast: clamp(Number(layer.fxContrast ?? 100), 0, 220),
      fxSaturation: clamp(Number(layer.fxSaturation ?? 100), 0, 300),
      fxHue: clamp(Number(layer.fxHue ?? 0), -180, 180),
      fxBlur: clamp(Number(layer.fxBlur ?? 0), 0, 20),
      edgeSoftness: clamp(Number(layer.edgeSoftness ?? 0), 0, 35),
      bgRemoveMode: ['off', 'green', 'blue', 'black', 'white', 'custom'].includes(layer.bgRemoveMode) ? layer.bgRemoveMode : 'off',
      bgRemoveColor: /^#[0-9a-fA-F]{6}$/.test(String(layer.bgRemoveColor || '')) ? String(layer.bgRemoveColor) : '#00ff00',
      bgRemoveThreshold: clamp(Number(layer.bgRemoveThreshold ?? 70), 0, 180),
      bgRemoveSoftness: clamp(Number(layer.bgRemoveSoftness ?? 25), 0, 140),
      z: layer.z,
      corners: cloneCorners(ensureLayerCorners(layer))
    };
  }

  function serializeCue(cue) {
    return {
      id: cue.id,
      name: cue.name,
      updatedAt: cue.updatedAt,
      previewDataUrl: cue.previewDataUrl || '',
      transitionMode: sanitizeCueTransitionMode(cue.transitionMode || 'cut'),
      transitionMs: sanitizeCueTransitionMs(cue.transitionMs || 0, cue.transitionMode || 'cut'),
      layers: Array.isArray(cue.layers) ? cue.layers.map((layer) => ({
        ...layer,
        corners: cloneCorners(layer.corners || [])
      })) : []
    };
  }

  function buildCueSnapshot(nameOverride = '') {
    const name = String(nameOverride || el.cueNameInput?.value || '').trim() || `Cue ${state.cues.length + 1}`;
    return {
      id: state.activeCueId || uid('cue'),
      name,
      updatedAt: Date.now(),
      previewDataUrl: captureProjectPreview(),
      transitionMode: sanitizeCueTransitionMode(state.cueTransitionMode || 'cut'),
      transitionMs: sanitizeCueTransitionMs(state.cueTransitionMs || 0, state.cueTransitionMode || 'cut'),
      layers: state.layers.map((layer) => serializeLayer(layer))
    };
  }

  function resolveCueLayerMedia(snapshot) {
    if (!snapshot) return '';
    if (snapshot.mediaId && getMediaById(snapshot.mediaId)) return snapshot.mediaId;
    if (snapshot.pendingMediaFileName) {
      const match = state.media.find((item) => item.fileName === snapshot.pendingMediaFileName || item.name === snapshot.pendingMediaFileName);
      return match?.id || '';
    }
    return '';
  }

  function setCanvasSize(width, height) {
    state.width = width;
    state.height = height;
    el.stageCanvas.width = width;
    el.stageCanvas.height = height;
    renderAll();
  }

  function getSelectedLayer() {
    return state.layers.find((layer) => layer.id === state.selectedLayerId) || null;
  }

  function getMediaById(id) {
    return state.media.find((item) => item.id === id) || null;
  }

  function ensureLayerPlaylist(layer) {
    if (!layer) return [];
    const list = Array.isArray(layer.playlistMediaIds) ? layer.playlistMediaIds.filter(Boolean) : [];
    if (!list.length && layer.mediaId) list.push(layer.mediaId);
    if (layer.mediaId) {
      const currentIdx = list.indexOf(layer.mediaId);
      if (currentIdx === -1) list.unshift(layer.mediaId);
      layer.playlistIndex = list.indexOf(layer.mediaId);
    } else {
      layer.playlistIndex = Math.max(0, Math.min(Number(layer.playlistIndex || 0), Math.max(0, list.length - 1)));
      layer.mediaId = list[layer.playlistIndex] || '';
    }
    layer.playlistMediaIds = list;
    if (!Array.isArray(layer.pendingPlaylistFileNames)) layer.pendingPlaylistFileNames = [];
    return layer.playlistMediaIds;
  }

  function getLayerActiveMediaId(layer) {
    if (!layer) return '';
    ensureLayerPlaylist(layer);
    return layer.mediaId || layer.playlistMediaIds[layer.playlistIndex] || '';
  }

  function getLayerActiveMedia(layer) {
    return getMediaById(getLayerActiveMediaId(layer));
  }

  function setLayerPrimaryMedia(layer, mediaId, options = {}) {
    if (!layer) return;
    const keepPlaylist = !!options.keepPlaylist;
    if (!keepPlaylist) {
      layer.playlistMediaIds = mediaId ? [mediaId] : [];
      layer.playlistIndex = 0;
      layer.pendingPlaylistFileNames = mediaId ? [getMediaById(mediaId)?.fileName || ''] : [];
    } else {
      ensureLayerPlaylist(layer);
      if (!layer.playlistMediaIds.length && mediaId) layer.playlistMediaIds = [mediaId];
      const idx = layer.playlistMediaIds.indexOf(mediaId);
      if (idx >= 0) layer.playlistIndex = idx;
    }
    layer.mediaId = mediaId || '';
    layer.pendingMediaFileName = mediaId ? '' : layer.pendingMediaFileName;
    if (layer.mediaId && !layer.pendingPlaylistFileNames?.length) {
      layer.pendingPlaylistFileNames = [getMediaById(layer.mediaId)?.fileName || ''];
    }
    syncLayerLoopToMedia(layer);
  }

  function addMediaToLayerPlaylist(layerId, mediaId, options = {}) {
    const layer = state.layers.find((item) => item.id === layerId);
    const media = getMediaById(mediaId);
    if (!layer) return false;
    if (!mediaId || !media) {
      if (!options.silent) setStatus('Διάλεξε πρώτα media από τη library.');
      return false;
    }
    if (media.type !== 'video') {
      if (!options.silent) setStatus('Η σειρά layer δουλεύει προς το παρόν με video clips.');
      return false;
    }
    ensureLayerPlaylist(layer);
    layer.playlistMediaIds.push(mediaId);
    layer.pendingPlaylistFileNames.push(media.fileName || media.name || '');
    if (!layer.mediaId || options.makePrimary) {
      layer.mediaId = mediaId;
      layer.playlistIndex = Math.max(0, layer.playlistMediaIds.lastIndexOf(mediaId));
      layer.pendingMediaFileName = '';
    }
    if (layer.playlistMediaIds.length > 1) layer.sequenceEnabled = true;
    if (!options.skipRender) renderAll();
    if (!options.silent) setStatus(`Μπήκε στο ${layer.name} το ${media.name} στη σειρά.`);
    return true;
  }


  function removeMediaFromLayerPlaylist(layerId, index) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return false;
    ensureLayerPlaylist(layer);
    const safeIndex = Number(index);
    if (!Number.isInteger(safeIndex) || safeIndex < 0 || safeIndex >= layer.playlistMediaIds.length) return false;
    const removedMediaId = layer.playlistMediaIds.splice(safeIndex, 1)[0];
    if (Array.isArray(layer.pendingPlaylistFileNames)) layer.pendingPlaylistFileNames.splice(safeIndex, 1);
    if (!layer.playlistMediaIds.length) {
      layer.mediaId = '';
      layer.playlistIndex = 0;
      layer.sequenceEnabled = false;
    } else {
      if (safeIndex < layer.playlistIndex) layer.playlistIndex -= 1;
      if (removedMediaId === layer.mediaId || !layer.playlistMediaIds.includes(layer.mediaId)) {
        layer.playlistIndex = Math.max(0, Math.min(layer.playlistIndex, layer.playlistMediaIds.length - 1));
        layer.mediaId = layer.playlistMediaIds[layer.playlistIndex] || '';
      } else {
        layer.playlistIndex = Math.max(0, layer.playlistMediaIds.indexOf(layer.mediaId));
      }
      if (layer.playlistMediaIds.length < 2) layer.sequenceEnabled = false;
    }
    syncLayerLoopToMedia(layer);
    const nextEl = getLayerMediaElement(layer);
    if (state.playing && nextEl) {
      const p = nextEl.play();
      if (p && typeof p.catch === 'function') p.catch(() => {});
    }
    renderAll();
    setStatus(`${layer.name}: αφαιρέθηκε clip από τη σειρά.`);
    return true;
  }

  function reorderLayerPlaylist(layerId, fromIndex, toIndex) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return false;
    ensureLayerPlaylist(layer);
    const from = Number(fromIndex);
    const to = Number(toIndex);
    if (!Number.isInteger(from) || !Number.isInteger(to)) return false;
    if (from < 0 || to < 0 || from >= layer.playlistMediaIds.length || to >= layer.playlistMediaIds.length || from === to) return false;
    const activeMediaId = layer.mediaId;
    const movedId = layer.playlistMediaIds.splice(from, 1)[0];
    layer.playlistMediaIds.splice(to, 0, movedId);
    if (Array.isArray(layer.pendingPlaylistFileNames)) {
      const movedName = layer.pendingPlaylistFileNames.splice(from, 1)[0] || '';
      layer.pendingPlaylistFileNames.splice(to, 0, movedName);
    }
    layer.playlistIndex = Math.max(0, layer.playlistMediaIds.indexOf(activeMediaId));
    layer.mediaId = activeMediaId || layer.playlistMediaIds[layer.playlistIndex] || '';
    renderAll();
    setStatus(`${layer.name}: άλλαξε η σειρά των clips.`);
    return true;
  }

  function stepLayerPlaylist(layerId, direction = 1) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return false;
    ensureLayerPlaylist(layer);
    if (layer.playlistMediaIds.length < 2) {
      setStatus('Βάλε πρώτα δεύτερο video στο layer.');
      return false;
    }
    const count = layer.playlistMediaIds.length;
    let nextIndex = layer.playlistIndex + (direction >= 0 ? 1 : -1);
    if (nextIndex >= count) nextIndex = layer.loop ? 0 : count - 1;
    if (nextIndex < 0) nextIndex = layer.loop ? count - 1 : 0;
    const previousMediaId = layer.mediaId;
    layer.playlistIndex = nextIndex;
    layer.mediaId = layer.playlistMediaIds[nextIndex] || '';
    startLayerClipTransition(layer, previousMediaId, layer.mediaId);
    const mediaEl = getLayerMediaElement(layer);
    if (mediaEl) {
      try { mediaEl.currentTime = 0; } catch (_) {}
      syncLayerLoopToMedia(layer);
      if (state.playing) {
        const p = mediaEl.play();
        if (p && typeof p.catch === 'function') p.catch(() => {});
      }
    }
    renderAll();
    setStatus(`${layer.name}: επόμενο clip.`);
    return true;
  }


  function startLayerSequencePlayback(layerId, options = {}) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return false;
    ensureLayerPlaylist(layer);
    if (!layer.playlistMediaIds.length) {
      setStatus('Το layer δεν έχει clips ακόμα.');
      return false;
    }
    const restart = options.restart !== false;
    if (layer.playlistMediaIds.length > 1) layer.sequenceEnabled = true;
    if (restart) {
      const previousMediaId = layer.mediaId;
      layer.playlistIndex = 0;
      layer.mediaId = layer.playlistMediaIds[0] || layer.mediaId;
      startLayerClipTransition(layer, previousMediaId, layer.mediaId);
    }
    const mediaEl = getLayerMediaElement(layer);
    if (mediaEl) {
      try { mediaEl.currentTime = 0; } catch (_) {}
      syncLayerLoopToMedia(layer);
      const p = mediaEl.play();
      if (p && typeof p.catch === 'function') p.catch(() => {});
      state.playing = true;
    }
    renderAll();
    setStatus(`${layer.name}: play sequence ${layer.sequenceEnabled ? 'ON' : 'μόνο current clip'}.`);
    return true;
  }

  function deleteMediaById(mediaId) {
    const media = getMediaById(mediaId);
    if (!media) return false;
    state.layers.forEach((layer) => {
      ensureLayerPlaylist(layer);
      if (!layer.playlistMediaIds.includes(mediaId) && layer.mediaId !== mediaId) return;
      layer.playlistMediaIds = layer.playlistMediaIds.filter((id) => id !== mediaId);
      layer.pendingPlaylistFileNames = layer.playlistMediaIds.map((id) => getMediaById(id)?.fileName || getMediaById(id)?.name || '');
      if (layer.mediaId === mediaId || !layer.playlistMediaIds.includes(layer.mediaId)) {
        layer.playlistIndex = Math.max(0, Math.min(layer.playlistIndex || 0, Math.max(0, layer.playlistMediaIds.length - 1)));
        layer.mediaId = layer.playlistMediaIds[layer.playlistIndex] || '';
        layer.pendingMediaFileName = layer.mediaId ? '' : '';
      } else {
        layer.playlistIndex = Math.max(0, layer.playlistMediaIds.indexOf(layer.mediaId));
      }
      if (layer.playlistMediaIds.length < 2) layer.sequenceEnabled = false;
      syncLayerLoopToMedia(layer);
      const nextEl = getLayerMediaElement(layer);
      if (state.playing && nextEl) {
        const p = nextEl.play();
        if (p && typeof p.catch === 'function') p.catch(() => {});
      }
    });
    if (isLiveInputMedia(media)) stopLiveInputRecord(media);
    if (media.element && media.element.parentNode) media.element.parentNode.removeChild(media.element);
    try { if (media.url) URL.revokeObjectURL(media.url); } catch (_) {}
    state.media = state.media.filter((item) => item.id !== mediaId);
    if (state.selectedMediaId === mediaId) state.selectedMediaId = state.media[0]?.id || null;
    renderAll();
    setStatus(`Διαγράφηκε το media: ${media.name}.`);
    return true;
  }

  function importFilesIntoLayer(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return;
    state.layerImportTargetId = layer.id;
    if (el.layerFileInput) el.layerFileInput.click();
  }

  function setLayerPlaylistPrimary(layerId, mediaId) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer || !mediaId) return;
    ensureLayerPlaylist(layer);
    const idx = layer.playlistMediaIds.indexOf(mediaId);
    if (idx < 0) return;
    const previousMediaId = layer.mediaId;
    layer.mediaId = mediaId;
    layer.playlistIndex = idx;
    startLayerClipTransition(layer, previousMediaId, layer.mediaId);
    syncLayerLoopToMedia(layer);
    renderAll();
    const media = getMediaById(mediaId);
    setStatus(`${layer.name}: ενεργό clip ${media?.name || 'playlist item'}.`);
  }

  function toggleLayerSequence(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return;
    ensureLayerPlaylist(layer);
    if (layer.playlistMediaIds.length < 2) {
      setStatus('Βάλε πρώτα δεύτερο video στο layer με το ➕.');
      return;
    }
    layer.sequenceEnabled = !layer.sequenceEnabled;
    syncLayerLoopToMedia(layer);
    renderAll();
    setStatus(`${layer.name}: σειρά ${layer.sequenceEnabled ? 'ON' : 'OFF'}.`);
  }

  function advanceLayerSequence(layer, options = {}) {
    if (!layer) return false;
    ensureLayerPlaylist(layer);
    const count = layer.playlistMediaIds.length;
    if (!layer.sequenceEnabled || count < 2) return false;
    const nextIndex = layer.playlistIndex + 1;
    const previousMediaId = layer.mediaId;
    if (nextIndex >= count) {
      if (!layer.loop) {
        const currentEl = getLayerMediaElement(layer);
        if (currentEl) currentEl.pause();
        return false;
      }
      layer.playlistIndex = 0;
    } else {
      layer.playlistIndex = nextIndex;
    }
    layer.mediaId = layer.playlistMediaIds[layer.playlistIndex] || '';
    startLayerClipTransition(layer, previousMediaId, layer.mediaId);
    const nextEl = getLayerMediaElement(layer);
    if (nextEl) {
      try { nextEl.currentTime = 0; } catch (_) {}
      syncLayerLoopToMedia(layer);
      if (state.playing || options.forcePlay) {
        const p = nextEl.play();
        if (p && typeof p.catch === 'function') p.catch(() => {});
      }
    }
    renderAll();
    return true;
  }

  function processLayerSequences() {
    let changed = false;
    state.layers.forEach((layer) => {
      if (!layer.sequenceEnabled) return;
      const media = getLayerActiveMedia(layer);
      const mediaEl = media?.type === 'video' ? media.element : null;
      if (!mediaEl) return;
      if (mediaEl.ended) {
        changed = advanceLayerSequence(layer, { forcePlay: state.playing }) || changed;
      }
    });
    return changed;
  }

  function getRenderableLayers(outputFilter = 'ALL') {
    return state.layers
      .filter((layer) => layer.visible && (outputFilter === 'ALL' || layer.output === outputFilter))
      .sort((a, b) => (a.z || 0) - (b.z || 0));
  }

  const VIDEO_EXTENSIONS = new Set(['mp4','m4v','mov','webm','ogv','ogg','avi','mkv','wmv','mts','m2ts','ts','mpeg','mpg','mpe','3gp','3g2','mxf']);
  const IMAGE_EXTENSIONS = new Set(['png','jpg','jpeg','gif','webp','bmp','svg','avif']);

  function getFileExtension(name) {
    const lower = String(name || '').toLowerCase();
    const dot = lower.lastIndexOf('.');
    return dot >= 0 ? lower.slice(dot + 1) : '';
  }

  function detectMediaType(file) {
    const mime = String(file?.type || '').toLowerCase();
    const ext = getFileExtension(file?.name);
    if (mime.startsWith('video/')) return 'video';
    if (mime.startsWith('image/')) return 'image';
    if (VIDEO_EXTENSIONS.has(ext)) return 'video';
    if (IMAGE_EXTENSIONS.has(ext)) return 'image';
    if (mime === 'application/pdf' || PDF_EXTENSIONS.has(ext)) return 'pdf';
    return '';
  }

  function isCameraMedia(item) {
    return !!(item && item.isLiveCamera);
  }

  function isScreenMedia(item) {
    return !!(item && item.isLiveScreen);
  }

  function isLiveInputMedia(item) {
    return isCameraMedia(item) || isScreenMedia(item);
  }

  function getMediaChipLabel(item) {
    if (isCameraMedia(item)) return 'CAM';
    if (isScreenMedia(item)) return 'SCR';
    if (item?.type === 'video') return 'VID';
    if (item?.type === 'text') return 'TXT';
    if (item?.type === 'pdf') return 'PDF';
    return 'IMG';
  }

  function getMediaOverlayIcon(item) {
    if (isCameraMedia(item)) return '📷';
    if (isScreenMedia(item)) return '🖥';
    if (item?.type === 'video') return '▶';
    if (item?.type === 'text') return '✎';
    if (item?.type === 'pdf') return '📄';
    return '🖼';
  }

  function getMediaPlaceholderIcon(item) {
    if (isCameraMedia(item)) return '📷';
    if (isScreenMedia(item)) return '🖥';
    if (item?.type === 'video') return '▶';
    if (item?.type === 'text') return '✎';
    if (item?.type === 'pdf') return '📄';
    return '🖼';
  }

  async function pickNextCameraDevice() {
    if (!navigator.mediaDevices?.getUserMedia) throw new Error('Η κάμερα δεν υποστηρίζεται σε αυτό το build/browser.');
    let stream = null;
    try {
      stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
      const devices = await navigator.mediaDevices.enumerateDevices();
      const videoInputs = devices.filter((d) => d.kind === 'videoinput');
      const used = new Set(state.media.filter(isCameraMedia).map((item) => item.deviceId).filter(Boolean));
      const preferred = videoInputs.find((d) => d.deviceId && !used.has(d.deviceId)) || videoInputs[0] || null;
      if (preferred?.deviceId) {
        stream.getTracks().forEach((track) => track.stop());
        stream = await navigator.mediaDevices.getUserMedia({
          video: { deviceId: { exact: preferred.deviceId } },
          audio: false
        });
        return { stream, device: preferred };
      }
      return { stream, device: videoInputs[0] || null };
    } catch (error) {
      if (stream) stream.getTracks().forEach((track) => track.stop());
      throw error;
    }
  }

  function stopLiveInputRecord(record) {
    try { record?.element?.pause?.(); } catch (_) {}
    const stream = record?.stream || record?.element?.srcObject || null;
    if (stream?.getTracks) {
      try { stream.getTracks().forEach((track) => track.stop()); } catch (_) {}
    }
  }

  function stopCameraRecord(record) {
    stopLiveInputRecord(record);
  }

  async function addCameraSource() {
    try {
      setStatus('Άνοιγμα κάμερας... δώσε άδεια αν ζητηθεί.');
      const { stream, device } = await pickNextCameraDevice();
      const id = uid('media');
      const existingCams = state.media.filter(isCameraMedia).length;
      const name = device?.label || `Camera ${existingCams + 1}`;
      const record = {
        id,
        type: 'video',
        name: name.startsWith('📷') ? name : `📷 ${name}`,
        fileName: `camera:${device?.deviceId || existingCams + 1}`,
        url: '',
        element: null,
        ready: false,
        width: 0,
        height: 0,
        duration: 0,
        error: null,
        thumbUrl: '',
        isLiveCamera: true,
        deviceId: device?.deviceId || '',
        stream
      };
      applyDefaultCameraFx(record);
      applyDefaultClipFx(record);
      const video = document.createElement('video');
      video.autoplay = true;
      video.muted = true;
      video.loop = true;
      video.playsInline = true;
      video.preload = 'auto';
      video.srcObject = stream;
      const markReady = () => {
        if (record.ready) return;
        record.ready = true;
        record.width = video.videoWidth || 0;
        record.height = video.videoHeight || 0;
        record.duration = 0;
        record.thumbUrl = buildThumbnailDataUrl(video, record.width || MEDIA_THUMB_W, record.height || MEDIA_THUMB_H);
        renderAll();
      };
      video.addEventListener('loadedmetadata', markReady);
      video.addEventListener('loadeddata', markReady);
      video.addEventListener('canplay', markReady);
      video.addEventListener('error', () => {
        record.error = 'Camera failed';
        setStatus(`Η κάμερα δεν άνοιξε σωστά: ${record.name}`);
        renderMediaList();
      });
      record.element = video;
      el.hiddenMediaHost.appendChild(video);
      state.media.push(record);
      state.selectedMediaId = record.id;
      try {
        const p = video.play();
        if (p && typeof p.catch === 'function') p.catch(() => {});
      } catch (_) {}
      renderAll();
      setStatus(`${record.name} μπήκε στο Media Library. Σημείωση: live camera source.`);
    } catch (error) {
      const msg = error?.message || String(error || 'Άγνωστο σφάλμα');
      setStatus(`Η κάμερα δεν άνοιξε: ${msg}`);
    }
  }


  async function addScreenSource() {
    try {
      if (!navigator.mediaDevices?.getDisplayMedia) throw new Error('Το screen capture δεν υποστηρίζεται σε αυτό το build/browser.');
      setStatus('Άνοιγμα screen capture... επίλεξε οθόνη ή παράθυρο.');
      const stream = await navigator.mediaDevices.getDisplayMedia({
        video: { frameRate: { ideal: 30, max: 60 } },
        audio: false
      });
      const id = uid('media');
      const existingScreens = state.media.filter(isScreenMedia).length;
      const track = stream.getVideoTracks?.()[0] || null;
      const rawLabel = (track?.label || '').trim();
      const name = rawLabel ? `🖥 ${rawLabel}` : `🖥 Screen ${existingScreens + 1}`;
      const record = {
        id,
        type: 'video',
        name,
        fileName: `screen:${existingScreens + 1}`,
        url: '',
        element: null,
        ready: false,
        width: 0,
        height: 0,
        duration: 0,
        error: null,
        thumbUrl: '',
        isLiveScreen: true,
        stream
      };
      applyDefaultCameraFx(record);
      applyDefaultClipFx(record);
      const video = document.createElement('video');
      video.autoplay = true;
      video.muted = true;
      video.loop = true;
      video.playsInline = true;
      video.preload = 'auto';
      video.srcObject = stream;
      const markReady = () => {
        if (record.ready) return;
        record.ready = true;
        record.width = video.videoWidth || 0;
        record.height = video.videoHeight || 0;
        record.duration = 0;
        record.thumbUrl = buildThumbnailDataUrl(video, record.width || MEDIA_THUMB_W, record.height || MEDIA_THUMB_H);
        renderAll();
      };
      const onEnded = () => {
        record.error = 'Screen capture stopped';
        renderMediaList();
        setStatus(`${record.name}: το screen capture σταμάτησε.`);
      };
      video.addEventListener('loadedmetadata', markReady);
      video.addEventListener('loadeddata', markReady);
      video.addEventListener('canplay', markReady);
      video.addEventListener('error', () => {
        record.error = 'Screen failed';
        setStatus(`Το screen capture δεν άνοιξε σωστά: ${record.name}`);
        renderMediaList();
      });
      if (track) track.addEventListener('ended', onEnded, { once: true });
      record.element = video;
      el.hiddenMediaHost.appendChild(video);
      state.media.push(record);
      state.selectedMediaId = record.id;
      try {
        const p = video.play();
        if (p && typeof p.catch === 'function') p.catch(() => {});
      } catch (_) {}
      renderAll();
      setStatus(`${record.name} μπήκε στο Media Library ως live screen source.`);
    } catch (error) {
      const msg = error?.message || String(error || 'Άγνωστο σφάλμα');
      setStatus(`Το screen capture δεν άνοιξε: ${msg}`);
    }
  }

  function makeMediaRecord(file) {
    const type = detectMediaType(file) || 'video';
    if (type === 'text') return createTextMediaRecord({ name: file.name, fileName: file.name });
    if (type === 'pdf') return createPdfMediaRecord(file);
    const id = uid('media');
    const url = URL.createObjectURL(file);
    const record = {
      id,
      type,
      name: file.name,
      fileName: file.name,
      url,
      element: null,
      ready: false,
      width: 0,
      height: 0,
      duration: 0,
      error: null,
      thumbUrl: ''
    };
    applyDefaultCameraFx(record);

    if (type === 'video') {
      const video = document.createElement('video');
      video.src = url;
      video.muted = true;
      video.loop = true;
      video.playsInline = true;
      video.preload = 'metadata';
      video.crossOrigin = 'anonymous';
      const markVideoReady = () => {
        if (record.ready) return;
        record.ready = true;
        record.width = video.videoWidth || 0;
        record.height = video.videoHeight || 0;
        record.duration = video.duration || 0;
        if (video.readyState >= 2) {
          record.thumbUrl = buildThumbnailDataUrl(video, record.width || MEDIA_THUMB_W, record.height || MEDIA_THUMB_H);
        }
        generateVideoThumbnail(record);
        if (state.playing) {
          const p = video.play();
          if (p && typeof p.catch === 'function') p.catch(() => {});
        }
        renderInspector();
        renderAll();
      };
      video.addEventListener('loadedmetadata', markVideoReady);
      video.addEventListener('loadeddata', markVideoReady);
      video.addEventListener('canplay', markVideoReady);
      video.addEventListener('error', () => {
        const code = video.error && typeof video.error.code === 'number' ? ` (code ${video.error.code})` : '';
        record.error = `Video failed to load${code}`;
        setStatus(`Απέτυχε φόρτωση video: ${file.name}${code}`);
        renderMediaList();
      });
      record.element = video;
      el.hiddenMediaHost.appendChild(video);
      try {
        video.load();
      } catch (_) {}
    } else {
      const img = new Image();
      img.onload = () => {
        record.ready = true;
        record.width = img.naturalWidth || 0;
        record.height = img.naturalHeight || 0;
        record.thumbUrl = buildThumbnailDataUrl(img, record.width || MEDIA_THUMB_W, record.height || MEDIA_THUMB_H);
        renderInspector();
        renderAll();
      };
      img.onerror = () => {
        record.error = 'Image failed to load';
        renderMediaList();
      };
      img.src = url;
      record.element = img;
      el.hiddenMediaHost.appendChild(img);
    }

    return record;
  }

  async function importFiles(fileList, { autoCreateLayers = false, preferRelink = false, assignToLayerId = null } = {}) {
    const files = Array.from(fileList || []);
    if (!files.length) return;

    setStatus(`Επιλέχθηκαν ${files.length} αρχεία. Γίνεται import...`);
    const created = [];
    let skipped = 0;
    let finalStatus = '';
    for (const file of files) {
      const mediaType = detectMediaType(file) || 'video';
      const existing = preferRelink
        ? state.media.find((item) => item.fileName === file.name && !item.ready)
        : null;

      if (existing) {
        if (existing.element && existing.element.parentNode) existing.element.parentNode.removeChild(existing.element);
        URL.revokeObjectURL(existing.url);
        const replacement = makeMediaRecord(file);
        Object.assign(existing, replacement);
        state.missingMediaNames.delete(file.name);
        created.push(existing);
      } else {
        const record = makeMediaRecord(file);
        state.media.push(record);
        created.push(record);
        renderMediaList();
      }
    }

    if (autoCreateLayers) {
      created.forEach((media, index) => {
        createLayer({
          name: `${media.type === 'video' ? 'Video' : (media.type === 'text' ? 'Text' : (media.type === 'pdf' ? 'PDF' : 'Image'))} ${state.layers.length + 1}`,
          mediaId: media.id,
          output: OUTPUTS[index % OUTPUTS.length]
        });
      });
    }

    if (created.length && !state.selectedMediaId) {
      state.selectedMediaId = created[0].id;
    }

    if (assignToLayerId && created.length) {
      const targetLayer = state.layers.find((item) => item.id === assignToLayerId);
      if (targetLayer) {
        const wasEmpty = !getLayerActiveMediaId(targetLayer);
        created.forEach((media, index) => {
          if (media.type === 'video') {
            addMediaToLayerPlaylist(targetLayer.id, media.id, {
              silent: true,
              skipRender: true,
              makePrimary: wasEmpty && index === 0
            });
          } else if (wasEmpty && index === 0) {
            setLayerPrimaryMedia(targetLayer, media.id);
          }
        });
        if (wasEmpty && created[0]) {
          const first = created[0];
          if (first.ready && first.width && first.height) {
            const ratio = first.width / first.height;
            const targetH = Math.max(120, targetLayer.h || 270);
            let targetW = targetH * ratio;
            if (targetW > state.width * 0.9) targetW = state.width * 0.9;
            targetLayer.w = Math.round(targetW);
            targetLayer.h = Math.round(targetW / ratio);
            setLayerRect(targetLayer, targetLayer.x, targetLayer.y, targetLayer.w, targetLayer.h, targetLayer.rotation || 0);
          }
        }
        state.selectedLayerId = targetLayer.id;
        state.selectedMediaId = created[created.length - 1]?.id || state.selectedMediaId;
        finalStatus = `Το ${targetLayer.name} φόρτωσε ${created.length} νέο${created.length === 1 ? '' : 'α'} media μέσα στο layer.`;
      }
    }

    tryAutoRelinkMissingLayers();
    renderAll();
    if (finalStatus) {
      setStatus(finalStatus);
    } else if (!created.length) {
      setStatus('Δεν πέρασε κανένα αρχείο στο import. Δοκίμασε drag and drop ή άλλο build.');
    } else if (skipped) {
      setStatus(`Φορτώθηκαν ${created.length} media. Αγνοήθηκαν ${skipped} αρχεία.`);
    } else {
      setStatus(`Φορτώθηκαν ${created.length} media.`);
    }
  }

  function createLayer(partial = {}) {
    const nextZ = state.layers.length ? Math.max(...state.layers.map((layer) => layer.z || 0)) + 1 : 1;
    const layer = {
      id: partial.id || uid('layer'),
      name: partial.name || `Layer ${state.layers.length + 1}`,
      mediaId: partial.mediaId || '',
      output: partial.output || 'P1',
      x: partial.x ?? 100,
      y: partial.y ?? 100,
      w: partial.w ?? 480,
      h: partial.h ?? 270,
      opacity: partial.opacity ?? 1,
      rotation: partial.rotation ?? 0,
      visible: partial.visible ?? true,
      locked: partial.locked ?? false,
      loop: partial.loop ?? true,
      sequenceEnabled: !!partial.sequenceEnabled,
      playlistIndex: Math.max(0, Number(partial.playlistIndex || 0)),
      playlistMediaIds: Array.isArray(partial.playlistMediaIds) ? [...partial.playlistMediaIds] : (partial.mediaId ? [partial.mediaId] : []),
      pendingMediaFileName: partial.pendingMediaFileName || '',
      pendingPlaylistFileNames: Array.isArray(partial.pendingPlaylistFileNames) ? [...partial.pendingPlaylistFileNames] : [],
      mappingMode: (partial.mappingMode === 'pin6' || partial.mappingMode === 'pin8') ? partial.mappingMode : 'bbox',
      effectMirrorX: !!partial.effectMirrorX,
      effectMono: !!partial.effectMono,
      effectBoost: !!partial.effectBoost,
      fxBrightness: clamp(Number(partial.fxBrightness ?? 100), 0, 200),
      fxContrast: clamp(Number(partial.fxContrast ?? 100), 0, 220),
      fxSaturation: clamp(Number(partial.fxSaturation ?? 100), 0, 300),
      fxHue: clamp(Number(partial.fxHue ?? 0), -180, 180),
      fxBlur: clamp(Number(partial.fxBlur ?? 0), 0, 20),
      edgeSoftness: clamp(Number(partial.edgeSoftness ?? 0), 0, 35),
      bgRemoveMode: ['off', 'green', 'blue', 'black', 'white', 'custom'].includes(partial.bgRemoveMode) ? partial.bgRemoveMode : 'off',
      bgRemoveColor: /^#[0-9a-fA-F]{6}$/.test(String(partial.bgRemoveColor || '')) ? String(partial.bgRemoveColor) : '#00ff00',
      bgRemoveThreshold: clamp(Number(partial.bgRemoveThreshold ?? 70), 0, 180),
      bgRemoveSoftness: clamp(Number(partial.bgRemoveSoftness ?? 25), 0, 140),
      clipTransitionMode: sanitizeClipTransitionMode(partial.clipTransitionMode || 'cut'),
      clipTransitionMs: sanitizeClipTransitionMs(partial.clipTransitionMs || 0),
      clipTransitionActive: null,
      z: partial.z ?? nextZ,
      corners: cloneCorners(partial.corners || [])
    };
    ensureLayerPlaylist(layer);
    ensureLayerCorners(layer);
    state.layers.push(layer);
    state.selectedLayerId = layer.id;
    normalizeLayerZ();
    renderAll();
    return layer;
  }

  function duplicateSelectedLayer() {
    const selected = getSelectedLayer();
    if (!selected) return;
    createLayer({
      ...selected,
      name: `${selected.name} Copy`,
      x: selected.x + 20,
      y: selected.y + 20,
      z: Math.max(...state.layers.map((layer) => layer.z || 0)) + 1
    });
    setStatus('Έγινε duplicate το layer.');
  }

  function deleteSelectedLayer() {
    const selected = getSelectedLayer();
    if (!selected) return;
    state.layers = state.layers.filter((layer) => layer.id !== selected.id);
    state.selectedLayerId = state.layers[0]?.id || null;
    if (!state.layers.some((layer) => layer.id === state.layerDetailsOpenId)) state.layerDetailsOpenId = state.selectedLayerId;
    normalizeLayerZ();
    renderAll();
    setStatus('Το layer διαγράφηκε.');
  }

  function normalizeLayerZ() {
    state.layers
      .sort((a, b) => (a.z || 0) - (b.z || 0))
      .forEach((layer, index) => { layer.z = index + 1; });
  }

  function bringSelectedFront() {
    const selected = getSelectedLayer();
    if (!selected) return;
    selected.z = Math.max(...state.layers.map((layer) => layer.z || 0)) + 1;
    normalizeLayerZ();
    renderAll();
  }

  function sendSelectedBack() {
    const selected = getSelectedLayer();
    if (!selected) return;
    selected.z = 0;
    normalizeLayerZ();
    renderAll();
  }

  function fitLayerToMedia(mode = 'fit') {
    const layer = getSelectedLayer();
    const media = getLayerActiveMedia(layer);
    if (!layer || !media || !media.ready || !media.width || !media.height) return;
    const mediaRatio = media.width / media.height;
    const targetRatio = state.width / state.height;
    let w = state.width * 0.8;
    let h = state.height * 0.8;

    if (mode === 'fill') {
      if (mediaRatio > targetRatio) {
        h = state.height;
        w = h * mediaRatio;
      } else {
        w = state.width;
        h = w / mediaRatio;
      }
    } else {
      w = state.width * 0.6;
      h = w / mediaRatio;
      if (h > state.height * 0.8) {
        h = state.height * 0.8;
        w = h * mediaRatio;
      }
    }

    setLayerRect(layer, Math.round((state.width - w) / 2), Math.round((state.height - h) / 2), Math.round(w), Math.round(h), 0);
    renderAll();
  }

  function resetSelectedLayerTransform() {
    const layer = getSelectedLayer();
    if (!layer) return;
    layer.opacity = 1;
    layer.visible = true;
    layer.locked = false;
    const media = getLayerActiveMedia(layer);
    if (media?.ready && media.width && media.height) {
      layer.rotation = 0;
      fitLayerToMedia('fit');
      return;
    }
    setLayerRect(layer, 100, 100, 480, 270, 0);
    renderAll();
  }

  function assignSelectedLayerToOutput(outputId) {
    const layer = getSelectedLayer();
    if (!layer) {
      setStatus('Διάλεξε πρώτα layer.');
      return;
    }
    if (!OUTPUTS.includes(outputId)) return;
    layer.output = outputId;
    renderAll();
    setStatus(`Το ${layer.name} πήγε στο ${outputId}.`);
  }


  function getLayerMediaElement(layer) {
    const media = getLayerActiveMedia(layer);
    return media && media.type === 'video' ? media.element : null;
  }

  function syncLayerLoopToMedia(layer) {
    const mediaEl = getLayerMediaElement(layer);
    if (mediaEl) mediaEl.loop = !layer.sequenceEnabled && !!layer.loop;
  }

  function playLayerMedia(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    const mediaEl = getLayerMediaElement(layer);
    if (!layer || !mediaEl) return;
    syncLayerLoopToMedia(layer);
    const p = mediaEl.play();
    if (p && typeof p.catch === 'function') p.catch(() => {});
    state.playing = true;
    setStatus(`Play στο ${layer.name}.`);
    renderLayersList();
  }

  function pauseLayerMedia(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    const mediaEl = getLayerMediaElement(layer);
    if (!layer || !mediaEl) return;
    mediaEl.pause();
    setStatus(`Pause στο ${layer.name}.`);
    renderLayersList();
  }

  function stopLayerMedia(layerId, options = {}) {
    const layer = state.layers.find((item) => item.id === layerId);
    const mediaEl = getLayerMediaElement(layer);
    if (!layer) return false;
    if (mediaEl) {
      mediaEl.pause();
      try { mediaEl.currentTime = 0; } catch (_) {}
    }
    if (options.resetSequence) {
      ensureLayerPlaylist(layer);
      layer.playlistIndex = 0;
      layer.mediaId = layer.playlistMediaIds[0] || layer.mediaId || '';
    }
    if (!options.silent) setStatus(`Stop στο ${layer.name}.`);
    renderAll();
    return true;
  }

  function clearLayerPlaylist(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return false;
    const mediaEl = getLayerMediaElement(layer);
    if (mediaEl) mediaEl.pause();
    layer.playlistMediaIds = [];
    layer.pendingPlaylistFileNames = [];
    layer.playlistIndex = 0;
    layer.mediaId = '';
    layer.pendingMediaFileName = '';
    layer.sequenceEnabled = false;
    layer.clipTransitionActive = null;
    renderAll();
    setStatus(`${layer.name}: καθάρισε η λίστα clips.`);
    return true;
  }

  function getLayerPlaybackSummary(layer) {
    if (!layer) return 'No media';
    ensureLayerPlaylist(layer);
    const count = layer.playlistMediaIds.length || (layer.mediaId ? 1 : 0);
    const current = count ? Math.max(1, Math.min(count, Number(layer.playlistIndex || 0) + 1)) : 0;
    const parts = [`${count} clip${count === 1 ? '' : 's'}`];
    if (count) parts.push(`active ${current}/${count}`);
    parts.push(layer.sequenceEnabled ? 'Auto sequence ON' : 'Manual');
    parts.push(layer.loop ? 'Loop ON' : 'Loop OFF');
    return parts.join(' · ');
  }

  function toggleLayerLoop(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return;
    layer.loop = !layer.loop;
    syncLayerLoopToMedia(layer);
    setStatus(`${layer.name}: loop ${layer.loop ? 'ON' : 'OFF'}.`);
    renderAll();
  }

  function toggleLayerVisible(layerId) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return;
    layer.visible = !layer.visible;
    setStatus(`${layer.name}: ${layer.visible ? 'Visible' : 'Hidden'}.`);
    renderAll();
  }

  function playAllVideos() {
    state.playing = true;
    state.layers.forEach((layer) => {
      const mediaEl = getLayerMediaElement(layer);
      if (mediaEl) {
        syncLayerLoopToMedia(layer);
        try { mediaEl.currentTime = mediaEl.currentTime || 0; } catch (_) {}
        const p = mediaEl.play();
        if (p && typeof p.catch === 'function') p.catch(() => {});
      }
    });
    setStatus('Play όλων των video.');
    renderLayersList();
  }

  function pauseAllVideos() {
    state.playing = false;
    state.layers.forEach((layer) => {
      const mediaEl = getLayerMediaElement(layer);
      if (mediaEl) mediaEl.pause();
    });
    setStatus('Pause όλων των video.');
    renderLayersList();
  }

  function renderMediaList() {
    el.mediaCount.textContent = String(state.media.length);
    applyMediaPanelState();
    if (state.mediaLibraryCollapsed) {
      el.mediaList.className = 'list collapsed-hint';
      el.mediaList.innerHTML = '<div class="panel-collapsed-note">Η library είναι κλειστή. Χρησιμοποίησε το ➕ μέσα στα layers ή άνοιξέ την από το 📂.</div>';
      return;
    }
    if (!state.media.length) {
      el.mediaList.className = 'list empty';
      el.mediaList.textContent = 'Φόρτωσε video ή εικόνες.';
      return;
    }

    el.mediaList.className = 'list';
    el.mediaList.innerHTML = '';
    state.media.forEach((item) => {
      const div = document.createElement('div');
      div.className = `media-item${item.id === state.selectedMediaId ? ' selected' : ''}`;
      const status = item.error ? 'error' : item.ready ? 'ready' : 'loading';
      const thumb = item.thumbUrl
        ? `<img class="media-thumb-img" src="${item.thumbUrl}" alt="${escapeHtml(item.name)}" />`
        : `<div class="media-thumb-placeholder">${getMediaPlaceholderIcon(item)}</div>`;
      const mediaOverlay = isCameraMedia(item)
        ? '<span class="thumb-overlay-badge image">📷</span>'
        : (isScreenMedia(item)
          ? '<span class="thumb-overlay-badge image">🖥</span>'
          : (item.type === 'video' ? '<span class="thumb-overlay-badge">▶</span>' : (item.type === 'text' ? '<span class="thumb-overlay-badge image">✎</span>' : (item.type === 'pdf' ? '<span class="thumb-overlay-badge image">📄</span>' : '<span class="thumb-overlay-badge image">🖼</span>'))));
      div.innerHTML = `
        <div class="media-item-layout">
          <div class="media-thumb">${thumb}${mediaOverlay}<span class="media-type-chip">${getMediaChipLabel(item)}</span></div>
          <div class="media-item-meta">
            <div class="media-item-row">
              <div class="media-item-title">${escapeHtml(item.name)}</div>
              <button class="icon-btn danger media-delete-btn" data-action="delete-media" title="Delete media">✕</button>
            </div>
            <div class="media-item-sub">${isCameraMedia(item) ? 'CAMERA' : (isScreenMedia(item) ? 'SCREEN' : item.type.toUpperCase())} · ${(isLiveInputMedia(item)) ? 'live' : status}${item.width ? ` · ${item.width}×${item.height}` : ''}${item.type === 'pdf' ? ' · placeholder preview' : ''}${item.error ? ` · ${escapeHtml(item.error)}` : ''}</div>
          </div>
        </div>
      `;
      div.addEventListener('click', () => {
        state.selectedMediaId = item.id;
        const selectedLayer = getSelectedLayer();
        if (selectedLayer) {
          const wasEmpty = !getLayerActiveMediaId(selectedLayer);
          setLayerPrimaryMedia(selectedLayer, item.id);
          if (wasEmpty) {
            const media = getMediaById(item.id);
            if (media?.ready && media.width && media.height) {
              const ratio = media.width / media.height;
              const targetH = Math.max(120, selectedLayer.h || 270);
              let targetW = targetH * ratio;
              if (targetW > state.width * 0.9) {
                targetW = state.width * 0.9;
              }
              selectedLayer.w = Math.round(targetW);
              selectedLayer.h = Math.round(targetW / ratio);
            }
          }
          renderAll();
          setStatus(`Το media μπήκε στο ${selectedLayer.name}. Για σειρά βάλε επιπλέον clips με το ➕ στο layer.`);
          return;
        }
        renderMediaList();
      });
      div.addEventListener('dblclick', () => {
        if (item.type === 'pdf' && item.url) {
          try { window.open(item.url, '_blank', 'noopener'); } catch (_) {}
          setStatus(`Άνοιγμα PDF: ${item.name}`);
          return;
        }
        const selected = getSelectedLayer();
        if (selected) {
          setLayerPrimaryMedia(selected, item.id);
          fitLayerToMedia();
          renderAll();
        }
      });
      const deleteBtn = div.querySelector('[data-action="delete-media"]');
      if (deleteBtn) {
        deleteBtn.addEventListener('click', (event) => {
          event.stopPropagation();
          deleteMediaById(item.id);
        });
      }
      el.mediaList.appendChild(div);
    });
  }

  function renderLayersList() {
    el.layerCount.textContent = String(state.layers.length);
    if (!state.layers.length) {
      el.layersList.className = 'list empty';
      el.layersList.textContent = 'Δεν υπάρχουν layers.';
      return;
    }

    el.layersList.className = 'list';
    el.layersList.innerHTML = '';
    getRenderableLayers('ALL').slice().reverse().forEach((layer) => {
      ensureLayerPlaylist(layer);
      const media = getLayerActiveMedia(layer);
      const mediaEl = getLayerMediaElement(layer);
      const isPlaying = !!(mediaEl && !mediaEl.paused && !mediaEl.ended);
      const playlistCount = layer.playlistMediaIds.length || (media ? 1 : 0);
      const clipFxLabel = getLayerClipTransitionLabel(layer);
      const clipFxMs = getLayerClipTransitionMs(layer);
      const thumb = media?.thumbUrl
        ? `<img class="layer-thumb-img" src="${media.thumbUrl}" alt="${escapeHtml(media.name)}" />`
        : `<div class="layer-thumb-placeholder">${media ? getMediaPlaceholderIcon(media) : 'L'}</div>`;
      const layerOverlay = isCameraMedia(media) ? '<span class="thumb-overlay-badge image">📷</span>' : (isScreenMedia(media) ? '<span class="thumb-overlay-badge image">🖥</span>' : (media?.type === 'video' ? '<span class="thumb-overlay-badge">▶</span>' : (media?.type === 'image' ? '<span class="thumb-overlay-badge image">🖼</span>' : (media?.type === 'text' ? '<span class="thumb-overlay-badge image">✎</span>' : (media?.type === 'pdf' ? '<span class="thumb-overlay-badge image">📄</span>' : '')))));
      const playlistItems = ensureLayerPlaylist(layer).map((playlistMediaId, idx) => {
        const playlistMedia = getMediaById(playlistMediaId);
        const activeClass = playlistMediaId === layer.mediaId ? ' active' : '';
        const pThumb = playlistMedia?.thumbUrl
          ? `<img class="playlist-thumb-img" src="${playlistMedia.thumbUrl}" alt="${escapeHtml(playlistMedia?.name || 'clip')}" />`
          : `<div class="playlist-thumb-placeholder">${playlistMedia ? getMediaPlaceholderIcon(playlistMedia) : '◻'}</div>`;
        return `
          <div class="playlist-entry${activeClass}" draggable="true" data-action="playlist-drag" data-index="${idx}" title="Σύρε για αλλαγή σειράς">
            <button class="playlist-thumb" data-action="playlist-item" data-media-id="${playlistMediaId}" data-index="${idx}" title="${escapeHtml(playlistMedia?.name || `Clip ${idx + 1}`)}">${pThumb}<span class="playlist-index">${idx + 1}</span></button>
            <button class="playlist-shift left" data-action="playlist-left" data-index="${idx}" title="Πιο πριν στη σειρά">‹</button>
            <button class="playlist-shift right" data-action="playlist-right" data-index="${idx}" title="Πιο μετά στη σειρά">›</button>
            <button class="playlist-delete" data-action="playlist-remove" data-index="${idx}" title="Remove clip">✕</button>
          </div>`;
      }).join('');
      const playlistRows = ensureLayerPlaylist(layer).map((playlistMediaId, idx) => {
        const playlistMedia = getMediaById(playlistMediaId);
        const active = playlistMediaId === layer.mediaId;
        return `
          <div class="playlist-row${active ? ' active' : ''}">
            <button class="playlist-row-main" data-action="playlist-item" data-media-id="${playlistMediaId}" data-index="${idx}" title="Άνοιγμα clip">
              <span class="playlist-row-index">${idx + 1}</span>
              <span class="playlist-row-name">${escapeHtml(playlistMedia?.name || `Clip ${idx + 1}`)}</span>
              <span class="playlist-row-meta">${active ? 'ACTIVE' : 'READY'}</span>
            </button>
            <div class="playlist-row-actions">
              <button class="icon-btn slim" data-action="playlist-left" data-index="${idx}" title="Πιο πριν">‹</button>
              <button class="icon-btn slim" data-action="playlist-right" data-index="${idx}" title="Πιο μετά">›</button>
              <button class="icon-btn slim danger" data-action="playlist-remove" data-index="${idx}" title="Διαγραφή clip">✕</button>
            </div>
          </div>`;
      }).join('');
      const playbackSummary = getLayerPlaybackSummary(layer);
      const detailsOpen = layer.id === state.layerDetailsOpenId;
      const div = document.createElement('div');
      div.className = `layer-item${layer.id === state.selectedLayerId ? ' selected' : ''}${detailsOpen ? ' details-open' : ''}`;
      div.innerHTML = `
        <div class="layer-item-layout compact">
          <div class="layer-thumb-wrap">
            <div class="layer-thumb">${thumb}${layerOverlay}<span class="layer-thumb-chip">${layer.output}</span></div>
          </div>
          <div class="layer-item-main">
            <div class="layer-item-row">
              <div class="layer-item-title">${escapeHtml(layer.name)}</div>
              <div class="layer-item-badges"><div class="pill">Z${layer.z || 1}</div><button class="icon-btn slim${detailsOpen ? ' active' : ''}" data-action="toggle-details" title="Άνοιγμα / κλείσιμο layer panel">${detailsOpen ? '▾' : '▸'}</button></div>
            </div>
            <div class="layer-item-sub">${media ? escapeHtml(media.name) : (layer.pendingMediaFileName ? `Missing: ${escapeHtml(layer.pendingMediaFileName)}` : 'No media')} · ${Math.round(layer.x)},${Math.round(layer.y)} · ${Math.round(layer.w)}×${Math.round(layer.h)} · rot ${Math.round(layer.rotation || 0)}°</div>
            <div class="layer-item-sub">${playbackSummary}${clipFxLabel !== 'CUT' || clipFxMs ? ` · FX ${clipFxLabel}${clipFxMs ? ` ${clipFxMs}ms` : ''}` : ''}${isLayerPin6Mode(layer) ? ' · 6-point' : ''}${getLayerEffectSummary(layer) ? ` · ${getLayerEffectSummary(layer)}` : ''}</div>
            <div class="layer-item-tools compact-tools">
              <button class="icon-btn" data-action="play" title="Play/Pause">${isPlaying ? '⏸' : '▶'}</button>
              <button class="icon-btn" data-action="import" title="Import video directly into this layer">➕</button>
              <button class="icon-btn" data-action="queue" title="Add selected media from library">📚</button>
              <button class="icon-btn" data-action="nextclip" title="Next clip">⏭</button>
              <button class="icon-btn${layer.visible ? ' active' : ''}" data-action="visible" title="Show/Hide">${layer.visible ? '◉' : '○'}</button>
              <button class="icon-btn${layer.locked ? ' active' : ''}" data-action="lock" title="Lock">${layer.locked ? '🔒' : '🔓'}</button>
              <button class="icon-btn danger" data-action="delete-layer" title="Delete layer">✕</button>
            </div>
          </div>
        </div>
        <div class="layer-details-panel${detailsOpen ? ' open' : ''}">
          <div class="layer-details-grid">
            <div class="layer-detail-section layer-monitor-section">
              <div class="layer-detail-title">Preview monitor</div>
              <div class="layer-monitor-wrap">
                <canvas class="layer-monitor-canvas" width="320" height="180" data-layer-monitor="${layer.id}"></canvas>
              </div>
              <div class="layer-monitor-meta">Active preview του layer με loop / autoplay κατάσταση και γρήγορη εικόνα του current clip.</div>
            </div>
            <div class="layer-detail-section">
              <div class="layer-detail-title">Clips</div>
              <div class="layer-playlist-strip" data-layer-playlist="${layer.id}">${playlistItems || '<div class="playlist-empty">Το layer δεν έχει clips ακόμα.</div>'}</div>
              <div class="playlist-rows">${playlistRows || '<div class="playlist-empty">Πρόσθεσε videos για να φανεί η λίστα του layer.</div>'}</div>
            </div>
            <div class="layer-detail-section">
              <div class="layer-detail-title">Layer services</div>
              <div class="layer-service-summary">${playbackSummary}</div>
              <div class="layer-item-tools detail-tools">
                <button class="icon-btn wide" data-action="play-current" title="Play active clip">▶ Clip</button>
                <button class="icon-btn wide" data-action="pause" title="Pause active clip">⏸ Pause</button>
                <button class="icon-btn wide" data-action="stop" title="Stop active clip">⏹ Stop</button>
                <button class="icon-btn wide" data-action="prevclip" title="Previous clip">⏮ Prev</button>
                <button class="icon-btn wide" data-action="nextclip" title="Next clip">⏭ Next</button>
                <button class="icon-btn wide" data-action="play-sequence" title="Ξεκίνα όλα τα clips από την αρχή">▶🎞 Auto</button>
                <button class="icon-btn${layer.sequenceEnabled ? ' active' : ''}" data-action="sequence" title="Auto sequence ON/OFF">🎞</button>
                <button class="icon-btn${layer.loop ? ' active' : ''}" data-action="loop" title="Loop">↻</button>
                <button class="icon-btn wide${clipFxLabel !== 'CUT' ? ' active' : ''}" data-action="clipfx-mode" title="Clip transition mode">🎬 ${clipFxLabel}</button>
                <button class="icon-btn${getLayerFxTarget(layer)?.effectMirrorX ? ' active' : ''}" data-action="fx-mirror" title="Mirror horizontal">⇋</button>
                <button class="icon-btn${getLayerFxTarget(layer)?.effectMono ? ' active' : ''}" data-action="fx-mono" title="Black and white">◐</button>
                <button class="icon-btn${getLayerFxTarget(layer)?.effectBoost ? ' active' : ''}" data-action="fx-boost" title="Boost colors">✨</button>
                <button class="icon-btn${isLayerFreeformMode(layer) ? ' active' : ''}" data-action="mapping-plus" title="Add mapping dots / freeform mode">＋•</button>
                <button class="icon-btn${!isLayerFreeformMode(layer) ? ' active' : ''}" data-action="mapping-minus" title="Back to resize frame mode">－□</button>
                <button class="icon-btn wide danger" data-action="clear-layer-clips" title="Clear clips from layer">🗑 Clear</button>
              </div>
              <div class="clipfx-row">
                <span class="clipfx-label">🎚 διάρκεια</span>
                <input class="clipfx-range" type="range" min="0" max="2000" step="50" value="${clipFxMs}" data-range-action="clipfx-ms" title="Διάρκεια transition" />
                <span class="clipfx-value">${clipFxMs}ms</span>
              </div>
              <div class="layer-details-hint">Κάθε layer τώρα έχει δικά του clip services: λίστα video, auto σειρά, loop, delete, clear και transport controls.</div>
            </div>
          </div>
        </div>
      `;
      div.addEventListener('click', () => {
        state.selectedLayerId = layer.id;
        renderAll();
      });
      div.querySelectorAll('[data-action]').forEach((btn) => {
        btn.addEventListener('click', (event) => {
          event.stopPropagation();
          state.selectedLayerId = layer.id;
          const action = btn.dataset.action;
          if (action === 'toggle-details') {
            state.layerDetailsOpenId = state.layerDetailsOpenId === layer.id ? null : layer.id;
            saveUiPrefs();
            renderAll();
            setStatus(state.layerDetailsOpenId === layer.id ? `${layer.name}: άνοιξε το layer panel.` : `${layer.name}: έκλεισε το layer panel.`);
            return;
          }
          if (action === 'play') {
            if (isPlaying) pauseLayerMedia(layer.id);
            else playLayerMedia(layer.id);
          } else if (action === 'play-current') {
            playLayerMedia(layer.id);
          } else if (action === 'pause') {
            pauseLayerMedia(layer.id);
          } else if (action === 'stop') {
            stopLayerMedia(layer.id, { resetSequence: false });
          } else if (action === 'prevclip') {
            stepLayerPlaylist(layer.id, -1);
          } else if (action === 'play-sequence') {
            startLayerSequencePlayback(layer.id, { restart: true });
          } else if (action === 'import') {
            importFilesIntoLayer(layer.id);
          } else if (action === 'queue') {
            addMediaToLayerPlaylist(layer.id, state.selectedMediaId);
          } else if (action === 'playlist-item') {
            setLayerPlaylistPrimary(layer.id, btn.dataset.mediaId);
          } else if (action === 'playlist-remove') {
            removeMediaFromLayerPlaylist(layer.id, Number(btn.dataset.index));
          } else if (action === 'playlist-left') {
            reorderLayerPlaylist(layer.id, Number(btn.dataset.index), Math.max(0, Number(btn.dataset.index) - 1));
          } else if (action === 'playlist-right') {
            reorderLayerPlaylist(layer.id, Number(btn.dataset.index), Math.min(ensureLayerPlaylist(layer).length - 1, Number(btn.dataset.index) + 1));
          } else if (action === 'sequence') {
            toggleLayerSequence(layer.id);
          } else if (action === 'clipfx-mode') {
            cycleLayerClipTransitionMode(layer.id);
          } else if (action === 'nextclip') {
            stepLayerPlaylist(layer.id, 1);
          } else if (action === 'loop') {
            toggleLayerLoop(layer.id);
          } else if (action === 'fx-mirror') {
            toggleLayerEffect(layer.id, 'effectMirrorX');
          } else if (action === 'fx-mono') {
            toggleLayerEffect(layer.id, 'effectMono');
          } else if (action === 'fx-boost') {
            toggleLayerEffect(layer.id, 'effectBoost');
          } else if (action === 'pin8' || action === 'pin6' || action === 'mapping-plus') {
            layer.mappingMode = 'pin8';
            state.activeHandleName = null;
            renderAll();
            setStatus(`${layer.name}: 8-point mapping ON.`);
          } else if (action === 'mapping-minus') {
            layer.mappingMode = 'bbox';
            state.activeHandleName = null;
            renderAll();
            setStatus(`${layer.name}: bbox resize ON.`);
          } else if (action === 'clear-layer-clips') {
            clearLayerPlaylist(layer.id);
          } else if (action === 'visible') {
            toggleLayerVisible(layer.id);
          } else if (action === 'lock') {
            layer.locked = !layer.locked;
            setStatus(`${layer.name}: ${layer.locked ? 'Lock' : 'Unlock'}.`);
            renderAll();
          } else if (action === 'delete-layer') {
            state.selectedLayerId = layer.id;
            deleteSelectedLayer();
          }
        });
      });
      div.querySelectorAll('[data-range-action="clipfx-ms"]').forEach((range) => {
        const valueEl = div.querySelector('.clipfx-value');
        range.addEventListener('input', (event) => {
          event.stopPropagation();
          const value = Number(range.value || 0);
          if (valueEl) valueEl.textContent = `${value}ms`;
        });
        range.addEventListener('change', (event) => {
          event.stopPropagation();
          state.selectedLayerId = layer.id;
          setLayerClipTransitionMs(layer.id, Number(range.value || 0));
        });
        range.addEventListener('click', (event) => event.stopPropagation());
      });
      div.querySelectorAll('[data-action="playlist-drag"]').forEach((entry) => {
        entry.addEventListener('dragstart', (event) => {
          state.selectedLayerId = layer.id;
          entry.classList.add('dragging');
          event.dataTransfer.effectAllowed = 'move';
          event.dataTransfer.setData('text/plain', JSON.stringify({ layerId: layer.id, fromIndex: Number(entry.dataset.index) }));
        });
        entry.addEventListener('dragend', () => entry.classList.remove('dragging'));
        entry.addEventListener('dragover', (event) => {
          event.preventDefault();
          entry.classList.add('drag-over');
          if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
        });
        entry.addEventListener('dragleave', () => entry.classList.remove('drag-over'));
        entry.addEventListener('drop', (event) => {
          event.preventDefault();
          event.stopPropagation();
          entry.classList.remove('drag-over');
          try {
            const payload = JSON.parse(event.dataTransfer.getData('text/plain') || '{}');
            if (payload.layerId === layer.id) reorderLayerPlaylist(layer.id, payload.fromIndex, Number(entry.dataset.index));
          } catch (_) {}
        });
      });
      el.layersList.appendChild(div);
    });
  }

  function updateFxValueChips(target) {
    if (!target) return;
    if (el.layerBrightnessValue) el.layerBrightnessValue.textContent = `${Math.round(Number(target.fxBrightness ?? 100))}%`;
    if (el.layerContrastValue) el.layerContrastValue.textContent = `${Math.round(Number(target.fxContrast ?? 100))}%`;
    if (el.layerSaturationValue) el.layerSaturationValue.textContent = `${Math.round(Number(target.fxSaturation ?? 100))}%`;
    if (el.layerHueValue) el.layerHueValue.textContent = `${Math.round(Number(target.fxHue ?? 0))}°`;
    if (el.layerBlurValue) el.layerBlurValue.textContent = `${Number(target.fxBlur ?? 0).toFixed(Number(target.fxBlur ?? 0) % 1 ? 1 : 0)}px`;
    if (el.layerEdgeSoftnessValue) el.layerEdgeSoftnessValue.textContent = `${Math.round(Number(target.edgeSoftness ?? 0))}%`;
    if (el.layerBgThresholdValue) el.layerBgThresholdValue.textContent = String(Math.round(Number(target.bgRemoveThreshold ?? 70)));
    if (el.layerBgSoftnessValue) el.layerBgSoftnessValue.textContent = String(Math.round(Number(target.bgRemoveSoftness ?? 25)));
    if (el.layerMediaZoomValue) el.layerMediaZoomValue.textContent = `${Math.round(Number(target.mediaZoom ?? 100))}%`;
    if (el.fxTargetHint) el.fxTargetHint.textContent = `Editing current video: ${getLayerFxTargetLabel(getSelectedLayer())}`;
  }

  function getBgRemovePresetColor(mode) {
    if (mode === 'green') return '#00ff00';
    if (mode === 'blue') return '#1f6dff';
    if (mode === 'black') return '#000000';
    if (mode === 'white') return '#ffffff';
    return '#00ff00';
  }

  function getLayerKeyColor(layer, media = null) {
    const target = getLayerFxTarget(layer, media);
    const mode = ['off', 'green', 'blue', 'black', 'white', 'custom'].includes(target?.bgRemoveMode) ? target.bgRemoveMode : 'off';
    if (mode === 'custom') return /^#[0-9a-fA-F]{6}$/.test(String(target?.bgRemoveColor || '')) ? String(target.bgRemoveColor) : '#00ff00';
    if (mode === 'off') return '#00ff00';
    return getBgRemovePresetColor(mode);
  }

  function isLayerBackgroundRemoveActive(layer, media = null) {
    const target = getLayerFxTarget(layer, media);
    return !!target && ['green', 'blue', 'black', 'white', 'custom'].includes(String(target.bgRemoveMode || 'off'));
  }

  function layerHasToneFx(layer, media = null) {
    const target = getLayerFxTarget(layer, media);
    if (!target) return false;
    return Math.round(Number(target.fxBrightness ?? 100)) !== 100
      || Math.round(Number(target.fxContrast ?? 100)) !== 100
      || Math.round(Number(target.fxSaturation ?? 100)) !== 100
      || Math.round(Number(target.fxHue ?? 0)) !== 0
      || Math.abs(Number(target.fxBlur ?? 0)) > 0.001;
  }

  function shouldUseProcessedLayerCanvas(layer, media = null) {
    const activeMedia = media || getLayerActiveMedia(layer);
    const target = getLayerFxTarget(layer, activeMedia);
    return !!layer && !!target && (Number(target.edgeSoftness ?? 0) > 0 || isLayerBackgroundRemoveActive(layer, activeMedia) || mediaHasCameraFx(activeMedia));
  }

  function getLayerFxCache(layer) {
    if (!layer) return null;
    let meta = state.layerFxCache.get(layer);
    if (!meta) {
      meta = { canvas: document.createElement('canvas'), signature: '' };
      state.layerFxCache.set(layer, meta);
    }
    return meta;
  }

  function getProcessedLayerSignature(layer, media, element, width, height) {
    const sourceStamp = element?.currentTime != null ? Number(element.currentTime || 0).toFixed(2) : `${element?.naturalWidth || element?.width || 0}x${element?.naturalHeight || element?.height || 0}`;
    return [
      width,
      height,
      sourceStamp,
      media?.id || '',
      getLayerFilterString(layer, media),
      Number(getLayerFxTarget(layer, media)?.edgeSoftness ?? 0).toFixed(2),
      String(getLayerFxTarget(layer, media)?.bgRemoveMode || 'off'),
      getLayerKeyColor(layer, media),
      Number(getLayerFxTarget(layer, media)?.bgRemoveThreshold ?? 70).toFixed(2),
      Number(getLayerFxTarget(layer, media)?.bgRemoveSoftness ?? 25).toFixed(2)
    ].join('|');
  }

  function hexToRgb(hex) {
    const safe = String(hex || '').trim();
    const match = /^#?([0-9a-fA-F]{6})$/.exec(safe);
    if (!match) return { r: 0, g: 255, b: 0 };
    const raw = match[1];
    return { r: parseInt(raw.slice(0, 2), 16), g: parseInt(raw.slice(2, 4), 16), b: parseInt(raw.slice(4, 6), 16) };
  }

  function applyColorKeyToCanvas(ctx, width, height, keyHex, threshold, softness) {
    const image = ctx.getImageData(0, 0, width, height);
    const data = image.data;
    const key = hexToRgb(keyHex);
    const start = Math.max(0, Number(threshold || 0));
    const fade = Math.max(0, Number(softness || 0));
    const end = start + fade;
    for (let i = 0; i < data.length; i += 4) {
      const dr = data[i] - key.r;
      const dg = data[i + 1] - key.g;
      const db = data[i + 2] - key.b;
      const dist = Math.sqrt((dr * dr) + (dg * dg) + (db * db));
      let alphaFactor = 1;
      if (dist <= start) alphaFactor = 0;
      else if (dist < end && fade > 0) alphaFactor = (dist - start) / fade;
      data[i + 3] = Math.round(data[i + 3] * alphaFactor);
    }
    ctx.putImageData(image, 0, 0);
  }

  function applySoftEdgesToCanvas(ctx, width, height, softnessPercent) {
    const percent = clamp(Number(softnessPercent || 0), 0, 35);
    if (!percent) return;
    const feather = Math.max(1, Math.round(Math.min(width, height) * (percent / 100)));
    const image = ctx.getImageData(0, 0, width, height);
    const data = image.data;
    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        const i = ((y * width) + x) * 4;
        const dist = Math.min(x, y, (width - 1) - x, (height - 1) - y);
        const alphaFactor = clamp(dist / feather, 0, 1);
        data[i + 3] = Math.round(data[i + 3] * alphaFactor);
      }
    }
    ctx.putImageData(image, 0, 0);
  }

  function buildProcessedLayerCanvas(layer, media, element) {
    const sourceW = element.videoWidth || element.naturalWidth || element.width || Math.max(1, Math.round(layer.w || 1));
    const sourceH = element.videoHeight || element.naturalHeight || element.height || Math.max(1, Math.round(layer.h || 1));
    const targetW = clamp(Math.round(layer.w || sourceW), 64, Math.max(64, state.width));
    const targetH = clamp(Math.round(layer.h || sourceH), 64, Math.max(64, state.height));
    const cache = getLayerFxCache(layer);
    if (!cache) return element;
    const signature = getProcessedLayerSignature(layer, media, element, targetW, targetH);
    if (cache.signature === signature && cache.canvas.width === targetW && cache.canvas.height === targetH) return cache.canvas;
    cache.canvas.width = targetW;
    cache.canvas.height = targetH;
    const fxCtx = cache.canvas.getContext('2d', { willReadFrequently: true });
    fxCtx.clearRect(0, 0, targetW, targetH);
    fxCtx.save();
    fxCtx.filter = getLayerFilterString(layer, media);
    fxCtx.drawImage(element, 0, 0, targetW, targetH);
    fxCtx.restore();
    const fxTarget = getLayerFxTarget(layer, media);
    if (isLayerBackgroundRemoveActive(layer, media)) {
      applyColorKeyToCanvas(fxCtx, targetW, targetH, getLayerKeyColor(layer, media), Number(fxTarget?.bgRemoveThreshold ?? 70), Number(fxTarget?.bgRemoveSoftness ?? 25));
    }
    if (Number(fxTarget?.edgeSoftness ?? 0) > 0) {
      applySoftEdgesToCanvas(fxCtx, targetW, targetH, Number(fxTarget?.edgeSoftness || 0));
    }
    cache.signature = signature;
    return cache.canvas;
  }

  function resetSelectedLayerFx() {
    const layer = getSelectedLayer();
    if (!layer) return;
    const target = getLayerFxTarget(layer);
    if (!target) return;
    applyDefaultClipFx(target);
    renderInspector();
    renderAll();
    setStatus(`${getLayerFxTargetLabel(layer)}: FX reset.`);
  }

  function renderInspector() {
    const layer = getSelectedLayer();
    el.layerMediaInput.innerHTML = '<option value="">-- No media --</option>';
    state.media.forEach((item) => {
      const option = document.createElement('option');
      option.value = item.id;
      option.textContent = item.name;
      el.layerMediaInput.appendChild(option);
    });

    if (!layer) {
      el.inspectorEmpty.classList.remove('hidden');
      el.inspectorForm.classList.add('hidden');
      if (el.cameraFxPanel) el.cameraFxPanel.classList.add('hidden');
      if (el.textMediaPanel) el.textMediaPanel.classList.add('hidden');
      return;
    }

    el.inspectorEmpty.classList.add('hidden');
    el.inspectorForm.classList.remove('hidden');
    el.layerNameInput.value = layer.name;
    el.layerOutputInput.value = layer.output;
    el.layerMediaInput.value = getLayerActiveMediaId(layer) || '';
    el.layerXInput.value = Math.round(layer.x);
    el.layerYInput.value = Math.round(layer.y);
    el.layerWInput.value = Math.round(layer.w);
    el.layerHInput.value = Math.round(layer.h);
    el.layerOpacityInput.value = String(layer.opacity);
    el.layerRotationInput.value = String(layer.rotation || 0);
    el.layerZInput.value = String(layer.z || 1);
    el.layerVisibleInput.checked = !!layer.visible;
    el.layerLockInput.checked = !!layer.locked;
    el.layerLoopInput.checked = !!layer.loop;
    const fxTarget = getLayerFxTarget(layer);
    if (el.layerBrightnessInput) el.layerBrightnessInput.value = String(clamp(Number(fxTarget?.fxBrightness ?? 100), 0, 200));
    if (el.layerContrastInput) el.layerContrastInput.value = String(clamp(Number(fxTarget?.fxContrast ?? 100), 0, 220));
    if (el.layerSaturationInput) el.layerSaturationInput.value = String(clamp(Number(fxTarget?.fxSaturation ?? 100), 0, 300));
    if (el.layerHueInput) el.layerHueInput.value = String(clamp(Number(fxTarget?.fxHue ?? 0), -180, 180));
    if (el.layerBlurInput) el.layerBlurInput.value = String(clamp(Number(fxTarget?.fxBlur ?? 0), 0, 20));
    if (el.layerEdgeSoftnessInput) el.layerEdgeSoftnessInput.value = String(clamp(Number(fxTarget?.edgeSoftness ?? 0), 0, 35));
    if (el.layerBgRemoveModeInput) el.layerBgRemoveModeInput.value = ['off', 'green', 'blue', 'black', 'white', 'custom'].includes(fxTarget?.bgRemoveMode) ? fxTarget.bgRemoveMode : 'off';
    if (el.layerBgRemoveColorInput) el.layerBgRemoveColorInput.value = getLayerKeyColor(layer);
    if (el.layerBgThresholdInput) el.layerBgThresholdInput.value = String(clamp(Number(fxTarget?.bgRemoveThreshold ?? 70), 0, 180));
    if (el.layerBgSoftnessInput) el.layerBgSoftnessInput.value = String(clamp(Number(fxTarget?.bgRemoveSoftness ?? 25), 0, 140));
    if (el.layerMediaZoomInput) el.layerMediaZoomInput.value = String(clamp(Number(fxTarget?.mediaZoom ?? 100), 100, 300));
    updateFxValueChips(fxTarget || layer);
    el.handleNudgeBtns.forEach((btn) => {
      const on = state.activeHandleName === btn.dataset.handleSelect;
      btn.classList.toggle('active', on);
    });

    const media = getLayerActiveMedia(layer);
    const showCameraFx = isCameraMedia(media);
    if (el.cameraFxPanel) el.cameraFxPanel.classList.toggle('hidden', !showCameraFx);
    const showVideoZoom = !!(media && media.type === 'video');
    if (el.videoZoomPanel) el.videoZoomPanel.classList.toggle('hidden', !showVideoZoom);
    const showTextPanel = isTextMedia(media);
    if (el.textMediaPanel) el.textMediaPanel.classList.toggle('hidden', !showTextPanel);
    if (showTextPanel) {
      if (el.textContentInput) el.textContentInput.value = media.textContent || '';
      if (el.textFontSizeInput) el.textFontSizeInput.value = String(clamp(Number(media.textFontSize || 72), 16, 240));
      if (el.textAlignInput) el.textAlignInput.value = media.textAlign || 'center';
      if (el.textColorInput) el.textColorInput.value = media.textColor || '#ffffff';
      if (el.textBgColorInput) el.textBgColorInput.value = media.textBgColor || '#000000';
      if (el.textBgOpacityInput) el.textBgOpacityInput.value = String(clamp(Number(media.textBgOpacity || 0), 0, 100));
      if (el.textBgOpacityValue) el.textBgOpacityValue.textContent = `${clamp(Number(media.textBgOpacity || 0), 0, 100)}%`;
      if (el.textBoldInput) el.textBoldInput.value = String([400,700].includes(Number(media.textBold)) ? Number(media.textBold) : 700);
    }
    if (el.cameraMirrorBtn) el.cameraMirrorBtn.classList.toggle('active', !!media?.cameraFxMirrorX);
    if (el.cameraMonoBtn) el.cameraMonoBtn.classList.toggle('active', !!media?.cameraFxMono);
    if (el.cameraBoostBtn) el.cameraBoostBtn.classList.toggle('active', !!media?.cameraFxBoost);
    if (el.cameraInvertBtn) el.cameraInvertBtn.classList.toggle('active', !!media?.cameraFxInvert);

    const missingCount = state.layers.filter((item) => getLayerActiveMediaId(item) && !getLayerActiveMedia(item)?.ready).length;
    el.missingMediaText.textContent = missingCount ? `Missing media σε ${missingCount} layer(s)` : '';
  }

  function escapeHtml(str) {
    return String(str).replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ch]));
  }

  function drawChecker(ctx, width, height) {
    const size = 32;
    for (let y = 0; y < height; y += size) {
      for (let x = 0; x < width; x += size) {
        ctx.fillStyle = ((x / size + y / size) % 2 === 0) ? '#101820' : '#0a0f16';
        ctx.fillRect(x, y, size, size);
      }
    }
  }

  function drawTestPattern(ctx, mode) {
    ctx.clearRect(0, 0, state.width, state.height);
    if (mode === 'white') {
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, state.width, state.height);
      return;
    }
    if (mode === 'bars') {
      const colors = ['#ffffff', '#f6d32d', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff', '#111111'];
      const barW = state.width / colors.length;
      colors.forEach((color, index) => {
        ctx.fillStyle = color;
        ctx.fillRect(index * barW, 0, Math.ceil(barW), state.height);
      });
      ctx.fillStyle = 'rgba(0,0,0,0.18)';
      ctx.fillRect(0, state.height * 0.78, state.width, state.height * 0.22);
      return;
    }
    drawChecker(ctx, state.width, state.height);
    ctx.save();
    ctx.strokeStyle = 'rgba(255,255,255,0.28)';
    ctx.lineWidth = 1;
    for (let x = 0; x <= state.width; x += Math.max(32, Math.round(state.width / 12))) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, state.height);
      ctx.stroke();
    }
    for (let y = 0; y <= state.height; y += Math.max(32, Math.round(state.height / 8))) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(state.width, y);
      ctx.stroke();
    }
    ctx.strokeStyle = 'rgba(255,255,255,0.16)';
    ctx.setLineDash([10, 8]);
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(state.width, state.height);
    ctx.moveTo(state.width, 0);
    ctx.lineTo(0, state.height);
    ctx.stroke();
    ctx.restore();
  }


  function drawFocusedLayerFullscreen(ctx) {
    const layer = getSelectedLayer();
    if (!layer || layer.id !== state.stageFocusLayerId) {
      state.stageFocusLayerId = null;
      return false;
    }
    const media = getLayerActiveMedia(layer);
    const element = media?.element;
    if (!media?.ready || !element) {
      ctx.clearRect(0, 0, state.width, state.height);
      ctx.fillStyle = '#000000';
      ctx.fillRect(0, 0, state.width, state.height);
      return true;
    }
    const srcW = Math.max(1, Number(media.width || element.videoWidth || element.naturalWidth || state.width) || state.width);
    const srcH = Math.max(1, Number(media.height || element.videoHeight || element.naturalHeight || state.height) || state.height);
    const fit = fitRectIntoBox(srcW, srcH, state.width, state.height);
    ctx.clearRect(0, 0, state.width, state.height);
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, state.width, state.height);
    try {
      const processedElement = shouldUseProcessedLayerCanvas(layer, media) ? buildProcessedLayerCanvas(layer, media, element) : null;
      const source = processedElement || element;
      const previousFilter = ctx.filter || 'none';
      ctx.filter = processedElement ? 'none' : getLayerFilterString(layer, media);
      const srcRect = getMediaSourceRect(source, media);
      if (shouldMirrorLayerMedia(layer, media)) {
        ctx.save();
        ctx.translate(fit.drawX + fit.drawW, fit.drawY);
        ctx.scale(-1, 1);
        ctx.drawImage(source, srcRect.sx, srcRect.sy, srcRect.sw, srcRect.sh, 0, 0, fit.drawW, fit.drawH);
        ctx.restore();
      } else {
        ctx.drawImage(source, srcRect.sx, srcRect.sy, srcRect.sw, srcRect.sh, fit.drawX, fit.drawY, fit.drawW, fit.drawH);
      }
      ctx.filter = previousFilter;
    } catch (_) {
      drawPlaceholderLayer(ctx, { x: fit.drawX, y: fit.drawY, w: fit.drawW, h: fit.drawH }, media?.name || 'Media');
    }
    return true;
  }

  async function toggleSelectedLayerFullscreen() {
    const selected = getSelectedLayer();
    if (!selected) {
      setStatus('Διάλεξε layer για fullscreen.');
      return;
    }
    const stageWrap = el.stageCanvas?.parentElement || el.stageCanvas;
    if (!stageWrap) return;
    const alreadyFullscreen = document.fullscreenElement === stageWrap || document.fullscreenElement === el.stageCanvas;
    if (alreadyFullscreen) {
      state.stageFocusLayerId = null;
      if (document.exitFullscreen) {
        try { await document.exitFullscreen(); } catch (_) {}
      }
      renderAll();
      setStatus('Έξοδος από layer fullscreen.');
      return;
    }
    state.stageFocusLayerId = selected.id;
    renderAll();
    if (stageWrap.requestFullscreen) {
      try { await stageWrap.requestFullscreen(); } catch (_) {}
    }
    setStatus(`Fullscreen preview: ${selected.name}`);
  }


  function drawLayerPreviewMonitor(layer, canvas) {
    if (!layer || !canvas) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    const targetW = Math.max(1, Number(canvas.width || 320));
    const targetH = Math.max(1, Number(canvas.height || 180));
    ctx.clearRect(0, 0, targetW, targetH);
    ctx.fillStyle = '#040b13';
    ctx.fillRect(0, 0, targetW, targetH);
    const media = getLayerActiveMedia(layer);
    const element = media?.element;
    if (!media?.ready || !element) {
      drawChecker(ctx, targetW, targetH);
      ctx.fillStyle = 'rgba(0,0,0,0.45)';
      ctx.fillRect(0, targetH - 34, targetW, 34);
      ctx.fillStyle = '#d9e8f7';
      ctx.font = 'bold 13px sans-serif';
      ctx.fillText(layer.name || 'Layer', 12, 22);
      ctx.font = '12px sans-serif';
      ctx.fillText('No active media', 12, targetH - 12);
      return;
    }
    const srcW = Math.max(1, Number(media.width || element.videoWidth || element.naturalWidth || targetW) || targetW);
    const srcH = Math.max(1, Number(media.height || element.videoHeight || element.naturalHeight || targetH) || targetH);
    const fit = fitRectIntoBox(srcW, srcH, targetW, targetH);
    try {
      const processedElement = shouldUseProcessedLayerCanvas(layer, media) ? buildProcessedLayerCanvas(layer, media, element) : null;
      const source = processedElement || element;
      const previousFilter = ctx.filter || 'none';
      ctx.filter = processedElement ? 'none' : getLayerFilterString(layer, media);
      const srcRect = getMediaSourceRect(source, media);
      if (shouldMirrorLayerMedia(layer, media)) {
        ctx.save();
        ctx.translate(fit.drawX + fit.drawW, fit.drawY);
        ctx.scale(-1, 1);
        ctx.drawImage(source, srcRect.sx, srcRect.sy, srcRect.sw, srcRect.sh, 0, 0, fit.drawW, fit.drawH);
        ctx.restore();
      } else {
        ctx.drawImage(source, srcRect.sx, srcRect.sy, srcRect.sw, srcRect.sh, fit.drawX, fit.drawY, fit.drawW, fit.drawH);
      }
      ctx.filter = previousFilter;
    } catch (_) {
      drawChecker(ctx, targetW, targetH);
    }
    ctx.strokeStyle = 'rgba(113,157,202,0.55)';
    ctx.lineWidth = 1;
    ctx.strokeRect(0.5, 0.5, targetW - 1, targetH - 1);
    ctx.fillStyle = 'rgba(0,0,0,0.55)';
    ctx.fillRect(0, targetH - 36, targetW, 36);
    ctx.fillStyle = '#f5fbff';
    ctx.font = 'bold 12px sans-serif';
    ctx.fillText((media?.name || layer.name || 'Layer').slice(0, 34), 10, targetH - 14);
    ctx.fillStyle = '#9cc4ec';
    ctx.font = '11px sans-serif';
    const previewMode = isCameraMedia(media) ? 'CAM FX' : (layer.loop ? 'LOOP' : 'PLAY');
    ctx.fillText(`${layer.output || 'P?'} · ${previewMode}`, targetW - 90, 16);
  }

  function renderLayerMonitorCanvases() {
    const nodes = document.querySelectorAll('[data-layer-monitor]');
    nodes.forEach((canvas) => {
      const layerId = canvas.getAttribute('data-layer-monitor');
      const layer = state.layers.find((item) => item.id === layerId);
      drawLayerPreviewMonitor(layer, canvas);
    });
  }

  function drawLiveScene(ctx, outputFilter = 'ALL', includeSelection = false) {
    ctx.clearRect(0, 0, state.width, state.height);
    if (outputFilter === 'ALL' && state.stageFocusLayerId) {
      if (drawFocusedLayerFullscreen(ctx)) return;
    }
    if (outputFilter === 'ALL') {
      drawChecker(ctx, state.width, state.height);
    } else {
      ctx.fillStyle = '#000000';
      ctx.fillRect(0, 0, state.width, state.height);
    }

    const layers = getRenderableLayers(outputFilter);
    layers.forEach((layer) => {
      const media = getLayerActiveMedia(layer);
      ctx.save();
      ctx.globalAlpha = Math.max(0, Math.min(1, Number(layer.opacity ?? 1)));
      if (media?.ready && media.element) {
        try {
          if (!drawLayerClipTransition(ctx, layer)) drawLayerMedia(ctx, layer, media.element, media);
        } catch (_) {
          if (outputFilter === 'ALL') drawPlaceholderLayer(ctx, layer, media?.name || 'Media');
        }
      } else if (outputFilter === 'ALL') {
        drawPlaceholderLayer(ctx, layer, media?.name || 'No media');
      }
      ctx.restore();
    });

    if (includeSelection) {
      if (outputFilter === 'ALL') drawStageGuides(ctx);
      const selected = getSelectedLayer();
      if (selected && (outputFilter === 'ALL' || selected.output === outputFilter)) {
        drawSelection(ctx, selected);
      }
    }
  }

  function drawScene(ctx, outputFilter = 'ALL', includeSelection = false) {
    if (outputFilter !== 'ALL') {
      const ops = getOutputOps(outputFilter);
      if (ops.blackout) {
        ctx.clearRect(0, 0, state.width, state.height);
        ctx.fillStyle = '#000000';
        ctx.fillRect(0, 0, state.width, state.height);
        return;
      }
      if (ops.frozen && ops.freezeCanvas) {
        ctx.clearRect(0, 0, state.width, state.height);
        ctx.drawImage(ops.freezeCanvas, 0, 0, state.width, state.height);
        return;
      }
      if (ops.testPattern && ops.testPattern !== 'live') {
        drawTestPattern(ctx, ops.testPattern);
        return;
      }
    }
    if (!drawCueTransitionFrame(ctx, outputFilter)) {
      drawLiveScene(ctx, outputFilter, includeSelection);
    }
    applyEdgeBlendMask(ctx, outputFilter);
  }

  function getLayerCenter(layer) {
    const corners = ensureLayerCorners(layer);
    const sum = corners.reduce((acc, pt) => ({ x: acc.x + pt.x, y: acc.y + pt.y }), { x: 0, y: 0 });
    return { x: sum.x / corners.length, y: sum.y / corners.length };
  }


  function getLayerEffectSummary(layer) {
    const bits = [];
    const media = getLayerActiveMedia(layer);
    if (media?.effectMirrorX || media?.cameraFxMirrorX || layer?.effectMirrorX) bits.push('mirror');
    if (media?.effectMono || media?.cameraFxMono || layer?.effectMono) bits.push('mono');
    if (media?.effectBoost || media?.cameraFxBoost || layer?.effectBoost) bits.push('boost');
    if (media?.cameraFxInvert) bits.push('invert');
    if (layerHasToneFx(layer, media)) bits.push('tone');
    if (Number(getLayerFxTarget(layer, media)?.edgeSoftness ?? 0) > 0) bits.push('soft');
    if (isLayerBackgroundRemoveActive(layer, media)) bits.push('key');
    return bits.join(' · ');
  }

  function mediaHasCameraFx(media) {
    return !!(isCameraMedia(media) && (media.cameraFxMirrorX || media.cameraFxMono || media.cameraFxBoost || media.cameraFxInvert));
  }

  function getCameraFilterString(media) {
    if (!mediaHasCameraFx(media)) return 'none';
    const parts = [];
    if (media?.cameraFxMono) parts.push('grayscale(1)');
    if (media?.cameraFxInvert) parts.push('invert(1)');
    if (media?.cameraFxBoost) {
      parts.push('brightness(1.050)');
      parts.push('contrast(1.160)');
      parts.push('saturate(1.220)');
    }
    return parts.length ? parts.join(' ') : 'none';
  }

  function shouldMirrorLayerMedia(layer, media) {
    return (!!layer?.effectMirrorX || !!media?.effectMirrorX) !== (!!media?.cameraFxMirrorX);
  }

  function getLayerFilterString(layer, media = null) {
    const parts = [];
    const activeMedia = media || getLayerActiveMedia(layer);
    const cameraFilter = getCameraFilterString(activeMedia);
    if (cameraFilter !== 'none') parts.push(cameraFilter);
    const fxTarget = getLayerFxTarget(layer, activeMedia);
    let brightness = clamp(Number(fxTarget?.fxBrightness ?? 100), 0, 200) / 100;
    let contrast = clamp(Number(fxTarget?.fxContrast ?? 100), 0, 220) / 100;
    let saturation = clamp(Number(fxTarget?.fxSaturation ?? 100), 0, 300) / 100;
    const hue = clamp(Number(fxTarget?.fxHue ?? 0), -180, 180);
    const blur = clamp(Number(fxTarget?.fxBlur ?? 0), 0, 20);
    if (fxTarget?.effectBoost) {
      brightness *= 1.03;
      contrast *= 1.12;
      saturation *= 1.2;
    }
    if (fxTarget?.effectMono) parts.push('grayscale(1)');
    if (Math.abs(brightness - 1) > 0.001) parts.push(`brightness(${brightness.toFixed(3)})`);
    if (Math.abs(contrast - 1) > 0.001) parts.push(`contrast(${contrast.toFixed(3)})`);
    if (Math.abs(saturation - 1) > 0.001) parts.push(`saturate(${saturation.toFixed(3)})`);
    if (Math.abs(hue) > 0.001) parts.push(`hue-rotate(${hue.toFixed(1)}deg)`);
    if (blur > 0.001) parts.push(`blur(${blur.toFixed(1)}px)`);
    return parts.length ? parts.join(' ') : 'none';
  }

  function getMediaSourceRect(element, media = null) {
    const sw = Math.max(1, Number(element?.videoWidth || element?.naturalWidth || element?.width || 1));
    const sh = Math.max(1, Number(element?.videoHeight || element?.naturalHeight || element?.height || 1));
    const zoom = (media?.type === 'video') ? clamp(Number(media.mediaZoom ?? 100), 100, 300) / 100 : 1;
    if (zoom <= 1.001) return { sx: 0, sy: 0, sw, sh };
    const cropW = Math.max(1, sw / zoom);
    const cropH = Math.max(1, sh / zoom);
    return {
      sx: Math.max(0, (sw - cropW) / 2),
      sy: Math.max(0, (sh - cropH) / 2),
      sw: cropW,
      sh: cropH
    };
  }

  function drawElementInBounds(ctx, element, bounds, flipX, media = null) {
    const width = Math.max(1, bounds.maxX - bounds.minX);
    const height = Math.max(1, bounds.maxY - bounds.minY);
    const source = getMediaSourceRect(element, media);
    if (flipX) {
      ctx.translate(bounds.minX + width, bounds.minY);
      ctx.scale(-1, 1);
      ctx.drawImage(element, source.sx, source.sy, source.sw, source.sh, 0, 0, width, height);
    } else {
      ctx.drawImage(element, source.sx, source.sy, source.sw, source.sh, bounds.minX, bounds.minY, width, height);
    }
  }

  function toggleLayerEffect(layerId, effectKey) {
    const layer = state.layers.find((item) => item.id === layerId);
    if (!layer) return;
    const target = getLayerFxTarget(layer) || layer;
    target[effectKey] = !target[effectKey];
    renderInspector();
    renderAll();
    const summary = getLayerEffectSummary(layer);
    setStatus(summary ? `${getLayerFxTargetLabel(layer)}: ${summary}.` : `${getLayerFxTargetLabel(layer)}: effects off.`);
  }

  function drawLayerMedia(ctx, layer, element, media = null) {
    const activeMedia = media || getLayerActiveMedia(layer);
    const previousFilter = ctx.filter || 'none';
    const processedElement = shouldUseProcessedLayerCanvas(layer, activeMedia) ? buildProcessedLayerCanvas(layer, activeMedia, element) : null;
    ctx.filter = processedElement ? 'none' : getLayerFilterString(layer, activeMedia);
    drawMediaToQuad(ctx, layer, processedElement || element, activeMedia);
    ctx.filter = previousFilter;
  }

  function drawPlaceholderLayer(ctx, layer, label) {
    const corners = ensureLayerCorners(layer);
    ctx.save();
    ctx.fillStyle = 'rgba(25,44,63,0.88)';
    ctx.strokeStyle = 'rgba(113,157,202,0.9)';
    ctx.lineWidth = 2;
    ctx.beginPath();
    corners.forEach((pt, index) => {
      if (!index) ctx.moveTo(pt.x, pt.y);
      else ctx.lineTo(pt.x, pt.y);
    });
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
    ctx.fillStyle = '#d9e8f7';
    ctx.font = '14px sans-serif';
    const bounds = getCornerBounds(layer);
    ctx.fillText(label, bounds.minX + 12, bounds.minY + 24);
    ctx.restore();
  }

  function getRotatedCorners(layer) {
    return cloneCorners(ensureLayerCorners(layer));
  }

  function pointInPolygon(point, polygon) {
    let inside = false;
    for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
      const xi = polygon[i].x, yi = polygon[i].y;
      const xj = polygon[j].x, yj = polygon[j].y;
      const intersect = ((yi > point.y) !== (yj > point.y)) &&
        (point.x < ((xj - xi) * (point.y - yi)) / ((yj - yi) || 1e-9) + xi);
      if (intersect) inside = !inside;
    }
    return inside;
  }

  function pointInLayer(point, layer) {
    const corners = getRotatedCorners(layer);
    return pointInPolygon(point, corners);
  }

  function drawSelection(ctx, layer) {
    const corners = ensureLayerCorners(layer);
    ctx.save();
    ctx.strokeStyle = '#49a3ff';
    ctx.lineWidth = 2;
    ctx.setLineDash([8, 6]);
    ctx.beginPath();
    corners.forEach((pt, index) => {
      if (!index) ctx.moveTo(pt.x, pt.y);
      else ctx.lineTo(pt.x, pt.y);
    });
    ctx.closePath();
    ctx.stroke();
    ctx.setLineDash([]);
    getHandles(layer).forEach((handle) => {
      const active = state.activeHandleName === handle.name;
      const radius = active ? 10 : 9;
      ctx.fillStyle = active ? '#ffcf5a' : (isLayerFreeformMode(layer) ? '#ffcf5a' : '#49a3ff');
      ctx.strokeStyle = '#08131e';
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.arc(handle.x, handle.y, radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.stroke();
      if (isLayerFreeformMode(layer)) {
        ctx.fillStyle = '#e8f1fb';
        ctx.font = '11px sans-serif';
        ctx.fillText(handle.name.toUpperCase(), handle.x + 10, handle.y - 10);
      }
    });
    const center = getLayerCenter(layer);
    ctx.fillStyle = '#ffcf5a';
    ctx.beginPath();
    ctx.arc(center.x, center.y, 4, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  }

  function getHandles(layer) {
    const corners = cloneCorners(ensureLayerCorners(layer));
    if (!isLayerFreeformMode(layer)) return corners;
    const tl = corners.find((pt) => pt.name === 'tl');
    const tr = corners.find((pt) => pt.name === 'tr');
    const br = corners.find((pt) => pt.name === 'br');
    const bl = corners.find((pt) => pt.name === 'bl');
    if (isLayerPin8Mode(layer)) {
      return [
        tl,
        { name: 'tm', x: (tl.x + tr.x) / 2, y: (tl.y + tr.y) / 2 },
        tr,
        { name: 'mr', x: (tr.x + br.x) / 2, y: (tr.y + br.y) / 2 },
        br,
        { name: 'bm', x: (bl.x + br.x) / 2, y: (bl.y + br.y) / 2 },
        bl,
        { name: 'ml', x: (tl.x + bl.x) / 2, y: (tl.y + bl.y) / 2 }
      ];
    }
    return [
      tl,
      { name: 'ml', x: (tl.x + bl.x) / 2, y: (tl.y + bl.y) / 2 },
      bl,
      tr,
      { name: 'mr', x: (tr.x + br.x) / 2, y: (tr.y + br.y) / 2 },
      br
    ];
  }

  function hitTestLayer(px, py, outputFilter) {
    const layers = getRenderableLayers(outputFilter).slice().reverse();
    for (const layer of layers) {
      if (layer.locked) continue;
      const handles = getHandles(layer);
      for (const handle of handles) {
        const radius = isLayerFreeformMode(layer) ? PIN_HANDLE_SIZE : HANDLE_SIZE;
        if (Math.hypot(px - handle.x, py - handle.y) <= radius) {
          return { layer, mode: handle.name };
        }
      }
      if (pointInLayer({ x: px, y: py }, layer)) {
        return { layer, mode: 'move' };
      }
    }
    return null;
  }

  function pointerToCanvas(event) {
    const rect = el.stageCanvas.getBoundingClientRect();
    const scaleX = state.width / rect.width;
    const scaleY = state.height / rect.height;
    return {
      x: (event.clientX - rect.left) * scaleX,
      y: (event.clientY - rect.top) * scaleY
    };
  }

  function bindStageInteraction() {
    el.stageCanvas.addEventListener('pointerdown', (event) => {
      const pos = pointerToCanvas(event);
      const outputFilter = el.stageViewSelect.value;
      const hit = hitTestLayer(pos.x, pos.y, outputFilter);
      if (!hit) {
        state.selectedLayerId = null;
        state.activeHandleName = null;
        renderAll();
        return;
      }
      state.selectedLayerId = hit.layer.id;
      const handleNames = ['tl', 'tr', 'bl', 'br', 'ml', 'mr', 'tm', 'bm'];
      state.activeHandleName = handleNames.includes(hit.mode) ? hit.mode : null;
      state.drag = {
        layerId: hit.layer.id,
        mode: hit.mode,
        startX: pos.x,
        startY: pos.y,
        originCorners: cloneCorners(ensureLayerCorners(hit.layer))
      };
      el.stageCanvas.setPointerCapture(event.pointerId);
      renderAll();
    });

    el.stageCanvas.addEventListener('pointermove', (event) => {
      if (!state.drag) return;
      const layer = getSelectedLayer();
      if (!layer) return;
      const pos = pointerToCanvas(event);
      const dx = pos.x - state.drag.startX;
      const dy = pos.y - state.drag.startY;
      layer.corners = cloneCorners(state.drag.originCorners);
      if (state.drag.mode === 'move') {
        moveLayerBy(layer, dx, dy);
      } else if (isLayerFreeformMode(layer)) {
        moveHandlePoint(layer, state.drag.mode, dx, dy, state.drag.originCorners);
      } else {
        resizeLayerFromHandle(layer, state.drag.mode, dx, dy, state.drag.originCorners);
      }
      snapLayerToGrid(layer, state.drag.mode);
      renderAll();
    });

    const endDrag = () => {
      if (!state.drag) return;
      state.drag = null;
      renderInspector();
      renderAll();
    };
    el.stageCanvas.addEventListener('pointerup', endDrag);
    el.stageCanvas.addEventListener('pointercancel', endDrag);
  }


  function renderCueSettings() {
    if (el.cueTransitionModeInput) el.cueTransitionModeInput.value = state.cueTransitionMode || 'cut';
    if (el.cueTransitionDurationInput) el.cueTransitionDurationInput.value = String(Math.max(0, Number(state.cueTransitionMs || 0)));
  }

  function renderCueList() {
    if (!el.cueList || !el.cueCount) return;
    el.cueCount.textContent = String(state.cues.length);
    if (!state.cues.length) {
      el.cueList.className = 'list empty';
      el.cueList.textContent = 'Δεν υπάρχουν cues ακόμα. Στήσε σκηνή και πάτα Save Cue.';
      return;
    }
    el.cueList.className = 'list cue-list';
    el.cueList.innerHTML = '';
    state.cues.forEach((cue, index) => {
      const div = document.createElement('div');
      div.className = `cue-item${cue.id === state.activeCueId ? ' selected' : ''}`;
      const cueMode = sanitizeCueTransitionMode(cue.transitionMode || state.cueTransitionMode || 'cut');
      const cueMs = sanitizeCueTransitionMs(cue.transitionMs || state.cueTransitionMs || 0, cueMode);
      const preview = cue.previewDataUrl
        ? `<img class="cue-thumb-img" src="${cue.previewDataUrl}" alt="${escapeHtml(cue.name || 'Cue')}" />`
        : `<div class="cue-thumb-placeholder">🎬</div>`;
      const transitionMeta = cueMode === 'cut'
        ? `<div class="clipfx-row"><span class="clipfx-label">🎬 transition</span><span class="clipfx-value">CUT</span></div>`
        : `<div class="clipfx-row"><span class="clipfx-label">🎬 transition</span><span class="clipfx-value">${getCueTransitionLabel(cueMode)} · ${cueMs}ms</span></div>`;
      const orderMeta = `<div class="clipfx-row"><span class="clipfx-label">📍 θέση</span><span class="clipfx-value">${index + 1}/${state.cues.length}</span></div>`;
      div.innerHTML = `
        <div class="cue-item-layout">
          <div class="cue-thumb">${preview}<span class="cue-thumb-chip">#${index + 1}</span></div>
          <div class="cue-item-main">
            <div class="cue-item-row">
              <div class="cue-item-title">${escapeHtml(cue.name || `Cue ${index + 1}`)}</div>
              <div class="cue-item-badges">
                <div class="pill">${(cue.layers || []).length}L</div>
                ${cue.id === state.activeCueId ? '<div class="pill cue-live-pill">LIVE</div>' : ''}
              </div>
            </div>
            <div class="cue-item-sub">${new Date(cue.updatedAt || Date.now()).toLocaleString('el-GR')} · Recall full layer state · ${getCueTransitionSummary(cueMode, cueMs)}</div>
            <div class="cue-action-grid">
              <button class="icon-btn" data-action="load" title="Go Cue">▶</button>
              <button class="icon-btn" data-action="update" title="Update Cue">💾</button>
              <button class="icon-btn" data-action="duplicate" title="Duplicate Cue">⧉</button>
              <button class="icon-btn" data-action="up" title="Move Cue Up">↑</button>
              <button class="icon-btn" data-action="down" title="Move Cue Down">↓</button>
              <button class="icon-btn danger" data-action="delete" title="Delete Cue">✕</button>
            </div>
            ${orderMeta}
            ${transitionMeta}
          </div>
        </div>
      `;
      div.addEventListener('click', () => loadCueById(cue.id));
      div.querySelectorAll('[data-action]').forEach((btn) => {
        btn.addEventListener('click', (event) => {
          event.stopPropagation();
          const action = btn.dataset.action;
          if (action === 'load') loadCueById(cue.id);
          else if (action === 'update') {
            state.activeCueId = cue.id;
            if (el.cueNameInput) el.cueNameInput.value = cue.name || '';
            saveCurrentCue();
          } else if (action === 'duplicate') duplicateCueById(cue.id);
          else if (action === 'up') moveCueById(cue.id, -1);
          else if (action === 'down') moveCueById(cue.id, 1);
          else if (action === 'delete') deleteCueById(cue.id);
        });
      });
      el.cueList.appendChild(div);
    });
  }

  function saveCurrentCue() {
    const snapshot = buildCueSnapshot();
    const existingIndex = state.cues.findIndex((cue) => cue.id === snapshot.id);
    if (existingIndex >= 0) state.cues.splice(existingIndex, 1, snapshot);
    else state.cues.push(snapshot);
    state.activeCueId = snapshot.id;
    if (el.cueNameInput) el.cueNameInput.value = snapshot.name;
    renderCueList();
    setStatus(`Σώθηκε cue: ${snapshot.name}`);
  }

  function duplicateCueById(id) {
    const idx = state.cues.findIndex((item) => item.id === id);
    if (idx < 0) return;
    const source = state.cues[idx];
    const copy = serializeCue(source);
    copy.id = uid('cue');
    copy.name = `${source.name || `Cue ${idx + 1}`} Copy`;
    copy.updatedAt = Date.now();
    state.cues.splice(idx + 1, 0, copy);
    state.activeCueId = copy.id;
    if (el.cueNameInput) el.cueNameInput.value = copy.name;
    renderCueList();
    setStatus(`Έγινε duplicate cue: ${copy.name}`);
  }

  function moveCueById(id, direction) {
    const idx = state.cues.findIndex((item) => item.id === id);
    if (idx < 0) return;
    const target = idx + direction;
    if (target < 0 || target >= state.cues.length) return;
    const [cue] = state.cues.splice(idx, 1);
    state.cues.splice(target, 0, cue);
    renderCueList();
    setStatus(`Μετακινήθηκε cue στη θέση ${target + 1}.`);
  }

  function applyCueData(cue) {
    if (!cue) return;
    const cueMode = sanitizeCueTransitionMode(cue.transitionMode || state.cueTransitionMode || 'cut');
    const cueMs = sanitizeCueTransitionMs(cue.transitionMs || state.cueTransitionMs || 0, cueMode);
    const previousFrames = (cueMode !== 'cut' && cueMs > 0) ? captureCueFrames() : null;
    state.cueTransitionMode = cueMode;
    state.cueTransitionMs = cueMs;
    renderCueSettings();
    state.layers = [];
    state.selectedLayerId = null;
    state.activeHandleName = null;
    (cue.layers || []).forEach((savedLayer) => {
      createLayer({
        id: savedLayer.id,
        name: savedLayer.name,
        mediaId: resolveCueLayerMedia(savedLayer),
        output: savedLayer.output || 'P1',
        pendingMediaFileName: savedLayer.pendingMediaFileName || '',
        playlistMediaIds: Array.isArray(savedLayer.playlistMediaIds) ? savedLayer.playlistMediaIds.map((id) => getMediaById(id) ? id : '').filter(Boolean) : [],
        pendingPlaylistFileNames: Array.isArray(savedLayer.playlistFileNames) ? savedLayer.playlistFileNames.filter(Boolean) : [],
        sequenceEnabled: !!savedLayer.sequenceEnabled,
        playlistIndex: savedLayer.playlistIndex || 0,
        x: savedLayer.x,
        y: savedLayer.y,
        w: savedLayer.w,
        h: savedLayer.h,
        opacity: savedLayer.opacity,
        rotation: savedLayer.rotation || 0,
        visible: savedLayer.visible,
        locked: savedLayer.locked,
        loop: savedLayer.loop,
        clipTransitionMode: savedLayer.clipTransitionMode || 'cut',
        clipTransitionMs: savedLayer.clipTransitionMs || 0,
        mappingMode: (savedLayer.mappingMode === 'pin6' || savedLayer.mappingMode === 'pin8') ? savedLayer.mappingMode : 'bbox',
        fxBrightness: savedLayer.fxBrightness,
        fxContrast: savedLayer.fxContrast,
        fxSaturation: savedLayer.fxSaturation,
        fxHue: savedLayer.fxHue,
        fxBlur: savedLayer.fxBlur,
        edgeSoftness: savedLayer.edgeSoftness,
        bgRemoveMode: savedLayer.bgRemoveMode,
        bgRemoveColor: savedLayer.bgRemoveColor,
        bgRemoveThreshold: savedLayer.bgRemoveThreshold,
        bgRemoveSoftness: savedLayer.bgRemoveSoftness,
        effectMirrorX: savedLayer.effectMirrorX,
        effectMono: savedLayer.effectMono,
        effectBoost: savedLayer.effectBoost,
        z: savedLayer.z,
        corners: savedLayer.corners || []
      });
    });
    normalizeLayerZ();
    const nextFrames = (cueMode !== 'cut' && cueMs > 0) ? captureCueFrames() : null;
    startCueTransition(previousFrames, nextFrames, cueMode, cueMs);
    state.activeCueId = cue.id;
    if (el.cueNameInput) el.cueNameInput.value = cue.name || '';
    renderAll();
    setStatus(`Recall cue: ${cue.name} (${getCueTransitionSummary(cueMode, cueMs)}).`);
  }

  function loadCueById(id) {
    const cue = state.cues.find((item) => item.id === id);
    if (!cue) return;
    applyCueData(cue);
  }

  function deleteCueById(id) {
    const idx = state.cues.findIndex((item) => item.id === id);
    if (idx < 0) return;
    const [removed] = state.cues.splice(idx, 1);
    if (state.activeCueId === id) {
      const fallback = state.cues[idx] || state.cues[idx - 1] || state.cues[0] || null;
      state.activeCueId = fallback?.id || null;
      if (el.cueNameInput) el.cueNameInput.value = fallback?.name || '';
    }
    renderCueList();
    setStatus(`Διαγράφηκε cue: ${removed.name}`);
  }

  function goActiveCue() {
    if (!state.cues.length) return;
    const activeId = state.activeCueId || state.cues[0]?.id;
    if (activeId) loadCueById(activeId);
  }

  function stepCue(direction) {
    if (!state.cues.length) return;
    const currentIndex = Math.max(0, state.cues.findIndex((item) => item.id === state.activeCueId));
    const nextIndex = Math.min(state.cues.length - 1, Math.max(0, currentIndex + direction));
    loadCueById(state.cues[nextIndex].id);
  }

  function renderProjectBank() {
    updateAutosaveInfo();
    if (!el.projectBankList) return;
    const query = String(el.projectSearchInput?.value || '').trim().toLowerCase();
    const filteredProjects = state.localProjects.filter((entry) => {
      if (!query) return true;
      return String(entry.name || '').toLowerCase().includes(query);
    });
    if (!filteredProjects.length) {
      el.projectBankList.className = 'list empty';
      el.projectBankList.textContent = query ? 'Δεν βρέθηκε saved project με αυτό το όνομα.' : 'Δεν υπάρχουν saved projects μέσα στο app.';
      return;
    }
    el.projectBankList.className = 'list project-bank-list';
    el.projectBankList.innerHTML = '';
    filteredProjects.forEach((entry) => {
      const div = document.createElement('div');
      div.className = 'project-bank-item';
      const preview = entry.previewDataUrl
        ? `<img class="project-bank-thumb-img" src="${entry.previewDataUrl}" alt="${escapeHtml(entry.name || 'Untitled Project')}" />`
        : `<div class="project-bank-thumb-placeholder">🗂</div>`;
      div.innerHTML = `
        <div class="project-bank-layout">
          <div class="project-bank-thumb">${preview}</div>
          <div class="project-bank-meta">
            <div class="project-bank-title">${escapeHtml(entry.name || 'Untitled Project')}</div>
            <div class="project-bank-sub">${new Date(entry.updatedAt).toLocaleString('el-GR')} · ${entry.layers || 0} layer(s)</div>
          </div>
        </div>
        <div class="project-bank-actions">
          <button class="icon-btn" data-action="load" title="Load">📂</button>
          <button class="icon-btn danger" data-action="delete" title="Delete">✕</button>
        </div>
      `;
      div.querySelector('[data-action="load"]').addEventListener('click', () => loadLocalProject(entry.id));
      div.querySelector('[data-action="delete"]').addEventListener('click', () => deleteLocalProject(entry.id));
      el.projectBankList.appendChild(div);
    });
  }

  function persistLocalProjectBank() {
    try {
      localStorage.setItem(LOCAL_PROJECTS_KEY, JSON.stringify(state.localProjects));
    } catch (_) {}
  }

  function captureLiveOutputCanvas(outputId) {
    const canvas = document.createElement('canvas');
    canvas.width = state.width;
    canvas.height = state.height;
    const ctx = canvas.getContext('2d');
    drawLiveScene(ctx, outputId, false);
    return canvas;
  }

  function setOutputBlackout(outputId, value) {
    if (!OUTPUTS.includes(outputId)) return;
    const ops = getOutputOps(outputId);
    ops.blackout = !!value;
    if (ops.blackout) {
      ops.frozen = false;
      ops.freezeCanvas = null;
    }
    renderAll();
    setStatus(`${outputId}: ${ops.blackout ? 'BLACKOUT' : 'LIVE'}.`);
  }

  function setAllBlackout(value) {
    OUTPUTS.forEach((outputId) => {
      const ops = getOutputOps(outputId);
      ops.blackout = !!value;
      if (ops.blackout) {
        ops.frozen = false;
        ops.freezeCanvas = null;
      }
    });
    renderAll();
    setStatus(value ? 'BLACKOUT σε όλα τα outputs.' : 'Έφυγε το blackout από όλα τα outputs.');
  }

  function setOutputPattern(outputId, pattern) {
    if (!OUTPUTS.includes(outputId)) return;
    const nextPattern = ['live', 'grid', 'white', 'bars'].includes(pattern) ? pattern : 'live';
    const ops = getOutputOps(outputId);
    ops.testPattern = nextPattern;
    if (nextPattern !== 'live') {
      ops.blackout = false;
      ops.frozen = false;
      ops.freezeCanvas = null;
    }
    renderAll();
    setStatus(`${outputId}: mode ${nextPattern.toUpperCase()}.`);
  }

  function toggleOutputFreeze(outputId) {
    if (!OUTPUTS.includes(outputId)) return;
    const ops = getOutputOps(outputId);
    if (ops.frozen) {
      ops.frozen = false;
      ops.freezeCanvas = null;
      renderAll();
      setStatus(`${outputId}: LIVE.`);
      return;
    }
    ops.freezeCanvas = captureLiveOutputCanvas(outputId);
    ops.frozen = true;
    ops.blackout = false;
    renderAll();
    setStatus(`${outputId}: FROZEN.`);
  }

  function setAllFreeze(value) {
    OUTPUTS.forEach((outputId) => {
      const ops = getOutputOps(outputId);
      if (value) {
        ops.freezeCanvas = captureLiveOutputCanvas(outputId);
        ops.frozen = true;
        ops.blackout = false;
      } else {
        ops.frozen = false;
        ops.freezeCanvas = null;
      }
    });
    renderAll();
    setStatus(value ? 'FREEZE σε όλα τα outputs.' : 'Έφυγε το freeze από όλα τα outputs.');
  }

  function setOutputBlend(outputId, edge, value) {
    const ops = getOutputOps(outputId);
    ops.blend = normalizeBlend(ops.blend);
    if (!(edge in ops.blend)) return;
    ops.blend[edge] = sanitizeBlendValue(value);
    renderAll();
    setStatus(`${outputId}: blend ${edge} ${ops.blend[edge]}%.`);
  }

  function applyEdgeBlendMask(ctx, outputId) {
    if (outputId === 'ALL') return;
    const ops = getOutputOps(outputId);
    const blend = normalizeBlend(ops.blend);
    const leftPx = Math.round((blend.left / 100) * state.width);
    const rightPx = Math.round((blend.right / 100) * state.width);
    const topPx = Math.round((blend.top / 100) * state.height);
    const bottomPx = Math.round((blend.bottom / 100) * state.height);
    if (!(leftPx || rightPx || topPx || bottomPx)) return;

    const maskCanvas = document.createElement('canvas');
    maskCanvas.width = state.width;
    maskCanvas.height = state.height;
    const mctx = maskCanvas.getContext('2d');
    mctx.clearRect(0, 0, state.width, state.height);
    mctx.fillStyle = '#ffffff';
    mctx.fillRect(0, 0, state.width, state.height);
    mctx.globalCompositeOperation = 'destination-in';

    if (leftPx > 0) {
      const g = mctx.createLinearGradient(0, 0, leftPx, 0);
      g.addColorStop(0, 'rgba(255,255,255,0)');
      g.addColorStop(1, 'rgba(255,255,255,1)');
      mctx.fillStyle = g;
      mctx.fillRect(0, 0, leftPx, state.height);
    }
    if (rightPx > 0) {
      const g = mctx.createLinearGradient(state.width - rightPx, 0, state.width, 0);
      g.addColorStop(0, 'rgba(255,255,255,1)');
      g.addColorStop(1, 'rgba(255,255,255,0)');
      mctx.fillStyle = g;
      mctx.fillRect(state.width - rightPx, 0, rightPx, state.height);
    }
    if (topPx > 0) {
      const g = mctx.createLinearGradient(0, 0, 0, topPx);
      g.addColorStop(0, 'rgba(255,255,255,0)');
      g.addColorStop(1, 'rgba(255,255,255,1)');
      mctx.fillStyle = g;
      mctx.fillRect(0, 0, state.width, topPx);
    }
    if (bottomPx > 0) {
      const g = mctx.createLinearGradient(0, state.height - bottomPx, 0, state.height);
      g.addColorStop(0, 'rgba(255,255,255,1)');
      g.addColorStop(1, 'rgba(255,255,255,0)');
      mctx.fillStyle = g;
      mctx.fillRect(0, state.height - bottomPx, state.width, bottomPx);
    }

    ctx.save();
    ctx.globalCompositeOperation = 'destination-in';
    ctx.drawImage(maskCanvas, 0, 0, state.width, state.height);
    ctx.restore();
  }

  function renderOutputOps() {
    if (!el.outputOpsList) return;
    el.outputOpsList.className = 'list output-ops-list';
    el.outputOpsList.innerHTML = '';
    OUTPUTS.forEach((outputId) => {
      const ops = getOutputOps(outputId);
      const isOpen = !!(state.projectorWins.get(outputId) && !state.projectorWins.get(outputId).closed);
      const status = ops.blackout ? 'BLACKOUT' : ops.frozen ? 'FROZEN' : ops.testPattern !== 'live' ? ops.testPattern.toUpperCase() : 'LIVE';
      const blend = normalizeBlend(ops.blend);
      const card = document.createElement('div');
      card.className = `output-ops-card${ops.blackout ? ' is-blackout' : ''}${ops.frozen ? ' is-frozen' : ''}`;
      card.innerHTML = `
        <div class="output-ops-head">
          <div class="output-ops-title">${outputId}</div>
          <div class="pill">${status}</div>
        </div>
        <div class="output-ops-sub">${isOpen ? 'Window open' : 'Window closed'} · Test pattern + edge blend</div>
        <div class="output-ops-actions">
          <button class="btn btn-small" type="button" data-output-action="open" data-output-id="${outputId}">Open</button>
          <button class="btn btn-small${ops.testPattern === 'live' && !ops.blackout && !ops.frozen ? ' active' : ''}" type="button" data-output-action="mode" data-output-mode="live" data-output-id="${outputId}">Live</button>
          <button class="btn btn-small${ops.testPattern === 'grid' ? ' active' : ''}" type="button" data-output-action="mode" data-output-mode="grid" data-output-id="${outputId}">Grid</button>
          <button class="btn btn-small${ops.testPattern === 'white' ? ' active' : ''}" type="button" data-output-action="mode" data-output-mode="white" data-output-id="${outputId}">White</button>
          <button class="btn btn-small${ops.testPattern === 'bars' ? ' active' : ''}" type="button" data-output-action="mode" data-output-mode="bars" data-output-id="${outputId}">Bars</button>
          <button class="btn btn-small${ops.frozen ? ' active' : ''}" type="button" data-output-action="freeze" data-output-id="${outputId}">Freeze</button>
          <button class="btn btn-small${ops.blackout ? ' active danger' : ''}" type="button" data-output-action="blackout" data-output-id="${outputId}">Blackout</button>
        </div>
        <div class="output-blend-grid">
          <label>🡄 L <input type="range" min="0" max="40" step="1" value="${blend.left}" data-output-id="${outputId}" data-blend-edge="left" /><span class="output-blend-value">${blend.left}%</span></label>
          <label>R 🡆 <input type="range" min="0" max="40" step="1" value="${blend.right}" data-output-id="${outputId}" data-blend-edge="right" /><span class="output-blend-value">${blend.right}%</span></label>
          <label>🡅 T <input type="range" min="0" max="40" step="1" value="${blend.top}" data-output-id="${outputId}" data-blend-edge="top" /><span class="output-blend-value">${blend.top}%</span></label>
          <label>B 🡇 <input type="range" min="0" max="40" step="1" value="${blend.bottom}" data-output-id="${outputId}" data-blend-edge="bottom" /><span class="output-blend-value">${blend.bottom}%</span></label>
        </div>
      `;
      el.outputOpsList.appendChild(card);
    });
  }

  function loadLocalProjectBank() {
    try {
      const raw = localStorage.getItem(LOCAL_PROJECTS_KEY);
      state.localProjects = raw ? JSON.parse(raw) : [];
      state.localProjects = Array.isArray(state.localProjects) ? state.localProjects.map((entry) => ({ ...entry, previewDataUrl: typeof entry.previewDataUrl === 'string' ? entry.previewDataUrl : '' })) : [];
    } catch (_) {
      state.localProjects = [];
    }
  }

  function saveProjectInsideApp() {
    const payload = projectToSerializable();
    const previewDataUrl = captureProjectPreview();
    const name = (el.projectName.value || 'Untitled Project').trim() || 'Untitled Project';
    const entry = {
      id: uid('proj'),
      name,
      updatedAt: Date.now(),
      layers: state.layers.length,
      previewDataUrl,
      data: payload
    };
    const existingIndex = state.localProjects.findIndex((item) => item.name === name);
    if (existingIndex >= 0) {
      entry.id = state.localProjects[existingIndex].id;
      state.localProjects.splice(existingIndex, 1, entry);
    } else {
      state.localProjects.unshift(entry);
    }
    state.localProjects = state.localProjects
      .sort((a, b) => b.updatedAt - a.updatedAt)
      .slice(0, 30);
    persistLocalProjectBank();
    persistAutosaveNow();
    renderProjectBank();
    setStatus(`Το project σώθηκε μέσα στο app: ${name}`);
  }

  function loadLocalProject(id) {
    const entry = state.localProjects.find((item) => item.id === id);
    if (!entry) return;
    applyProjectData(entry.data || {});
    if (el.projectName) el.projectName.value = entry.name || 'Untitled Project';
    persistAutosaveNow();
    setStatus(`Φορτώθηκε από το app: ${entry.name}. Αν λείπουν media, πάτα Relink.`);
  }

  function deleteLocalProject(id) {
    const idx = state.localProjects.findIndex((item) => item.id === id);
    if (idx < 0) return;
    const [removed] = state.localProjects.splice(idx, 1);
    persistLocalProjectBank();
    renderProjectBank();
    setStatus(`Διαγράφηκε από το app: ${removed.name}.`);
  }


  function openProjectsPanel() {
    if (el.projectBankPanel) {
      el.projectBankPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
      el.projectBankPanel.classList.remove('pulse-outline');
      void el.projectBankPanel.offsetWidth;
      el.projectBankPanel.classList.add('pulse-outline');
      window.setTimeout(() => el.projectBankPanel?.classList.remove('pulse-outline'), 1200);
    }
    if (el.projectSearchInput) {
      el.projectSearchInput.focus();
      el.projectSearchInput.select();
    }
  }

  function setActiveHandle(name) {
    state.activeHandleName = name || null;
    renderInspector();
    renderStage();
  }

  function nudgeActiveHandleOrLayer(dx, dy, options = {}) {
    const layer = getSelectedLayer();
    if (!layer) return;
    const mode = state.activeHandleName;
    const resize = !!options.resize;
    const handleNames = ['tl', 'tr', 'bl', 'br', 'ml', 'mr', 'tm', 'bm'];
    if (mode && !resize && handleNames.includes(mode)) {
      if (!isLayerFreeformMode(layer)) layer.mappingMode = 'pin8';
      moveHandlePoint(layer, mode, dx, dy);
      setStatus(`Nudge point ${mode.toUpperCase()} στο ${layer.name}.`);
    } else if (resize) {
      const bounds = getCornerBounds(layer);
      scaleLayerToBounds(layer, {
        minX: bounds.minX,
        minY: bounds.minY,
        maxX: bounds.maxX + dx,
        maxY: bounds.maxY + dy
      });
      setStatus(`Resize ${layer.name}.`);
    } else {
      moveLayerBy(layer, dx, dy);
      setStatus(`Move ${layer.name}.`);
    }
    renderAll();
  }

  function openProjectorWindow(outputId) {
    const existing = state.projectorWins.get(outputId);
    if (existing && !existing.closed) {
      existing.focus();
      return;
    }
    const outputIndex = Math.max(0, OUTPUTS.indexOf(outputId));
    const left = 60 + outputIndex * 40;
    const top = 60 + outputIndex * 30;
    const features = `left=${left},top=${top},width=1366,height=820,resizable=yes,scrollbars=no,status=no,toolbar=no,menubar=no,location=no`;
    const win = window.open('', `ELLOS_${outputId}`, features);
    if (!win) {
      setStatus(`Το popup για ${outputId} μπλοκαρίστηκε.`);
      return;
    }
    win.document.write(`<!DOCTYPE html>
<html lang="el">
<head>
<meta charset="utf-8">
<title>${outputId} Output</title>
<style>
html,body{margin:0;height:100%;width:100%;background:#000;overflow:hidden}
.stage{position:fixed;inset:0;background:#000}
canvas{display:block;width:100vw;height:100vh;background:#000}
</style>
</head>
<body>
  <div class="stage" id="stageRoot"><canvas id="projectorCanvas" width="${state.width}" height="${state.height}"></canvas></div>
<script>
  const stage = document.getElementById('stageRoot');
  let fakeFs = false;
  let prevRect = null;
  async function toggleFs() {
    try {
      if (document.fullscreenElement) {
        await document.exitFullscreen();
        return;
      }
      if (stage && stage.requestFullscreen) {
        await stage.requestFullscreen();
        return;
      }
    } catch (e) {}
    try {
      if (!fakeFs) {
        prevRect = { x: window.screenX, y: window.screenY, w: window.outerWidth, h: window.outerHeight };
        window.moveTo(0, 0);
        window.resizeTo(screen.availWidth, screen.availHeight);
        fakeFs = true;
      } else if (prevRect) {
        window.moveTo(prevRect.x, prevRect.y);
        window.resizeTo(prevRect.w, prevRect.h);
        fakeFs = false;
      }
    } catch (e) {}
  }
  window.addEventListener('keydown', (event) => {
    if (event.key === 'f' || event.key === 'F') toggleFs();
    if (event.key === 'Escape' && fakeFs && prevRect) {
      try {
        window.moveTo(prevRect.x, prevRect.y);
        window.resizeTo(prevRect.w, prevRect.h);
        fakeFs = false;
      } catch (e) {}
    }
  });
  document.addEventListener('dblclick', toggleFs);
</script>
</body>
</html>`);
    win.document.close();
    win.focus();
    state.projectorWins.set(outputId, win);
    setStatus(`Άνοιξε το ${outputId}. Μετάφερέ το στον projector και μετά πάτα F.`);
  }

  function renderProjectors() {
    for (const outputId of OUTPUTS) {
      const win = state.projectorWins.get(outputId);
      if (!win || win.closed) {
        state.projectorWins.delete(outputId);
        continue;
      }
      const canvas = win.document.getElementById('projectorCanvas');
      if (!canvas) continue;
      if (canvas.width !== state.width || canvas.height !== state.height) {
        canvas.width = state.width;
        canvas.height = state.height;
      }
      const ctx = canvas.getContext('2d');
      drawScene(ctx, outputId, false);
    }
  }

  function renderStage() {
    drawScene(stageCtx, el.stageViewSelect.value, true);
  }

  function renderAll() {
    renderMediaList();
    renderLayersList();
    renderCueList();
    renderCueSettings();
    renderProjectBank();
    renderOutputOps();
    renderInspector();
    renderStageToolStates();
    renderStage();
    renderProjectors();
    updateHistoryButtons();
    scheduleHistoryCapture();
    scheduleAutosave();
  }

  function buildHistorySnapshot() {
    return {
      project: projectToSerializable(),
      view: {
        stageView: el.stageViewSelect?.value || 'ALL',
        snapEnabled: !!state.snapEnabled,
        guidesEnabled: !!state.guidesEnabled,
        mediaLibraryCollapsed: !!state.mediaLibraryCollapsed,
        stageZoom: Number(state.stageZoom || 1)
      },
      selectedLayerId: state.selectedLayerId || '',
      selectedMediaId: state.selectedMediaId || ''
    };
  }

  function historyProjectKey(snapshot) {
    return JSON.stringify(snapshot?.project || {});
  }

  function updateHistoryButtons() {
    const undoDisabled = !state.history || state.history.undoStack.length <= 1;
    const redoDisabled = !state.history || state.history.redoStack.length === 0;
    if (el.undoBtn) {
      el.undoBtn.disabled = undoDisabled;
      el.undoBtn.classList.toggle('is-disabled', undoDisabled);
    }
    if (el.redoBtn) {
      el.redoBtn.disabled = redoDisabled;
      el.redoBtn.classList.toggle('is-disabled', redoDisabled);
    }
  }

  function captureHistoryState(force = false) {
    if (!state.history || state.history.isRestoring) return;
    const snapshot = buildHistorySnapshot();
    const key = historyProjectKey(snapshot);
    if (!force && key === state.history.lastSerialized) {
      updateHistoryButtons();
      return;
    }
    state.history.undoStack.push(snapshot);
    if (state.history.undoStack.length > state.history.max) state.history.undoStack.shift();
    state.history.redoStack = [];
    state.history.lastSerialized = key;
    updateHistoryButtons();
  }

  function scheduleHistoryCapture() {
    if (!state.history || state.history.isRestoring) return;
    if (state.history.timer) window.clearTimeout(state.history.timer);
    state.history.timer = window.setTimeout(() => {
      state.history.timer = null;
      captureHistoryState(false);
    }, 180);
  }

  function applyHistorySnapshot(snapshot, directionLabel) {
    if (!snapshot) return;
    state.history.isRestoring = true;
    try {
      applyProjectData(JSON.parse(JSON.stringify(snapshot.project)));
      if (snapshot.view) {
        if (el.stageViewSelect) el.stageViewSelect.value = snapshot.view.stageView || 'ALL';
        state.snapEnabled = !!snapshot.view.snapEnabled;
        state.guidesEnabled = snapshot.view.guidesEnabled !== false;
        state.mediaLibraryCollapsed = !!snapshot.view.mediaLibraryCollapsed;
        state.stageZoom = sanitizeStageZoom(snapshot.view.stageZoom ?? 1);
        applyMediaPanelState();
        renderStageToolStates();
        applyStageZoom();
      }
      state.selectedLayerId = state.layers.some((layer) => layer.id === snapshot.selectedLayerId) ? snapshot.selectedLayerId : (state.layers[0]?.id || null);
      state.selectedMediaId = state.media.some((item) => item.id === snapshot.selectedMediaId) ? snapshot.selectedMediaId : (state.selectedMediaId || '');
      renderAll();
      state.history.lastSerialized = historyProjectKey(snapshot);
      setStatus(directionLabel);
    } finally {
      state.history.isRestoring = false;
      updateHistoryButtons();
    }
  }

  function undoHistory() {
    if (!state.history || state.history.undoStack.length <= 1) return;
    const current = state.history.undoStack.pop();
    state.history.redoStack.push(current);
    const previous = state.history.undoStack[state.history.undoStack.length - 1];
    applyHistorySnapshot(previous, 'Undo ολοκληρώθηκε.');
  }

  function redoHistory() {
    if (!state.history || !state.history.redoStack.length) return;
    const next = state.history.redoStack.pop();
    state.history.undoStack.push(next);
    applyHistorySnapshot(next, 'Redo ολοκληρώθηκε.');
  }


  function projectToSerializable() {
    return {
      version: 'rebuild-core-v21g-per-video-fx',
      projectName: el.projectName.value || 'Untitled Project',
      canvas: { width: state.width, height: state.height },
      media: state.media.map((item) => ({
        id: item.id,
        type: item.type,
        name: item.name,
        fileName: item.fileName,
        isLiveCamera: !!item.isLiveCamera,
        isLiveScreen: !!item.isLiveScreen,
        deviceId: item.deviceId || '',
        cameraFxMirrorX: !!item.cameraFxMirrorX,
        cameraFxMono: !!item.cameraFxMono,
        cameraFxBoost: !!item.cameraFxBoost,
        cameraFxInvert: !!item.cameraFxInvert,
        effectMirrorX: !!item.effectMirrorX,
        effectMono: !!item.effectMono,
        effectBoost: !!item.effectBoost,
        fxBrightness: clamp(Number(item.fxBrightness ?? 100), 0, 200),
        fxContrast: clamp(Number(item.fxContrast ?? 100), 0, 220),
        fxSaturation: clamp(Number(item.fxSaturation ?? 100), 0, 300),
        fxHue: clamp(Number(item.fxHue ?? 0), -180, 180),
        fxBlur: clamp(Number(item.fxBlur ?? 0), 0, 20),
        edgeSoftness: clamp(Number(item.edgeSoftness ?? 0), 0, 35),
        bgRemoveMode: ['off', 'green', 'blue', 'black', 'white', 'custom'].includes(item.bgRemoveMode) ? item.bgRemoveMode : 'off',
        bgRemoveColor: /^#[0-9a-fA-F]{6}$/.test(String(item.bgRemoveColor || '')) ? String(item.bgRemoveColor) : '#00ff00',
        bgRemoveThreshold: clamp(Number(item.bgRemoveThreshold ?? 70), 0, 180),
        bgRemoveSoftness: clamp(Number(item.bgRemoveSoftness ?? 25), 0, 140),
        mediaZoom: clamp(Number(item.mediaZoom ?? 100), 100, 300),
        textContent: isTextMedia(item) ? String(item.textContent || '') : '',
        textFontSize: isTextMedia(item) ? clamp(Number(item.textFontSize ?? 72), 16, 240) : 72,
        textColor: isTextMedia(item) ? String(item.textColor || '#ffffff') : '#ffffff',
        textBgColor: isTextMedia(item) ? String(item.textBgColor || '#000000') : '#000000',
        textBgOpacity: isTextMedia(item) ? clamp(Number(item.textBgOpacity ?? 0), 0, 100) : 0,
        textAlign: isTextMedia(item) ? String(item.textAlign || 'center') : 'center',
        textBold: isTextMedia(item) ? ([400, 700].includes(Number(item.textBold)) ? Number(item.textBold) : 700) : 700
      })),
      playing: state.playing,
      layers: state.layers.map((layer) => serializeLayer(layer)),
      cues: state.cues.map((cue) => serializeCue(cue)),
      activeCueId: state.activeCueId || '',
      outputOps: serializeOutputOps(),
      showSettings: {
        cueTransitionMode: state.cueTransitionMode || 'cut',
        cueTransitionMs: Math.max(0, Number(state.cueTransitionMs || 0))
      }
    };
  }

  function saveProject() {
    const data = projectToSerializable();
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    const safeName = (el.projectName.value || 'ellos_project').replace(/[^a-z0-9_\-]+/gi, '_');
    a.href = url;
    a.download = `${safeName}.json`;
    a.click();
    persistAutosaveNow();
    setTimeout(() => URL.revokeObjectURL(url), 500);
    setStatus('Το project σώθηκε σε JSON.');
  }

  async function loadProjectFile(file) {
    if (!file) return;
    const text = await file.text();
    const data = JSON.parse(text);
    applyProjectData(data);
    if (el.projectName) el.projectName.value = String(file.name || 'Loaded Project').replace(/\.json$/i, '');
    persistAutosaveNow();
  }

  function applyProjectData(data) {
    state.layers = [];
    state.media = state.media.filter((item) => {
      if (isTextMedia(item) || isPdfMedia(item)) {
        try { if (item.element && item.element.parentNode) item.element.parentNode.removeChild(item.element); } catch (_) {}
        try { if (item.url) URL.revokeObjectURL(item.url); } catch (_) {}
        return false;
      }
      return true;
    });
    state.selectedLayerId = null;
    state.activeHandleName = null;
    state.missingMediaNames.clear();
    state.playing = !!data.playing;
    state.cues = [];
    state.activeCueId = data.activeCueId || null;
    applyOutputOpsData(data.outputOps || {});
    state.cueTransitionMode = sanitizeCueTransitionMode(data.showSettings?.cueTransitionMode || 'cut');
    state.cueTransitionMs = sanitizeCueTransitionMs(data.showSettings?.cueTransitionMs || CUE_TRANSITION_DEFAULTS[state.cueTransitionMode] || 700, state.cueTransitionMode);
    state.cueTransitionActive = null;

    if (data.canvas?.width && data.canvas?.height) {
      setCanvasSize(Number(data.canvas.width), Number(data.canvas.height));
      const preset = `${data.canvas.width}x${data.canvas.height}`;
      if ([...el.canvasPresetSelect.options].some((opt) => opt.value === preset)) {
        el.canvasPresetSelect.value = preset;
      }
    }

    el.projectName.value = data.projectName || 'Untitled Project';

    const mediaIdMap = new Map();
    (data.media || []).forEach((saved) => {
      if (saved.type === 'text') {
        const created = createTextMediaRecord(saved);
        state.media.push(created);
        mediaIdMap.set(saved.id, created.id);
        return;
      }
      if (saved.type === 'pdf') {
        const created = createPdfMediaRecord(saved);
        state.media.push(created);
        mediaIdMap.set(saved.id, created.id);
        return;
      }
      const existing = state.media.find((item) => item.fileName === saved.fileName || item.name === saved.name);
      if (existing) {
        applyDefaultCameraFx(existing, saved);
        applyDefaultClipFx(existing, saved);
        mediaIdMap.set(saved.id, existing.id);
      } else {
        mediaIdMap.set(saved.id, '');
        if (saved.fileName) state.missingMediaNames.add(saved.fileName);
      }
    });

    state.cues = Array.isArray(data.cues) ? data.cues.map((cue) => serializeCue({
      ...cue,
      layers: Array.isArray(cue.layers) ? cue.layers.map((savedLayer) => ({
        ...savedLayer,
        mediaId: mediaIdMap.get(savedLayer.mediaId) || '',
        pendingMediaFileName: savedLayer.pendingMediaFileName || (data.media || []).find((m) => m.id === savedLayer.mediaId)?.fileName || '',
        playlistMediaIds: Array.isArray(savedLayer.playlistMediaIds) ? savedLayer.playlistMediaIds.map((id) => mediaIdMap.get(id) || '').filter(Boolean) : [],
        playlistFileNames: Array.isArray(savedLayer.playlistFileNames) ? savedLayer.playlistFileNames.filter(Boolean) : []
      })) : []
    })) : [];
    state.activeCueId = data.activeCueId || state.cues[0]?.id || null;

    (data.layers || []).forEach((savedLayer) => {
      const layer = createLayer({
        id: savedLayer.id,
        name: savedLayer.name,
        mediaId: mediaIdMap.get(savedLayer.mediaId) || '',
        output: savedLayer.output || 'P1',
        pendingMediaFileName: (data.media || []).find((m) => m.id === savedLayer.mediaId)?.fileName || '',
        playlistMediaIds: Array.isArray(savedLayer.playlistMediaIds) ? savedLayer.playlistMediaIds.map((id, idx) => mediaIdMap.get(id) || '').filter(Boolean) : [],
        pendingPlaylistFileNames: Array.isArray(savedLayer.playlistFileNames) ? savedLayer.playlistFileNames.filter(Boolean) : [],
        sequenceEnabled: !!savedLayer.sequenceEnabled,
        playlistIndex: savedLayer.playlistIndex || 0,
        x: savedLayer.x,
        y: savedLayer.y,
        w: savedLayer.w,
        h: savedLayer.h,
        opacity: savedLayer.opacity,
        rotation: savedLayer.rotation || 0,
        visible: savedLayer.visible,
        locked: savedLayer.locked,
        loop: savedLayer.loop,
        clipTransitionMode: savedLayer.clipTransitionMode || 'cut',
        clipTransitionMs: savedLayer.clipTransitionMs || 0,
        mappingMode: (savedLayer.mappingMode === 'pin6' || savedLayer.mappingMode === 'pin8') ? savedLayer.mappingMode : 'bbox',
        z: savedLayer.z,
        corners: savedLayer.corners || []
      });
    });

    normalizeLayerZ();
    if (el.cueNameInput) {
      const activeCue = state.cues.find((cue) => cue.id === state.activeCueId);
      el.cueNameInput.value = activeCue?.name || '';
    }
    renderAll();
    setStatus('Το project φορτώθηκε. Αν λείπει media, πάτα Relink. Τα live inputs (📷 Camera / 🖥 Screen) ανοίγουν χειροκίνητα.');
  }

  function tryAutoRelinkMissingLayers() {
    state.layers.forEach((layer) => {
      ensureLayerPlaylist(layer);
      if (!layer.mediaId) {
        const fallback = state.media.find((item) => item.ready && item.fileName === layer.pendingMediaFileName);
        if (fallback) {
          setLayerPrimaryMedia(layer, fallback.id);
          layer.pendingMediaFileName = '';
          state.missingMediaNames.delete(fallback.fileName);
        }
      }
      if (Array.isArray(layer.pendingPlaylistFileNames) && layer.pendingPlaylistFileNames.length) {
        const rebuilt = [];
        layer.pendingPlaylistFileNames.forEach((fileName) => {
          const match = state.media.find((item) => item.ready && item.fileName === fileName);
          if (match) rebuilt.push(match.id);
        });
        if (rebuilt.length) {
          layer.playlistMediaIds = rebuilt;
          layer.playlistIndex = Math.max(0, Math.min(layer.playlistIndex || 0, rebuilt.length - 1));
          layer.mediaId = rebuilt[layer.playlistIndex] || rebuilt[0] || layer.mediaId;
        }
      }
    });
  }

  function clamp(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

  function applyTransformStep(action, rawDelta) {
    const layer = getSelectedLayer();
    if (!layer || layer.locked) return;
    ensureLayerCorners(layer);
    const delta = Number(rawDelta) || 0;
    if (!Number.isFinite(delta) || !delta) return;

    const currentX = Number(el.layerXInput?.value ?? layer.x ?? 0) || 0;
    const currentY = Number(el.layerYInput?.value ?? layer.y ?? 0) || 0;
    const currentW = Math.max(MIN_LAYER_SIZE, Number(el.layerWInput?.value ?? layer.width ?? 0) || getLayerSize(layer).width || MIN_LAYER_SIZE);
    const currentH = Math.max(MIN_LAYER_SIZE, Number(el.layerHInput?.value ?? layer.height ?? 0) || getLayerSize(layer).height || MIN_LAYER_SIZE);
    const currentOpacity = clamp(Number(el.layerOpacityInput?.value ?? layer.opacity ?? 1) || 1, 0, 1);

    if (action === 'x') {
      moveLayerBy(layer, delta, 0);
    } else if (action === 'y') {
      moveLayerBy(layer, 0, delta);
    } else if (action === 'w' || action === 'h') {
      const nextW = action === 'w' ? Math.max(MIN_LAYER_SIZE, currentW + delta) : currentW;
      const nextH = action === 'h' ? Math.max(MIN_LAYER_SIZE, currentH + delta) : currentH;
      scaleLayerToBounds(layer, {
        minX: currentX,
        minY: currentY,
        maxX: currentX + nextW,
        maxY: currentY + nextH
      });
    } else if (action === 'scale') {
      const factor = 1 + delta;
      const nextW = Math.max(MIN_LAYER_SIZE, currentW * factor);
      const nextH = Math.max(MIN_LAYER_SIZE, currentH * factor);
      const cx = currentX + currentW / 2;
      const cy = currentY + currentH / 2;
      scaleLayerToBounds(layer, {
        minX: cx - nextW / 2,
        minY: cy - nextH / 2,
        maxX: cx + nextW / 2,
        maxY: cy + nextH / 2
      });
    } else if (action === 'opacity') {
      layer.opacity = clamp(currentOpacity + delta, 0, 1);
    }

    renderInspector();
    renderLayersList();
    renderAll();

    const size = getLayerSize(layer);
    const opacityPct = Math.round(clamp(Number(layer.opacity ?? 1), 0, 1) * 100);
    if (action === 'opacity') setStatus(`${layer.name}: opacity ${opacityPct}%`);
    else if (action === 'scale') setStatus(`${layer.name}: scale ${Math.round(size.width)}x${Math.round(size.height)}`);
    else if (action === 'h') setStatus(`${layer.name}: H ${Math.round(size.height)}`);
    else if (action === 'w') setStatus(`${layer.name}: W ${Math.round(size.width)}`);
  }

  function toggleSelectedCameraFx(key) {
    const layer = getSelectedLayer();
    const media = getLayerActiveMedia(layer);
    if (!layer || !isCameraMedia(media)) {
      setStatus('Διάλεξε layer με live camera για camera FX.');
      return;
    }
    media[key] = !media[key];
    renderInspector();
    renderAll();
    setStatus(`${media.name}: camera FX updated.`);
  }

  function resetSelectedCameraFx() {
    const layer = getSelectedLayer();
    const media = getLayerActiveMedia(layer);
    if (!layer || !isCameraMedia(media)) return;
    applyDefaultCameraFx(media);
    renderInspector();
    renderAll();
    setStatus(`${media.name}: camera FX reset.`);
  }

  function bindCameraFxEvents() {
    el.cameraMirrorBtn?.addEventListener('click', () => toggleSelectedCameraFx('cameraFxMirrorX'));
    el.cameraMonoBtn?.addEventListener('click', () => toggleSelectedCameraFx('cameraFxMono'));
    el.cameraBoostBtn?.addEventListener('click', () => toggleSelectedCameraFx('cameraFxBoost'));
    el.cameraInvertBtn?.addEventListener('click', () => toggleSelectedCameraFx('cameraFxInvert'));
    el.resetCameraFxBtn?.addEventListener('click', resetSelectedCameraFx);
  }

  function bindInspectorEvents() {
    const syncLayerMediaFromInspector = (layer, mediaIdRaw) => {
      if (!layer) return;
      const mediaId = String(mediaIdRaw || '');
      if (!mediaId) {
        setLayerPrimaryMedia(layer, '');
        return;
      }
      const media = getMediaById(mediaId);
      if (!media) return;
      if (media.type === 'video') {
        ensureLayerPlaylist(layer);
        if (!layer.playlistMediaIds.includes(mediaId)) {
          layer.playlistMediaIds.push(mediaId);
          if (!Array.isArray(layer.pendingPlaylistFileNames)) layer.pendingPlaylistFileNames = [];
          layer.pendingPlaylistFileNames.push(media.fileName || media.name || '');
        }
        layer.playlistIndex = Math.max(0, layer.playlistMediaIds.indexOf(mediaId));
        layer.mediaId = mediaId;
        layer.pendingMediaFileName = '';
        if (layer.playlistMediaIds.length > 1) layer.sequenceEnabled = true;
        syncLayerLoopToMedia(layer);
        return;
      }
      setLayerPrimaryMedia(layer, mediaId);
      if (layer.mediaId) layer.pendingMediaFileName = '';
    };

    const update = () => {
      const layer = getSelectedLayer();
      if (!layer) return;
      ensureLayerCorners(layer);
      const prevRotation = Number(layer.rotation || 0);
      layer.name = el.layerNameInput.value;
      layer.output = el.layerOutputInput.value;
      const nextX = Number(el.layerXInput.value);
      const nextY = Number(el.layerYInput.value);
      const nextW = Math.max(1, Number(el.layerWInput.value));
      const nextH = Math.max(1, Number(el.layerHInput.value));
      scaleLayerToBounds(layer, {
        minX: nextX,
        minY: nextY,
        maxX: nextX + nextW,
        maxY: nextY + nextH
      });
      layer.opacity = Number(el.layerOpacityInput.value);
      const nextRotation = Number(el.layerRotationInput.value) || 0;
      const deltaRotation = nextRotation - prevRotation;
      if (Math.abs(deltaRotation) > 0.0001) rotateLayerBy(layer, deltaRotation);
      layer.rotation = nextRotation;
      layer.z = Number(el.layerZInput.value);
      layer.visible = el.layerVisibleInput.checked;
      layer.locked = el.layerLockInput.checked;
      layer.loop = el.layerLoopInput.checked;
      const fxTarget = getLayerFxTarget(layer) || layer;
      fxTarget.fxBrightness = clamp(Number(el.layerBrightnessInput?.value ?? 100), 0, 200);
      fxTarget.fxContrast = clamp(Number(el.layerContrastInput?.value ?? 100), 0, 220);
      fxTarget.fxSaturation = clamp(Number(el.layerSaturationInput?.value ?? 100), 0, 300);
      fxTarget.fxHue = clamp(Number(el.layerHueInput?.value ?? 0), -180, 180);
      fxTarget.fxBlur = clamp(Number(el.layerBlurInput?.value ?? 0), 0, 20);
      fxTarget.edgeSoftness = clamp(Number(el.layerEdgeSoftnessInput?.value ?? 0), 0, 35);
      fxTarget.bgRemoveMode = ['off', 'green', 'blue', 'black', 'white', 'custom'].includes(el.layerBgRemoveModeInput?.value) ? el.layerBgRemoveModeInput.value : 'off';
      fxTarget.bgRemoveColor = /^#[0-9a-fA-F]{6}$/.test(String(el.layerBgRemoveColorInput?.value || '')) ? String(el.layerBgRemoveColorInput.value) : '#00ff00';
      fxTarget.bgRemoveThreshold = clamp(Number(el.layerBgThresholdInput?.value ?? 70), 0, 180);
      fxTarget.bgRemoveSoftness = clamp(Number(el.layerBgSoftnessInput?.value ?? 25), 0, 140);
      fxTarget.mediaZoom = clamp(Number(el.layerMediaZoomInput?.value ?? 100), 100, 300);
      updateFxValueChips(fxTarget);
      const media = getLayerActiveMedia(layer);
      if (media?.type === 'video' && media.element) media.element.loop = layer.loop;
      normalizeLayerZ();
      renderAll();
    };

    [
      el.layerNameInput,
      el.layerOutputInput,
      el.layerXInput,
      el.layerYInput,
      el.layerWInput,
      el.layerHInput,
      el.layerOpacityInput,
      el.layerRotationInput,
      el.layerZInput,
      el.layerVisibleInput,
      el.layerLockInput,
      el.layerLoopInput,
      el.layerBrightnessInput,
      el.layerContrastInput,
      el.layerSaturationInput,
      el.layerHueInput,
      el.layerBlurInput,
      el.layerEdgeSoftnessInput,
      el.layerBgRemoveModeInput,
      el.layerBgRemoveColorInput,
      el.layerBgThresholdInput,
      el.layerBgSoftnessInput,
      el.layerMediaZoomInput
    ].filter(Boolean).forEach((node) => {
      node.addEventListener('input', update);
      node.addEventListener('change', update);
    });

    el.layerMediaInput?.addEventListener('change', () => {
      const layer = getSelectedLayer();
      if (!layer) return;
      syncLayerMediaFromInspector(layer, el.layerMediaInput.value);
      renderInspector();
      renderLayersList();
      renderAll();
      const activeMedia = getLayerActiveMedia(layer);
      if (activeMedia?.type === 'video' && activeMedia.element) activeMedia.element.loop = layer.loop;
      setStatus(`${layer.name}: το clip άλλαξε χωρίς να χαθεί η λίστα videos.`);
    });

    el.layerBgRemoveModeInput?.addEventListener('change', () => {
      const mode = el.layerBgRemoveModeInput.value;
      if (mode !== 'custom' && mode !== 'off' && el.layerBgRemoveColorInput) el.layerBgRemoveColorInput.value = getBgRemovePresetColor(mode);
      update();
    });
    el.layerBgRemoveColorInput?.addEventListener('input', () => {
      if (el.layerBgRemoveModeInput && el.layerBgRemoveModeInput.value !== 'custom' && el.layerBgRemoveModeInput.value !== 'off') el.layerBgRemoveModeInput.value = 'custom';
      update();
    });
    el.resetVideoZoomBtn?.addEventListener('click', () => {
      const layer = getSelectedLayer();
      const target = getLayerFxTarget(layer);
      if (!target) return;
      target.mediaZoom = 100;
      renderInspector();
      renderAll();
      setStatus(`${getLayerFxTargetLabel(layer)}: video zoom 100%.`);
    });
  }

  function triggerTransformStepButton(btn) {
    if (!btn) return;
    const raw = String(btn.dataset.transformStep || '');
    const [action, delta] = raw.split(':');
    applyTransformStep(action, delta);
  }

  function bindTransformStepButtons() {
    const repeatTimers = new WeakMap();

    const clearRepeat = (btn) => {
      const meta = repeatTimers.get(btn);
      if (!meta) return;
      if (meta.delay) window.clearTimeout(meta.delay);
      if (meta.interval) window.clearInterval(meta.interval);
      repeatTimers.delete(btn);
      btn.classList.remove('holding');
    };

    const startRepeat = (btn) => {
      if (!btn || repeatTimers.has(btn)) return;
      triggerTransformStepButton(btn);
      btn.classList.add('holding');
      const meta = { delay: null, interval: null };
      meta.delay = window.setTimeout(() => {
        meta.interval = window.setInterval(() => triggerTransformStepButton(btn), 60);
      }, 280);
      repeatTimers.set(btn, meta);
    };

    const stopAll = () => {
      (el.transformStepBtns || []).forEach((btn) => clearRepeat(btn));
    };

    (el.transformStepBtns || []).forEach((btn) => {
      btn.addEventListener('click', (event) => {
        event.preventDefault();
        if (!btn.classList.contains('holding')) triggerTransformStepButton(btn);
      });
      btn.addEventListener('mousedown', (event) => {
        if (event.button !== 0) return;
        event.preventDefault();
        startRepeat(btn);
      });
      btn.addEventListener('pointerdown', (event) => {
        if (event.pointerType === 'mouse' && event.button !== 0) return;
        event.preventDefault();
        startRepeat(btn);
      });
      btn.addEventListener('mouseup', () => clearRepeat(btn));
      btn.addEventListener('pointerup', () => clearRepeat(btn));
      btn.addEventListener('pointercancel', () => clearRepeat(btn));
      btn.addEventListener('mouseleave', () => clearRepeat(btn));
      btn.addEventListener('blur', () => clearRepeat(btn));
      btn.addEventListener('dragstart', (event) => event.preventDefault());
      btn.addEventListener('touchstart', (event) => {
        event.preventDefault();
        startRepeat(btn);
      }, { passive: false });
      btn.addEventListener('touchend', () => clearRepeat(btn));
      btn.addEventListener('touchcancel', () => clearRepeat(btn));
    });

    document.addEventListener('mouseup', stopAll);
    document.addEventListener('pointerup', stopAll);
    document.addEventListener('touchend', stopAll, { passive: true });
  }

  function bindDropImport() {
    const stop = (event) => {
      event.preventDefault();
      event.stopPropagation();
    };
    ['dragenter', 'dragover'].forEach((type) => {
      document.addEventListener(type, (event) => {
        stop(event);
        setStatus('Άφησε εδώ τα αρχεία για import.');
      });
    });
    ['dragleave', 'dragend'].forEach((type) => {
      document.addEventListener(type, (event) => {
        stop(event);
      });
    });
    document.addEventListener('drop', async (event) => {
      stop(event);
      const files = event.dataTransfer?.files;
      if (files && files.length) {
        await importFiles(files);
      }
    });
  }

  function bindCueSettingsEvents() {
    el.cueTransitionModeInput?.addEventListener('change', () => {
      state.cueTransitionMode = sanitizeCueTransitionMode(el.cueTransitionModeInput.value || 'cut');
      if (state.cueTransitionMode === 'cut') state.cueTransitionMs = 0;
      else if (sanitizeCueTransitionMs(state.cueTransitionMs, state.cueTransitionMode) <= 0) state.cueTransitionMs = CUE_TRANSITION_DEFAULTS[state.cueTransitionMode] || 700;
      else state.cueTransitionMs = sanitizeCueTransitionMs(state.cueTransitionMs, state.cueTransitionMode);
      renderCueSettings();
      setStatus(`Cue transition: ${getCueTransitionSummary(state.cueTransitionMode, state.cueTransitionMs)}.`);
    });
    el.cueTransitionDurationInput?.addEventListener('change', () => {
      state.cueTransitionMs = sanitizeCueTransitionMs(el.cueTransitionDurationInput.value || 0, state.cueTransitionMode || 'cut');
      renderCueSettings();
      setStatus(`Cue transition time: ${state.cueTransitionMs}ms.`);
    });
  }

  function bindUI() {
    el.addMediaBtn.addEventListener('click', () => el.mediaFileInput.click());
    el.addTextBtn?.addEventListener('click', addTextSource);
    el.addCameraBtn?.addEventListener('click', () => { addCameraSource(); });
    el.addScreenBtn?.addEventListener('click', () => { addScreenSource(); });
    el.mediaPanelToggle?.addEventListener('click', () => setMediaLibraryCollapsed(!state.mediaLibraryCollapsed));
    el.snapToggleBtn?.addEventListener('click', () => setSnapEnabled(!state.snapEnabled));
    el.guidesToggleBtn?.addEventListener('click', () => setGuidesEnabled(!state.guidesEnabled));
    el.zoomOutBtn?.addEventListener('click', () => adjustStageZoom(-0.1));
    el.zoomInBtn?.addEventListener('click', () => adjustStageZoom(0.1));
    el.zoomResetBtn?.addEventListener('click', () => setStageZoom(1));
    el.stageZoomInput?.addEventListener('input', () => setStageZoom(Number(el.stageZoomInput.value || 100) / 100, { silent: true }));
    el.stageZoomInput?.addEventListener('change', () => setStageZoom(Number(el.stageZoomInput.value || 100) / 100));
    el.stageWrap?.addEventListener('wheel', (event) => {
      if (!(event.ctrlKey || event.metaKey || event.altKey)) return;
      event.preventDefault();
      const direction = event.deltaY > 0 ? -0.05 : 0.05;
      adjustStageZoom(direction);
    }, { passive: false });
    el.mediaFileInput.addEventListener('change', async (event) => {
      await importFiles(event.target.files);
      event.target.value = '';
    });
    el.layerFileInput?.addEventListener('change', async (event) => {
      const targetLayerId = state.layerImportTargetId;
      state.layerImportTargetId = null;
      await importFiles(event.target.files, { assignToLayerId: targetLayerId });
      event.target.value = '';
    });

    el.addLayerBtn.addEventListener('click', () => {
      createLayer();
      renderAll();
      setStatus('Προστέθηκε νέο κενό layer. Κάνε κλικ σε media για assign στο επιλεγμένο layer.');
    });
    el.duplicateLayerBtn.addEventListener('click', duplicateSelectedLayer);
    el.deleteLayerBtn.addEventListener('click', deleteSelectedLayer);
    el.playAllBtn.addEventListener('click', playAllVideos);
    el.pauseAllBtn.addEventListener('click', pauseAllVideos);
    el.undoBtn?.addEventListener('click', undoHistory);
    el.redoBtn?.addEventListener('click', redoHistory);
    el.saveProjectBtn.addEventListener('click', saveProject);
    el.saveLocalBtn.addEventListener('click', saveProjectInsideApp);
    el.showProjectsBtn?.addEventListener('click', openProjectsPanel);
    el.restoreAutosaveBtn?.addEventListener('click', restoreAutosave);
    el.exportAppBankBtn?.addEventListener('click', exportAppSaveBundle);
    el.importAppBankBtn?.addEventListener('click', () => el.appBankLoadInput?.click());
    el.clearAutosaveBtn?.addEventListener('click', clearAutosave);
    el.saveCueBtn?.addEventListener('click', saveCurrentCue);
    el.prevCueBtn?.addEventListener('click', () => stepCue(-1));
    el.nextCueBtn?.addEventListener('click', () => stepCue(1));
    el.cueActionBtns.forEach((btn) => {
      btn.addEventListener('click', () => {
        const action = btn.dataset.cueAction;
        if (action === 'save') saveCurrentCue();
        else if (action === 'prev') stepCue(-1);
        else if (action === 'next') stepCue(1);
        else if (action === 'go') goActiveCue();
      });
    });
    el.projectSearchInput?.addEventListener('input', renderProjectBank);
    el.appBankLoadInput?.addEventListener('change', async (event) => {
      await importAppSaveBundleFile(event.target.files?.[0]);
      event.target.value = '';
    });
    el.loadProjectBtn.addEventListener('click', () => el.projectLoadInput.click());
    el.projectLoadInput.addEventListener('change', async (event) => {
      await loadProjectFile(event.target.files?.[0]);
      event.target.value = '';
    });
    el.relinkBtn.addEventListener('click', () => el.relinkFileInput.click());
    el.relinkFileInput.addEventListener('change', async (event) => {
      await importFiles(event.target.files, { preferRelink: true });
      event.target.value = '';
    });
    el.canvasPresetSelect.addEventListener('change', () => {
      const [w, h] = el.canvasPresetSelect.value.split('x').map(Number);
      setCanvasSize(w, h);
      setStatus(`Canvas ${w}×${h}`);
    });
    document.querySelectorAll('.output-btn').forEach((btn) => {
      btn.addEventListener('click', () => openProjectorWindow(btn.dataset.output));
    });
    el.outputOpsList?.addEventListener('click', (event) => {
      const btn = event.target.closest('[data-output-action]');
      if (!btn) return;
      const outputId = btn.dataset.outputId;
      const action = btn.dataset.outputAction;
      if (action === 'open') openProjectorWindow(outputId);
      else if (action === 'mode') setOutputPattern(outputId, btn.dataset.outputMode || 'live');
      else if (action === 'freeze') toggleOutputFreeze(outputId);
      else if (action === 'blackout') setOutputBlackout(outputId, !getOutputOps(outputId).blackout);
    });
    el.outputOpsList?.addEventListener('input', (event) => {
      const input = event.target.closest('[data-blend-edge]');
      if (!input) return;
      setOutputBlend(input.dataset.outputId, input.dataset.blendEdge, input.value);
    });
    el.bringFrontBtn.addEventListener('click', bringSelectedFront);
    el.sendBackBtn.addEventListener('click', sendSelectedBack);
    el.fitMediaBtn.addEventListener('click', () => fitLayerToMedia('fit'));
    el.fillMediaBtn.addEventListener('click', () => fitLayerToMedia('fill'));
    el.resetTransformBtn.addEventListener('click', resetSelectedLayerTransform);
    el.resetFxBtn?.addEventListener('click', resetSelectedLayerFx);
    el.textContentInput?.addEventListener('input', () => updateSelectedTextMedia({ textContent: el.textContentInput.value }));
    el.textFontSizeInput?.addEventListener('input', () => updateSelectedTextMedia({ textFontSize: el.textFontSizeInput.value }));
    el.textAlignInput?.addEventListener('change', () => updateSelectedTextMedia({ textAlign: el.textAlignInput.value }));
    el.textColorInput?.addEventListener('input', () => updateSelectedTextMedia({ textColor: el.textColorInput.value }));
    el.textBgColorInput?.addEventListener('input', () => updateSelectedTextMedia({ textBgColor: el.textBgColorInput.value }));
    el.textBgOpacityInput?.addEventListener('input', () => { if (el.textBgOpacityValue) el.textBgOpacityValue.textContent = `${el.textBgOpacityInput.value}%`; updateSelectedTextMedia({ textBgOpacity: el.textBgOpacityInput.value }); });
    el.textBoldInput?.addEventListener('change', () => updateSelectedTextMedia({ textBold: Number(el.textBoldInput.value || 700) }));
    el.resetTextBtn?.addEventListener('click', () => { const layer = getSelectedLayer(); const media = getLayerActiveMedia(layer) || getMediaById(state.selectedMediaId); if (!isTextMedia(media)) return; applyDefaultTextStyle(media); renderTextMediaCanvas(media); renderAll(); setStatus(`${media.name}: reset text style.`); });
    el.quickOutputBtns.forEach((btn) => {
      btn.addEventListener('click', () => assignSelectedLayerToOutput(btn.dataset.quickOutput));
    });
    el.handleNudgeBtns.forEach((btn) => {
      btn.addEventListener('click', () => {
        const value = btn.dataset.handleSelect;
        setActiveHandle(value === 'none' ? null : value);
      });
    });
  }

  function bindKeyboardShortcuts() {
    document.addEventListener('keydown', (event) => {
      const tag = String(event.target?.tagName || '').toLowerCase();
      const editing = tag === 'input' || tag === 'textarea' || tag === 'select' || event.target?.isContentEditable;
      if (event.ctrlKey || event.metaKey) {
        const key = event.key.toLowerCase();
        if (key === 'z') {
          event.preventDefault();
          if (event.shiftKey) redoHistory();
          else undoHistory();
          return;
        }
        if (key === 'y') {
          event.preventDefault();
          redoHistory();
          return;
        }
        if (key === 's') {
          event.preventDefault();
          if (event.shiftKey) saveProject();
          else saveProjectInsideApp();
          return;
        }
        if (key === 'p') {
          event.preventDefault();
          openProjectsPanel();
          return;
        }
      }
      if (editing) return;
      if ((event.key === '+' || event.key === '=')) {
        event.preventDefault();
        adjustStageZoom(0.1);
        return;
      }
      if (event.key === '-' || event.key === '_') {
        event.preventDefault();
        adjustStageZoom(-0.1);
        return;
      }
      if (event.key === '0') {
        event.preventDefault();
        setStageZoom(1);
        return;
      }
      if (event.repeat) return;
      if (event.code === 'Space') {
        event.preventDefault();
        if (state.playing) pauseAllVideos();
        else playAllVideos();
        return;
      }
      if (event.key === 'PageUp') {
        event.preventDefault();
        stepCue(-1);
        return;
      }
      if (event.key === 'PageDown') {
        event.preventDefault();
        stepCue(1);
        return;
      }
      if (event.key === 'F9') {
        event.preventDefault();
        saveCurrentCue();
        return;
      }
      if (event.key.toLowerCase() === 'b') {
        event.preventDefault();
        setAllBlackout(!OUTPUTS.every((outputId) => getOutputOps(outputId).blackout));
        return;
      }
      if (event.key.toLowerCase() === 'f') {
        event.preventDefault();
        if (event.shiftKey) {
          setAllFreeze(!OUTPUTS.every((outputId) => getOutputOps(outputId).frozen));
        } else {
          toggleSelectedLayerFullscreen();
        }
        return;
      }
      if (event.key.toLowerCase() === 'n') {
        event.preventDefault();
        setSnapEnabled(!state.snapEnabled);
        return;
      }
      if (event.key.toLowerCase() === 'g') {
        event.preventDefault();
        setGuidesEnabled(!state.guidesEnabled);
        return;
      }
      if (/^[1-5]$/.test(event.key)) {
        assignSelectedLayerToOutput(`P${event.key}`);
        return;
      }
      const step = event.shiftKey ? 10 : 1;
      if (event.key === 'ArrowLeft') {
        event.preventDefault();
        nudgeActiveHandleOrLayer(-step, 0, { resize: event.altKey });
      } else if (event.key === 'ArrowRight') {
        event.preventDefault();
        nudgeActiveHandleOrLayer(step, 0, { resize: event.altKey });
      } else if (event.key === 'ArrowUp') {
        event.preventDefault();
        nudgeActiveHandleOrLayer(0, -step, { resize: event.altKey });
      } else if (event.key === 'ArrowDown') {
        event.preventDefault();
        nudgeActiveHandleOrLayer(0, step, { resize: event.altKey });
      } else if (event.key === '[' || event.key === ']') {
        const layer = getSelectedLayer();
        if (!layer) return;
        const rotStep = event.shiftKey ? 5 : 1;
        const delta = event.key === '[' ? -rotStep : rotStep;
        rotateLayerBy(layer, delta);
        setStatus(`Rotation ${layer.name}: ${Math.round(layer.rotation)}°`);
        renderAll();
      } else if (event.key === 'm' || event.key === 'M') {
        const layer = getSelectedLayer();
        if (!layer) return;
        layer.mappingMode = isLayerFreeformMode(layer) ? 'bbox' : 'pin8';
        state.activeHandleName = null;
        renderAll();
        setStatus(`${layer.name}: ${isLayerFreeformMode(layer) ? '8-point mapping ON' : 'bbox resize ON'}.`);
      }
    });
  }

  function animationLoop(ts) {
    if (ts - state.lastTick > 15) {
      processLayerSequences();
      processLayerClipTransitions();
      clearCueTransitionIfExpired();
      renderStage();
      renderProjectors();
      renderLayerMonitorCanvases();
      state.lastTick = ts;
    }
    requestAnimationFrame(animationLoop);
  }

  function seedDemo() {
    createLayer({ name: 'Layer 1', output: 'P1', x: 90, y: 90, w: 420, h: 240 });
    createLayer({ name: 'Layer 2', output: 'P2', x: 560, y: 140, w: 420, h: 240 });
    state.selectedLayerId = state.layers[0]?.id || null;
  }

  function init() {
    loadUiPrefs();
    loadLocalProjectBank();
    loadAutosaveMeta();
    bindUI();
    bindDropImport();
    bindInspectorEvents();
    bindCameraFxEvents();
    bindTransformStepButtons();
    bindStageInteraction();
    bindKeyboardShortcuts();
    bindCueSettingsEvents();
    seedDemo();
    applyMediaPanelState();
    applyStageZoom();
    renderAll();
    captureHistoryState(true);
    state.persistenceReady = true;
    updateAutosaveInfo();
    requestAnimationFrame(animationLoop);
    setStatus(`Καθαρή rebuild βάση ${APP_VERSION} έτοιμη. layer preview monitor + focus + visible clip services + autoplay sequence.`);
  }

  init();
})();
