/* HTML5 & Rich Media Creator v3.1 */
(function($){
  const STORAGE_KEY = "rmhc:project:v1";
  const BACKUP_KEY = "rmhc:backup:v1";
  const AUTOSAVE_DEBOUNCE = 500;
  const HISTORY_LIMIT = 50;
  const GRID_SIZE = 8;
  const ICONS = ["bxs-star","bx-star","bxs-heart","bx-heart","bxs-like","bx-like","bxs-dislike","bx-dislike","bxs-bell","bx-bell","bxs-bolt","bx-bolt","bxs-bulb","bx-bulb","bxs-rocket","bx-rocket","bxs-gift","bx-gift","bxs-badge-check","bx-badge-check","bxs-check-circle","bx-check-circle","bxs-x-circle","bx-x-circle","bxs-info-circle","bx-info-circle","bxs-error","bx-error","bxs-happy","bx-happy","bxs-sad","bx-sad","bxs-time","bx-time","bxs-calendar","bx-calendar","bxs-coupon","bx-coupon","bxs-cart","bx-cart","bxs-dollar-circle","bx-dollar-circle","bxs-credit-card","bx-credit-card","bxs-download","bx-download","bxs-upload","bx-upload","bxs-share","bx-share","bxs-share-alt","bx-share-alt","bxs-play","bx-play","bxs-pause","bx-pause","bxs-stop","bx-stop","bxs-chevron-left","bx-chevron-left","bxs-chevron-right","bx-chevron-right","bxs-chevron-up","bx-chevron-up","bxs-chevron-down","bx-chevron-down","bxs-map","bx-map","bxs-pin","bx-pin","bxs-phone","bx-phone","bxs-envelope","bx-envelope","bxs-user","bx-user","bxs-group","bx-group","bxs-cog","bx-cog","bxs-camera","bx-camera","bxs-image","bx-image","bxs-video","bx-video","bxs-music","bx-music","bxs-printer","bx-printer","bxs-leaf","bx-leaf","bxs-hot","bx-hot","bxs-trophy","bx-trophy","bxs-trending-up","bx-trending-up","bxs-trending-down","bx-trending-down"];

const I18N = {
    pl:{new:"Nowy",import_json:"Import JSON",save:"Zapisz",autosave:"Zapisuj automatycznie",export_json:"Export JSON",export_html:"Export HTML",zoom:"Zoom:",grid:"Siatka",snap:"Przyciąganie",help:"Pomoc",undo:"Cofnij (Ctrl+Z)",redo:"Ponów (Ctrl+Y)",layers:"Warstwy",add_text:"+ Tekst",add_image:"+ Obraz",add_icon:"+ Ikona",canvas:"Pole robocze",width:"W:",height:"H:",background:"Tło",bg_type_transparent:"Przezroczyste",bg_type_color:"Kolor",bg_type_image:"Obraz",color:"Kolor:",upload:"Upload",fit:"Fit:",repeat:"Repeat:",position:"Position:",timeline_time:"Czas:",no_selection:"Brak zaznaczenia",element:"Element",name:"Nazwa:",x:"X:",y:"Y:",w:"Szer.:",h:"Wys.:",rotate:"Obrót:",opacity:"Opacity:",pad_tb:"Padding TB:",pad_lr:"Padding LR:",radius:"Radius:",background2:"Tło:",transparent:"przeźroczyste",text:"Tekst",font:"Font:",size_px:"Rozm. (px):",weight:"Waga:",line:"Linia:",track:"Litery:",text_color:"Kolor:",align:"Align:",note_edit:"Podwójny klik na elemencie, aby edytować tekst.",image:"Obraz",url:"URL:",position2:"Pozycja:",icon:"Ikona (Boxicons)",icon_name:"Nazwa:",icon_size:"Rozmiar (px):",icon_color:"Kolor:",link:"Link",href:"href:",target:"target:",clicktag:"clickTag:",clickfn:"clickFn:",datalink:"data-link:",animations:"Animacje",effect:"Efekt",start_ms:"Start (ms)",end_ms:"Koniec (ms)",duration_ms:"Czas (ms)",delay_ms:"Delay (ms)",iter:"Powtórzenia",add_clip:"Dodaj klip",delete:"Usuń",restored:"Przywrócono z kopii zapasowej",saved:"Zapisano do localStorage"},
    en:{new:"New",import_json:"Import JSON",save:"Save",autosave:"Autosave",export_json:"Export JSON",export_html:"Export HTML",zoom:"Zoom:",grid:"Grid",snap:"Snap",help:"Help",undo:"Undo (Ctrl+Z)",redo:"Redo (Ctrl+Y)",layers:"Layers",add_text:"+ Text",add_image:"+ Image",add_icon:"+ Icon",canvas:"Canvas",width:"W:",height:"H:",background:"Background",bg_type_transparent:"Transparent",bg_type_color:"Color",bg_type_image:"Image",color:"Color:",upload:"Upload",fit:"Fit:",repeat:"Repeat:",position:"Position:",timeline_time:"Time:",no_selection:"No selection",element:"Element",name:"Name:",x:"X:",y:"Y:",w:"Width:",h:"Height:",rotate:"Rotate:",opacity:"Opacity:",pad_tb:"Padding TB:",pad_lr:"Padding LR:",radius:"Radius:",background2:"Background:",transparent:"transparent",text:"Text",font:"Font:",size_px:"Size (px):",weight:"Weight:",line:"Line:",track:"Tracking:",text_color:"Color:",align:"Align:",note_edit:"Double-click the element to edit text.",image:"Image",url:"URL:",position2:"Position:",icon:"Icon (Boxicons)",icon_name:"Name:",icon_size:"Size (px):",icon_color:"Color:",link:"Link",href:"href:",target:"target:",clicktag:"clickTag:",clickfn:"clickFn:",datalink:"data-link:",animations:"Animations",effect:"Effect",start_ms:"Start (ms)",end_ms:"End (ms)",duration_ms:"Duration (ms)",delay_ms:"Delay (ms)",iter:"Iterations",add_clip:"Add clip",delete:"Delete",restored:"Restored from backup",saved:"Saved to localStorage"}
  };
  let LANG = localStorage.getItem("rmhc:lang") || "en";
  function applyLang(){
    const t = I18N[LANG];
    $("#btn-new").text(t.new);
    $(".btn-file:contains('Import JSON')").contents().first()[0].textContent = t.import_json + " ";
    $("#btn-save").text(t.save);
    $("label:has(#auto-save)").contents().first()[0].textContent = t.autosave + " ";
    $("#btn-export-json").text(t.export_json);
    $("#btn-export-html").text(t.export_html);
    $("label:has(#zoom)")[0].childNodes[0].nodeValue = t.zoom + " ";
    $("label:has(#gridToggle)").contents().first()[0].textContent = t.grid + " ";
    $("label:has(#snapToggle)").contents().first()[0].textContent = t.snap + " ";
    $("#btn-help").attr("title", t.help);
    $("#btn-undo").attr("title", t.undo);
    $("#btn-redo").attr("title", t.redo);
    $("#btn-lang").text(LANG.toUpperCase());

    // Left sidebar
    $(".sidebar.left h3").eq(0).text(t.layers);
    $("#add-text").text(t.add_text);
    $("#add-image").text(t.add_image);
    $("#add-icon").text(t.add_icon);
    $(".sidebar.left h3").eq(1).text(t.canvas);
    $("label:has(#canvas-w)").contents().first()[0].textContent = t.width + " ";
    $("label:has(#canvas-h)").contents().first()[0].textContent = t.height + " ";
    $(".sidebar.left h4:contains('Tło'), .sidebar.left h4:contains('Background')").text(t.background);
    $("#bg-type option[value='transparent']").text(t.bg_type_transparent);
    $("#bg-type option[value='color']").text(t.bg_type_color);
    $("#bg-type option[value='image']").text(t.bg_type_image);
    $("#bg-color-wrap").contents().first()[0].textContent = t.color + " ";
    $("#bg-image-wrap .btn-file.small").contents().first()[0].textContent = t.upload + " ";
    $("label:has(#bg-fit)").contents().first()[0].textContent = t.fit + " ";
    $("label:has(#bg-repeat)").contents().first()[0].textContent = t.repeat + " ";
    $("label:has(#bg-position)").contents().first()[0].textContent = t.position + " ";
    // Timeline
    $("label:has(#tl-now)").contents().first()[0].textContent = t.timeline_time + " ";
    // Right sidebar
    $(".sidebar.right h3:contains('Element'), .sidebar.right h3:contains('Element')").text(t.element);
    $("#no-selection").text(t.no_selection);
    $("label:has(#prop-name)").contents().first()[0].textContent = t.name + " ";
    $("label:has(#prop-x)").contents().first()[0].textContent = t.x + " ";
    $("label:has(#prop-y)").contents().first()[0].textContent = t.y + " ";
    $("label:has(#prop-w)").contents().first()[0].textContent = t.w + " ";
    $("label:has(#prop-h)").contents().first()[0].textContent = t.h + " ";
    $("label:has(#prop-rot)").contents().first()[0].textContent = t.rotate + " ";
    $("label:has(#prop-op)").contents().first()[0].textContent = t.opacity + " ";
    $("label:has(#prop-pad-tb)").contents().first()[0].textContent = t.pad_tb + " ";
    $("label:has(#prop-pad-lr)").contents().first()[0].textContent = t.pad_lr + " ";
    $("label:has(#prop-radius)").contents().first()[0].textContent = t.radius + " ";
    $("#prop-bg").prev().text(t.background2 + " ");
    if($("#prop-bg-transp")[0]) $("#prop-bg-transp")[0].nextSibling.nodeValue = " " + t.transparent;

    $("#text-props h4").text(t.text);
    $("label:has(#text-font)").contents().first()[0].textContent = t.font + " ";
    $("label:has(#text-size)").contents().first()[0].textContent = t.size_px + " ";
    $("label:has(#text-weight)").contents().first()[0].textContent = t.weight + " ";
    $("label:has(#text-line)").contents().first()[0].textContent = t.line + " ";
    $("label:has(#text-track)").contents().first()[0].textContent = t.track + " ";
    $("label:has(#text-color)").contents().first()[0].textContent = t.text_color + " ";
    $("label:has(#text-align)").contents().first()[0].textContent = t.align + " ";
    $(".note:contains('Podwójny'), .note:contains('Double-click')").text(t.note_edit);

    $("#image-props h4").text(t.image);
    $("label:has(#img-url)").contents().first()[0].textContent = t.url + " ";
    $("label:has(#img-fit)").contents().first()[0].textContent = t.fit + " ";
    $("label:has(#img-pos)").contents().first()[0].textContent = t.position2 + " ";

    $("#icon-props h4").text(t.icon);
    $("label:has(#icon-name)").contents().first()[0].textContent = t.icon_name + " ";
    $("label:has(#icon-size)").contents().first()[0].textContent = t.icon_size + " ";
    $("label:has(#icon-color)").contents().first()[0].textContent = t.icon_color + " ";

    $("#link-props h4").text(t.link);
    $("label:has(#link-href)").contents().first()[0].textContent = t.href + " ";
    $("label:has(#link-target)").contents().first()[0].textContent = t.target + " ";
    $("label:has(#link-clicktag)").contents().first()[0].textContent = t.clicktag + " ";
    $("label:has(#link-clickfn)").contents().first()[0].textContent = t.clickfn + " ";
    $("label:has(#link-datalink)").contents().first()[0].textContent = t.datalink + " ";

    $("#anim-props h4").text(t.animations);
    $("label:has(#anim-name)").contents().first()[0].textContent = t.effect + " ";
    $("label:has(#anim-start)").contents().first()[0].textContent = t.start_ms + " ";
    $("label:has(#anim-end)").contents().first()[0].textContent = t.end_ms + " ";
    $("label:has(#anim-duration)").contents().first()[0].textContent = t.duration_ms + " ";
    $("label:has(#anim-delay)").contents().first()[0].textContent = t.delay_ms + " ";
    $("label:has(#anim-iter)").contents().first()[0].textContent = t.iter + " ";
    $("#anim-add").text(t.add_clip);
    $("#anim-list .anim-del").text(t.delete);

    $("#toast").data("i18n-saved", t.saved).data("i18n-restored", t.restored);
  }
  function setLang(next){ LANG = next; localStorage.setItem("rmhc:lang", LANG); applyLang(); }

  let project = null;
  let selectedId = null;
  let history = [];
  let future = [];
  let autosaveTimer = null;
  let isPlaying = false;
  let playTimer = null;
  let playTasks = [];
  let autoSaveTimer = null;
  let clipDragState = null; // v3.5: state for dragging timeline clips

  let currentAnimIdx = null; // v3.4: currently edited animation index (null = new)


  function uuid(){ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    const r = Math.random()*16|0, v = c === 'x' ? r : (r&0x3|0x8);
    return v.toString(16);
  });}

  function nowISO(){ return new Date().toISOString(); }

  function toast(msg){
    const t = $("#toast");
    t.text(msg).addClass("show");
    setTimeout(()=>t.removeClass("show"), 1600);
  }

  function defaultProject(){
    return {
      meta: { version: "2.5.0", title: "creator", createdAt: nowISO(), updatedAt: nowISO() },
      canvas: {
        width: 300, height: 250,
        background: { type: "transparent", color: "#ffffff", image: { src: "", fit: "cover", position: "center center", repeat: "no-repeat" } }
      },
      settings: { grid: true, snap: true, loop: true, totalDuration: 0, fontFamilies: ["Inter:400,600,800"] },
      layers: []
    };
  }

  function pushHistory(){
    history.push(JSON.stringify(project));
    if(history.length > HISTORY_LIMIT) history.shift();
    future = [];
  }

  function loadFromStorage(){
    const backup = localStorage.getItem(BACKUP_KEY);
    const saved = localStorage.getItem(STORAGE_KEY);
    if(backup && (!saved || backup.length > saved.length)){
      project = JSON.parse(backup);
      toast($("#toast").data("i18n-restored")||"Restored");
    } else if(saved){
      project = JSON.parse(saved);
    } else {
      project = defaultProject();
    }
    pushHistory();
    renderAll();
    const autoOn = localStorage.getItem("rmhc:autoSaveEnabled")==="1";
    $("#auto-save").prop("checked", autoOn);
    setAutoSaveEnabled(autoOn);
  }

  function autosave(){
    if(autosaveTimer) clearTimeout(autosaveTimer);
    autosaveTimer = setTimeout(()=>{
      project.meta.updatedAt = nowISO();
      localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
      localStorage.setItem(BACKUP_KEY, JSON.stringify(project));
    }, AUTOSAVE_DEBOUNCE);
  }

  function setAutoSaveEnabled(on){
    if(autoSaveTimer){ clearInterval(autoSaveTimer); autoSaveTimer = null; }
    if(on){
      autoSaveTimer = setInterval(()=>{
        $("#btn-save").trigger("click");
      }, 60000);
    }
    localStorage.setItem("rmhc:autoSaveEnabled", on ? "1" : "0");
  }

  
  // v3.6: ensure tl-total is at least the maximum animation end time
  function enforceTimelineTotalMin(){
    if(!project || !project.settings) return;
    if(typeof getMaxTimeline !== "function") return;
    const maxEnd = getMaxTimeline() || 0;
    let current = project.settings.totalDuration || 0;
    if(maxEnd > current){
      project.settings.totalDuration = maxEnd;
      $("#tl-total").val(maxEnd || "");
    }
  }

