<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings — Bulk Mailer</title>
<link rel="stylesheet" href="/static/css/app.css">
<style>
.set-grid{display:grid;grid-template-columns:280px 1fr;gap:18px}
@media(max-width:900px){.set-grid{grid-template-columns:1fr}}
.cat-list{display:flex;flex-direction:column;gap:6px}
.cat-item{padding:10px 14px;border-radius:8px;cursor:pointer;font-size:.9rem;border:1px solid transparent}
.cat-item:hover{background:#f1f5f9}
.cat-item.active{background:#101828;color:#fff;font-weight:600}
.set-row{display:grid;grid-template-columns:1fr 260px auto;gap:12px;align-items:center;padding:10px 0;border-bottom:1px solid #eef2f7}
.set-row:last-child{border-bottom:0}
.set-row .lab{font-size:.9rem}
.set-row .lab .def{display:block;font-size:.75rem;color:#64748b;margin-top:2px}
@media(max-width:700px){.set-row{grid-template-columns:1fr;gap:6px}}
.mod{font-size:.75rem;padding:2px 8px;border-radius:20px;background:#eef2ff;color:#4338ca}
</style>
<script src="https://sites.super.myninja.ai/_assets/ninja-daytona-script.js"></script>
</head>
<body>
<div id="layout"></div>
<main id="content">
  <div class="page-head">
    <div>
      <h1>Settings</h1>
      <p class="sub">Application-wide settings by category. Values are validated server-side; Reset All restores every default.</p>
    </div>
    <div class="btns">
      <button class="btn" id="btnSave">Save All Changes</button>
      <button class="btn danger-o" id="btnResetAll">Reset All to Defaults</button>
    </div>
  </div>

  <div class="set-grid">
    <div class="card"><div class="card-bd">
      <div class="cat-list" id="cats"></div>
    </div></div>
    <div class="card">
      <div class="card-hd"><h2 id="catTitle">—</h2><button class="btn xs secondary" id="btnResetCat">Reset category</button></div>
      <div class="card-bd" id="rows"></div>
    </div>
  </div>
</main>
<script src="/static/js/app.js"></script>
<script>
window.pageBoot = async (me) => {
  let settings = [], cat = null;
  const dirty = new Set();

  async function load(){
    const d = await GET("/settings/list");
    settings = d.settings || [];
    const cats = [...new Set(settings.map(s => s.category))];
    if (!cat && cats.length) cat = cats[0];
    $("#cats").innerHTML = cats.map(c =>
      `<div class="cat-item ${c===cat?"active":""}" data-cat="${esc(c)}">${esc(c)}
===JS
/* ============================================================
   Shared app core: API client (CSRF), auth guard, layout chrome,
   toasts, modals, confirm dialogs, helpers.
   ============================================================ */
"use strict";

/* ---------- tiny DOM helpers ---------- */
const $ = (sel, root) => (root || document).querySelector(sel);
const $$ = (sel, root) => Array.from((root || document).querySelectorAll(sel));
function el(tag, attrs = {}, ...children) {
  const node = document.createElement(tag);
  for (const [k, v] of Object.entries(attrs)) {
    if (k === "class") node.className = v;
    else if (k === "html") node.innerHTML = v;
    else if (k.startsWith("on") && typeof v === "function") node.addEventListener(k.slice(2), v);
    else if (v !== null && v !== undefined) node.setAttribute(k, v);
  }
  for (const c of children) {
    if (c === null || c === undefined) continue;
    node.append(c instanceof Node ? c : document.createTextNode(String(c)));
  }
  return node;
}
function esc(s) {
  return String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
function debounce(fn, ms = 300) {
  let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
}
function fmtBytes(n) {
  if (n == null) return "—";
  if (n < 1024) return n + " B";
  if (n < 1048576) return (n / 1024).toFixed(1) + " KB";
  return (n / 1048576).toFixed(2) + " MB";
}
function fmtDT(iso) {
  if (!iso) return "—";
  const d = new Date(iso);
  return d.toLocaleString([], { year: "numeric", month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" });
}
function fmtMoney(n) {
  if (n == null) return "—";
  return "$" + Number(n).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function plural(n, w) { return `${n} ${w}${n === 1 ? "" : "s"}`; }

/* ---------- API client ---------- */
const CSRF = {
  get() { return getCookie("mailer_csrf"); },
};
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");
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click();
  setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 1500);
}
const PATCH = (p, json) => api(p, { method: "PATCH", json });
const PUT = (p, json) => api(p, { method: "PUT", json });
const DEL = p => api(p, { method: "DELETE" });

/* ---------- 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) {
