};
function getCookie(name) {
  return document.cookie.split("; ").find(r => r.startsWith(name + "="))?.split("=").slice(1).join("=") || "";
}

async function api(path, opts = {}) {
  const headers = Object.assign({}, opts.headers || {});
  if (opts.json !== undefined) {
    headers["Content-Type"] = "application/json";
    opts.body = JSON.stringify(opts.json);
    delete opts.json;
  }
  if ((opts.method || "GET").toUpperCase() !== "GET") headers["X-CSRF-Token"] = CSRF.get();
  const res = await fetch(path, Object.assign({}, opts, { headers, credentials: "same-origin" }));
  let data = null;
  try { data = await res.json(); } catch { /* no body */ }
  if (res.status === 401 && !location.pathname.startsWith("/login")) {
    location.href = "/login?next=" + encodeURIComponent(location.pathname);
    throw new Error("Not authenticated");
  }
  if (!res.ok) {
    const detail = data && (data.detail?.message || data.detail || data.error);
    const err = new Error(typeof detail === "string" ? detail : (res.status + " " + res.statusText));
    err.status = res.status;
    err.data = data;
    throw err;
  }
  return data;
}
const GET = p => api(p);
const POST = (p, json) => api(p, { method: "POST", json });

function b64Blob(b64, mime) {
  const bin = atob(b64);
  const arr = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
  return new Blob([arr], { type: mime });
}
function dlBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
/* ---------- Toasts ---------- */
function toast(msg, kind = "info", ms = 4200) {
  let box = $("#toasts");
  if (!box) { box = el("div", { id: "toasts" }); document.body.append(box); }
  const icons = { info: "ℹ", ok: "✓", err: "✕", warn: "⚠" };
  const t = el("div", { class: "toast " + (kind === "info" ? "" : kind) },
    el("span", { class: "n-ico" }, icons[kind] || "ℹ"),
    el("span", {}, msg));
  box.append(t);
  setTimeout(() => { t.style.opacity = "0"; t.style.transition = "opacity .3s"; setTimeout(() => t.remove(), 320); }, ms);
  return t;
}

/* ---------- Modal / confirm ---------- */
function openModal(title, bodyNode, { buttons = [], wide = false, xw = false, onClose } = {}) {
  const back = el("div", { class: "modal-back open" });
  const modal = el("div", { class: "modal" + (wide ? " wide" : "") + (xw ? " xw" : "") });
  const close = () => { back.remove(); document.body.style.overflow = ""; onClose && onClose(); };
  const hd = el("div", { class: "m-hd" }, el("h3", {}, title), el("button", { class: "x-btn", onclick: close }, "×"));
  const ft = el("div", { class: "m-ft" });
  if (!buttons.length) buttons = [{ label: "Close", onClick: close }];
  for (const b of buttons) {
    ft.append(el("button", { class: "btn " + (b.cls || "secondary"), onclick: () => b.onClick ? b.onClick(close, modal) : close() }, b.label));
  }
  modal.append(hd, el("div", { class: "m-bd" }, bodyNode), ft);
  back.append(modal);
  back.addEventListener("mousedown", e => { if (e.target === back) close(); });
  document.body.append(back);
  document.body.style.overflow = "hidden";
  return { close, modal };
}

function confirmDialog(message, { title = "Are you sure?", danger = true, confirmLabel = "Confirm", onConfirm } = {}) {
  const p = el("p", { class: "small" }, message);
  return openModal(title, p, {
    buttons: [
      { label: "Cancel", cls: "secondary", onClick: c => c() },
      { label: confirmLabel, cls: danger ? "danger" : "primary", onClick: c => { c(); onConfirm && onConfirm(); } },
    ],
  });
}

/* confirm wrapper for Clear-All style destructive API calls */
async function confirmedAction(message, fn, { title, confirmLabel = "Yes, proceed" } = {}) {
  return new Promise(resolve => {
    confirmDialog(message, { title, confirmLabel, onConfirm: async () => {
      try {