function markChange(){
    pushHistory();
    autosave();
    enforceTimelineTotalMin();
  }

  function setCanvasSize(){
    const w = project.canvas.width;
    const h = project.canvas.height;
    const $c = $("#canvas");
    $c.css({ width: w+"px", height: h+"px" });
  }

  function applyCanvasBackground(){
    const bg = project.canvas.background;
    const $c = $("#canvas");
    if(bg.type === "transparent"){
      $c.css({ backgroundColor: "transparent", backgroundImage: "" });
    } else if(bg.type === "color"){
      $c.css({ backgroundColor: bg.color, backgroundImage: "" });
    } else if(bg.type === "image"){
      $c.css({
        backgroundColor: "transparent",
        backgroundImage: `url('${bg.image.src}')`,
        backgroundSize: bg.image.fit,
        backgroundRepeat: bg.image.repeat,
        backgroundPosition: bg.image.position
      });
    }
  }

  function layerTemplate(layer){
    return $(`<div class="el ${layer.type}" data-id="${layer.id}" tabindex="0">
      <div class="rotate" title="Obrót"></div>
      <div class="resize nw"></div>
      <div class="resize ne"></div>
      <div class="resize sw"></div>
      <div class="resize se"></div>
      <div class="content"></div>
    </div>`);
  }

  
function applyTextStyles($el, layer){
    if(layer.type !== "text") return;
    const t = layer.text || {};
    const $te = $el.find(".text-edit");
    const align = t.align || "left";
    let justify = "flex-start";
    if(align === "center"){ justify = "center"; }
    else if(align === "right"){ justify = "flex-end"; }
    $te.css({
      "font-family": (t.fontFamily || "Inter") + ",sans-serif",
      "font-size": (t.fontSize || 18) + "px",
      "font-weight": (t.fontWeight || 600),
      "line-height": (t.lineHeight || 1.3),
      "letter-spacing": (t.letterSpacing || 0) + "px",
      "color": (t.color || "#111111"),
      "text-align": align,
      "width": "100%",
      "height": "100%",
      "display": "flex",
      "align-items": "center",
      "justify-content": justify
    });
  }


