(() => {
  const root = document.getElementById("lab-app");
  if (!root) return;

  const boot = JSON.parse(root.dataset.boot || "{}");
  const products = boot.products || [];
  const csrf = boot.csrf;
  const urls = boot.urls;
  const mode = boot.mode || "kiosk";
  const i18n = boot.i18n || {};
  const lang = boot.lang || "el";
  const t = (key, fallback) => i18n[key] || fallback || key;
  const euro = (cents) => (cents / 100).toFixed(2).replace(".", ",") + " €";

  let cart = { photos: [], print_count: 0, print_cents: 0 };
  let screen = "product";
  let editing = null;
  let idleTimer = null;
  let productMode = "prints";
  let calendarStyle = "wall";
  let defaultSku = "10x15";
  let drag = null;
  let editDrafts = {};
  let lastOrder = null;
  let placedOrder = null;
  let selected = new Set();
  let photoQuery = "";

  function photoList() {
    const photos = cart?.photos;
    if (Array.isArray(photos)) return photos;
    if (photos && typeof photos === "object") return Object.values(photos);
    return [];
  }

  const $ = (sel, el = document) => el.querySelector(sel);
  const $$ = (sel, el = document) => [...el.querySelectorAll(sel)];

  function qtyObject(raw) {
    const o = {};
    if (!raw) return o;
    const src = Array.isArray(raw) ? Object.assign({}, raw) : raw;
    Object.entries(src).forEach(([k, v]) => {
      if (/^\d+$/.test(k) && Array.isArray(raw)) return;
      const n = Number(v);
      if (n > 0) o[k] = n;
    });
    return o;
  }

  async function api(url, opts = {}) {
    const headers = Object.assign({ "X-CSRF-Token": csrf }, opts.headers || {});
    const res = await fetch(url, Object.assign({}, opts, { headers }));
    const data = await res.json().catch(() => ({ ok: false, error: "Σφάλμα δικτύου" }));
    if (!res.ok || data.ok === false) {
      throw new Error(data.error || "Κάτι πήγε στραβά");
    }
    return data;
  }

  function skuMatches(sku) {
    const s = String(sku || "");
    if (productMode === "calendar") return s.startsWith("cal");
    if (productMode === "docs") return s === "id9x13";
    return !s.startsWith("cal") && s !== "id35x45" && s !== "passport" && s !== "id9x13" && s !== "notebook" && s !== "a4";
  }

  function catalog() {
    return products.filter((p) => skuMatches(p.sku));
  }

  function defaultSkuFor(modeName) {
    if (modeName === "calendar") return "cal2027-1";
    if (modeName === "docs") return "id9x13";
    return "10x15";
  }

  function applyProduct() {
    defaultSku = defaultSkuFor(productMode);
    const sizes = catalog();
    const defSel = $("[data-default-sku]");
    if (defSel) {
      defSel.innerHTML = sizes.map((p) => `<option value="${p.sku}">${p.name}</option>`).join("");
      if (sizes.some((p) => p.sku === defaultSku)) defSel.value = defaultSku;
      else if (sizes[0]) {
        defaultSku = sizes[0].sku;
        defSel.value = defaultSku;
      }
    }
    $$("[data-for-product]").forEach((el) => {
      const allow = (el.dataset.forProduct || "").split(/\s+/).filter(Boolean);
      el.hidden = allow.length > 0 && !allow.includes(productMode);
    });
    root.classList.toggle("is-calendar", productMode === "calendar");
    root.classList.toggle("is-docs", productMode === "docs");
  }

  async function pickProduct(modeName) {
    productMode = modeName;
    applyProduct();
    try {
      await api(urls.cart, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ options: { product: productMode, lang } }),
      });
    } catch (e) {}
    if (modeName === "calendar") setScreen("cal-style");
    else setScreen("source");
  }

  function setScreen(name) {
    screen = name;
    $$(".screen").forEach((s) => {
      s.hidden = s.dataset.screen !== name;
    });
    $$("[data-step-ind]").forEach((el) => {
      const step = el.dataset.stepInd;
      el.classList.toggle(
        "on",
        step === name
          || (name === "route" && step === "checkout")
          || (name === "cal-style" && step === "product")
          || (name === "cal-place" && step === "gallery")
          || (name === "id-place" && step === "gallery")
      );
    });
    $("[data-back]").hidden = name === "product" || name === "finish";
    const next = $("[data-next]");
    root.classList.toggle("is-finish", name === "finish");
    next.hidden = name === "finish" || name === "product" || name === "cal-style" || name === "route";
    if (name === "source") {
      next.textContent = t("continue", "Συνέχεια");
      next.disabled = photoList().length === 0;
      next.hidden = false;
    } else if (name === "gallery") {
      next.textContent =
        productMode === "calendar"
          ? t("cal_place", "Τοποθέτηση στο ημερολόγιο")
          : productMode === "docs"
            ? t("id_place", "Τοποθέτηση πορτρέτου")
            : t("to_order", "Προς παραγγελία");
      next.disabled =
        productMode === "calendar" || productMode === "docs" ? photoList().length === 0 : cart.print_count < 1;
    } else if (name === "cal-place") {
      next.hidden = false;
      next.textContent = "Έτοιμο — βάλε στο ημερολόγιο";
      next.disabled = false;
    } else if (name === "id-place") {
      next.hidden = false;
      next.textContent = t("id_place_ready", "Έτοιμο — 4 σε 9×13");
      next.disabled = false;
    } else if (name === "cal-style") {
      next.hidden = true;
    } else if (name === "finish") {
      next.hidden = true;
    } else if (name === "route") {
      next.hidden = true;
    } else if (name !== "product") {
      next.hidden = false;
      next.textContent = t("place_order", "Ολοκλήρωση παραγγελίας");
      next.disabled = cart.print_count < 1;
    }
    render();
    bumpIdle();
  }

  function afterPhotos() {
    if (mode === "kiosk") setScreen("route");
    else setScreen("checkout");
  }

  function showGallery(rebuild) {
    if (rebuild) {
      const grid = $("[data-photo-grid]");
      if (grid) grid.dataset.ids = "";
    }
    setScreen("gallery");
  }

  function cardHtml(p) {
    const n = (p.lines || []).reduce((s, l) => s + l.qty, 0);
    const q = qtyObject(p.qty)[defaultSku] || 0;
    return `<article class="photo-card ${selected.has(p.id) ? "selected" : ""}" data-open="${p.id}">
      <input class="pick" type="checkbox" data-pick="${p.id}" ${selected.has(p.id) ? "checked" : ""}>
      <img src="${p.thumb}" alt="" loading="lazy" decoding="async" style="${previewFilter(p)}">
      <div class="meta">
        <div class="name">${escapeHtml(p.name)}</div>
        <div class="qty">${n ? n + " " + t("prints_n", "εκτυπώσεις") : t("no_qty", "χωρίς ποσότητα")}</div>
        <div class="card-tools">
          <button type="button" data-card-qty="${p.id}" data-d="-1">−</button>
          <strong>${q}</strong>
          <button type="button" data-card-qty="${p.id}" data-d="1">+</button>
          <button type="button" data-dup="${p.id}">${t("copy", "Αντίγραφο")}</button>
          <button type="button" data-del="${p.id}">${t("delete", "Διαγραφή")}</button>
        </div>
      </div>
    </article>`;
  }

  function bindCard(card) {
    if (!card || card.dataset.bound) return;
    card.dataset.bound = "1";
    card.addEventListener("click", () => openEditor(card.dataset.open));
    card.querySelectorAll("[data-card-qty]").forEach((btn) => {
      btn.addEventListener("click", async (e) => {
        e.preventDefault();
        e.stopPropagation();
        const photo = photoById(btn.dataset.cardQty);
        if (!photo) return;
        const qty = qtyObject(photo.qty);
        qty[defaultSku] = Math.max(0, Math.min(99, (qty[defaultSku] || 0) + Number(btn.dataset.d)));
        if (!qty[defaultSku]) delete qty[defaultSku];
        await saveEdits({ [photo.id]: Object.assign(editPayload(photo), { qty }) });
      });
    });
    card.querySelector("[data-dup]")?.addEventListener("click", async (e) => {
      e.preventDefault();
      e.stopPropagation();
      const data = await api(urls.cart, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ duplicate: e.currentTarget.dataset.dup }),
      });
      cart = data.cart;
      render();
    });
    card.querySelector("[data-del]")?.addEventListener("click", async (e) => {
      e.preventDefault();
      e.stopPropagation();
      if (!confirm("Διαγραφή φωτογραφίας;")) return;
      const data = await api(urls.cart, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ remove: [e.currentTarget.dataset.del] }),
      });
      cart = data.cart;
      render();
    });
    const pick = card.querySelector("[data-pick]");
    if (pick) {
      pick.addEventListener("click", (e) => e.stopPropagation());
      pick.addEventListener("change", () => {
        if (pick.checked) selected.add(pick.dataset.pick);
        else selected.delete(pick.dataset.pick);
        card.classList.toggle("selected", pick.checked);
      });
    }
  }

  function render() {
    try {
      renderInner();
    } catch (e) {
      const status = $("[data-upload-status]");
      if (status) status.textContent = e.message || "Σφάλμα εμφάνισης φωτογραφιών.";
    }
  }

  function renderInner() {
    $("[data-cart-pill]").textContent = `${cart.print_count} εκτυπώσεις · ${euro(cart.print_cents)}`;
    $("[data-dock-total]").textContent = `${cart.print_count} εκτυπώσεις · ${euro(cart.print_cents)}`;
    const next = $("[data-next]");
    const photos = photoList();
    if (screen === "source") next.disabled = photos.length === 0;
    if (screen === "gallery") {
      next.disabled =
        productMode === "calendar" || productMode === "docs" ? photos.length === 0 : cart.print_count < 1;
    }
    if (screen === "checkout") next.disabled = cart.print_count < 1;
    if (lastOrder) {
      const codeEl = $("[data-finish-code]");
      const totEl = $("[data-finish-total]");
      if (codeEl) codeEl.textContent = lastOrder.code;
      if (totEl) totEl.textContent = `${lastOrder.print_count || 0} εκτυπώσεις · ${euro(lastOrder.total_cents || 0)}`;
    }

    const grid = $("[data-photo-grid]");
    const empty = $("[data-empty]");
    const query = String(photoQuery || "").trim().toLowerCase();
    const visible = photos.filter((p) => !query || String(p.name || "").toLowerCase().includes(query));
    const ids = visible.map((p) => p.id).join(",");
    if (empty) empty.hidden = photos.length > 0;
    if (!grid) return;
    const prevIds = grid.dataset.ids || "";
    if (ids && ids === prevIds) {
      visible.forEach((p) => {
        const card = grid.querySelector(`[data-open="${CSS.escape(p.id)}"]`);
        if (!card) return;
        const n = (p.lines || []).reduce((s, l) => s + l.qty, 0);
        const q = qtyObject(p.qty)[defaultSku] || 0;
        const qtyEl = card.querySelector(".qty");
        const qEl = card.querySelector(".card-tools strong");
        if (qtyEl) qtyEl.textContent = n ? n + " εκτυπώσεις" : "χωρίς ποσότητα";
        if (qEl) qEl.textContent = q;
        card.classList.toggle("selected", selected.has(p.id));
        const img = card.querySelector("img");
        if (img) img.style.cssText = previewFilter(p);
      });
    } else if (!query && prevIds && (ids === prevIds || ids.startsWith(prevIds + ","))) {
      const have = new Set(prevIds.split(",").filter(Boolean));
      visible.forEach((p) => {
        if (have.has(p.id)) return;
        grid.insertAdjacentHTML("beforeend", cardHtml(p));
        bindCard(grid.querySelector(`[data-open="${CSS.escape(p.id)}"]`));
      });
      grid.dataset.ids = ids;
    } else {
      grid.dataset.ids = ids;
      grid.innerHTML = visible.map((p) => cardHtml(p)).join("");
      grid.querySelectorAll("[data-open]").forEach((card) => bindCard(card));
    }

    const summary = $("[data-summary]");
    if (summary) {
      const lines = [];
      photos.forEach((p) => (p.lines || []).forEach((l) => lines.push(l)));
      const groups = {};
      lines.forEach((l) => {
        groups[l.sku] = groups[l.sku] || { name: l.name, qty: 0, cents: 0 };
        groups[l.sku].qty += l.qty;
        groups[l.sku].cents += l.line_cents;
      });
      const rows = Object.values(groups)
        .map((g) => `<li><span>${escapeHtml(g.name)} ×${g.qty}</span><b>${euro(g.cents)}</b></li>`)
        .join("");
      const acs = mode === "web" && document.querySelector('[name="delivery"][value="acs"]')?.checked ? boot.acsCents : 0;
      const paper = document.querySelector('[name="paper"]:checked')?.value || cart.options?.paper || "glossy";
      summary.innerHTML = `<h2>Σύνοψη</h2>
        <ul>${rows || "<li>Δεν επιλέχθηκαν εκτυπώσεις</li>"}</ul>
        <p>${paper === "matte" ? "Χαρτί ματ" : "Χαρτί γυαλιστερό"}</p>
        ${acs ? `<p>ACS ${euro(acs)}</p>` : ""}
        <p><strong>Σύνολο ${euro(cart.print_cents + acs)}</strong></p>`;
    }
  }

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

  async function refreshCart() {
    const data = await api(urls.cart);
    cart = data.cart;
    render();
    return cart;
  }

  function previewFilter(photo) {
    const b = 1 + (photo.brightness || 0) / 100 + (photo.midtones || 0) / 220;
    const c = 1 + (photo.contrast || 0) / 100;
    const s = 1 + (photo.saturation || 0) / 100;
    let extra = "";
    if (photo.filter === "bw") extra = " grayscale(1)";
    if (photo.filter === "sepia") extra = " sepia(0.85)";
    if (photo.filter === "vivid") extra = " saturate(1.35) contrast(1.12)";
    const flip = photo.flip ? " scaleX(-1)" : "";
    const rot = (photo.rotate || 0) + (Number(photo.straighten) || 0);
    return `filter: brightness(${b}) contrast(${c}) saturate(${s})${extra}; transform: rotate(${rot}deg)${flip};`;
  }

  function editPayload(photo) {
    return {
      fit: photo.fit || "fill",
      rotate: photo.rotate || 0,
      straighten: Number(photo.straighten) || 0,
      caption: photo.caption || "",
      brightness: photo.brightness || 0,
      contrast: photo.contrast || 0,
      saturation: photo.saturation || 0,
      midtones: photo.midtones || 0,
      wbR: Number(photo.wbR) || 1,
      wbG: Number(photo.wbG) || 1,
      wbB: Number(photo.wbB) || 1,
      filter: photo.filter || "none",
      flip: photo.flip ? 1 : 0,
      sharpen: photo.sharpen ? 1 : 0,
      paper_ori: photo.paperOri || photo.paper_ori || "auto",
      crop: photo.crop || { x: 0, y: 0, w: 1, h: 1 },
      qty: qtyObject(photo.qty),
    };
  }

  async function saveEdits(extra = {}) {
    const edits = {};
    photoList().forEach((p) => {
      edits[p.id] = extra[p.id] || editDrafts[p.id] || editPayload(p);
    });
    Object.keys(extra).forEach((id) => {
      edits[id] = extra[id];
    });
    const data = await api(urls.cart, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ edits }),
    });
    cart = data.cart;
    render();
  }

  async function uploadFiles(fileList) {
    const files = [...fileList].filter((f) => /^image\/(jpeg|png|webp)$/i.test(f.type) || /\.(jpe?g|png|webp)$/i.test(f.name));
    if (!files.length) {
      $("[data-upload-status]").textContent = "Δεχόμαστε JPEG, PNG ή WebP (όχι HEIC).";
      return;
    }
    const bar = $("[data-progress]");
    const fill = $("i", bar);
    bar.hidden = false;
    fill.style.width = "8%";
    $("[data-upload-status]").textContent = `Μεταφόρτωση ${files.length} αρχείων…`;
    const chunk = 2;
    let added = 0;
    const errors = [];
    let shown = false;
    try {
      for (let i = 0; i < files.length; i += chunk) {
        const body = new FormData();
        files.slice(i, i + chunk).forEach((f) => body.append("photos[]", f));
        body.append("csrf_token", csrf);
        const res = await fetch(urls.upload, { method: "POST", headers: { "X-CSRF-Token": csrf }, body });
        const data = await res.json().catch(() => ({ ok: false, error: "Σφάλμα διακομιστή" }));
        if (data.cart) cart = data.cart;
        added += data.added?.length || 0;
        if (data.error && !errors.includes(data.error)) errors.push(data.error);
        (data.errors || []).forEach((e) => {
          if (e && !errors.includes(e)) errors.push(e);
        });
        fill.style.width = Math.round(((i + chunk) / files.length) * 100) + "%";
        $("[data-upload-status]").textContent =
          errors.length && added === 0
            ? errors[0]
            : `Φορτώθηκαν ${added} από ${files.length}…`;
        if (photoList().length && !shown) {
          shown = true;
          showGallery(true);
        } else if (shown) {
          render();
        }
      }
      $("[data-upload-status]").textContent = added
        ? `Φορτώθηκαν ${added} φωτογραφίες.` + (errors.length ? " " + errors[0] : "")
        : errors[0] || "Δεν φορτώθηκε καμία φωτογραφία.";
      if (photoList().length) showGallery(true);
      else render();
    } catch (e) {
      $("[data-upload-status]").textContent = e.message;
    } finally {
      setTimeout(() => {
        bar.hidden = true;
        fill.style.width = "0";
      }, 600);
    }
  }

  async function importLocal(payload) {
    $("[data-upload-status]").textContent = "Φόρτωση φωτογραφιών…";
    const bar = $("[data-progress]");
    bar.hidden = false;
    $("i", bar).style.width = "40%";
    try {
      const data = await api(urls.importDrive, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      cart = data.cart;
      $("i", bar).style.width = "100%";
      $("[data-upload-status]").textContent = `Φορτώθηκαν ${(data.added || []).length} φωτογραφίες από ${data.label || "τον φάκελο"}.`;
      if (photoList().length) showGallery(true);
    } catch (e) {
      $("[data-upload-status]").textContent = e.message;
    } finally {
      bar.hidden = true;
    }
  }

  async function nativeImagePaths() {
    if (typeof pickImages !== "function") return null;
    try {
      let raw = await pickImages();
      if (raw == null || raw === "") return [];
      if (typeof raw === "string") {
        raw = raw.trim();
        if (!raw) return [];
        try {
          raw = JSON.parse(raw);
        } catch (e) {
          return raw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
        }
      }
      if (typeof raw === "string") {
        try {
          raw = JSON.parse(raw);
        } catch (e) {
          return raw ? [raw] : [];
        }
      }
      if (Array.isArray(raw)) {
        return raw.map((x) => String(x || "").trim()).filter(Boolean);
      }
      return [];
    } catch (e) {
      return [];
    }
  }

  async function nativeFolderPath() {
    if (typeof browseFolder !== "function") return "";
    try {
      return String((await browseFolder("")) || "").trim();
    } catch (e) {
      return "";
    }
  }

  async function importDrive(id) {
    await importLocal({ drive: id });
  }

  async function pickCard() {
    try {
      const data = await api(urls.drives);
      const drives = data.drives || [];
      if (drives.length === 0) {
        $("[data-upload-status]").textContent = "Δεν φάνηκε αυτόματα κάρτα. Διάλεξε τον φάκελο DCIM.";
        await pickFolder();
        return;
      }
      if (drives.length === 1) {
        await importDrive(drives[0].id);
        return;
      }
      const box = $("[data-drive-list]");
      box.innerHTML = drives
        .map((d) => `<button class="btn" type="button" data-drive-id="${escapeHtml(d.id)}">${escapeHtml(d.label)}</button>`)
        .join("");
      box.querySelectorAll("[data-drive-id]").forEach((btn) => {
        btn.addEventListener("click", async () => {
          $("#drive-dialog").close();
          await importDrive(btn.dataset.driveId);
        });
      });
      $("#drive-dialog").showModal();
    } catch (e) {
      $("[data-upload-status]").textContent = e.message;
    }
  }

  async function pickFolder() {
    const native = await nativeFolderPath();
    if (native) {
      await importLocal({ folder: native });
      return;
    }
    if (!window.showDirectoryPicker) {
      $("[data-upload-status]").textContent = "Διάλεξε φάκελο από το παράθυρο που ανοίγει, ή βάλε κάρτα/USB.";
      return;
    }
    try {
      const dir = await window.showDirectoryPicker();
      const files = [];
      async function walk(handle) {
        for await (const entry of handle.values()) {
          if (entry.kind === "file") {
            const file = await entry.getFile();
            if (/\.(jpe?g|png|webp)$/i.test(file.name)) files.push(file);
          } else if (entry.kind === "directory" && !entry.name.startsWith(".")) {
            await walk(entry);
          }
        }
      }
      await walk(dir);
      await uploadFiles(files);
    } catch (e) {
      if (e.name !== "AbortError") $("[data-upload-status]").textContent = e.message;
    }
  }

  function photoById(id) {
    return photoList().find((p) => p.id === id);
  }

  function currentEditSku() {
    if (editing) {
      const keys = Object.keys(editing.qty || {}).filter((s) => Number(editing.qty[s]) > 0);
      if (keys.length) return keys[0];
    }
    const skuSel = $("[data-edit-sku]");
    if (skuSel && skuSel.value) return skuSel.value;
    return defaultSku || catalog()[0]?.sku || products[0]?.sku || "10x15";
  }

  function paperRatio() {
    const { w, h } = paperSides();
    return w / Math.max(0.01, h);
  }

  function autoOrientSku(sku) {
    const s = String(sku || "");
    return !s.startsWith("cal") && s !== "id35x45" && s !== "passport" && s !== "id9x13" && s !== "notebook" && s !== "a4";
  }

  function nativePhotoSize() {
    const img = $("[data-edit-img]");
    let nw = img?.naturalWidth || 0;
    let nh = img?.naturalHeight || 0;
    if (!nw || !nh) {
      const photo = editing ? photoById(editing.id) : null;
      nw = Number(photo?.width) || 3;
      nh = Number(photo?.height) || 2;
    }
    return { nw, nh };
  }

  function rotatedSrcSize() {
    const { nw, nh } = nativePhotoSize();
    const swap = editQuad() === 90 || editQuad() === 270;
    return { srcW: swap ? nh : nw, srcH: swap ? nw : nh, natW: nw, natH: nh, swap };
  }

  function coverCrop(srcW, srcH, ratio) {
    const srcRatio = srcW / Math.max(0.01, srcH);
    if (srcRatio > ratio) {
      const w = ratio / srcRatio;
      return { x: (1 - w) / 2, y: 0, w, h: 1 };
    }
    const h = srcRatio / ratio;
    return { x: 0, y: (1 - h) / 2, w: 1, h };
  }

  function ensureCover(crop, srcW, srcH, ratio) {
    const cover = coverCrop(srcW, srcH, ratio);
    let w = Number(crop?.w) || cover.w;
    let h = (w * srcW) / (ratio * srcH);
    if (w > cover.w + 0.0001 || h > cover.h + 0.0001) {
      return { x: cover.x, y: cover.y, w: cover.w, h: cover.h };
    }
    if (h > 1) {
      h = 1;
      w = (h * ratio * srcH) / srcW;
    }
    if (w > 1) {
      w = 1;
      h = (w * srcW) / (ratio * srcH);
    }
    w = Math.max(0.08, Math.min(cover.w, w));
    h = (w * srcW) / (ratio * srcH);
    let x = Number(crop?.x) || 0;
    let y = Number(crop?.y) || 0;
    x = Math.max(0, Math.min(1 - w, x));
    y = Math.max(0, Math.min(1 - h, y));
    return { x, y, w, h };
  }

  function paperSides() {
    const product = paperProduct();
    let w = Number(product.width_cm) || 10;
    let h = Number(product.height_cm) || 15;
    if (!autoOrientSku(product.sku)) return { w, h };
    const short = Math.min(w, h);
    const long = Math.max(w, h);
    const ori = editing?.paperOri || "auto";
    if (ori === "landscape") return { w: long, h: short };
    if (ori === "portrait") return { w: short, h: long };
    const { nw, nh } = nativePhotoSize();
    if (nw > nh * 1.05) return { w: long, h: short };
    if (nh > nw * 1.05) return { w: short, h: long };
    return { w, h };
  }

  function resolvedPaperOri() {
    const { w, h } = paperSides();
    return w >= h ? "landscape" : "portrait";
  }

  function syncPaperOriButtons() {
    const allow = autoOrientSku(currentEditSku());
    const ori = resolvedPaperOri();
    $$("[data-paper-ori]").forEach((b) => {
      b.hidden = !allow;
      b.classList.toggle("on", allow && b.dataset.paperOri === ori);
    });
  }

  function hasCustomCrop(c) {
    if (!c) return false;
    const x = Number(c.x) || 0;
    const y = Number(c.y) || 0;
    const w = Number(c.w) || 1;
    const h = Number(c.h) || 1;
    return x > 0.015 || y > 0.015 || w < 0.985 || h < 0.985;
  }

  function stampEdit(photoId, payload) {
    if (!photoId || !payload) return;
    editDrafts[photoId] = payload;
    const p = photoById(photoId);
    if (!p) return;
    Object.assign(p, {
      fit: payload.fit,
      rotate: payload.rotate,
      straighten: payload.straighten,
      caption: payload.caption,
      brightness: payload.brightness,
      contrast: payload.contrast,
      saturation: payload.saturation,
      midtones: payload.midtones,
      wbR: payload.wbR,
      wbG: payload.wbG,
      wbB: payload.wbB,
      filter: payload.filter,
      flip: payload.flip,
      sharpen: payload.sharpen,
      paperOri: payload.paper_ori || payload.paperOri || "auto",
      paper_ori: payload.paper_ori || payload.paperOri || "auto",
      crop: payload.crop,
      qty: payload.qty,
    });
  }

  function cropFrame() {
    const wrap = $("[data-crop-wrap]");
    const wrapW = wrap?.clientWidth || 0;
    const wrapH = wrap?.clientHeight || 0;
    return { wrapW, wrapH };
  }

  function analyzeImageStats(img) {
    const clamp = (n, a, b) => Math.max(a, Math.min(b, n));
    const empty = {
      brightness: 8,
      contrast: 12,
      saturation: 8,
      midtones: 6,
      wbR: 1,
      wbG: 1,
      wbB: 1,
    };
    if (!img || !img.naturalWidth) return empty;
    try {
      const c = document.createElement("canvas");
      const w = 96;
      const h = 96;
      c.width = w;
      c.height = h;
      const ctx = c.getContext("2d", { willReadFrequently: true });
      if (!ctx) return empty;
      ctx.drawImage(img, 0, 0, w, h);
      const data = ctx.getImageData(0, 0, w, h).data;
      const luma = [];
      let sum = 0;
      let sumSat = 0;
      let sumR = 0;
      let sumG = 0;
      let sumB = 0;
      let midN = 0;
      let n = 0;
      for (let i = 0; i < data.length; i += 4) {
        const r = data[i];
        const g = data[i + 1];
        const b = data[i + 2];
        const y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
        luma.push(y);
        sum += y;
        const mx = Math.max(r, g, b);
        const mn = Math.min(r, g, b);
        sumSat += mx === 0 ? 0 : (mx - mn) / mx;
        if (y > 28 && y < 230) {
          sumR += r;
          sumG += g;
          sumB += b;
          midN++;
        }
        n++;
      }
      if (!n) return empty;
      luma.sort((a, b) => a - b);
      const pct = (p) => luma[Math.max(0, Math.min(luma.length - 1, Math.round(p * (luma.length - 1))))];
      const pLow = pct(0.005);
      const pHigh = pct(0.995);
      const avg = sum / n;
      const range = Math.max(1, pHigh - pLow);
      const sat = sumSat / n;
      const mR = midN ? sumR / midN : 128;
      const mG = midN ? sumG / midN : 128;
      const mB = midN ? sumB / midN : 128;
      const gray = (mR + mG + mB) / 3 || 1;
      const round = (v, a, b) => Math.max(a, Math.min(b, Math.round(v)));
      const targetMid = 118;
      const stretchedMid = (avg - pLow) / range * 255;
      return {
        brightness: round((targetMid - stretchedMid) * 0.22 + (138 - avg) * 0.18, -36, 36),
        contrast: round(clamp((210 - range) * 0.42, 0, 48), 0, 48),
        saturation: round((0.36 - sat) * 85, -6, 24),
        midtones: round((targetMid - avg) * 0.2, -28, 28),
        wbR: clamp(gray / Math.max(8, mR), 0.72, 1.38),
        wbG: clamp(gray / Math.max(8, mG), 0.72, 1.38),
        wbB: clamp(gray / Math.max(8, mB), 0.72, 1.38),
      };
    } catch (e) {
      return empty;
    }
  }

  function analyzeAutoLook(img) {
    return analyzeImageStats(img);
  }

  function applyLookFilter(img) {
    if (!img || !editing) return;
    const b = Number(editing.brightness) || 0;
    const c = Number(editing.contrast) || 0;
    const s = Number(editing.saturation) || 0;
    const m = Number(editing.midtones) || 0;
    const wr = Number(editing.wbR) || 1;
    const wg = Number(editing.wbG) || 1;
    const wb = Number(editing.wbB) || 1;
    $$("[data-wb-matrix]").forEach((el) => {
      el.setAttribute("values", `${wr} 0 0 0 0  0 ${wg} 0 0 0  0 0 ${wb} 0 0  0 0 0 1 0`);
    });
    const add = (b * 2.2) / 255;
    $$("[data-br-i]").forEach((el) => el.setAttribute("intercept", String(add)));
    const exp = Math.pow(2, -m / 40);
    $$("[data-mid-exp]").forEach((el) => el.setAttribute("exponent", String(Math.max(0.25, Math.min(4, exp)))));
    const slope = 1 + c / 100;
    const intercept = (127 / 255) * (1 - slope);
    $$("[data-con-lin]").forEach((el) => {
      el.setAttribute("slope", String(slope));
      el.setAttribute("intercept", String(intercept));
    });
    let sat = 1 + s / 100;
    $$("[data-sat-matrix]").forEach((el) => el.setAttribute("values", String(sat)));
    let extra = "";
    if (editing.filter === "bw") extra = " grayscale(1)";
    if (editing.filter === "sepia") extra = " sepia(0.85)";
    if (editing.filter === "vivid") extra = " contrast(1.18) brightness(1.02)";
    img.style.filter = `url(#lab-look)${extra}`;
  }

  function applyPreview() {
    const img = $("[data-edit-img]");
    if (!img || !editing) return;
    applyLookFilter(img);
    img.style.transform = editing.flip ? "scaleX(-1)" : "none";
    img.style.objectFit = "contain";
    if (editing.fit === "fit") {
      layoutFitPaper();
    } else {
      layoutCrop(false);
    }
    const cap = $("[data-caption-preview]");
    if (cap) {
      cap.hidden = !editing.caption;
      cap.textContent = editing.caption || "";
    }
    const guide = $("[data-id-guide]");
    if (guide) guide.hidden = currentEditSku() !== "id35x45";
  }

  function editQuad() {
    return (((editing?.rotate || 0) % 360) + 360) % 360;
  }

  function stageCropMax() {
    const wrap = $("[data-crop-wrap]");
    const viewport = wrap?.parentElement;
    const maxW = Math.max(140, (viewport?.clientWidth || 800) - 8);
    const maxH = Math.max(140, (viewport?.clientHeight || 600) - 24);
    return { maxW, maxH };
  }

  function sizeFillImage() {
    const img = $("[data-edit-img]");
    const wrap = $("[data-crop-wrap]");
    if (!img || !wrap) return { wrapW: 0, wrapH: 0, imgW: 0, imgH: 0, swap: false };
    const { nw, nh } = nativePhotoSize();
    const { maxW, maxH } = stageCropMax();
    const swap = editQuad() === 90 || editQuad() === 270;
    const visW = swap ? nh : nw;
    const visH = swap ? nw : nh;
    const scale = Math.min(maxW / Math.max(1, visW), maxH / Math.max(1, visH), 1);
    const imgW = Math.max(40, nw * scale);
    const imgH = Math.max(40, nh * scale);
    img.style.maxWidth = "none";
    img.style.maxHeight = "none";
    img.style.objectFit = "contain";
    img.style.width = imgW + "px";
    img.style.height = imgH + "px";
    wrap.style.width = (swap ? imgH : imgW) + "px";
    wrap.style.height = (swap ? imgW : imgH) + "px";
    wrap.style.overflow = "hidden";
    wrap.style.position = "relative";
    clampWrapAboveBar();
    return {
      wrapW: wrap.clientWidth,
      wrapH: wrap.clientHeight,
      imgW: img.clientWidth,
      imgH: img.clientHeight,
      swap,
    };
  }

  function clampWrapAboveBar() {
    const wrap = $("[data-crop-wrap]");
    const img = $("[data-edit-img]");
    const bar = document.querySelector(".crop-rotate-bar");
    if (!wrap || !img || !bar) return;
    const gap = 12;
    const wrapRect = wrap.getBoundingClientRect();
    const barTop = bar.getBoundingClientRect().top;
    if (wrapRect.bottom <= barTop - gap) return;
    const need = Math.max(80, barTop - gap - wrapRect.top);
    const scale = need / Math.max(1, wrapRect.height);
    if (!isFinite(scale) || scale >= 0.999) return;
    wrap.style.width = Math.max(80, Math.round(wrapRect.width * scale)) + "px";
    wrap.style.height = Math.max(80, Math.round(wrapRect.height * scale)) + "px";
    img.style.width = Math.max(40, Math.round(img.clientWidth * scale)) + "px";
    img.style.height = Math.max(40, Math.round(img.clientHeight * scale)) + "px";
  }

  function paperProduct() {
    const sku = currentEditSku();
    return products.find((p) => p.sku === sku) || catalog()[0] || products[0] || {};
  }

  function layoutFitPaper() {
    const img = $("[data-edit-img]");
    const box = $("[data-crop-box]");
    const wrap = $("[data-crop-wrap]");
    const spin = $("[data-crop-spin]");
    const hint = $("[data-fit-hint]");
    const label = $("[data-paper-label]");
    if (!img || !wrap || !spin || !editing) return;
    wrap.classList.add("is-fit");
    wrap.classList.remove("is-fill");
    if (box) box.style.display = "none";
    $$("[data-crop-zoom-in], [data-crop-zoom-out]").forEach((b) => {
      b.hidden = true;
    });
    const product = paperProduct();
    const ratio = paperRatio();
    const viewport = wrap.parentElement;
    const maxW = Math.max(160, (viewport?.clientWidth || 800) - 16);
    const maxH = Math.max(160, (viewport?.clientHeight || 600) - 16);
    let paperW;
    let paperH;
    if (maxW / maxH > ratio) {
      paperH = maxH;
      paperW = paperH * ratio;
    } else {
      paperW = maxW;
      paperH = paperW / ratio;
    }
    wrap.style.width = paperW + "px";
    wrap.style.height = paperH + "px";
    const quad = editQuad();
    const swap = quad === 90 || quad === 270;
    const { nw: natW, nh: natH } = nativePhotoSize();
    const srcW = swap ? natH : natW;
    const srcH = swap ? natW : natH;
    const srcRatio = srcW / srcH;
    let drawW;
    let drawH;
    if (srcRatio > ratio) {
      drawW = paperW;
      drawH = paperW / srcRatio;
    } else {
      drawH = paperH;
      drawW = paperH * srcRatio;
    }
    const scale = swap ? drawW / natH : drawW / natW;
    const boxW = natW * scale;
    const boxH = natH * scale;
    img.style.maxWidth = "none";
    img.style.maxHeight = "none";
    img.style.objectFit = "contain";
    img.style.width = boxW + "px";
    img.style.height = boxH + "px";
    spin.style.position = "absolute";
    spin.style.width = boxW + "px";
    spin.style.height = boxH + "px";
    spin.style.left = (paperW - boxW) / 2 + "px";
    spin.style.top = (paperH - boxH) / 2 + "px";
    spin.style.transformOrigin = "50% 50%";
    spin.style.transform = `rotate(${quad + (Number(editing.straighten) || 0)}deg)`;
    const name = product.name || currentEditSku();
    if (label) {
      const sides = paperSides();
      label.hidden = false;
      label.textContent = name + (sides.w >= sides.h ? " · οριζόντιο" : " · κάθετο");
    }
    if (hint) {
      hint.hidden = false;
      hint.textContent = "Θα τυπωθεί ολόκληρη η φωτογραφία με λευκές λωρίδες όπως το βλέπετε.";
    }
    syncPaperOriButtons();
  }

  function clearFitPaper() {
    const wrap = $("[data-crop-wrap]");
    const spin = $("[data-crop-spin]");
    const img = $("[data-edit-img]");
    const hint = $("[data-fit-hint]");
    const label = $("[data-paper-label]");
    wrap?.classList.remove("is-fit");
    wrap?.classList.remove("is-fill");
    if (wrap) {
      wrap.style.width = "";
      wrap.style.height = "";
    }
    if (spin) {
      spin.style.position = "";
      spin.style.left = "";
      spin.style.top = "";
      spin.style.width = "";
      spin.style.height = "";
    }
    if (img) {
      img.style.width = "";
      img.style.height = "";
      img.style.maxWidth = "";
      img.style.maxHeight = "";
    }
    if (hint) hint.hidden = true;
    if (label) label.hidden = true;
    $$("[data-crop-zoom-in], [data-crop-zoom-out]").forEach((b) => {
      b.hidden = false;
    });
  }

  function openEditor(id) {
    if (productMode === "docs") {
      const photo = photoById(id);
      if (photo && isIdSheetPhoto(photo)) {
        afterPhotos();
        return;
      }
      try {
        startIdPlace(id);
      } catch (e) {
        alert(e.message);
      }
      return;
    }
    const photo = photoById(id);
    if (!photo) return;
    const draft = editDrafts[id] || {};
    const src = Object.assign({}, photo, draft);
    editing = {
      id: photo.id,
      name: photo.name,
      fit: src.fit || "fill",
      rotate: src.rotate || 0,
      straighten: Number(src.straighten) || 0,
      caption: src.caption || "",
      brightness: src.brightness || 0,
      contrast: src.contrast || 0,
      saturation: src.saturation || 0,
      midtones: src.midtones || 0,
      wbR: Number(src.wbR) || 1,
      wbG: Number(src.wbG) || 1,
      wbB: Number(src.wbB) || 1,
      filter: src.filter || "none",
      flip: src.flip ? 1 : 0,
      sharpen: src.sharpen ? 1 : 0,
      paperOri: src.paperOri || src.paper_ori || "auto",
      crop: Object.assign({ x: 0, y: 0, w: 1, h: 1 }, src.crop || {}),
      qty: qtyObject(src.qty),
    };
    const dialog = $("#editor");
    const img = $("[data-edit-img]");
    img.draggable = false;
    img.ondragstart = (e) => e.preventDefault();
    const previewUrl = photo.preview || photo.thumb || photo.original;
    $("[data-editor-status]").textContent = "Φόρτωση…";
    const paintCrop = () => {
      if (!editing || editing.id !== id) return;
      $("[data-editor-status]").textContent = "";
      layoutCrop(!hasCustomCrop(editing.crop));
    };
    img.onload = paintCrop;
    img.src = previewUrl;
    if (img.complete && img.naturalWidth) paintCrop();
    $$("[data-fit-val]").forEach((b) => b.classList.toggle("on", b.dataset.fitVal === editing.fit));
    const skuSel = $("[data-edit-sku]");
    if (skuSel) {
      skuSel.innerHTML = catalog().map((p) => `<option value="${p.sku}">${p.name}</option>`).join("");
      skuSel.value = Object.keys(editing.qty)[0] || defaultSku;
    }
    $("[data-brightness]").value = editing.brightness;
    $("[data-brightness-val]").textContent = editing.brightness;
    $("[data-contrast]").value = editing.contrast;
    $("[data-contrast-val]").textContent = editing.contrast;
    $("[data-saturation]").value = editing.saturation;
    $("[data-saturation-val]").textContent = editing.saturation;
    $("[data-midtones]").value = editing.midtones || 0;
    $("[data-midtones-val]").textContent = editing.midtones || 0;
    $$("[data-filter-val]").forEach((b) => b.classList.toggle("on", b.dataset.filterVal === editing.filter));
    $("[data-sharpen]")?.classList.toggle("on", !!editing.sharpen);
    $("[data-straighten]").value = editing.straighten || 0;
    $("[data-straighten-val]").textContent = (editing.straighten || 0) + "°";
    if ($("[data-caption]")) $("[data-caption]").value = editing.caption || "";
    renderQty();
    applyPreview();
    dialog.showModal();
    requestAnimationFrame(() => {
      if (editing && editing.id === id) layoutCrop(!hasCustomCrop(editing.crop));
    });
  }

  function renderQty() {
    const wrap = $("[data-edit-qty]");
    wrap.innerHTML = catalog()
      .map((p) => {
        const n = editing.qty[p.sku] || 0;
        return `<div class="qty-line"><span>${p.name}<br><small>${euro(p.price_cents)}</small></span>
          <div class="stepper">
            <button type="button" data-qty-sku="${p.sku}" data-d="-1">−</button>
            <strong>${n}</strong>
            <button type="button" data-qty-sku="${p.sku}" data-d="1">+</button>
          </div></div>`;
      })
      .join("");
    wrap.querySelectorAll("[data-qty-sku]").forEach((btn) => {
      btn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        const sku = btn.dataset.qtySku;
        const d = Number(btn.dataset.d);
        editing.qty = qtyObject(editing.qty);
        editing.qty[sku] = Math.max(0, Math.min(99, (editing.qty[sku] || 0) + d));
        if (!editing.qty[sku]) delete editing.qty[sku];
        renderQty();
        layoutCrop(false);
      });
    });
  }

  function layoutCrop(init) {
    const img = $("[data-edit-img]");
    const box = $("[data-crop-box]");
    const wrap = $("[data-crop-wrap]");
    const spin = $("[data-crop-spin]");
    if (!img || !editing) return;
    if (editing.fit === "fit") {
      layoutFitPaper();
      syncPaperOriButtons();
      return;
    }
    clearFitPaper();
    wrap?.classList.add("is-fill");
    applyLookFilter(img);
    img.style.transform = editing.flip ? "scaleX(-1)" : "none";
    img.style.objectFit = "contain";
    const sized = sizeFillImage();
    const wrapW = sized.wrapW;
    const wrapH = sized.wrapH;
    const iw = sized.imgW;
    const ih = sized.imgH;
    if (!wrapW || !wrapH || !iw || !ih || !box || !wrap || !spin) return;
    const ratio = paperRatio();
    const { srcW, srcH } = rotatedSrcSize();
    if (init === true || !hasCustomCrop(editing.crop)) {
      editing.crop = coverCrop(srcW, srcH, ratio);
    } else {
      editing.crop = ensureCover(editing.crop, srcW, srcH, ratio);
    }
    spin.style.position = "absolute";
    spin.style.width = iw + "px";
    spin.style.height = ih + "px";
    spin.style.left = (wrapW - iw) / 2 + "px";
    spin.style.top = (wrapH - ih) / 2 + "px";
    spin.style.transformOrigin = "50% 50%";
    spin.style.transform = `rotate(${editQuad() + (Number(editing.straighten) || 0)}deg)`;
    box.style.display = "block";
    let w = (Number(editing.crop.w) || 1) * wrapW;
    let h = w / ratio;
    let x = (Number(editing.crop.x) || 0) * wrapW;
    let y = (Number(editing.crop.y) || 0) * wrapH;
    w = Math.min(wrapW, Math.max(56, w));
    h = w / ratio;
    if (h > wrapH) {
      h = wrapH;
      w = h * ratio;
    }
    if (w > wrapW) {
      w = wrapW;
      h = w / ratio;
    }
    x = Math.max(0, Math.min(wrapW - w, x));
    y = Math.max(0, Math.min(wrapH - h, y));
    box.style.left = x + "px";
    box.style.top = y + "px";
    box.style.width = w + "px";
    box.style.height = h + "px";
    editing.crop = { x: x / wrapW, y: y / wrapH, w: w / wrapW, h: h / wrapH };
    const label = $("[data-paper-label]");
    if (label) {
      const product = paperProduct();
      const sides = paperSides();
      label.hidden = false;
      label.textContent = (product.name || currentEditSku()) + (sides.w >= sides.h ? " · οριζόντιο" : " · κάθετο");
    }
    const cap = $("[data-caption-preview]");
    if (cap) {
      cap.hidden = !editing.caption;
      cap.textContent = editing.caption || "";
    }
    const guide = $("[data-id-guide]");
    if (guide) guide.hidden = currentEditSku() !== "id35x45";
    syncPaperOriButtons();
  }

  function zoomCrop(dir) {
    if (!editing) return;
    if (editing.fit === "fit") {
      editing.fit = "fill";
      $$("[data-fit-val]").forEach((b) => b.classList.toggle("on", b.dataset.fitVal === "fill"));
      layoutCrop(true);
      return;
    }
    const { wrapW, wrapH } = cropFrame();
    const box = $("[data-crop-box]");
    if (!wrapW || !box) return;
    const ratio = paperRatio();
    let w = parseFloat(box.style.width) || wrapW * (editing.crop.w || 1);
    let h = parseFloat(box.style.height) || w / ratio;
    let left = parseFloat(box.style.left) || 0;
    let top = parseFloat(box.style.top) || 0;
    const cx = left + w / 2;
    const cy = top + h / 2;
    const factor = dir > 0 ? 0.86 : 1.16;
    w = Math.max(56, Math.min(wrapW, w * factor));
    h = w / ratio;
    if (h > wrapH) {
      h = wrapH;
      w = h * ratio;
    }
    left = Math.max(0, Math.min(wrapW - w, cx - w / 2));
    top = Math.max(0, Math.min(wrapH - h, cy - h / 2));
    box.style.left = left + "px";
    box.style.top = top + "px";
    box.style.width = w + "px";
    box.style.height = h + "px";
    readCropFromBox();
  }

  function readCropFromBox() {
    const box = $("[data-crop-box]");
    if (!editing) return;
    const { wrapW, wrapH } = cropFrame();
    if (!wrapW || !wrapH || !box) return;
    const { srcW, srcH } = rotatedSrcSize();
    editing.crop = ensureCover(
      {
        x: parseFloat(box.style.left) / wrapW,
        y: parseFloat(box.style.top) / wrapH,
        w: parseFloat(box.style.width) / wrapW,
        h: parseFloat(box.style.height) / wrapH,
      },
      srcW,
      srcH,
      paperRatio()
    );
  }

  function startDrag(e) {
    if (!editing || editing.fit === "fit") return;
    if (e.target.closest("button, input, label, a")) return;
    const handle = e.target.dataset.handle || "move";
    const box = $("[data-crop-box]");
    if (!box || box.style.display === "none") return;
    const pt = e.touches ? e.touches[0] : e;
    const { wrapW, wrapH } = cropFrame();
    if (!wrapW || !wrapH) return;
    drag = {
      handle,
      pointerId: e.pointerId,
      x: pt.clientX,
      y: pt.clientY,
      left: parseFloat(box.style.left) || 0,
      top: parseFloat(box.style.top) || 0,
      w: parseFloat(box.style.width) || wrapW,
      h: parseFloat(box.style.height) || wrapH,
      maxW: wrapW,
      maxH: wrapH,
      ratio: paperRatio(),
    };
    if (typeof e.pointerId === "number" && box.setPointerCapture) {
      try {
        box.setPointerCapture(e.pointerId);
      } catch (err) {}
    }
    e.preventDefault();
    e.stopPropagation();
  }

  function moveDrag(e) {
    if (!drag || drag.handle == null) return;
    if (drag.pointerId != null && e.pointerId != null && e.pointerId !== drag.pointerId) return;
    const pt = e.touches ? e.touches[0] : e;
    const dx = pt.clientX - drag.x;
    const dy = pt.clientY - drag.y;
    const box = $("[data-crop-box]");
    let left = drag.left;
    let top = drag.top;
    let w = drag.w;
    let h = drag.h;
    const ratio = drag.ratio;
    if (drag.handle === "move") {
      left = Math.max(0, Math.min(drag.maxW - w, drag.left + dx));
      top = Math.max(0, Math.min(drag.maxH - h, drag.top + dy));
    } else {
      if (drag.handle.includes("e")) w = drag.w + dx;
      if (drag.handle.includes("w")) {
        w = drag.w - dx;
        left = drag.left + dx;
      }
      w = Math.max(56, w);
      h = w / ratio;
      if (drag.handle.includes("n")) top = drag.top + (drag.h - h);
      if (h > drag.maxH) {
        h = drag.maxH;
        w = h * ratio;
      }
      if (left < 0) {
        w += left;
        h = w / ratio;
        left = 0;
      }
      if (top < 0) {
        h += top;
        w = h * ratio;
        top = 0;
      }
      if (left + w > drag.maxW) {
        w = drag.maxW - left;
        h = w / ratio;
      }
      if (top + h > drag.maxH) {
        h = drag.maxH - top;
        w = h * ratio;
      }
    }
    box.style.left = left + "px";
    box.style.top = top + "px";
    box.style.width = w + "px";
    box.style.height = h + "px";
    readCropFromBox();
    e.preventDefault();
  }

  function endDrag(e) {
    if (drag && e && drag.pointerId != null && e.pointerId != null && e.pointerId !== drag.pointerId) return;
    drag = null;
    readCropFromBox();
  }

  async function closeEditor(save) {
    const btn = $("[data-editor-done]");
    const status = $("[data-editor-status]");
    if (save && editing) {
      const payload = editPayload(editing);
      stampEdit(editing.id, payload);
      btn.disabled = true;
      status.textContent = "Αποθήκευση…";
      try {
        await saveEdits({ [editing.id]: payload });
        stampEdit(editing.id, payload);
        status.textContent = "Αποθηκεύτηκε.";
      } catch (e) {
        status.textContent = e.message;
        btn.disabled = false;
        return;
      }
      btn.disabled = false;
    }
    editing = null;
    $("#editor").close();
  }

  function bumpIdle() {
    if (mode !== "kiosk") return;
    clearTimeout(idleTimer);
    idleTimer = setTimeout(() => {
      if (screen === "finish") {
        location.href = urls.attract;
        return;
      }
      if (confirm("Να ακυρωθεί η παραγγελία λόγω αδράνειας;")) {
        location.href = urls.attract;
      } else {
        bumpIdle();
      }
    }, screen === "finish" ? 90000 : 180000);
  }

  const storeSel = $("[data-store]");
  if (storeSel) {
    storeSel.innerHTML = (boot.stores || []).map((s) => `<option value="${s.id}">${s.name}</option>`).join("");
  }
  const defSel = $("[data-default-sku]");
  if (defSel) {
    defSel.addEventListener("change", () => {
      defaultSku = defSel.value;
    });
  }
  applyProduct();

  $$("[data-product]").forEach((btn) => {
    btn.addEventListener("click", () => pickProduct(btn.dataset.product));
  });

  const nativeApp = typeof pickImages === "function" || typeof browseFolder === "function";
  if (!nativeApp) {
    $("[data-card-import]")?.setAttribute("hidden", "");
    $("[data-folder-import]")?.setAttribute("hidden", "");
    $("[data-screen='source'] .source-grid")?.classList.add("is-files-only");
  }

  async function pickFromFiles(input) {
    const status = $("[data-upload-status]");
    if (typeof pickImages === "function") {
      const paths = await nativeImagePaths();
      if (!paths.length) {
        if (status) status.textContent = "Δεν επιλέχθηκαν φωτογραφίες. Δοκίμασε «Φάκελος» αν το παράθυρο δεν άνοιξε.";
        return;
      }
      await importLocal({ paths });
      return;
    }
    if (typeof browseFolder === "function") {
      if (status) status.textContent = "Διάλεξε τον φάκελο με τις φωτογραφίες.";
      await pickFolder();
      return;
    }
    input?.click();
  }

  $$("[data-file-input]").forEach((input) => {
    input.addEventListener("click", (e) => {
      if (typeof pickImages !== "function" && typeof browseFolder !== "function") return;
      e.preventDefault();
      e.stopPropagation();
      pickFromFiles(input);
    }, true);
    input.addEventListener("change", () => {
      if (input.files?.length) {
        uploadFiles(input.files);
      } else if (typeof pickImages !== "function" && typeof browseFolder !== "function") {
        $("[data-upload-status]").textContent = "Δεν φορτώθηκαν αρχεία. Δοκίμασε Φάκελος.";
      }
      input.value = "";
    });
  });
  $("[data-card-import]")?.addEventListener("click", pickCard);
  $("[data-folder-import]")?.addEventListener("click", pickFolder);
  $("[data-drive-cancel]")?.addEventListener("click", () => $("#drive-dialog").close());

  $("[data-finish-later]")?.addEventListener("click", () => finishPickup("later"));
  $("[data-finish-soon]")?.addEventListener("click", () => finishPickup("soon"));

  async function placeSilentOrder() {
    if (placedOrder) return placedOrder;
    const form = $("[data-checkout-form]");
    const payload = form ? Object.fromEntries(new FormData(form).entries()) : {};
    const name = ($("[data-route-name]")?.value || "").trim();
    payload.edits = {};
    photoList().forEach((p) => {
      payload.edits[p.id] = editPayload(p);
    });
    payload.options = {
      paper: payload.paper || "glossy",
      border: payload.border || "none",
      rush: 0,
      payment: payload.payment || "counter",
    };
    payload.skip_customer = 1;
    payload.name = name || "Πελάτης κιόσκ";
    payload.phone = payload.phone || "0000000000";
    payload.delivery = "pickup";
    const data = await api(urls.checkout, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    placedOrder = {
      code: data.code,
      total_cents: data.total_cents || cart.print_cents,
      print_count: data.print_count || cart.print_count,
    };
    lastOrder = placedOrder;
    return placedOrder;
  }

  async function chooseRoute(kind) {
    const status = $("[data-route-status]");
    const buttons = $$("[data-route-print], [data-route-archive]");
    if (status) status.textContent = kind === "print" ? "Καταχώρηση παραγγελίας…" : "Αρχειοθέτηση…";
    buttons.forEach((b) => {
      b.disabled = true;
    });
    try {
      await placeSilentOrder();
      if (kind === "print") {
        if (status) status.textContent = "";
        buttons.forEach((b) => {
          b.disabled = false;
        });
        $("[data-finish-print]")?.click();
        return;
      }
      await finishPickup("later");
    } catch (e) {
      if (status) status.textContent = e.message || "Κάτι πήγε στραβά.";
      buttons.forEach((b) => {
        b.disabled = false;
      });
    }
  }

  $("[data-route-print]")?.addEventListener("click", () => chooseRoute("print"));
  $("[data-route-archive]")?.addEventListener("click", () => chooseRoute("archive"));

  async function finishPickup(when) {
    const status = $("[data-finish-status]");
    try {
      const data = await api(urls.finishLater, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ when }),
      });
      if (status && data.folder) status.textContent = "Αποθηκεύτηκαν στο " + data.folder;
      location.href = data.redirect || urls.thanks + "?how=" + encodeURIComponent(when);
    } catch (e) {
      if (status) status.textContent = e.message;
    }
  }

  let staffPin = "";
  let printerCache = [];
  function paintPrinterList(query) {
    const box = $("[data-printer-list]");
    if (!box) return;
    const q = (query || "").trim().toLowerCase();
    const list = printerCache.filter((p) => {
      const hay = `${p.name || ""} ${p.id || ""} ${p.kind || ""} ${p.port || ""}`.toLowerCase();
      return !q || hay.includes(q);
    });
    const kindLabel = (p) => (p.kind === "network" ? " · δίκτυο" : p.kind === "default" ? " · προεπιλογή" : p.default ? " · αποθηκευμένος" : "");
    box.innerHTML =
      list
        .map(
          (p) =>
            `<button class="btn ${p.id === chosenPrinter ? "" : "btn-ghost"}" type="button" data-printer-id="${escapeHtml(p.id)}">${escapeHtml(p.name)}${kindLabel(p)}</button>`
        )
        .join("") || "<p class='hint'>Δεν βρέθηκαν εκτυπωτές Windows.</p>";
    box.querySelectorAll("[data-printer-id]").forEach((btn) => {
      btn.addEventListener("click", () => {
        chosenPrinter = btn.dataset.printerId || "";
        box.querySelectorAll("[data-printer-id]").forEach((b) => {
          b.classList.toggle("btn-ghost", b !== btn);
        });
      });
    });
  }
  $("[data-finish-print]")?.addEventListener("click", () => {
    const form = $("[data-print-pin-form]");
    const err = $("[data-print-pin-error]");
    if (err) err.hidden = true;
    if (form) form.reset();
    $("#print-pin-dialog")?.showModal();
    form?.querySelector("[name=pin]")?.focus();
  });
  $("[data-print-pin-cancel]")?.addEventListener("click", () => $("#print-pin-dialog").close());
  let chosenPrinter = "";
  let chosenSize = "all";
  $("[data-print-pin-form]")?.addEventListener("submit", async (e) => {
    e.preventDefault();
    const err = $("[data-print-pin-error]");
    err.hidden = true;
    staffPin = String(new FormData(e.target).get("pin") || "");
    try {
      const data = await api(urls.printers, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ pin: staffPin }),
      });
      $("#print-pin-dialog").close();
      chosenPrinter = "";
      printerCache = data.printers || [];
      paintPrinterList($("[data-printer-search]")?.value);
      const sizes = data.sizes || [];
      const fourBySix = sizes.find((s) => s.inch === "4x6" || s.id === "10x15");
      chosenSize = fourBySix ? fourBySix.id : sizes[0]?.id || "all";
      const sizeBox = $("[data-print-sizes]");
      if (sizeBox) {
        sizeBox.innerHTML = sizes
          .map(
            (s) =>
              `<button class="btn ${s.id === chosenSize ? "" : "btn-ghost"}" type="button" data-print-sku="${escapeHtml(s.id)}">${escapeHtml(s.name)}${s.qty ? " · " + s.qty : ""}</button>`
          )
          .join("");
        sizeBox.querySelectorAll("[data-print-sku]").forEach((btn) => {
          btn.addEventListener("click", () => {
            chosenSize = btn.dataset.printSku || "all";
            sizeBox.querySelectorAll("[data-print-sku]").forEach((b) => {
              b.classList.toggle("btn-ghost", b !== btn);
            });
          });
        });
      }
      const hot = $("[data-print-hotfolder]");
      if (hot) hot.hidden = !data.hotfolder;
      $("[data-printer-error]").hidden = true;
      $("#printer-dialog").showModal();
    } catch (ex) {
      err.textContent = ex.message;
      err.hidden = false;
      staffPin = "";
    }
  });
  $("[data-printer-cancel]")?.addEventListener("click", () => {
    staffPin = "";
    $("#printer-dialog").close();
  });
  $("[data-print-go]")?.addEventListener("click", () => sendPrintNow(chosenPrinter, chosenSize));
  $("[data-printer-search]")?.addEventListener("input", (e) => paintPrinterList(e.target.value));
  $("[data-printer-unc-add]")?.addEventListener("click", () => {
    const v = ($("[data-printer-unc]")?.value || "").trim();
    if (!v) return;
    if (!printerCache.some((p) => p.id === v)) {
      printerCache.unshift({ id: v, name: v, kind: "network", port: v });
    }
    chosenPrinter = v;
    paintPrinterList($("[data-printer-search]")?.value);
  });

  async function sendPrintNow(printer, sku) {
    const err = $("[data-printer-error]");
    const status = $("[data-finish-status]");
    err.hidden = true;
    try {
      $$("[data-printer-id], [data-print-sku], [data-print-go]").forEach((b) => {
        b.disabled = true;
      });
      err.hidden = false;
      err.textContent = "Στέλνω στον εκτυπωτή Windows…";
      const data = await api(urls.printNow, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ pin: staffPin, printer, sku: sku || "all" }),
      });
      staffPin = "";
      $("#printer-dialog").close();
      if (status) {
        const bits = [];
        if (data.windows === false) bits.push(`Στάλθηκαν ${data.printed || 0} εκτυπώσεις στον εκτυπωτή.`);
        else bits.push(`Στάλθηκαν ${data.printed || 0} εκτυπώσεις στα Windows.`);
        if (data.copied) bits.push("Αντίγραφο και στο DNP hot folder.");
        status.textContent = bits.join(" ");
      }
      location.href = data.redirect || urls.thanks + "?how=now";
    } catch (ex) {
      $$("[data-printer-id], [data-print-sku], [data-print-go]").forEach((b) => {
        b.disabled = false;
      });
      err.textContent = ex.message;
      err.hidden = false;
    }
  }

  $("[data-next]").addEventListener("click", async () => {
    if (screen === "source") {
      if (photoList().length) setScreen("gallery");
      return;
    }
    if (screen === "gallery") {
      if (productMode === "calendar") {
        try {
          if (!hasCalendarPages()) {
            startCalPlace();
            return;
          }
          if (cart.print_count < 1) return;
          afterPhotos();
        } catch (e) {
          alert(e.message);
        }
        return;
      }
      if (productMode === "docs") {
        try {
          if (!hasIdSheets()) {
            startIdPlace();
            return;
          }
          if (cart.print_count < 1) return;
          afterPhotos();
        } catch (e) {
          alert(e.message);
        }
        return;
      }
      if (cart.print_count < 1) return;
      afterPhotos();
      return;
    }
    if (screen === "cal-place") {
      try {
        $("[data-next]").disabled = true;
        await confirmCalPlace();
      } catch (e) {
        alert(e.message);
        $("[data-next]").disabled = false;
      }
      return;
    }
    if (screen === "id-place") {
      try {
        $("[data-next]").disabled = true;
        await confirmIdPlace();
      } catch (e) {
        alert(e.message);
        $("[data-next]").disabled = false;
      }
      return;
    }
    if (screen === "finish") return;
    const form = $("[data-checkout-form]");
    const err = $("[data-form-error]");
    err.hidden = true;
    const payload = Object.fromEntries(new FormData(form).entries());
    payload.edits = {};
    photoList().forEach((p) => {
      payload.edits[p.id] = editPayload(p);
    });
    payload.options = {
      paper: payload.paper || "glossy",
      border: payload.border || "none",
      rush: 0,
      payment: payload.payment || "counter",
    };
    payload.skip_customer = payload.skip_customer ? 1 : 0;
    if (payload.skip_customer) {
      payload.name = payload.name || "Πελάτης κιόσκ";
      payload.phone = payload.phone || "0000000000";
    }
    try {
      if (!payload.skip_customer) {
        localStorage.setItem(
          "lab_last_customer",
          JSON.stringify({ name: payload.name, phone: payload.phone, email: payload.email })
        );
      }
      $("[data-next]").disabled = true;
      const data = await api(urls.checkout, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      lastOrder = {
        code: data.code,
        total_cents: data.total_cents || cart.print_cents,
        print_count: data.print_count || cart.print_count,
      };
      if (data.print_dir) {
        const saved = $("[data-print-saved]");
        if (saved) saved.hidden = false;
      }
      if (mode === "kiosk") {
        setScreen("finish");
        $("[data-next]").disabled = false;
        return;
      }
      location.href = data.redirect || urls.thanks;
    } catch (e) {
      err.textContent = e.message;
      err.hidden = false;
      $("[data-next]").disabled = false;
    }
  });

  $("[data-back]").addEventListener("click", () => {
    if (screen === "route") setScreen("gallery");
    else if (screen === "checkout") setScreen(mode === "kiosk" ? "route" : "gallery");
    else if (screen === "gallery") setScreen("source");
    else if (screen === "source") setScreen(productMode === "calendar" ? "cal-style" : "product");
    else if (screen === "cal-style") setScreen("product");
    else if (screen === "cal-place") setScreen("gallery");
    else if (screen === "id-place") setScreen("gallery");
  });

  $("[data-cancel]")?.addEventListener("click", async () => {
    if (!confirm("Ακύρωση και επιστροφή στην αρχή;")) return;
    await api(urls.reset, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ channel: "kiosk" }),
    });
    location.href = urls.attract;
  });

  $("[data-exit-windows]")?.addEventListener("click", () => $("#exit-dialog").showModal());
  $("[data-exit-cancel]")?.addEventListener("click", () => $("#exit-dialog").close());
  $("[data-exit-form]")?.addEventListener("submit", async (e) => {
    e.preventDefault();
    const err = $("[data-exit-error]");
    err.hidden = true;
    try {
      await api(urls.exit, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ pin: new FormData(e.target).get("pin") }),
      });
      document.exitFullscreen?.();
      window.close();
      setTimeout(() => {
        location.href = "about:blank";
      }, 400);
    } catch (ex) {
      err.textContent = ex.message;
      err.hidden = false;
    }
  });

  $("[data-all-plus]")?.addEventListener("click", async () => {
    const extra = {};
    photoList().forEach((p) => {
      const qty = qtyObject(p.qty);
      qty[defaultSku] = (qty[defaultSku] || 0) + 1;
      extra[p.id] = Object.assign(editPayload(p), { qty });
    });
    await saveEdits(extra);
  });
  $("[data-all-zero]")?.addEventListener("click", async () => {
    const extra = {};
    photoList().forEach((p) => {
      extra[p.id] = Object.assign(editPayload(p), { qty: {} });
    });
    await saveEdits(extra);
  });
  $("[data-id-set]")?.addEventListener("click", async () => {
    defaultSku = "id35x45";
    if (defSel) defSel.value = defaultSku;
    const extra = {};
    photoList().forEach((p) => {
      extra[p.id] = Object.assign(editPayload(p), { qty: { id35x45: 4 }, fit: "fill" });
    });
    await saveEdits(extra);
  });
  $("[data-selected-plus]")?.addEventListener("click", async () => {
    if (!selected.size) {
      alert("Τσέκαρε τις φωτογραφίες που θέλεις (κουτάκι πάνω αριστερά).");
      return;
    }
    const extra = {};
    const ids = selected.size ? [...selected] : photoList().map((p) => p.id);
    ids.forEach((id) => {
      const p = photoById(id);
      if (!p) return;
      const qty = qtyObject(p.qty);
      qty[defaultSku] = (qty[defaultSku] || 0) + 1;
      extra[id] = Object.assign(editPayload(p), { qty });
    });
    await saveEdits(extra);
  });
  function hasCalendarPages() {
    return photoList().some((p) => (p.lines || []).some((l) => String(l.sku || "").startsWith("cal")));
  }

  function isIdSheetPhoto(p) {
    const n = String(p?.name || p?.original_name || "");
    return n.startsWith("tautotites-9x13") || (p?.lines || []).some((l) => l.sku === "id9x13");
  }

  function idSourcePhotos() {
    return photoList().filter((p) => !isIdSheetPhoto(p));
  }

  function hasIdSheets() {
    return photoList().some((p) => isIdSheetPhoto(p));
  }

  let calPlace = { ids: [], index: 0, zoom: 1, panX: 0, panY: 0, views: {} };
  const CAL_MONTHS = ["Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος"];

  function startCalPlace() {
    const ids = selected.size ? [...selected] : photoList().map((p) => p.id);
    if (!ids.length) throw new Error("Φόρτωσε τουλάχιστον μία φωτογραφία.");
    calPlace = { ids, index: 0, zoom: 1, panX: 0, panY: 0, views: {} };
    setScreen("cal-place");
    paintCalPlace();
  }

  function rememberCalView() {
    const id = calPlace.ids[calPlace.index];
    if (!id) return;
    calPlace.views[id] = { zoom: calPlace.zoom, panX: calPlace.panX, panY: calPlace.panY };
  }

  function restoreCalView(id) {
    const saved = calPlace.views[id];
    if (saved) {
      calPlace.zoom = saved.zoom;
      calPlace.panX = saved.panX;
      calPlace.panY = saved.panY;
    } else {
      calPlace.zoom = 1;
      calPlace.panX = 0;
      calPlace.panY = 0;
    }
  }

  function monthGridHtml(year, month, compact) {
    const days = ["Δε", "Τρ", "Τε", "Πε", "Πα", "Σα", "Κυ"];
    const first = new Date(year, month - 1, 1);
    let start = first.getDay();
    start = start === 0 ? 6 : start - 1;
    const dim = new Date(year, month, 0).getDate();
    let cells = "";
    for (let i = 0; i < start; i++) cells += "<span></span>";
    for (let d = 1; d <= dim; d++) {
      const dow = (start + d - 1) % 7;
      cells += `<span${dow >= 5 ? ' class="sun"' : ""}>${d}</span>`;
    }
    const title = compact ? `<b>${CAL_MONTHS[month - 1].slice(0, 3)}</b>` : "";
    return `<div class="cal-mini ${compact ? "compact" : ""}">${title}<div class="cal-weekdays">${days.map((d) => `<span>${d}</span>`).join("")}</div><div class="cal-days">${cells}</div></div>`;
  }

  function paintCalChrome() {
    const page = $("[data-cal-page]");
    const head = $("[data-cal-head]");
    const foot = $("[data-cal-chrome]");
    const bar = $("[data-cal-slot-bar]");
    if (!page || !foot) return;
    page.dataset.style = calendarStyle;
    const month = (calPlace.index % 12) + 1;
    if (bar) {
      bar.hidden = calendarStyle !== "wall";
      bar.textContent = "Ημερολόγιο 2027 · Πανόραμα";
    }
    if (head) {
      if (calendarStyle === "classic") {
        head.innerHTML = `<em>Πανόραμα</em><strong>${CAL_MONTHS[month - 1]} 2027</strong>`;
      } else if (calendarStyle === "months") {
        head.innerHTML = "";
      } else {
        head.innerHTML = "";
      }
    }
    if (calendarStyle === "wall") {
      foot.innerHTML = `<div class="cal-year-grid">${CAL_MONTHS.map((_, i) => monthGridHtml(2027, i + 1, true)).join("")}</div>`;
    } else {
      foot.innerHTML = `<div class="cal-month-block"><h3>${CAL_MONTHS[month - 1]} 2027</h3>${monthGridHtml(2027, month, false)}</div>`;
    }
  }

  function paintCalPlace() {
    const photo = photoById(calPlace.ids[calPlace.index]);
    const page = $("[data-cal-page]");
    const img = $("[data-cal-img]");
    if (!photo || !page || !img) return;
    paintCalChrome();
    img.onload = () => applyCalTransform();
    img.src = photo.preview || photo.preview || photo.thumb;
    applyCalTransform();
    requestAnimationFrame(() => applyCalTransform());
    const many = calPlace.ids.length > 1;
    const prev = $("[data-cal-prev-photo]");
    const next = $("[data-cal-next-photo]");
    if (prev) prev.hidden = !many;
    if (next) next.hidden = !many;
    const status = $("[data-cal-place-status]");
    if (status) {
      status.textContent = many
        ? `Φωτογραφία ${calPlace.index + 1} από ${calPlace.ids.length} · σύρε μέσα στο πλαίσιο, zoom in / zoom out`
        : "Σύρε τη φωτογραφία μέσα στο πλαίσιο. Zoom in / zoom out αν θέλεις πιο κοντά.";
    }
  }

  function calPhotoSize(img) {
    const w = img?.naturalWidth || 0;
    const h = img?.naturalHeight || 0;
    if (w > 0 && h > 0) return { w, h };
    const photo = photoById(calPlace.ids[calPlace.index]);
    return { w: Number(photo?.width) || 3, h: Number(photo?.height) || 2 };
  }

  function calCoverSize(slotW, slotH, imgW, imgH, zoom) {
    const z = Math.max(1, zoom);
    const imgR = imgW / Math.max(1, imgH);
    const slotR = slotW / Math.max(1, slotH);
    if (imgR > slotR) {
      const drawH = slotH * z;
      return { drawW: drawH * imgR, drawH };
    }
    const drawW = slotW * z;
    return { drawW, drawH: drawW / imgR };
  }

  function applyCalTransform() {
    const slot = $("[data-cal-slot]");
    const move = $("[data-cal-move]");
    const img = $("[data-cal-img]");
    if (!slot || !move) return;
    const slotW = Math.max(1, slot.clientWidth);
    const slotH = Math.max(1, slot.clientHeight);
    const src = calPhotoSize(img);
    const { drawW, drawH } = calCoverSize(slotW, slotH, src.w, src.h, calPlace.zoom);
    const maxX = Math.max(0, (drawW - slotW) / 2);
    const maxY = Math.max(0, (drawH - slotH) / 2);
    calPlace.panX = Math.max(-maxX, Math.min(maxX, calPlace.panX));
    calPlace.panY = Math.max(-maxY, Math.min(maxY, calPlace.panY));
    move.style.width = drawW + "px";
    move.style.height = drawH + "px";
    move.style.left = (slotW - drawW) / 2 + calPlace.panX + "px";
    move.style.top = (slotH - drawH) / 2 + calPlace.panY + "px";
    move.style.transform = "none";
    if (img) {
      img.style.width = "100%";
      img.style.height = "100%";
      img.style.objectFit = "fill";
      img.style.maxWidth = "none";
      img.style.maxHeight = "none";
      img.style.pointerEvents = "none";
    }
  }

  function cropFromView(photo, view) {
    const slot = $("[data-cal-slot]");
    const slotW = Math.max(1, slot?.clientWidth || 1);
    const slotH = Math.max(1, slot?.clientHeight || 1);
    const imgW = Number(photo.width) || 3;
    const imgH = Number(photo.height) || 2;
    const { drawW, drawH } = calCoverSize(slotW, slotH, imgW, imgH, view.zoom || 1);
    const left = (slotW - drawW) / 2 + (view.panX || 0);
    const top = (slotH - drawH) / 2 + (view.panY || 0);
    const bw = Math.min(1, Math.max(0.08, slotW / drawW));
    const bh = Math.min(1, Math.max(0.08, slotH / drawH));
    let x = -left / drawW;
    let y = -top / drawH;
    x = Math.max(0, Math.min(1 - bw, x));
    y = Math.max(0, Math.min(1 - bh, y));
    return { x, y, w: bw, h: bh };
  }

  function cropFromCalPlace(photo) {
    return cropFromView(photo, calPlace);
  }

  async function confirmCalPlace() {
    rememberCalView();
    const extra = {};
    calPlace.ids.forEach((id) => {
      const p = photoById(id);
      if (!p) return;
      const view = calPlace.views[id] || { zoom: calPlace.zoom, panX: calPlace.panX, panY: calPlace.panY };
      extra[id] = Object.assign(editPayload(p), { crop: cropFromView(p, view), fit: "fill" });
    });
    if (Object.keys(extra).length) await saveEdits(extra);
    selected = new Set(calPlace.ids);
    await placeOnCalendar();
    if (cart.print_count < 1) throw new Error("Δεν φτιάχτηκε το ημερολόγιο.");
    afterPhotos();
    $("[data-next]").disabled = false;
  }

  const ID_HEAD_FRAC = 0.7;
  const ID_HEAD_TOP = 0.08;
  const ID_ASPECT = 3.5 / 4.5;
  let idPlace = { ids: [], index: 0, rotate: 0, phase: "mark-top", top: null, bot: null, crop: null, views: {} };

  function startIdPlace(focusId) {
    const pool = idSourcePhotos();
    const selectedIds = [...selected].filter((id) => pool.some((p) => p.id === id));
    const ids = (focusId || selectedIds.length === 0 ? pool.map((p) => p.id) : selectedIds).filter(Boolean);
    if (!ids.length) throw new Error("Φόρτωσε τουλάχιστον μία φωτογραφία.");
    let index = 0;
    if (focusId) {
      const i = ids.indexOf(focusId);
      index = i >= 0 ? i : 0;
    }
    idPlace = { ids, index, rotate: 0, phase: "mark-top", top: null, bot: null, crop: null, views: {} };
    setScreen("id-place");
    restoreIdView(ids[index]);
    paintIdPlace();
  }

  function rememberIdView() {
    const id = idPlace.ids[idPlace.index];
    if (!id) return;
    idPlace.views[id] = {
      rotate: idPlace.rotate || 0,
      phase: idPlace.phase,
      top: idPlace.top,
      bot: idPlace.bot,
      crop: idPlace.crop,
    };
  }

  function restoreIdView(id) {
    const photo = photoById(id);
    const saved = idPlace.views[id];
    if (saved) {
      idPlace.rotate = saved.rotate || 0;
      idPlace.phase = saved.phase || "mark-top";
      idPlace.top = saved.top;
      idPlace.bot = saved.bot;
      idPlace.crop = saved.crop;
      return;
    }
    const rot = ((Number(photo?.rotate) || 0) % 360 + 360) % 360;
    const crop = photo?.crop;
    const hasCrop = crop && Number(crop.w) > 0 && Number(crop.w) < 0.999;
    idPlace.rotate = rot;
    idPlace.top = null;
    idPlace.bot = null;
    idPlace.crop = hasCrop ? { x: crop.x, y: crop.y, w: crop.w, h: crop.h } : null;
    idPlace.phase = hasCrop ? "adjust" : "mark-top";
  }

  function idWrapSize() {
    const wrap = $("[data-id-wrap]");
    return { w: Math.max(1, wrap?.clientWidth || 1), h: Math.max(1, wrap?.clientHeight || 1) };
  }

  function cropFromIdMarks(top, bot, wrapW, wrapH) {
    const ty = Math.min(top.y, bot.y);
    const by = Math.max(top.y, bot.y);
    const midX = (top.x + bot.x) / 2;
    const headH = Math.max(0.03, by - ty);
    let h = headH / ID_HEAD_FRAC;
    let w = (h * wrapH * ID_ASPECT) / wrapW;
    let y = ty - ID_HEAD_TOP * h;
    let x = midX - w / 2;
    if (w > 1) {
      const s = 1 / w;
      w = 1;
      h *= s;
    }
    if (h > 1) {
      const s = 1 / h;
      h = 1;
      w *= s;
    }
    x = Math.max(0, Math.min(1 - w, x));
    y = Math.max(0, Math.min(1 - h, y));
    return { x, y, w, h };
  }

  function keepIdAspect(crop, wrapW, wrapH, around) {
    const cx = around ? around.x : crop.x + crop.w / 2;
    const cy = around ? around.y : crop.y + crop.h / 2;
    let w = Math.max(0.08, Math.min(1, crop.w));
    let h = (w * wrapW) / (ID_ASPECT * wrapH);
    if (h > 1) {
      h = 1;
      w = (h * ID_ASPECT * wrapH) / wrapW;
    }
    if (w > 1) {
      w = 1;
      h = (w * wrapW) / (ID_ASPECT * wrapH);
    }
    let x = cx - w / 2;
    let y = cy - h / 2;
    x = Math.max(0, Math.min(1 - w, x));
    y = Math.max(0, Math.min(1 - h, y));
    return { x, y, w, h };
  }

  function layoutIdImage() {
    const wrap = $("[data-id-wrap]");
    const img = $("[data-id-img]");
    const stage = $("[data-id-stage]");
    if (!wrap || !img || !stage) return;
    const rot = ((idPlace.rotate % 360) + 360) % 360;
    const swapped = rot === 90 || rot === 270;
    const natW = img.naturalWidth || Number(photoById(idPlace.ids[idPlace.index])?.width) || 3;
    const natH = img.naturalHeight || Number(photoById(idPlace.ids[idPlace.index])?.height) || 4;
    const srcW = swapped ? natH : natW;
    const srcH = swapped ? natW : natH;
    const maxW = Math.max(120, stage.clientWidth - 8);
    const maxH = Math.max(120, stage.clientHeight - 8);
    const scale = Math.min(maxW / srcW, maxH / srcH);
    const dw = Math.max(80, srcW * scale);
    const dh = Math.max(80, srcH * scale);
    wrap.style.width = dw + "px";
    wrap.style.height = dh + "px";
    img.style.maxWidth = "none";
    img.style.maxHeight = "none";
    img.style.width = (swapped ? dh : dw) + "px";
    img.style.height = (swapped ? dw : dh) + "px";
    img.style.position = "absolute";
    img.style.left = (dw - (swapped ? dh : dw)) / 2 + "px";
    img.style.top = (dh - (swapped ? dw : dh)) / 2 + "px";
    img.style.transform = `rotate(${rot}deg)`;
    img.style.transformOrigin = "center center";
  }

  function paintIdOverlay() {
    const wrap = $("[data-id-wrap]");
    const markTop = $("[data-id-mark-top]");
    const markBot = $("[data-id-mark-bot]");
    const frame = $("[data-id-frame]");
    if (!wrap) return;
    const { w, h } = idWrapSize();
    if (markTop) {
      markTop.hidden = !idPlace.top;
      if (idPlace.top) {
        markTop.style.left = idPlace.top.x * w + "px";
        markTop.style.top = idPlace.top.y * h + "px";
      }
    }
    if (markBot) {
      markBot.hidden = !idPlace.bot;
      if (idPlace.bot) {
        markBot.style.left = idPlace.bot.x * w + "px";
        markBot.style.top = idPlace.bot.y * h + "px";
      }
    }
    if (frame) {
      const show = idPlace.phase === "adjust" && idPlace.crop;
      frame.hidden = !show;
      if (show) {
        frame.style.left = idPlace.crop.x * w + "px";
        frame.style.top = idPlace.crop.y * h + "px";
        frame.style.width = idPlace.crop.w * w + "px";
        frame.style.height = idPlace.crop.h * h + "px";
      }
    }
    wrap.classList.toggle("is-adjust", idPlace.phase === "adjust");
    const hint = $("[data-id-place-hint]");
    const n = idPlace.ids.length;
    const who = n > 1 ? `Φωτογραφία ${idPlace.index + 1} από ${n}. ` : "";
    if (hint) {
      if (idPlace.phase === "mark-top") hint.textContent = who + "Πάτα το πάνω μέρος του κεφαλιού.";
      else if (idPlace.phase === "mark-bot") hint.textContent = who + "Πάτα το κάτω μέρος (πηγούνι).";
      else hint.textContent = who + "Σύρε το πλαίσιο, zoom ή περιστροφή. Μετά το βελάκι επόμενη.";
    }
    const many = n > 1;
    const prev = $("[data-id-prev]");
    const next = $("[data-id-next]");
    if (prev) prev.hidden = !many;
    if (next) next.hidden = !many;
    const zoomOff = idPlace.phase !== "adjust";
    ["[data-id-zoom-in]", "[data-id-zoom-out]"].forEach((sel) => {
      const b = $(sel);
      if (b) b.disabled = zoomOff;
    });
  }

  function paintIdPlace() {
    const photo = photoById(idPlace.ids[idPlace.index]);
    const img = $("[data-id-img]");
    if (!photo || !img) return;
    const src = photo.preview || photo.thumb || photo.original;
    img.onload = () => {
      layoutIdImage();
      paintIdOverlay();
    };
    if (img.src !== src) img.src = src;
    else {
      layoutIdImage();
      paintIdOverlay();
    }
    requestAnimationFrame(() => {
      layoutIdImage();
      paintIdOverlay();
    });
  }

  function idNormFromEvent(e) {
    const wrap = $("[data-id-wrap]");
    if (!wrap) return null;
    const t = e.touches ? e.touches[0] : e;
    const r = wrap.getBoundingClientRect();
    const x = (t.clientX - r.left) / Math.max(1, r.width);
    const y = (t.clientY - r.top) / Math.max(1, r.height);
    return {
      x: Math.max(0, Math.min(1, x)),
      y: Math.max(0, Math.min(1, y)),
    };
  }

  function applyIdMarks() {
    if (!idPlace.top || !idPlace.bot) return;
    const { w, h } = idWrapSize();
    idPlace.crop = cropFromIdMarks(idPlace.top, idPlace.bot, w, h);
    idPlace.phase = "adjust";
    paintIdOverlay();
  }

  function idZoom(dir) {
    if (idPlace.phase !== "adjust" || !idPlace.crop) return;
    const { w, h } = idWrapSize();
    const factor = dir > 0 ? 0.9 : 1.12;
    idPlace.crop = keepIdAspect(
      { x: idPlace.crop.x, y: idPlace.crop.y, w: idPlace.crop.w * factor, h: idPlace.crop.h * factor },
      w,
      h
    );
    paintIdOverlay();
  }

  function idRotate(deg) {
    rememberIdView();
    idPlace.rotate = (((idPlace.rotate || 0) + deg) % 360 + 360) % 360;
    idPlace.top = null;
    idPlace.bot = null;
    idPlace.crop = null;
    idPlace.phase = "mark-top";
    layoutIdImage();
    paintIdOverlay();
  }

  function idResetMarks() {
    idPlace.top = null;
    idPlace.bot = null;
    idPlace.crop = null;
    idPlace.phase = "mark-top";
    paintIdOverlay();
  }

  function idGo(delta) {
    if (idPlace.ids.length < 2) return;
    rememberIdView();
    idPlace.index = (idPlace.index + delta + idPlace.ids.length) % idPlace.ids.length;
    restoreIdView(idPlace.ids[idPlace.index]);
    paintIdPlace();
  }

  async function confirmIdPlace() {
    rememberIdView();
    const extra = {};
    const missing = [];
    idPlace.ids.forEach((id) => {
      const p = photoById(id);
      if (!p) return;
      const view = idPlace.views[id] || {};
      const crop = view.crop || (p.crop && Number(p.crop.w) > 0.04 && Number(p.crop.w) < 0.999 ? p.crop : null);
      if (!crop) {
        missing.push(p.name || id);
        return;
      }
      extra[id] = Object.assign(editPayload(p), {
        crop,
        rotate: view.rotate != null ? view.rotate : p.rotate || 0,
        fit: "fill",
        qty: {},
      });
    });
    if (missing.length) {
      throw new Error("Σημάδεψε κεφάλι και πηγούνι σε όλες τις φωτογραφίες πριν το φύλλο 9×13.");
    }
    if (Object.keys(extra).length) await saveEdits(extra);
    selected = new Set(idPlace.ids);
    const data = await api(urls.compose, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ type: "idsheet", ids: idPlace.ids }),
    });
    cart = data.cart;
    selected = new Set();
    render();
    if (cart.print_count < 1) throw new Error("Δεν φτιάχτηκε το φύλλο ταυτοτήτων.");
    afterPhotos();
    $("[data-next]").disabled = false;
  }

  async function placeOnCalendar() {
    const ids = selected.size ? [...selected] : photoList().map((p) => p.id);
    if (!ids.length) throw new Error("Φόρτωσε τουλάχιστον μία φωτογραφία.");
    const data = await api(urls.compose, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ type: "calendar", style: calendarStyle, ids }),
    });
    cart = data.cart;
    selected = new Set();
    render();
  }

  $$("[data-cal-style]").forEach((btn) => {
    btn.addEventListener("click", async () => {
      calendarStyle = btn.dataset.calStyle || "wall";
      try {
        await api(urls.cart, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ options: { product: "calendar", calendar_style: calendarStyle, lang } }),
        });
      } catch (e) {}
      setScreen("source");
    });
  });

  async function compose(type) {
    const ids = selected.size ? [...selected] : photoList().map((p) => p.id);
    const data = await api(urls.compose, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ type, ids }),
    });
    cart = data.cart;
    selected = new Set();
    render();
  }
  $("[data-collage]")?.addEventListener("click", async () => {
    try {
      await compose("collage");
    } catch (e) {
      alert(e.message);
    }
  });
  $("[data-index-print]")?.addEventListener("click", async () => {
    try {
      await compose("index");
    } catch (e) {
      alert(e.message);
    }
  });
  $("[data-place-calendar]")?.addEventListener("click", async () => {
    try {
      startCalPlace();
    } catch (e) {
      alert(e.message);
    }
  });
  $("[data-photo-search]")?.addEventListener("input", (e) => {
    photoQuery = e.target.value || "";
    render();
  });

  $$("[data-fit-val]").forEach((btn) => {
    btn.addEventListener("click", () => {
      if (!editing) return;
      editing.fit = btn.dataset.fitVal;
      $$("[data-fit-val]").forEach((b) => b.classList.toggle("on", b === btn));
      layoutCrop(true);
    });
  });
  $("[data-edit-sku]")?.addEventListener("change", () => {
    layoutCrop(true);
    applyPreview();
  });
  $("[data-brightness]").addEventListener("input", () => {
    if (!editing) return;
    editing.brightness = Number($("[data-brightness]").value);
    $("[data-brightness-val]").textContent = editing.brightness;
    applyPreview();
  });
  $("[data-contrast]").addEventListener("input", () => {
    if (!editing) return;
    editing.contrast = Number($("[data-contrast]").value);
    $("[data-contrast-val]").textContent = editing.contrast;
    applyPreview();
  });
  $("[data-saturation]").addEventListener("input", () => {
    if (!editing) return;
    editing.saturation = Number($("[data-saturation]").value);
    $("[data-saturation-val]").textContent = editing.saturation;
    applyPreview();
  });
  $("[data-midtones]")?.addEventListener("input", () => {
    if (!editing) return;
    editing.midtones = Number($("[data-midtones]").value);
    $("[data-midtones-val]").textContent = editing.midtones;
    applyPreview();
  });
  $$("[data-filter-val]").forEach((btn) => {
    btn.addEventListener("click", () => {
      if (!editing) return;
      editing.filter = btn.dataset.filterVal;
      $$("[data-filter-val]").forEach((b) => b.classList.toggle("on", b === btn));
      applyPreview();
    });
  });
  function rotateEdit(deg) {
    if (!editing) return;
    editing.rotate = ((editing.rotate || 0) + deg + 360) % 360;
    const { srcW, srcH } = rotatedSrcSize();
    editing.crop = coverCrop(srcW, srcH, paperRatio());
    layoutCrop(false);
  }

  function setPaperOri(ori) {
    if (!editing) return;
    editing.paperOri = ori === "landscape" ? "landscape" : "portrait";
    const { srcW, srcH } = rotatedSrcSize();
    editing.crop = coverCrop(srcW, srcH, paperRatio());
    layoutCrop(true);
  }

  function paintToneSliders() {
    if (!editing) return;
    $("[data-brightness]").value = editing.brightness;
    $("[data-contrast]").value = editing.contrast;
    $("[data-saturation]").value = editing.saturation;
    $("[data-midtones]").value = editing.midtones || 0;
    $("[data-brightness-val]").textContent = editing.brightness;
    $("[data-contrast-val]").textContent = editing.contrast;
    $("[data-saturation-val]").textContent = editing.saturation;
    $("[data-midtones-val]").textContent = editing.midtones || 0;
  }

  function applyAutoLighting() {
    const look = analyzeImageStats($("[data-edit-img]"));
    editing.brightness = look.brightness;
    editing.contrast = look.contrast;
    editing.midtones = look.midtones;
    paintToneSliders();
    applyPreview();
  }

  function applyAutoColor() {
    const look = analyzeImageStats($("[data-edit-img]"));
    editing.saturation = look.saturation;
    editing.wbR = look.wbR;
    editing.wbG = look.wbG;
    editing.wbB = look.wbB;
    paintToneSliders();
    applyPreview();
  }

  $("[data-rotate]").addEventListener("click", () => rotateEdit(90));
  $$("[data-paper-ori]").forEach((btn) => {
    btn.addEventListener("click", () => setPaperOri(btn.dataset.paperOri));
  });
  $("[data-crop-zoom-in]")?.addEventListener("click", () => zoomCrop(1));
  $("[data-crop-zoom-out]")?.addEventListener("click", () => zoomCrop(-1));
  $("[data-crop-wrap]")?.addEventListener(
    "wheel",
    (e) => {
      if (!editing) return;
      if (editing.fit === "fit") return;
      e.preventDefault();
      zoomCrop(e.deltaY < 0 ? 1 : -1);
    },
    { passive: false }
  );
  $("[data-cal-zoom-in]")?.addEventListener("click", () => {
    calPlace.zoom = Math.min(3, calPlace.zoom + 0.12);
    applyCalTransform();
  });
  $("[data-cal-zoom-out]")?.addEventListener("click", () => {
    calPlace.zoom = Math.max(1, calPlace.zoom - 0.12);
    applyCalTransform();
  });
  $("[data-cal-prev-photo]")?.addEventListener("click", () => {
    if (!calPlace.ids.length) return;
    rememberCalView();
    calPlace.index = (calPlace.index + calPlace.ids.length - 1) % calPlace.ids.length;
    restoreCalView(calPlace.ids[calPlace.index]);
    paintCalPlace();
  });
  $("[data-cal-next-photo]")?.addEventListener("click", () => {
    if (!calPlace.ids.length) return;
    rememberCalView();
    calPlace.index = (calPlace.index + 1) % calPlace.ids.length;
    restoreCalView(calPlace.ids[calPlace.index]);
    paintCalPlace();
  });
  (() => {
    const slot = $("[data-cal-slot]");
    if (!slot) return;
    let pan = null;
    const pt = (e) => {
      const t = e.touches ? e.touches[0] : e;
      return { x: t.clientX, y: t.clientY };
    };
    const down = (e) => {
      const screenEl = document.querySelector('[data-screen="cal-place"]');
      if (!screenEl || screenEl.hidden) return;
      if (!slot.contains(e.target)) return;
      e.preventDefault();
      const p = pt(e);
      pan = { id: e.pointerId, x: p.x, y: p.y, panX: calPlace.panX, panY: calPlace.panY };
      if (e.pointerId != null && slot.setPointerCapture) {
        try {
          slot.setPointerCapture(e.pointerId);
        } catch (err) {}
      }
      slot.classList.add("is-panning");
    };
    const move = (e) => {
      if (!pan) return;
      if (e.pointerId != null && pan.id != null && e.pointerId !== pan.id) return;
      e.preventDefault();
      const p = pt(e);
      calPlace.panX = pan.panX + (p.x - pan.x);
      calPlace.panY = pan.panY + (p.y - pan.y);
      applyCalTransform();
    };
    const up = (e) => {
      if (!pan) return;
      if (e.pointerId != null && pan.id != null && e.pointerId !== pan.id) return;
      pan = null;
      slot.classList.remove("is-panning");
    };
    slot.addEventListener("pointerdown", down, { passive: false });
    slot.addEventListener("pointermove", move, { passive: false });
    slot.addEventListener("pointerup", up);
    slot.addEventListener("pointercancel", up);
    slot.addEventListener("touchstart", down, { passive: false });
    slot.addEventListener("touchmove", move, { passive: false });
    slot.addEventListener("touchend", up);
    document.addEventListener("pointermove", move, { passive: false });
    document.addEventListener("pointerup", up);
    document.addEventListener("pointercancel", up);
    slot.addEventListener("dragstart", (e) => e.preventDefault());
    slot.addEventListener(
      "wheel",
      (e) => {
        e.preventDefault();
        calPlace.zoom = Math.max(1, Math.min(3, calPlace.zoom + (e.deltaY < 0 ? 0.1 : -0.1)));
        applyCalTransform();
      },
      { passive: false }
    );
  })();
  $("[data-id-zoom-in]")?.addEventListener("click", () => idZoom(1));
  $("[data-id-zoom-out]")?.addEventListener("click", () => idZoom(-1));
  $("[data-id-rot]")?.addEventListener("click", () => idRotate(90));
  $("[data-id-rot-left]")?.addEventListener("click", () => idRotate(-90));
  $("[data-id-reset]")?.addEventListener("click", () => idResetMarks());
  $("[data-id-prev]")?.addEventListener("click", () => idGo(-1));
  $("[data-id-next]")?.addEventListener("click", () => idGo(1));
  (() => {
    const wrap = $("[data-id-wrap]");
    if (!wrap) return;
    let drag = null;
    const screenOn = () => {
      const el = document.querySelector('[data-screen="id-place"]');
      return el && !el.hidden;
    };
    const down = (e) => {
      if (!screenOn()) return;
      const pt = idNormFromEvent(e);
      if (!pt) return;
      e.preventDefault();
      if (idPlace.phase === "adjust" && idPlace.crop) {
        drag = {
          id: e.pointerId,
          x: pt.x,
          y: pt.y,
          crop: Object.assign({}, idPlace.crop),
          moved: false,
        };
        wrap.classList.add("is-panning");
        if (e.pointerId != null && wrap.setPointerCapture) {
          try {
            wrap.setPointerCapture(e.pointerId);
          } catch (err) {}
        }
        return;
      }
      drag = { id: e.pointerId, tap: pt, moved: false };
    };
    const move = (e) => {
      if (!drag || !screenOn()) return;
      if (e.pointerId != null && drag.id != null && e.pointerId !== drag.id) return;
      const pt = idNormFromEvent(e);
      if (!pt) return;
      if (drag.crop) {
        e.preventDefault();
        const dx = pt.x - drag.x;
        const dy = pt.y - drag.y;
        if (Math.abs(dx) + Math.abs(dy) > 0.008) drag.moved = true;
        const { w, h } = idWrapSize();
        idPlace.crop = keepIdAspect(
          {
            x: drag.crop.x + dx,
            y: drag.crop.y + dy,
            w: drag.crop.w,
            h: drag.crop.h,
          },
          w,
          h,
          { x: drag.crop.x + drag.crop.w / 2 + dx, y: drag.crop.y + drag.crop.h / 2 + dy }
        );
        paintIdOverlay();
      }
    };
    const up = (e) => {
      if (!drag) return;
      if (e.pointerId != null && drag.id != null && e.pointerId !== drag.id) return;
      wrap.classList.remove("is-panning");
      const was = drag;
      drag = null;
      if (!screenOn()) return;
      if (was.crop) return;
      const pt = was.tap || idNormFromEvent(e);
      if (!pt) return;
      if (idPlace.phase === "mark-top") {
        idPlace.top = pt;
        idPlace.phase = "mark-bot";
        paintIdOverlay();
        return;
      }
      if (idPlace.phase === "mark-bot") {
        idPlace.bot = pt;
        applyIdMarks();
      }
    };
    wrap.addEventListener("pointerdown", down, { passive: false });
    wrap.addEventListener("pointermove", move, { passive: false });
    wrap.addEventListener("pointerup", up);
    wrap.addEventListener("pointercancel", up);
    wrap.addEventListener("dragstart", (e) => e.preventDefault());
    wrap.addEventListener(
      "wheel",
      (e) => {
        if (!screenOn() || idPlace.phase !== "adjust") return;
        e.preventDefault();
        idZoom(e.deltaY < 0 ? 1 : -1);
      },
      { passive: false }
    );
    window.addEventListener("resize", () => {
      if (editing && $("#editor")?.open) layoutCrop(false);
      if (!screenOn()) return;
      layoutIdImage();
      paintIdOverlay();
    });
  })();
  $("[data-rotate-left]")?.addEventListener("click", () => rotateEdit(270));
  $("[data-straighten]")?.addEventListener("input", () => {
    if (!editing) return;
    editing.straighten = Number($("[data-straighten]").value) || 0;
    $("[data-straighten-val]").textContent = editing.straighten + "°";
    applyPreview();
  });
  $("[data-caption]")?.addEventListener("input", () => {
    if (!editing) return;
    editing.caption = $("[data-caption]").value || "";
    applyPreview();
  });
  async function stepEditor(delta) {
    if (!editing) return;
    const list = photoList();
    const i = list.findIndex((p) => p.id === editing.id);
    const next = list[i + delta];
    if (!next) return;
    await closeEditor(true);
    openEditor(next.id);
  }
  $("[data-edit-prev]")?.addEventListener("click", () => stepEditor(-1));
  $("[data-edit-next]")?.addEventListener("click", () => stepEditor(1));
  window.addEventListener("keydown", (e) => {
    if (!$("#editor")?.open || !editing) return;
    const tag = (e.target && e.target.tagName) || "";
    if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
    if (e.key === "ArrowLeft") {
      e.preventDefault();
      stepEditor(-1);
    } else if (e.key === "ArrowRight") {
      e.preventDefault();
      stepEditor(1);
    }
  });
  $("[data-flip]")?.addEventListener("click", () => {
    if (!editing) return;
    editing.flip = editing.flip ? 0 : 1;
    applyPreview();
  });
  $("[data-sharpen]")?.addEventListener("click", () => {
    if (!editing) return;
    editing.sharpen = editing.sharpen ? 0 : 1;
    $("[data-sharpen]").classList.toggle("on", !!editing.sharpen);
  });
  $$("[data-auto-light]").forEach((btn) => {
    btn.addEventListener("click", () => {
      if (!editing) return;
      applyAutoLighting();
    });
  });
  $$("[data-auto-color]").forEach((btn) => {
    btn.addEventListener("click", () => {
      if (!editing) return;
      applyAutoColor();
    });
  });
  $("[data-auto]")?.addEventListener("click", () => {
    if (!editing) return;
    const look = analyzeImageStats($("[data-edit-img]"));
    editing.brightness = look.brightness;
    editing.contrast = look.contrast;
    editing.saturation = look.saturation;
    editing.midtones = look.midtones;
    editing.wbR = look.wbR;
    editing.wbG = look.wbG;
    editing.wbB = look.wbB;
    editing.sharpen = 1;
    paintToneSliders();
    $("[data-sharpen]")?.classList.add("on");
    applyPreview();
  });
  $("[data-reset-edit]")?.addEventListener("click", () => {
    if (!editing) return;
    editing.brightness = 0;
    editing.contrast = 0;
    editing.saturation = 0;
    editing.midtones = 0;
    editing.wbR = 1;
    editing.wbG = 1;
    editing.wbB = 1;
    editing.filter = "none";
    editing.flip = 0;
    editing.sharpen = 0;
    editing.rotate = 0;
    editing.straighten = 0;
    editing.caption = "";
    $("[data-brightness]").value = 0;
    $("[data-contrast]").value = 0;
    $("[data-saturation]").value = 0;
    $("[data-midtones]").value = 0;
    $("[data-brightness-val]").textContent = "0";
    $("[data-contrast-val]").textContent = "0";
    $("[data-saturation-val]").textContent = "0";
    $("[data-midtones-val]").textContent = "0";
    $$("[data-filter-val]").forEach((b) => b.classList.toggle("on", b.dataset.filterVal === "none"));
    $("[data-sharpen]")?.classList.remove("on");
    $("[data-straighten]").value = 0;
    $("[data-straighten-val]").textContent = "0°";
    if ($("[data-caption]")) $("[data-caption]").value = "";
    applyPreview();
    layoutCrop(true);
  });
  $("[data-editor-done]").addEventListener("click", () => closeEditor(true));
  $("#editor").addEventListener("cancel", (e) => {
    e.preventDefault();
    closeEditor(true);
  });

  const box = $("[data-crop-box]");
  const wrap = $("[data-crop-wrap]");
  const editor = $("#editor");
  const bindCropDrag = (el) => {
    if (!el) return;
    el.addEventListener("pointerdown", startDrag);
    el.addEventListener("pointermove", moveDrag);
    el.addEventListener("pointerup", endDrag);
    el.addEventListener("pointercancel", endDrag);
    el.addEventListener("touchstart", startDrag, { passive: false });
    el.addEventListener("dragstart", (e) => e.preventDefault());
  };
  bindCropDrag(box);
  bindCropDrag(wrap);
  editor?.addEventListener("pointermove", moveDrag);
  editor?.addEventListener("pointerup", endDrag);
  editor?.addEventListener("pointercancel", endDrag);
  document.addEventListener("pointermove", moveDrag, true);
  document.addEventListener("pointerup", endDrag, true);
  window.addEventListener("pointermove", moveDrag);
  window.addEventListener("pointerup", endDrag);
  window.addEventListener("touchmove", moveDrag, { passive: false });
  window.addEventListener("touchend", endDrag);

  document.querySelectorAll('[name="delivery"], [name="paper"], [name="border"], [name="payment"]').forEach((el) => {
    el.addEventListener("change", () => {
      const wrap = $("[data-address-wrap]");
      const acs = document.querySelector('[name="delivery"][value="acs"]');
      if (wrap && acs) wrap.hidden = !acs.checked;
      render();
    });
  });

  try {
    const last = JSON.parse(localStorage.getItem("lab_last_customer") || "null");
    if (last && $("[data-checkout-form]")) {
      const form = $("[data-checkout-form]");
      if (last.name) form.name.value = last.name;
      if (last.phone) form.phone.value = last.phone;
      if (last.email) form.email.value = last.email;
    }
  } catch (e) {}

  const skipBox = $("[data-skip-customer]");
  const checkoutForm = $("[data-checkout-form]");
  function syncSkip() {
    if (!checkoutForm || !skipBox) return;
    checkoutForm.dataset.skip = skipBox.checked ? "1" : "0";
  }
  skipBox?.addEventListener("change", syncSkip);
  syncSkip();

  const osk = $("[data-osk]");
  let oskLang = "el";
  let oskInput = null;
  function paintOsk(mode) {
    if (!osk) return;
    const el = "ςερτυθιοπλκξηγφδσαζχψωβνμ".split("");
    const en = "qwertyuiopasdfghjklzxcvbnm".split("");
    const keys = mode === "num" ? "1234567890+".split("") : oskLang === "el" ? el : en;
    $("[data-osk-keys]").innerHTML = keys.map((k) => `<button type="button" data-osk-key="${k}">${k}</button>`).join("");
    $("[data-osk-keys]").querySelectorAll("[data-osk-key]").forEach((btn) => {
      btn.addEventListener("click", () => {
        if (!oskInput) return;
        oskInput.value += btn.dataset.oskKey;
        oskInput.dispatchEvent(new Event("input", { bubbles: true }));
      });
    });
  }
  function showOsk(input) {
    if (!osk || mode !== "kiosk") return;
    oskInput = input;
    osk.hidden = false;
    paintOsk(input.dataset.kbd === "num" ? "num" : "text");
  }
  document.querySelectorAll("[data-kbd]").forEach((input) => {
    input.addEventListener("focus", () => showOsk(input));
  });
  $("[data-osk-lang]")?.addEventListener("click", () => {
    oskLang = oskLang === "el" ? "en" : "el";
    paintOsk(oskInput?.dataset.kbd === "num" ? "num" : "text");
  });
  $("[data-osk-space]")?.addEventListener("click", () => {
    if (!oskInput) return;
    oskInput.value += " ";
    oskInput.dispatchEvent(new Event("input", { bubbles: true }));
  });
  $("[data-osk-back]")?.addEventListener("click", () => {
    if (!oskInput) return;
    oskInput.value = oskInput.value.slice(0, -1);
    oskInput.dispatchEvent(new Event("input", { bubbles: true }));
  });
  $("[data-osk-hide]")?.addEventListener("click", () => {
    if (osk) osk.hidden = true;
    oskInput = null;
  });

  root.addEventListener("click", bumpIdle);
  root.addEventListener("keydown", bumpIdle);

  refreshCart()
    .then(() => {
      productMode = cart.options?.product || "prints";
      applyProduct();
      if (photoList().length) setScreen("gallery");
      else setScreen("product");
    })
    .catch(() => setScreen("product"));
})();
