      const m = mapper(it);
      const on = cur.includes(m.v);
      return `<label class="pick-item">
        <input type="${radio ? "radio" : "checkbox"}" name="pk_${stateKey}" value="${m.v}" ${on ? "checked" : ""}>
        <span class="p-main">${esc(m.main)}${m.sub ? `<span class="p-sub"> — ${esc(m.sub)}</span>` : ""}</span>
      </label>`;
    }).join("") + "</div>";
  }

  function bindStep() {
    if (step === 1) $("#w_name").oninput = e => C.name = e.target.value;
    if (step === 2) bindPick("recipient_list_id", true);
    if (step === 3) { bindPick("subject_set_ids"); $$('input[name="subjRot"]').forEach(r => r.onchange = () => C.subject_rotation = r.value); }
    if (step === 4) { bindPick("template_ids"); $$('input[name="tplMode"]').forEach(r => r.onchange = () => C.template_sequence_mode = r.value); }
    if (step === 5) {
      $$('input[name="attMode"]').forEach(r => r.onchange = () => { C.attach_mode = r.value; if (C.attach_mode !== "pdf") C.attach_pdf = false; bindAttExtra(); renderStep(); });
    }
    if (step === 6) {
      const keep = (k, id) => { const n = $(id); if (n) n.oninput = () => C.variable_config[k] = n.value; };
      keep("invoice_prefix", "#w_invoice_prefix");
      keep("invoice_format", "#w_invoice_format");
      ["amount_min", "amount_max", "shipping_min", "shipping_max"].forEach(k => {
        const n = $("#w_" + k);
        if (n) n.oninput = () => C.variable_config[k] = n.value === "" ? undefined : parseFloat(n.value);
      });
    }
    if (step === 7) bindPick("account_ids");
    if (step === 8) $$('input[name="rotStrat"]').forEach(r => r.onchange = () => { C.rotation_strategy = r.value; bindRotExtra(); });
    if (step === 9) ["max_per_account", "send_interval_seconds", "rate_per_minute", "max_retries", "retry_delay_seconds"].forEach(k => {
      const n = $("#w_" + k); if (n) n.oninput = () => C[k] = n.value === "" ? undefined : parseFloat(n.value);
    });
    if (step === 10) {
      $$('input[name="phSel"]').forEach(r => r.onchange = () => { C.phone_selection = r.value; renderStep(); });
      bindPick("phone_fixed", true);
      $$('#pk_phone_fixed input').forEach(i => i.onchange = () => { C.variable_config.phone_id = +i.value; });
    }
    if (step === 11) {
      $("#w_unsub").onchange = e => C.unsubscribe_enabled = e.target.checked;
      $("#w_pss").oninput = e => C.preview_sample_size = parseInt(e.target.value, 10) || 5;
    }
  }
  function bindPick(key, radio = false) {
    const inputs = $$(`#pk_${key} input`);
    inputs.forEach(i => i.onchange = () => {
      if (radio) {
        C[key] = +i.value;
      } else {
        C[key] = inputs.filter(x => x.checked).map(x => +x.value);
      }
    });
  }
  function bindAttExtra() {
    const box = $("#attExtra");
    if (!box) return;
    const mode = C.attach_mode;
    if (mode === "none") { box.innerHTML = ""; return; }
    if (mode === "pdf") {
      C.attach_pdf = true;
      box.innerHTML = `<label class="lab mt-2">PDF source template (rendered per recipient)</label>` +
        pickList("templates", C.attach_pdf_template_id, t => ({ v: t.id, main: t.name, sub: "" }), "attach_pdf_template_id", true);
      bindPick("attach_pdf_template_id", true);
    } else {
      box.innerHTML = `<label class="lab mt-2">Stored attachment</label>` +
        pickList("attachments", C.attachment_id, a => ({ v: a.id, main: a.name + " (" + a.kind + ")", sub: a.filename }), "attachment_id", true);
      bindPick("attachment_id", true);
    }
  }
  function bindRotExtra() {
    const box = $("#rotExtra");
    if (!box) return;
    if (C.rotation_strategy !== "weighted") { box.innerHTML = ""; return; }
    const accs = (OPT.accounts || []).filter(a => C.account_ids.includes(a.id));
    if (!accs.length) { box.innerHTML = `<div class="notice warn mt-2"><span class="n-ico">⚠</span><div>Select accounts in Step 7 first, then return here to set weights.</div></div>`; return; }
    box.innerHTML = `<label class="lab mt-2">Weights (relative send share)</label>` +
      accs.map(a => `<div class="num-row"><label>${esc(a.email)}</label>
        <input type="number" min="1" data-acc="${a.id}" value="${esc(C.rotation_weights[String(a.id)] ?? 1)}"></div>`).join("");
    $$("#rotExtra input[data-acc]").forEach(inp => inp.oninput = () => {
      C.rotation_weights[String(inp.dataset.acc)] = parseInt(inp.value, 10) || 1;
    });
  }

  function collect() {
    // re-read any uncommitted inputs
    if (step === 1) C.name = ($("#w_name")?.value || "").trim();
    const keyMap = { 6: ["amount_min", "amount_max", "shipping_min", "shipping_max"], 9: ["max_per_account", "send_interval_seconds", "rate_per_minute", "max_retries", "retry_delay_seconds"] };
    (keyMap[step] || []).forEach(k => { const n = $("#w_" + k); if (n) C[k] = parseFloat(n.value); });
  }

  async function next() {
    collect();
    if (step < 12) {
      // client+server validation between steps
      const serverStep = step;
      try {
        const d = await POST("/campaigns/validate-step", { step: serverStep, data: snapshotFor(serverStep) });
        if (!d.valid) {
          toast(d.errors.join(" · "), "err", 6000);
          return;
        }
      } catch (e) { toast(e.message, "err", 6000); return; }
      step++;
      renderStep();
      return;
    }
    // step 12 → create or save
    const payload = payloadFromC();
    try {
      let saved;
      if (editingId) { saved = await POST(`/campaigns/${editingId}/patch`, payload); toast("Campaign updated", "ok"); }
      else { saved = await POST("/campaigns/create", payload); toast(`Campaign #${saved.id} created`, "ok"); }
      closeModal();
      loadList();
      openMonitor(saved.id);
    } catch (e) {
      const errs = e.data?.detail?.errors;
      toast(errs && errs.length ? errs.join(" · ") : e.message, "err", 7000);
    }
  }

  function snapshotFor(s) {
    return { name: C.name, recipient_list_id: C.recipient_list_id,
             subject_set_ids: C.subject_set_ids, subject_rotation: C.subject_rotation,
             template_ids: C.template_ids, template_sequence_mode: C.template_sequence_mode,
             attach_mode: C.attach_mode, attach_pdf_template_id: C.attach_pdf_template_id,
             attachment_id: C.attachment_id, variable_config: cleanVC(),
             account_ids: C.account_ids, rotation_strategy: C.rotation_strategy,
             rotation_weights: C.rotation_weights, max_per_account: C.max_per_account,
             send_interval_seconds: C.send_interval_seconds,
             phone_selection: C.phone_selection, unsubscribe_enabled: C.unsubscribe_enabled };
  }
  function cleanVC() {
    const out = {};
    for (const [k, v] of Object.entries(C.variable_config || {})) {
      if (v === undefined || v === null || v === "") continue;
      out[k] = v;
    }
    return out;
  }
  function payloadFromC() {
    const vc = cleanVC();
    const payload = {
      name: C.name, recipient_list_id: C.recipient_list_id,
      subject_set_ids: C.subject_set_ids, template_ids: C.template_ids,
      message_type: C.message_type, attach_mode: C.attach_mode,
      attach_pdf: C.attach_mode === "pdf", attach_pdf_template_id: C.attach_mode === "pdf" ? C.attach_pdf_template_id : null,
      attachment_id: (C.attach_mode === "file" || C.attach_mode === "image") ? C.attachment_id : null,
      variable_config: vc, account_ids: C.account_ids,
      rotation_strategy: C.rotation_strategy,
      rotation_weights: C.rotation_strategy === "weighted" ? C.rotation_weights : {},
      subject_rotation: C.subject_rotation, template_sequence_mode: C.template_sequence_mode,
      phone_selection: C.phone_selection, max_per_account: C.max_per_account,
      send_interval_seconds: C.send_interval_seconds,
    };
    payload.rate_per_minute = C.rate_per_minute;
    payload.max_retries = C.max_retries;
    payload.retry_delay_seconds = C.retry_delay_seconds;
    payload.unsubscribe_enabled = C.unsubscribe_enabled;
    payload.preview_sample_size = C.preview_sample_size;
    return payload;
  }

  /* ---------------- wire-up ---------------- */
  $("#btnNew").onclick = () => openWizard(null);
  $("#btnClear").onclick = () =>
    confirmedAction("Clear ALL campaigns? Drafts and finished campaigns are removed. This cannot be undone.",
      async () => { const r = await POST("/campaigns/clear-all"); loadList(); return `Cleared ${r.deleted ?? "?"} campaign(s)`; },
      { title: "Clear all campaigns", confirmLabel: "Clear all" });
  $("#fGo").onclick = () => { lst.status = $("#fStatus").value; lst.q = $("#fSearch").value.trim(); lst.page = 1; loadList(); };
  $("#fSearch").onkeydown = e => { if (e.key === "Enter") $("#fGo").click(); };
  $$("#modeTabs .mode-tab").forEach(t => t.onclick = () => setMode(t.dataset.mode));

  await loadList();
};