function renderLayers(){
    const $c = $("#canvas").empty();
    project.layers.forEach(layer => {
      const $el = layerTemplate(layer);
      $el.toggleClass("locked", !!layer.locked);
      applyLayerStyles($el, layer);
      if(layer.type === "text"){
        const html = layer.text?.html || "Text";
        $el.find(".content").html(`<div class="text-edit" contenteditable="false">${html}</div>`);
        applyTextStyles($el, layer);
      } else if(layer.type === "image"){
        const src = layer.media?.src || "";
        const fit = layer.media?.fit || "cover";
        const pos = layer.media?.position || "center";
        $el.find(".content").html(`<img src="${src}" alt="" style="width:100%;height:100%;object-fit:${fit};object-position:${pos}">`);
      } else if(layer.type === "video"){
        const src = layer.video?.src || "";
        const ratio = layer.video?.ratio || "";
        const autoplay = !!layer.video?.autoplay;
        const controls = layer.video?.controls !== false;
        const loop = !!layer.video?.loop;
        const isYouTube = src.includes("youtube.com") || src.includes("youtu.be");
        const isVimeo = src.includes("vimeo.com");
        if (!src){
          $el.find(".content").html(`<div class="video-placeholder">Dodaj URL wideo w panelu właściwości.</div>`);
        } else if(isYouTube || isVimeo){
          let embedUrl = src;
          let videoId = "";
          if(isYouTube){
            if(src.includes("watch?v=")){
              videoId = src.split("watch?v=")[1].split("&")[0];
              embedUrl = "https://www.youtube.com/embed/" + videoId;
            } else if(src.includes("youtu.be/")){
              videoId = src.split("youtu.be/")[1].split(/[?&]/)[0];
              embedUrl = "https://www.youtube.com/embed/" + videoId;
            }
            const params = [];
            if(autoplay){ params.push("autoplay=1"); }
            if(loop && videoId){ params.push("loop=1","playlist="+videoId); }
            if(!controls){ params.push("controls=0"); }
            if(params.length){
              embedUrl += (embedUrl.includes("?") ? "&" : "?") + params.join("&");
            }
          } else if(isVimeo){
            const m = src.match(/vimeo\.com\/(\d+)/);
            if(m) videoId = m[1];
            embedUrl = "https://player.vimeo.com/video/" + (videoId||"");
            const params = [];
            if(autoplay){ params.push("autoplay=1"); }
            if(loop){ params.push("loop=1"); }
            if(params.length){
              embedUrl += (embedUrl.includes("?") ? "&" : "?") + params.join("&");
            }
          }
          $el.find(".content").html(`
            <div class="video-embed">
              <iframe src="${embedUrl}" frameborder="0" allowfullscreen style="width:100%;height:100%;"></iframe>
            </div>
          `);
        } else {
          // direct video URL
          let attrs = 'style="width:100%;height:100%;object-fit:contain;"';
          if(autoplay){ attrs += ' autoplay'; }
          if(controls){ attrs += ' controls'; }
          if(loop){ attrs += ' loop'; }
          $el.find(".content").html(`
            <video src="${src}" ${attrs}></video>
          `);
        }
      } else if(layer.type === "icon"){
        const name = layer.icon?.name || "bxs-star";
        const size = layer.icon?.size || 48;
        const color = layer.icon?.color || "#111111";
        $el.find(".content").html(`<i class="bx ${name}" style="font-size:${size}px;color:${color}"></i>`);
      }
      // v5.0: mark layers without animations and those starting at t=0
      const anims = layer.animations || [];
      if (!anims.length){
        $el.addClass("noAnim");
      } else {
        const hasStartZero = anims.some(a => (a.start || 0) === 0);
        if (hasStartZero){
          $el.addClass("firstAnim");
        }
      }
      $c.append($el);
    });
    bindElementInteractions();
    renderLayerList();
    renderClips();
  }

  function applyLayerStyles($el, layer){
    const p = layer.position, s = layer.style;
    $el.css({
      left: p.x + "px",
      top: p.y + "px",
      width: p.width + "px",
      height: p.height + "px",
      opacity: p.opacity ?? 1,
      transform: `rotate(${p.rotation||0}deg)`,
      background: s.background === "transparent" || s.background === "" ? "transparent" : s.background,
      paddingTop: (s.padding?.tb||0)+"px",
      paddingBottom: (s.padding?.tb||0)+"px",
      paddingLeft: (s.padding?.lr||0)+"px",
      paddingRight: (s.padding?.lr||0)+"px",
      borderRadius: (s.borderRadius||0)+"px",
      zIndex: s.zIndex || 0
    });
    if(layer.visible === false) $el.css("display","none");
  }

  function renderLayerList(){
    const $list = $("#layers").empty();
    [...project.layers].reverse().forEach(layer => {
      const $li = $(`<li class="layer-item" data-id="${layer.id}">
        <span class="drag">≡</span>
        <input class="layer-name" value="${layer.name||layer.type}"/>
        <div class="mini">
          <button class="toggle-vis" title="Show/hide">${layer.visible===false?"🙈":"👁️"}</button>
          <button class="toggle-lock" title="Lock/unlock">${layer.locked?"🔒":"🔓"}</button>
          <button class="dup" title="Duplicate">⎘</button>
          <button class="del" title="Delete">🗑️</button>
        </div>
      </li>`);
      $list.append($li);
    });
  }

  
  // ---- v1.75: drag sorting of layers in the list ----
  (function(){
    let dragging = null;
    let $ghost = null;
    let $ph = null;
    let startY = 0;
    let grabDy = 0;
    let listTop = 0;
    let $list = $("#layers");

    function onMove(e){
      if(!$ghost) return;
      const mouseY = e.pageY;
      const top = mouseY - listTop - grabDy + $list.scrollTop();
      $ghost.css({ top: Math.max(0, top) + "px" }); // 0

      // find target position for placeholder
      let inserted = false;
      $list.children(".layer-item").not(".dragging,.placeholder").each(function(){
        const $it = $(this);
        const mid = $it.position().top + $it.outerHeight()/2;
        if(top < mid && !inserted){
          $ph.insertBefore($it);
          inserted = True;
        }
      });
      if(!inserted){
        $ph.appendTo($list);
      }
    }

    function applyNewOrder(){
      // Build UI order from top to bottom (excluding placeholder)
      const uiOrder = $list.children(".layer-item").not(".placeholder").map(function(){ return $(this).data("id"); }).get();
      // Map UI order to project.layers order: UI top is visual topmost -> should be last in array.
      const idToLayer = {}; project.layers.forEach(l => idToLayer[l.id] = l);
      const newArr = [];
      // from bottom to top -> reverse uiOrder
      uiOrder.slice().reverse().forEach(id => { if(idToLayer[id]) newArr.push(idToLayer[id]); });
      project.layers = newArr;
      // normalize z-index
      project.layers.forEach((l, idx) => { l.style = l.style || {}; l.style.zIndex = idx; });
      markChange();
      renderLayers();
      if(selectedId) selectLayer(selectedId);
    }

    $list.on("mousedown", ".drag", function(e){
      e.preventDefault();
      e.stopPropagation();
      const $item = $(this).closest(".layer-item");
      dragging = $item;
      const h = $item.outerHeight();
      $ph = $("<li class='layer-item placeholder'/>").css({height:h});
      $item.after($ph);

      const pos = $item.position();
      listTop = $list.offset().top;
      grabDy = e.pageY - ($item.offset().top);
      $ghost = $item.addClass("dragging").css({ top: pos.top, width: $list.innerWidth() });

      $(document).on("mousemove.sort", onMove);
      $(document).on("mouseup.sort", function(){
        $(document).off(".sort");
        if(!$ghost) return;
        $ghost.removeClass("dragging").removeAttr("style");
        $ph.replaceWith($ghost);
        $ghost = null; $ph = null; dragging = null;
        applyNewOrder();
      });
    });
  })();
function renderProps(){
    const layer = getSelectedLayer();
    if(!layer){ $("#no-selection").show(); $("#props").addClass("hidden"); return; }
    $("#no-selection").hide(); $("#props").removeClass("hidden");
    currentAnimIdx = null; // v3.4: reset animation edit index on layer change
    $("#prop-name").val(layer.name||"");
    $("#prop-x").val(layer.position.x);
    $("#prop-y").val(layer.position.y);
    $("#prop-w").val(layer.position.width);
    $("#prop-h").val(layer.position.height);
    $("#prop-rot").val(layer.position.rotation||0);
    $("#prop-op").val(layer.position.opacity??1);
    $("#prop-pad-tb").val(layer.style.padding?.tb||0);
    $("#prop-pad-lr").val(layer.style.padding?.lr||0);
    $("#prop-radius").val(layer.style.borderRadius||0);
    const transparent = (layer.style.background==="transparent"||!layer.style.background);
    $("#prop-bg-transp").prop("checked", transparent);
    $("#prop-bg").val(transparent ? "#ffffff" : layer.style.background);
    $("#text-props,#image-props,#video-props,#icon-props").addClass("hidden");
    if(layer.type==="text"){
      $("#text-props").removeClass("hidden");
      $("#text-font").val(layer.text.fontFamily||"Inter");
      $("#text-size").val(layer.text.fontSize||18);
      $("#text-weight").val(layer.text.fontWeight||600);
      $("#text-line").val(layer.text.lineHeight||1.3);
      $("#text-track").val(layer.text.letterSpacing||0);
      $("#text-color").val(layer.text.color||"#111111");
      $("#text-align").val(layer.text.align||"left");
    } else if(layer.type==="image"){
      $("#image-props").removeClass("hidden");
      $("#img-url").val(layer.media?.src||"");
      $("#img-fit").val(layer.media?.fit||"cover");
      $("#img-pos").val(layer.media?.position||"center");
    } else if(layer.type==="video"){
      $("#video-props").removeClass("hidden");
      $("#video-url").val(layer.video?.src||"");
      $("#video-ratio").val(layer.video?.ratio||"");
      $("#video-autoplay").prop("checked", !!layer.video?.autoplay);
      $("#video-controls").prop("checked", layer.video?.controls !== false);
      $("#video-loop").prop("checked", !!layer.video?.loop);
    } else if(layer.type==="icon"){
      $("#icon-props").removeClass("hidden");
      populateIconSelect(layer.icon?.name||"bxs-star");
      $("#icon-size").val(layer.icon?.size||48);
      $("#icon-color").val(layer.icon?.color||"#111111");
    }
    $("#link-href").val(layer.link?.href||"");
    $("#link-target").val(layer.link?.target||"_blank");
    $("#link-clicktag").val(layer.link?.clickTag||"");
    $("#link-clickfn").val(layer.link?.clickFn||"");
    $("#link-datalink").val(layer.link?.dataLink||"");
    const $list = $("#anim-list").empty();
    (layer.animations||[]).forEach((a,idx)=>{
      const $row = $(`<li class="anim-list-el" data-idx="${idx}" data-start="${a.start}" data-end="${a.end}">
        ${a.name} [${a.start}–${a.end}] d=${a.duration}ms delay=${a.delay} it=${a.iterationCount}
        <button class="anim-del">Delete</button></li>`);
      $list.append($row);
    });
  }

  
function renderClips(){
  const $clips = $("#clips");
  // v5.04: preserve existing timeline cursor while rebuilding clips
  const $cursor = $clips.find("#tl-cursor").detach();
  $clips.empty();
  const scale = 0.2; // left/width time scale
  let idx = 0;
  project.layers.forEach(layer => {
    (layer.animations||[]).forEach((a,aIdx) => {
      const left = a.start * scale;
      const width = Math.max(1, (a.end - a.start) * scale); // 1
      const $clip = $(`<div class="clip clip-layer" data-layer-id="${layer.id}" data-anim-idx="${aIdx}" anim-start="${a.start}" data-end="${a.end}" title="${layer.name||layer.type} ${a.name}">
        ${layer.name||layer.type}: ${a.name}
      </div>`);
      appendClipEndTimes();
      // v2.0.1: vertical stacking -> top = index * 35 (previously 8)
      $clip.css({ left: left + "px", width: width + "px", top: (idx * 35) + "px" }); // previously 8
      $clips.append($clip);
      idx++;
    });
  });
  // re-attach cursor on top of clips (or recreate if missing)
  if ($cursor && $cursor.length){
    $clips.append($cursor);
  } else if (typeof ensureCursorPresent === "function"){
    ensureCursorPresent();
  }
  const currentTime = parseInt($("#tl-now").text(),10) || 0;
  // v5.02: after rebuilding clips, sync canvas preview with current time
  if (typeof previewAt === "function"){
    previewAt(currentTime);
  } else if (typeof moveCursorToTime === "function"){
    moveCursorToTime(currentTime);
  }
}
function renderAll(){
    $("#canvas-w").val(project.canvas.width);
    $("#canvas-h").val(project.canvas.height);
    $("#bg-type").val(project.canvas.background.type);
    $("#bg-color").val(project.canvas.background.color||"#ffffff");
    $("#bg-image-url").val(project.canvas.background.image?.src||"");
    $("#bg-fit").val(project.canvas.background.image?.fit||"cover");
    $("#bg-repeat").val(project.canvas.background.image?.repeat||"no-repeat");
    $("#bg-position").val(project.canvas.background.image?.position||"center center");
    $("#gridToggle").prop("checked", project.settings.grid);
    $("#snapToggle").prop("checked", project.settings.snap);
    $("#tl-total").val((project.settings.totalDuration||0));
    setCanvasSize();
    applyCanvasBackground();
    $('#canvas').toggleClass('grid', project.settings.grid);
    renderLayers();
    renderProps();
    // v5.0: sync canvas preview with current timeline time (tl-now)
    const currentTime = parseInt($("#tl-now").text(),10) || 0;
    if (typeof previewAt === "function"){
      previewAt(currentTime);
    }
  }

  function getSelectedLayer(){
    return project.layers.find(l=>l.id===selectedId) || null;
  }

  function selectLayer(id){
    selectedId = id;
    $(".el").removeClass("selected");
    $(`.el[data-id="${id}"]`).addClass("selected");
    renderProps();
  }

  function addLayer(type){
    const id = uuid();
    const base = {
      id, name: (type.charAt(0).toUpperCase()+type.slice(1))+" "+(project.layers.length+1),
      type, visible:true, locked:false,
      position: { x: 20, y: 20, width: 160, height: type==="icon"? 64: 90, rotation:0, opacity:1 },
      style: { background:"transparent", padding:{tb:0,lr:0}, borderRadius:0, zIndex: project.layers.length },
      link: { href:"", target:"_blank", clickTag:"", clickFn:"", dataLink:"" },
      animations: []
    };
    if(type==="text"){
      base.text = { html:"Double-click to edit", fontFamily:"Inter", fontSize:18, lineHeight:1.3, letterSpacing:0, fontWeight:600, color:"#111111", align:"left" };
    } else if(type==="image"){
      base.media = { src:"", fit:"cover", position:"center" };
      base.position.width = 160; base.position.height = 100;
    } else if(type==="video"){
      base.video = { src:"", ratio:"16:9", autoplay:false, controls:true, loop:false };
      base.position.width = 160; base.position.height = 90;
    } else if(type==="icon"){
      base.icon = { name:"bxs-star", size:48, color:"#111111" };
      base.position.width = 64; base.position.height = 64;
    }
    project.layers.push(base);
    markChange();
    renderLayers();
    selectLayer(id);
  }

  function updateSelected(mutator){
    const layer = getSelectedLayer();
    if(!layer) return;
    mutator(layer);
    markChange();
    renderLayers();
    selectLayer(layer.id);
  }

  function snap(v){ return project.settings.snap ? Math.round(v/GRID_SIZE)*GRID_SIZE : v; }

  function bindElementInteractions(){
    $(".el").off();

    $(".el").on("mousedown", function(e){
      if($(this).hasClass("locked")) return;
      selectLayer($(this).data("id"));
    });

    $(".el").on("mousedown", function(e){
      const $el = $(this);
      if($el.hasClass("locked")) return;
      const id = $el.data("id");
      const layer = project.layers.find(l=>l.id===id);
      const startX = e.clientX, startY = e.clientY;
      const startLeft = layer.position.x, startTop = layer.position.y;
      const target = e.target;
      if($(target).hasClass("resize") || $(target).hasClass("rotate")) return;
      function mm(ev){
        const dx = ev.clientX - startX;
        const dy = ev.clientY - startY;
        layer.position.x = snap(startLeft + dx);
        layer.position.y = snap(startTop + dy);
        applyLayerStyles($el, layer);
      }
      function mu(){
        $(document).off("mousemove", mm).off("mouseup", mu);
        markChange();
        renderProps();
      }
      $(document).on("mousemove", mm).on("mouseup", mu);
    });

    $(".el .resize").on("mousedown", function(e){
      e.stopPropagation();
      const $el = $(this).closest(".el");
      const id = $el.data("id");
      const layer = project.layers.find(l=>l.id===id);
      const startX = e.clientX, startY = e.clientY;
      const start = {...layer.position};
      const cls = $(this).attr("class");
      function mm(ev){
        let dx = ev.clientX - startX;
        let dy = ev.clientY - startY;
        if(cls.includes("nw")){
          layer.position.x = snap(start.x + dx);
          layer.position.y = snap(start.y + dy);
          layer.position.width = snap(start.width - dx);
          layer.position.height = snap(start.height - dy);
        } else if(cls.includes("ne")){
          layer.position.y = snap(start.y + dy);
          layer.position.width = snap(start.width + dx);
          layer.position.height = snap(start.height - dy);
        } else if(cls.includes("sw")){
          layer.position.x = snap(start.x + dx);
          layer.position.width = snap(start.width - dx);
          layer.position.height = snap(start.height + dy);
        } else {
          layer.position.width = snap(start.width + dx);
          layer.position.height = snap(start.height + dy);
        }
        layer.position.width = Math.max(1, layer.position.width); // 10
        layer.position.height = Math.max(1, layer.position.height); // 10
        applyLayerStyles($el, layer);
      }
      function mu(){
        $(document).off("mousemove", mm).off("mouseup", mu);
        markChange();
        renderProps();
      }
      $(document).on("mousemove", mm).on("mouseup", mu);
    });

    $(".el .rotate").on("mousedown", function(e){
      e.stopPropagation();
      const $el = $(this).closest(".el");
      const id = $el.data("id");
      const layer = project.layers.find(l=>l.id===id);
      const rect = $el[0].getBoundingClientRect();
      const cx = rect.left + rect.width/2;
      const cy = rect.top + rect.height/2;
      function mm(ev){
        const ang = Math.atan2(ev.clientY - cy, ev.clientX - cx) * 180 / Math.PI;
        layer.position.rotation = Math.round(ang);
        applyLayerStyles($el, layer);
      }
      function mu(){
        $(document).off("mousemove", mm).off("mouseup", mu);
        markChange();
        renderProps();
      }
      $(document).on("mousemove", mm).on("mouseup", mu);
    });

    $(".el.text .content .text-edit").on("dblclick", function(e){
      $(this).attr("contenteditable","true").focus();
    }).on("blur", function(){
      $(this).attr("contenteditable","false");
      const id = $(this).closest(".el").data("id");
      const layer = project.layers.find(l=>l.id===id);
      layer.text.html = sanitizeInlineHTML($(this).html());
      markChange();
    });
  }

  function sanitizeInlineHTML(html){
    html = html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "");
    return html.replace(/<(?!\/?(b|i|u|s|span)\b)[^>]*>/gi, "");
  }

  $("#layers").on("click", ".layer-item", function(e){
    const id = $(this).data("id");
    selectLayer(id);
  }).on("click", ".toggle-vis", function(e){
    e.stopPropagation();
    const $btn = $(this);
    const id = $btn.closest(".layer-item").data("id");
    const layer = project.layers.find(l=>l.id===id);
    layer.visible = !(layer.visible !== false);
    $btn.text(layer.visible===false ? "🙈" : "👁️");
    const $el = $(`.el[data-id="${id}"]`);
    if(layer.visible===false){ $el.css("display","none"); } else { $el.css("display",""); }
    markChange(); renderLayers();
  }).on("click", ".toggle-lock", function(e){
    e.stopPropagation();
    const id = $(this).closest(".layer-item").data("id");
    const layer = project.layers.find(l=>l.id===id);
    layer.locked = !layer.locked;
    markChange(); renderLayers();
  }).on("click", ".dup", function(e){
    e.stopPropagation();
    const id = $(this).closest(".layer-item").data("id");
    const layer = project.layers.find(l=>l.id===id);
    const copy = JSON.parse(JSON.stringify(layer));
    copy.id = uuid();
    copy.name = (layer.name||layer.type) + " copy";
    copy.position.x += 10; copy.position.y += 10;
    project.layers.push(copy);
    markChange(); renderLayers(); selectLayer(copy.id);
  }).on("click", ".del", function(e){
    e.stopPropagation();
    const id = $(this).closest(".layer-item").data("id");
    project.layers = project.layers.filter(l=>l.id!==id);
    if(selectedId===id) selectedId=null;
    markChange(); renderLayers(); renderProps();
  }).on("input", ".layer-name", function(e){
    const id = $(this).closest(".layer-item").data("id");
    const layer = project.layers.find(l=>l.id===id);
    const newName = $(this).val();
    layer.name = newName;
    // v3.31: keep name in props panel in sync
    const selectedLayer = getSelectedLayer();
    if(selectedLayer && selectedLayer.id === id){
      $("#prop-name").val(newName);
    }
    markChange();
    // v3.3: update clips labels when layer name changes
    renderClips();
  });

  $("#add-text").on("click", ()=>addLayer("text"));
  $("#add-image").on("click", ()=>addLayer("image"));
  $("#add-video").on("click", ()=>addLayer("video"));
  $("#add-icon").on("click", ()=>addLayer("icon"));

  $("#canvas-w, #canvas-h").on("input", function(){
    const w = parseInt($("#canvas-w").val(),10);
    const h = parseInt($("#canvas-h").val(),10);
    project.canvas.width = Math.min(4000, Math.max(1, w||300)); // 50
    project.canvas.height = Math.min(4000, Math.max(1, h||250)); // 50
    setCanvasSize(); markChange();
  });

  $("#bg-type").on("change", function(){
    const v = $(this).val();
    project.canvas.background.type = v;
    $("#bg-color-wrap").toggleClass("hidden", v!=="color");
    $("#bg-image-wrap").toggleClass("hidden", v!=="image");
    applyCanvasBackground(); markChange();
  });

  $("#bg-color").on("input", function(){
    project.canvas.background.color = $(this).val();
    applyCanvasBackground(); markChange();
  });

  $("#bg-image-url").on("change", function(){
    const src = $("#bg-image-url").val();
    project.canvas.background.image.src = src;
    applyCanvasBackground(); markChange();
  });

  $("#bg-image-file").on("change", function(e){
    const file = e.target.files[0]; if(!file) return;
    const reader = new FileReader();
    reader.onload = evt => {
      project.canvas.background.image.src = evt.target.result;
      applyCanvasBackground(); markChange();
    };
    reader.readAsDataURL(file);
  });

  $("#bg-fit,#bg-repeat,#bg-position").on("change input", function(){
    const img = project.canvas.background.image;
    img.fit = $("#bg-fit").val();
    img.repeat = $("#bg-repeat").val();
    img.position = $("#bg-position").val();
    applyCanvasBackground(); markChange();
  });

  $("#zoom").on("input", function(){
    const v = parseInt($(this).val(),10);
    $("#zoomLabel").text(v+"%");
    $("#canvas").css("transform", `scale(${v/100})`);
  });

  $("#gridToggle").on("change", function(){
    project.settings.grid = $(this).is(":checked");
    $("#canvas").toggleClass("grid", project.settings.grid);
    markChange();
  });
  $("#snapToggle").on("change", function(){
    project.settings.snap = $(this).is(":checked");
    markChange();
  });

  $("#tl-total").on("change input", function(){
    var v = parseInt($(this).val(),10) || 0;
    if(!project.settings) project.settings = {};
    project.settings.totalDuration = v > 0 ? v : 0;
    // enforce minimum relative to current animations
    enforceTimelineTotalMin();
    markChange();
  });

  $("#prop-name,#prop-x,#prop-y,#prop-w,#prop-h,#prop-rot,#prop-op,#prop-pad-tb,#prop-pad-lr,#prop-radius").on("input", function(){
    updateSelected(layer => {
      layer.name = $("#prop-name").val();
      layer.position.x = parseInt($("#prop-x").val(),10)||0;
      layer.position.y = parseInt($("#prop-y").val(),10)||0;
      layer.position.width = Math.max(1, parseInt($("#prop-w").val(),10)||10); // 10
      layer.position.height = Math.max(1, parseInt($("#prop-h").val(),10)||10); // 10
      layer.position.rotation = parseInt($("#prop-rot").val(),10)||0;
      layer.position.opacity = parseFloat($("#prop-op").val())||1;
      layer.style.padding = { tb: parseInt($("#prop-pad-tb").val(),10)||0, lr: parseInt($("#prop-pad-lr").val(),10)||0 };
      layer.style.borderRadius = parseInt($("#prop-radius").val(),10)||0;
      const transparent = $("#prop-bg-transp").is(":checked");
      layer.style.background = transparent ? "transparent" : $("#prop-bg").val();
    });
  });

  $("#prop-bg-transp, #prop-bg").on("change input", function(){ $("#prop-name").trigger("input"); });

  $(".inline-tools button").on("click", function(e){
    e.preventDefault();
    const cmd = $(this).data("cmd");
    const layer = getSelectedLayer(); if(!layer || layer.type!=="text") return;
    const $te = $(".el.selected .text-edit");
    if($te.length===0) return;
    const wasEditable = $te.attr("contenteditable")==="true";
    if(!wasEditable) $te.attr("contenteditable","true");
    $te.focus();
    const sel = window.getSelection();
    let needSelectAll = true;
    if(sel && sel.rangeCount>0){
      const rng = sel.getRangeAt(0);
      if($.contains($te[0], rng.startContainer)) needSelectAll = false;
    }
    if(needSelectAll){
      const range = document.createRange();
      range.selectNodeContents($te[0]);
      sel.removeAllRanges();
      sel.addRange(range);
    }
    document.execCommand(String(cmd), false, null);
    const html = $te.html();
    layer.text.html = sanitizeInlineHTML(html);
    markChange();
    if(!wasEditable){ $te.attr("contenteditable","false"); }
  });

  $("#text-font,#text-size,#text-weight,#text-line,#text-track,#text-color,#text-align").on("input change", function(){
    updateSelected(layer => {
      layer.text.fontFamily = $("#text-font").val();
      layer.text.fontSize = parseInt($("#text-size").val(),10)||18;
      layer.text.fontWeight = parseInt($("#text-weight").val(),10)||600;
      layer.text.lineHeight = parseFloat($("#text-line").val())||1.3;
      layer.text.letterSpacing = parseFloat($("#text-track").val())||0;
      layer.text.color = $("#text-color").val();
      layer.text.align = $("#text-align").val();
    });
    const $sel = $(".el.selected"); if($sel.length){ applyTextStyles($sel, getSelectedLayer()); }
  });
  // v1.9: font preset <select> syncing with #text-font
  $("#font-preset").on("change", function(){
    const v = $(this).val();
    if(v){
      $("#text-font").val(v).trigger("input");
    }
  });
  $("#text-font").on("input", function(){
    // manual change resets preset select
    $("#font-preset").val("");
  });


  function populateIconSelect(selected){
    const $sel = $("#icon-name");
    if($sel.children().length === 0){
      const opts = ICONS.map(n=>`<option value="${n}">${n}</option>`).join("");
      $sel.html(opts);
    }
    if(selected){ $sel.val(selected); }
  }



  $("#video-url").on("change", function(){
    const src = $("#video-url").val();
    updateSelected(layer => {
      if(!layer.video) layer.video = { src:"", ratio:"16:9", autoplay:false, controls:true, loop:false };
      layer.video.src = src;
    });
    markChange();
    renderLayers();
  });
  $("#video-ratio").on("change", function(){
    const ratio = $("#video-ratio").val();
    updateSelected(layer => {
      if(!layer.video) layer.video = { src:"", ratio:"16:9", autoplay:false, controls:true, loop:false };
      layer.video.ratio = ratio;
    });
    markChange();
    renderLayers();
  });
  $("#video-autoplay").on("change", function(){
    const val = $("#video-autoplay").is(":checked");
    updateSelected(layer => {
      if(!layer.video) layer.video = { src:"", ratio:"16:9", autoplay:false, controls:true, loop:false };
      layer.video.autoplay = val;
    });
    markChange();
    renderLayers();
  });
  $("#video-controls").on("change", function(){
    const val = $("#video-controls").is(":checked");
    updateSelected(layer => {
      if(!layer.video) layer.video = { src:"", ratio:"16:9", autoplay:false, controls:true, loop:false };
      layer.video.controls = val;
    });
    markChange();
    renderLayers();
  });
  $("#video-loop").on("change", function(){
    const val = $("#video-loop").is(":checked");
    updateSelected(layer => {
      if(!layer.video) layer.video = { src:"", ratio:"16:9", autoplay:false, controls:true, loop:false };
      layer.video.loop = val;
    });
    markChange();
    renderLayers();
  });
  $("#img-url").on("change", function(){
    const src = $("#img-url").val();
    updateSelected(layer => { layer.media.src = src; });
    setImageNaturalSizeForSelected(src);
  });
  $("#img-file").on("change", function(e){
    const file = e.target.files[0]; if(!file) return;
    const reader = new FileReader();
    reader.onload = evt => { const src = evt.target.result; updateSelected(layer => { layer.media.src = src; });
  $("#img-file").on("change", function(e){
    const file = e.target && e.target.files ? e.target.files[0] : null;
    if(!file) return;
    // v3.3: set layer name from uploaded image file name
    updateSelected(layer => {
      if(layer.type === "image"){
        layer.name = file.name || layer.name;
      }
    });
    // Layer list and clips will be refreshed via updateSelected->renderLayers()
    // but in case there is no selected layer, we just skip.
  });
 setImageNaturalSizeForSelected(src); };
    reader.readAsDataURL(file);
  });
  $("#img-fit,#img-pos").on("change input", function(){
    updateSelected(layer => { layer.media.fit = $("#img-fit").val(); layer.media.position = $("#img-pos").val(); });
  });

  $("#icon-name,#icon-size,#icon-color").on("input change", function(){
    updateSelected(layer => {
      layer.icon.name = $("#icon-name").val();
      layer.icon.size = parseInt($("#icon-size").val(),10)||48;
      layer.icon.color = $("#icon-color").val();
    });
  });

  $("#link-href,#link-target,#link-clicktag,#link-clickfn,#link-datalink").on("input change", function(){
    updateSelected(layer => {
      layer.link.href = $("#link-href").val();
      layer.link.target = $("#link-target").val();
      layer.link.clickTag = $("#link-clicktag").val();
      layer.link.clickFn = $("#link-clickfn").val();
      layer.link.dataLink = $("#link-datalink").val();
    });
  });

  $("#anim-add").on("click", function(){
    const a = {
      name: $("#anim-name").val(),
      start: parseInt($("#anim-start").val(),10)||0,
      end: parseInt($("#anim-end").val(),10)||1200,
      duration: parseInt($("#anim-duration").val(),10)||800,
      delay: parseInt($("#anim-delay").val(),10)||0,
      iterationCount: $("#anim-iter").val()||"1",
      hideBefore: $("#anim-hide-before").is(":checked") ? true : false,
      hideAfter: $("#anim-hide-after").is(":checked") ? true : false
    };
    if(a.end < a.start){ const t=a.start; a.start=a.end; a.end=t; }
    updateSelected(layer => {
      layer.animations = layer.animations||[];
      if(currentAnimIdx != null && !isNaN(currentAnimIdx) && layer.animations[currentAnimIdx]){
        // v3.4: update existing clip
        layer.animations[currentAnimIdx] = a;
      } else {
        // add new clip
        layer.animations.push(a);
      }
    });
    // after saving, keep editing index but refresh UI
    renderProps();
    renderClips();
  });

  
  // v3.4: click animation row to edit its values in the anim-row controls
  $("#anim-list").on("click", "li", function(e){
    // ignore clicks on the delete button itself
    if($(e.target).closest(".anim-del").length) return;
    const idx = parseInt($(this).data("idx"), 10);
    const layer = getSelectedLayer();
    if(!layer || !Array.isArray(layer.animations) || isNaN(idx)) return;
    const a = layer.animations[idx];
    currentAnimIdx = idx;
    $("#anim-name").val(a.name || "fadeIn");
    $("#anim-start").val(a.start != null ? a.start : 0);
    $("#anim-end").val(a.end != null ? a.end : 0);
    $("#anim-duration").val(a.duration != null ? a.duration : 800);
    $("#anim-delay").val(a.delay != null ? a.delay : 0);
    $("#anim-iter").val(a.iterationCount != null ? a.iterationCount : "1");
    $("#anim-hide-before").prop("checked", !!a.hideBefore);
    $("#anim-hide-after").prop("checked", !!a.hideAfter);
  });
$("#anim-list").on("click", ".anim-del", function(){
    const idx = parseInt($(this).parent().data("idx"),10);
    updateSelected(layer => {
      layer.animations.splice(idx,1);
    });
  });
  // v3.5: drag timeline clips (.clip) horizontally to change anim-start and anim-end
  $("#clips").on("mousedown", ".clip", function(e){
    if(e.which !== 1) return; // only left mouse
    e.preventDefault();
    const $clip = $(this);
    const layerId = $clip.data("layer-id");
    selectLayer(layerId);
    const animIdx = parseInt($clip.data("anim-idx"), 10);
    if(!layerId || isNaN(animIdx)) return;
    const layer = project.layers.find(l => l.id === layerId);
    if(!layer || !Array.isArray(layer.animations) || !layer.animations[animIdx]) return;
    const a = layer.animations[animIdx];
    const scale = 0.2;
    const leftPx = parseFloat($clip.css("left")) || 0;
    const duration = (a.end||0) - (a.start||0);
    clipDragState = {
      $clip,
      layerId,
      animIdx,
      startMouseX: e.pageX,
      origLeftPx: leftPx,
      origStart: a.start || 0,
      origEnd: a.end || 0,
      duration: duration < 0 ? 0 : duration
    };
    $(document).on("mousemove.clipdrag", function(ev){
      if(!clipDragState) return;
      const dx = ev.pageX - clipDragState.startMouseX;
      let newLeftPx = clipDragState.origLeftPx + dx;
      const scale = 0.2;
      let newStart = Math.round(newLeftPx / scale);
      if(!isFinite(newStart)) newStart = clipDragState.origStart;

      // Clamp to 0..maxTime where maxTime = tl-total or 2000
      const tlTotal = parseInt($("#tl-total").val(),10) || 0;
      let maxTime = tlTotal > 0 ? tlTotal : 2000;
      let dur = clipDragState.duration;
      if(!isFinite(dur) || dur < 0) dur = 0;
      if(dur > maxTime) dur = maxTime;

      let maxStart = maxTime - dur;
      if(maxStart < 0) maxStart = 0;
      if(newStart < 0) newStart = 0;
      if(newStart > maxStart) newStart = maxStart;

      const newEnd = newStart + dur;
      const layer = project.layers.find(l => l.id === clipDragState.layerId);
      if(layer && Array.isArray(layer.animations) && layer.animations[clipDragState.animIdx]){
        const anim = layer.animations[clipDragState.animIdx];
        anim.start = newStart;
        anim.end = newEnd;
      }
      // update clip position visually without full re-render
      clipDragState.$clip.css("left", (newStart * scale) + "px");

      // if this animation is currently loaded into the anim-row controls, sync fields
      const selLayer = getSelectedLayer();
      if(selLayer && selLayer.id === clipDragState.layerId && currentAnimIdx != null && currentAnimIdx === clipDragState.animIdx){
        $("#anim-start").val(newStart);
        $("#anim-end").val(newEnd);
      }
    });
    $(document).on("mouseup.clipdrag", function(ev){
      if(!clipDragState) return;
      $(document).off(".clipdrag");
      // finalize changes into history/autosave and re-render timeline nicely
      markChange();
      renderClips();
      clipDragState = null;
    });

  // v3.6 + v3.8: click on a clip to load its animation data into the anim-row controls
  $("#clips").on("click", ".clip", function(e){
    const layerId = $(this).data("layer-id");
    const animIdx = parseInt($(this).data("anim-idx"), 10);
    if(!layerId || isNaN(animIdx)) return;
    const layer = project.layers.find(l => l.id === layerId);
    if(!layer || !Array.isArray(layer.animations) || !layer.animations[animIdx]) return;
    // select layer so that props/anim list correspond (same as clicking layer-item)
    selectLayer(layer.id);
    // explicitly refresh props panel to mirror layer-item click behavior
    renderProps();
    const a = layer.animations[animIdx];
    currentAnimIdx = animIdx;
    $("#anim-name").val(a.name || "fadeIn");
    $("#anim-start").val(a.start != null ? a.start : 0);
    $("#anim-end").val(a.end != null ? a.end : 0);
    $("#anim-duration").val(a.duration != null ? a.duration : 800);
    $("#anim-delay").val(a.delay != null ? a.delay : 0);
    $("#anim-iter").val(a.iterationCount != null ? a.iterationCount : "1");
  });

  // v4.2: draggable timeline cursor to scrub animation time
  let cursorDragState = null;

  function timeFromPageX(pageX){
    const $clips = $("#clips");
    const offset = $clips.offset();
    if(!offset) return 0;
    const x = pageX - offset.left + $clips.scrollLeft();
    const scale = 0.2;
    let t = Math.round(x / scale);
    const tlTotal = parseInt($("#tl-total").val(),10) || 0;
    let maxTime = tlTotal > 0 ? tlTotal : Math.max(0, getMaxTimeline()); // 2000
    if(!isFinite(maxTime) || maxTime <= 0) maxTime = 2000;
    if(t < 0) t = 0;
    if(t > maxTime) t = maxTime;
    return t;
  }

  $("#clips").on("mousedown", "#tl-cursor", function(e){
    if(e.which !== 1) return;
    e.preventDefault();
    if(typeof isPlaying !== "undefined" && isPlaying){
      stop();
    }
    const t = timeFromPageX(e.pageX);
    previewAt(t);
    cursorDragState = {
      startMouseX: e.pageX,
      startTime: t
    };
    $(document).on("mousemove.cursor", function(ev){
      if(!cursorDragState) return;

      const scale = 0.2; // ten sam co w renderClips
      const dx = ev.pageX - cursorDragState.startMouseX;
      let tNew = cursorDragState.startTime + Math.round(dx / scale);

      const tlTotal = parseInt($("#tl-total").val(),10) || 0;
      let maxTime = tlTotal > 0 ? tlTotal : Math.max(0, getMaxTimeline()); // 2000
      if(!isFinite(maxTime) || maxTime <= 0) maxTime = 2000;

      if(tNew < 0) tNew = 0;
      if(tNew > maxTime) tNew = maxTime;

      // tu dzieje się magia: przesuwasz kursor + zmieniasz stan canvas
      previewAt(tNew);
    });

    $(document).on("mouseup.cursor", function(){
      $(document).off(".cursor");
      cursorDragState = null;
    });
  });

  // click on empty timeline area to jump cursor & preview
    $("#clips").on("mousedown", function(e){
      // blokuj tylko kursor, bo on ma osobny drag&drop
      if($(e.target).attr("id")==="tl-cursor") return;
      if(e.which !== 1) return;
      e.preventDefault();
      if(typeof isPlaying !== "undefined" && isPlaying){
        stop();
      }
      const t = timeFromPageX(e.pageX);
      previewAt(t);
    });

});


  function clearAnimClasses(){
    $(".el").each(function(){
      this.classList.forEach(c=>{ if(c.startsWith("animate__")) $(this).removeClass(c); });
      $(this).removeClass("animationInEnded animationOutEnded animationPreIn");
      $(this).css({"--animate-duration":"", "--animate-delay":""});
    });
  }


function isExitAnimation(name){
  const n = (name || "").toString();
  return /Out/.test(n);
}

function isEntryAnimation(name){
  const n = (name || "").toString();
  if(isExitAnimation(n)) return false;
  return /In/.test(n);
}


  function moveCursorToTime(t){
    const $cursor = $("#tl-cursor");
    const scale = 0.2; // ten sam co w renderClips()
    const timeMs = Math.max(0, Math.floor(t));

    if ($cursor.length){
      $cursor.css("left", (timeMs * scale) + "px");
      const $label = $cursor.find(".tl-cursor-label");
      if ($label.length){
        $label.text(timeMs + " ms");
      }
    }

    $("#tl-now").text(timeMs);
  }

  // v4.2: static preview of layers at arbitrary timeline time (no animations playback)
  function previewAt(t){
    const tlTotal = parseInt($("#tl-total").val(),10) || 0;
    let maxTime = tlTotal > 0 ? tlTotal : Math.max(0, getMaxTimeline()); // 2000
    if(!isFinite(maxTime) || maxTime <= 0) maxTime = 2000;
    if(t < 0) t = 0;
    if(t > maxTime) t = maxTime;
    moveCursorToTime(t);
    clearAnimClasses();
    project.layers.forEach(layer => {
      const $el = $(`.el[data-id="${layer.id}"]`);
      if(!$el.length) return;
      const anims = layer.animations || [];
      const baseOpacity = (layer.position && typeof layer.position.opacity === "number") ? layer.position.opacity : 1;
      let opacity = baseOpacity;
      let visible;
      // Precompute ordered animations and hide groups to avoid scope issues
      const ordered = anims.slice().sort((a,b)=>(a.start||0)-(b.start||0));
      let hideBeforeAnims = ordered.filter(a => a && a.hideBefore);
      let hideAfterAnims  = ordered.filter(a => a && a.hideAfter);
      if(!anims.length){
        visible = layer.visible !== false;
      } else {
        const hasEntry = ordered.some(a => isEntryAnimation(a.name));
        visible = !hasEntry && layer.visible !== false;
        ordered.forEach(a => {
          const rawStart = a.start || 0;
          const delay = a.delay || 0;
          const startAt = rawStart + delay;
          const dur = a.duration || 800;
          const endAt = (a.end != null ? a.end + delay : startAt + dur);
          if(t < startAt) return;
          if(isEntryAnimation(a.name)){
            if(t >= startAt) visible = true;
            if(t >= endAt){
              $el.addClass("animationInEnded");
            }
          } else if(isExitAnimation(a.name)){
            if(t < endAt) visible = true;
            else visible = false;
            if(t >= endAt){
              $el.addClass("animationOutEnded");
            }
          } else {
            if(t >= startAt) visible = true;
          }
        });
        if(hideBeforeAnims.length){
          const firstStart = Math.min.apply(null, hideBeforeAnims.map(a => (a.start||0) + (a.delay||0)));
          if(t < firstStart){
            opacity = 0;
          }
        }
        if(hideAfterAnims.length){
          const firstEnd = Math.min.apply(null, hideAfterAnims.map(a => {
            const s0 = a.start || 0;
            const d = a.duration || 800;
            const delay = a.delay || 0;
            const startEff = s0 + delay;
            return a.end != null ? a.end + delay : (startEff + d);
          }));
          if(t >= firstEnd){
            opacity = 0;
          }
        }
      }
      // preIn (hide-before + entry) handling
      let preInAnims = ordered.filter(a => a && a.hideBefore && isEntryAnimation(a.name));
      let preInActive = false;
      if(preInAnims.length){
        const firstPreInStart = Math.min.apply(null, preInAnims.map(a => (a.start||0) + (a.delay||0)));
        if(t < firstPreInStart){
          preInActive = true;
        }
      }
      if(preInActive){
        $el.addClass("animationPreIn");
      } else {
        $el.removeClass("animationPreIn");
      }
      // final hidden state
      const isHiddenByAnim = (opacity === 0 && (anims.length > 0) && (hideBeforeAnims.length || hideAfterAnims.length));
      if(isHiddenByAnim){
        $el.addClass("hidden");
      } else {
        $el.removeClass("hidden");
      }
      $el.css("visibility", visible ? "" : "hidden");
      $el.css("opacity", opacity);
    });
  }


 
  function play(){
    if(isPlaying) return;
    isPlaying = true;
    if(playTimer) clearInterval(playTimer);
    clearAnimClasses();

    // v4.5: initial visibility based on timeline + entry/exit
    project.layers.forEach(layer => {
      const $el = $(`.el[data-id="${layer.id}"]`);
      if(!$el.length) return;
      const anims = layer.animations || [];
      const baseOpacity = (layer.position && typeof layer.position.opacity === "number") ? layer.position.opacity : 1;
      let visible;
      if(!anims.length){
        // no timeline clips → obey manual visibility only
        visible = layer.visible !== false;
        $el.removeClass("hidden");
        $el.css("opacity", baseOpacity);
      } else {
        const ordered = anims.slice().sort((a,b)=>(a.start||0)-(b.start||0));
        const hasEntry = ordered.some(a => isEntryAnimation(a.name));
        const hideBeforeAnims = ordered.filter(a => a && a.hideBefore);
        const preInAnims = ordered.filter(a => a && a.hideBefore && isEntryAnimation(a.name));
        if(hasEntry){
          // entry animations: start hidden, will become visible on first entry
          visible = false;
        } else {
          // no entry: element is visible from t=0 until an exit/hide-after/hide-before
          visible = layer.visible !== false;
        }
        if(hideBeforeAnims.length){
          // hide from t=0 until the first hide-before animation starts
          visible = false;
          $el.addClass("hidden");
          $el.css("opacity", 0);
        } else {
          $el.removeClass("hidden");
          $el.css("opacity", baseOpacity);
        }
        if(preInAnims.length){
          $el.addClass("animationPreIn");
        } else {
          $el.removeClass("animationPreIn");
        }
      }
      $el.css("visibility", visible ? "" : "hidden");
    });

    const start = performance.now();
    // v5.01: reset pending animation timeouts
    if (Array.isArray(playTasks)){
      playTasks.forEach(id => clearTimeout(id));
    }
    playTasks = [];
    project.layers.forEach(layer => {
      (layer.animations||[]).forEach(anim => {
        const $el = $(`.el[data-id="${layer.id}"]`);
        const startAt = anim.start || 0;
        const endAt = anim.end || (startAt + (anim.duration || 800));
        const baseOpacity = (layer.position && typeof layer.position.opacity === "number") ? layer.position.opacity : 1;
        playTasks.push(setTimeout(()=>{
          // v3.7: element becomes visible at the moment its animation starts
          if(isEntryAnimation(anim.name) && anim.hideBefore){
            $el.removeClass("animationPreIn");
          }
          $el.removeClass("hidden");
          $el.css("visibility","");
          $el.css("opacity", baseOpacity);
          $el.addClass(`animate__animated animate__${anim.name}`);
          $el.css({"--animate-duration": (anim.duration||800)+"ms", "--animate-delay": (anim.delay||0)+"ms"});
        }, startAt));
        // v4.0: keep final animation frame, no automatic removal at endAt
        // setTimeout(()=>{
        //   $el.removeClass(function(idx,cls){ return (cls.match(/animate__\S+/g)||[]).join(" "); });
        //   $el.css({"--animate-duration":"", "--animate-delay":""});
        // }, endAt);
        // v4.1: exit animations keep the element hidden after they finish
        if(isExitAnimation(anim.name)){
          playTasks.push(setTimeout(()=>{
            $el.css("visibility","hidden");
            $el.addClass("animationOutEnded");
          }, endAt));
        }
        // v4.7: mark end of entry animations
        if(isEntryAnimation(anim.name)){
          playTasks.push(setTimeout(()=>{
            $el.addClass("animationInEnded");
          }, endAt));
        }
        // v4.4 + v4.6: hide-after checkbox -> add .hidden (opacity 0) after this animation ends
        if(anim.hideAfter){
          playTasks.push(setTimeout(()=>{
            $el.addClass("hidden");
            $el.css("opacity", 0);
          }, endAt));
        }
      });
    });

    playTimer = setInterval(()=>{
      const t = Math.floor(performance.now()-start);
      moveCursorToTime(t);
      var total = (project.settings && project.settings.totalDuration) ? project.settings.totalDuration : 0;
      var limit = total > 10 ? (total - 10) : (total > 0 ? total : Math.max(1, getMaxTimeline())); // 1
      if(t > limit){
        if(total > 0){
          // stop at defined total duration (no looping), keep final frame
          stopAtEnd();
        } else {
          if(project.settings.loop){ clearInterval(playTimer); isPlaying=false; play(); }
          else stop();
        }
      }
    }, 60);
  }

function getMaxTimeline(){
    let max = 0;
    project.layers.forEach(l => (l.animations||[]).forEach(a => { if(a.end>max) max=a.end; }));
    return max;
  }

  function stop(){
    isPlaying = false;
    if(playTimer) clearInterval(playTimer);
    if (Array.isArray(playTasks)){
      playTasks.forEach(id => clearTimeout(id));
    }
    playTasks = [];
    clearAnimClasses();
    $("#tl-now").text("0");
    // v5.01: after stopping, show canvas state at t=0
    if (typeof previewAt === "function"){
      previewAt(0);
    }
  }

  // v4.0: stop at end of timeline but keep final frame (no clearAnimClasses)
  function stopAtEnd(){
    isPlaying = false;
    if(playTimer) clearInterval(playTimer);
    if (Array.isArray(playTasks)){
      playTasks.forEach(id => clearTimeout(id));
    }
    playTasks = [];
    // do not reset classes/visibility/tl-now to preserve last frame
  }

  $("#tl-play").on("click", play);
  $("#tl-stop").on("click", stop);

  $(function() {
    setTimeout(() => {
      play();
    }, 10);
    setTimeout(() => {
      stop();
    }, 20);
  });

  $(document).on("keydown", function(e){
    const ctrl = e.ctrlKey || e.metaKey;
    if(ctrl && e.key.toLowerCase()==="z"){ e.preventDefault(); undo(); }
    else if(ctrl && e.key.toLowerCase()==="y"){ e.preventDefault(); redo(); }
    else if(ctrl && e.key.toLowerCase()==="s"){ e.preventDefault(); saveManual(); }
    else if(e.key==="Delete"){ deleteSelected(); }
    else if(ctrl && e.key.toLowerCase()==="d"){ e.preventDefault(); duplicateSelected(); }
  });

  function deleteSelected(){
    if(!selectedId) return;
    project.layers = project.layers.filter(l=>l.id!==selectedId);
    selectedId = null;
    markChange(); renderLayers(); renderProps();
  }
  function duplicateSelected(){
    const layer = getSelectedLayer(); if(!layer) return;
    const copy = JSON.parse(JSON.stringify(layer));
    copy.id = uuid(); copy.name = (layer.name||layer.type) + " copy";
    copy.position.x += 10; copy.position.y += 10;
    project.layers.push(copy);
    markChange(); renderLayers(); selectLayer(copy.id);
  }

  function undo(){
    if(history.length<=1) return;
    const cur = history.pop();
    future.push(cur);
    const prev = history[history.length-1];
    project = JSON.parse(prev);
    renderAll();
  }
  function redo(){
    if(future.length===0) return;
    const state = future.pop();
    history.push(state);
    project = JSON.parse(state);
    renderAll();
  }

  function saveManual(){
    project.meta.updatedAt = nowISO();
    localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
    toast($("#toast").data("i18n-saved")||"Saved");
  }

  $("#btn-save").on("click", saveManual);
  $("#auto-save").on("change", function(){ setAutoSaveEnabled($(this).is(":checked")); });

  $("#btn-export-json").on("click", function(){
    const blob = new Blob([JSON.stringify(project, null, 2)], {type:"application/json"});
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    const slug = (project.meta.title||"project").toLowerCase().replace(/[^a-z0-9]+/g,'-');
    a.download = `project-${slug}-${Date.now()}.json`;
    a.click();
  });

  $("#import-json").on("change", function(e){
    const file = e.target.files[0]; if(!file) return;
    const reader = new FileReader();
    reader.onload = evt => {
      try {
        const data = JSON.parse(evt.target.result);
        if(!data.canvas || !data.layers) throw new Error("Nieprawidłowy plik.");
        project = data;
        pushHistory();
        renderAll();
        toast("Zaimportowano projekt");
      } catch(err){
        alert("Błąd importu: " + err.message);
      }
    };
    reader.readAsText(file, "utf-8");
  });

  $("#btn-export-html").on("click", function(){
    const html = generateExportHTML(project);
    const blob = new Blob([html], {type:"text/html"});
    const a = document.createElement("a");
    const slug = (project.meta.title||"creator").toLowerCase().replace(/[^a-z0-9]+/g,'-');
    a.href = URL.createObjectURL(blob);
    a.download = `${slug}-export.html`
    a.click();
  });

  function collectFonts(){
    const set = new Set();
    (project.settings.fontFamilies||[]).forEach(f=>set.add(f.split(":")[0]));
    project.layers.forEach(l => {
      if(l.type==="text" && l.text?.fontFamily) set.add(l.text.fontFamily);
    });
    return [...set];
  }

  // Auto size image on load
  function setImageNaturalSizeForSelected(src){
    const layer = getSelectedLayer();
    if(!layer || layer.type!=="image") return;
    const img = new Image();
    img.onload = function(){
      const nw = img.naturalWidth || this.width;
      const nh = img.naturalHeight || this.height;
      if(nw && nh){
        layer.position.width = nw;
        layer.position.height = nh;
        $("#prop-w").val(nw);
        $("#prop-h").val(nh);
        renderLayers();
        selectLayer(layer.id);
        markChange();
      }
    };
    img.crossOrigin = "anonymous";
    img.src = src;
  }

  function generateExportHTML(p){
    exportLinkCounter = 0;

    const fonts = collectFonts();
    const gf = fonts.length ? `<link href="https://fonts.googleapis.com/css2?${fonts.map(f=>`family=${encodeURIComponent(f)}:wght@400;600;800`).join("&")}&display=swap" rel="stylesheet">` : "";
    const rootStyle = `#ad-wrapper{position:relative;width:100%;max-width:${p.canvas.width}px;overflow:hidden;}#ad-root{position:relative;width:${p.canvas.width}px;height:${p.canvas.height}px;overflow:hidden;${exportBackground(p.canvas.background)}}.el{position:absolute;box-sizing:border-box}.hidden{opacity:0}.animationOutEnded,.animationPreIn{opacity:0!important}
#clips{position:relative;overflow:hidden}.animationOutEnded,.animationPreIn{opacity:0!important}#clips #tl-cursor{position:absolute;width:2px;top:0;bottom:0;left:0;background:var(--accent,#ff4d4f);z-index:9999;pointer-events:none}

.video-placeholder{display:flex;align-items:center;justify-content:center;width:100%;height:100%;font-size:12px;color:#666;text-align:center;padding:4px;box-sizing:border-box;}
.video-embed, .video-embed iframe{width:100%;height:100%;display:block;}
`;
    let bodyEls = "";
    p.layers.forEach(l => {
      if(l.visible===false) return;
      const st = exportLayerStyle(l);
      let inner = "";
      if(l.type==="text"){
        inner = `<div class="t" style="${exportTextStyle(l)}">${l.text.html||""}</div>`;
      } else if(l.type==="image"){
        const fit = l.media?.fit||"cover";
        const pos = l.media?.position||"center";
        inner = `<img src="${l.media?.src||""}" alt="" style="width:100%;height:100%;object-fit:${fit};object-position:${pos}">`;
      } else if(l.type==="video"){
        const src = l.video?.src||"";
        const ratio = l.video?.ratio||"";
        const autoplay = !!l.video?.autoplay;
        const controls = l.video?.controls !== false;
        const loop = !!l.video?.loop;
        const isYouTube = src.includes("youtube.com") || src.includes("youtu.be");
        const isVimeo = src.includes("vimeo.com");
        if(!src){
          inner = `<div class="video-placeholder">No video URL</div>`;
        } else if(isYouTube || isVimeo){
          let embedUrl = src;
          let videoId = "";
          if(isYouTube){
            if(src.includes("watch?v=")){
              videoId = src.split("watch?v=")[1].split("&")[0];
              embedUrl = "https://www.youtube.com/embed/" + videoId;
            } else if(src.includes("youtu.be/")){
              videoId = src.split("youtu.be/")[1].split(/[?&]/)[0];
              embedUrl = "https://www.youtube.com/embed/" + videoId;
            }
            const params = [];
            if(autoplay){ params.push("autoplay=1"); }
            if(loop && videoId){ params.push("loop=1","playlist="+videoId); }
            if(!controls){ params.push("controls=0"); }
            if(params.length){
              embedUrl += (embedUrl.includes("?") ? "&" : "?") + params.join("&");
            }
          } else if(isVimeo){
            const m = src.match(/vimeo\.com\/(\d+)/);
            if(m) videoId = m[1];
            embedUrl = "https://player.vimeo.com/video/" + (videoId||"");
            const params = [];
            if(autoplay){ params.push("autoplay=1"); }
            if(loop){ params.push("loop=1"); }
            if(params.length){
              embedUrl += (embedUrl.includes("?") ? "&" : "?") + params.join("&");
            }
          }
          inner = `<div class="video-embed"><iframe src="${embedUrl}" frameborder="0" allowfullscreen style="width:100%;height:100%;"></iframe></div>`;
        } else {
          let attrs = 'style="width:100%;height:100%;object-fit:contain;"';
          if(autoplay){ attrs += ' autoplay'; }
          if(controls){ attrs += ' controls'; }
          if(loop){ attrs += ' loop'; }
          inner = `<video src="${src}" ${attrs}></video>`;
        }
      } else if(l.type==="icon"){
        inner = `<i class="bx ${l.icon?.name||"bxs-star"}" style="font-size:${l.icon?.size||48}px;color:${l.icon?.color||"#111"}"></i>`;
      }
      const content = `<div class="el" data-id="${l.id}" style="${st}">${inner}</div>`;
      bodyEls += wrapLinkIfNeeded(content, l.link);
    });

    const player = exportPlayerScript(p);
    return `<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/animate.css@4.1.1/animate.min.css">
<link href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css" rel="stylesheet">
${gf}
<style>${rootStyle}body{margin:0;}</style>
</head>
<body>
<div id="ad-wrapper">
<div id="ad-root" role="img" aria-label="${escapeHtml(p.meta.title||"Creator")}">
${bodyEls}
</div>
</div>
<script>(function(){${player}})();
(function(){
  var root = document.getElementById('ad-root');
  if(!root) return;
  var designW = ${p.canvas.width};
  var designH = ${p.canvas.height};
  function rescale(){
    var parent = root.parentElement || root;
    var maxW = parent.clientWidth || designW;
    var scale = maxW / designW;
    if(scale > 1) scale = 1;
    if(scale <= 0 || !isFinite(scale)) scale = 1;
    root.style.transform = 'scale(' + scale + ')';
    root.style.transformOrigin = 'top left';
    parent.style.height = (designH * scale) + 'px';
  }
  window.addEventListener('resize', rescale);
  rescale();
})();
</script>
</body>
</html>`;
  }

  function exportBackground(bg){
    if(bg.type==="transparent") return "background:transparent;";
    if(bg.type==="color") return `background:${bg.color||"#fff"};`;
    if(bg.type==="image"){
      return `background-image:url('${bg.image?.src||""}');background-size:${bg.image?.fit||"cover"};background-repeat:${bg.image?.repeat||"no-repeat"};background-position:${bg.image?.position||"center"};`;
    }
    return "";
  }

  function exportLayerStyle(l){
    const p = l.position, s = l.style;
    return `left:${p.x}px;top:${p.y}px;width:${p.width}px;height:${p.height}px;opacity:${p.opacity??1};transform:rotate(${p.rotation||0}deg);background:${(s.background&&s.background!=="transparent")?s.background:"transparent"};padding:${(s.padding?.tb||0)}px ${(s.padding?.lr||0)}px;border-radius:${(s.borderRadius||0)}px;z-index:${s.zIndex||0};display:flex;align-items:center;justify-content:center;`;
  }

  function exportTextStyle(l){
    const t = l.text||{};
    return `font-family:${t.fontFamily||"Inter"},sans-serif;font-size:${t.fontSize||18}px;font-weight:${t.fontWeight||600};line-height:${t.lineHeight||1.3};letter-spacing:${t.letterSpacing||0}px;color:${t.color||"#111"};text-align:${t.align||"left"};width:100%;height:100%;display:flex;align-items:center;justify-content:center;`;
  }

  let exportLinkCounter = 0;

function wrapLinkIfNeeded(html, link){
    if(!link) return html;
    const {href, target, clickTag, clickFn, dataLink} = link;
    if(!(href||clickTag||clickFn)) return html;
    const t = target||"_blank";
    const dataAttr = dataLink ? ` data-link="${escapeHtml(dataLink)}"` : "";
    // unique id for each exported link
    exportLinkCounter = (typeof exportLinkCounter === "number" ? exportLinkCounter : 0) + 1;
    const idAttr = ` id="el-link-${exportLinkCounter}"`;
    return `<a href="${escapeHtml(href||clickTag||"#")}" target="${t}" rel="${t==="_blank"?"noopener noreferrer":""}"${dataAttr}${idAttr} class="el-link">${html}</a>`;
  }

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

  function exportPlayerScript(p){
    const anims = [];
    p.layers.forEach(l => (l.animations||[]).forEach(a => {
      anims.push({ id:l.id, name:a.name, start:(a.start||0)+(a.delay||0), end:(a.end!=null ? a.end+(a.delay||0) : (a.start||0)+(a.delay||0)+(a.duration||800)), duration:a.duration||800, delay:a.delay||0, iteration:a.iterationCount||1, hideBefore:a.hideBefore||false });
    }));
    const tasks = JSON.stringify(anims);
    return `
      var tasks=${tasks};
      function clear(el){ (el.className||"").split(" ").forEach(function(c){ if(c.indexOf("animate__")===0) el.classList.remove(c); }); el.style.removeProperty("--animate-duration"); el.style.removeProperty("--animate-delay"); }
      function start(){
        var els = {}; document.querySelectorAll('.el').forEach(function(el){ els[el.getAttribute('data-id')] = el; });
        // v4.8: initial animationPreIn for entry animations with hideBefore
        var preInStart = {};
        tasks.forEach(function(t){
          if(/In/.test(t.name) && t.hideBefore){
            if(preInStart[t.id] == null || t.start < preInStart[t.id]) preInStart[t.id] = t.start;
          }
        });
        Object.keys(preInStart).forEach(function(id){
          var el = els[id]; if(!el) return;
          el.classList.add('animationPreIn');
        });
        tasks.forEach(function(t){
          var el = els[t.id]; if(!el) return;
          setTimeout(function(){
            if(/In/.test(t.name) && t.hideBefore){
              el.classList.remove('animationPreIn');
            }
            el.classList.add('animate__animated','animate__'+t.name);
            el.style.setProperty('--animate-duration', t.duration+'ms');
            el.style.setProperty('--animate-delay', '0ms');
          }, t.start);
          setTimeout(function(){
            // v4.7+v4.8: mark end of entry/exit animations on exported HTML
            if(/Out/.test(t.name)){
              el.classList.add('animationOutEnded');
            } else if(/In/.test(t.name)){
              el.classList.add('animationInEnded');
            }
            clear(el);
          }, t.end);
        });
      }
      start();
      document.addEventListener('click', function(ev){
        var a = ev.target.closest('.el-link'); if(!a) return;
      });`;
  }

  $("#btn-new").on("click", function(){
    if(confirm("Reset projektu?")){ project = defaultProject(); pushHistory(); renderAll(); }
  });
  $("#btn-lang").on("click", function(){ setLang(LANG==="pl"?"en":"pl"); });

  $("#btn-help").on("click", function(){
    alert([
      "Sterowanie:",
      "• Przeciągaj elementy myszą, narożnikami zmieniasz rozmiar, kółkiem nad elementem – obrót.",
      "• Double-click na tekście – edycja inline.",
      "• Ctrl+Z / Ctrl+Y – cofanie/ponawianie.",
      "• Export JSON/HTML w górnym pasku.",
      "• Autosave do localStorage ~0.5s + opcja 1-min zapisu manualnego."
    ].join("\n"));
  });

  $("#btn-undo").on("click", undo);
  $("#btn-redo").on("click", redo);

  const THEME_KEY = "rmhc:theme";

  function applyTheme(mode){
    let finalMode = mode || "auto";
    if(finalMode === "auto"){
      const hour = new Date().getHours();
      finalMode = (hour >= 19 || hour < 7) ? "dark" : "light";
    }
    document.body.setAttribute("data-theme", finalMode);
  }

  const storedTheme = localStorage.getItem(THEME_KEY) || "auto";
  $("#theme-select").val(storedTheme);
  applyTheme(storedTheme);

  $("#theme-select").on("change", function(){
    const mode = $(this).val();
    localStorage.setItem(THEME_KEY, mode);
    applyTheme(mode);
  });

  loadFromStorage();
  applyLang();
  $("#zoom").trigger("input");

// v4.91: ensure timeline cursor exists and is visible/working (confined to #clips)
(function(){
  function ensureCursorPresent(){
    try{
      let $clips = document.getElementById('clips');
      let totalHeight = $clips.scrollHeight;
      var $cursor = $("#tl-cursor");
      if(!$cursor.length){
        // create and append to body
        $cursor = $(`<div id="tl-cursor"></div>`);
        $("body").append($cursor);
      }
      if (!$cursor.find(".tl-cursor-label").length) {
        $cursor.append('<div class="tl-cursor-label">0 ms</div>');
      }
      $cursor.css({"display":"block","opacity":1,"pointer-events":"none","height":totalHeight});
      // position at 0 initially
      if(typeof moveCursorToTime === "function"){
        moveCursorToTime(0);
      } else {
        $cursor.css("left","0px");
      }
    }catch(e){
      console && console.warn && console.warn("ensureCursorPresent failed", e);
    }
  }
  // Run on DOM ready and also after a short delay to counter race conditions
  $(function(){ ensureCursorPresent(); setTimeout(ensureCursorPresent, 200); setTimeout(ensureCursorPresent, 1000); });
})();


// v4.98 button text swap
$(function(){
  $("#anim-list").on("click", '.anim-list-el', function(){
    $("#anim-add").text("Save clip");
  });
  $("#anim-add").on("click", function(){
    $("#anim-add").text("Add clip");
  });
});

})(jQuery);


// v4.96: append end time to each .clip
function appendClipEndTimes(){
  $('.clip').each(function(){
    var $c=$(this);
    var end=$c.data('end')||$c.attr('data-end');
    if(end){
      if(!$c.find('.clip-endtime').length){
        $c.append('<span class="clip-endtime" style="position:absolute;right:2px;bottom:2px;font-size:10px;color:#555;">'+end+'ms</span>');
      }
    }
  });
}
