    try { d = await GET(`/campaigns/${MON.id}/recipients?` + p); }
    catch (e) { box.innerHTML = `<div class="notice err"><span class="n-ico">✕</span><div>${esc(e.message)}</div></div>`; return; }
    const rows = d.recipients || [];
    const RS_BADGE = { pending: "b-gray", queued: "b-blue", processing: "b-amber", sent: "b-green",
                       temp_failed: "b-amber", perm_failed: "b-red", suppressed: "b-violet",
                       skipped: "b-gray", bounced: "b-red" };
    box.innerHTML = `
      <div class="card"><div class="card-hd"><h2>Per-recipient monitor</h2></div><div class="card-bd">
      <div class="filters flex mb-1">
        <select id="recF"><option value="">All statuses</option>
          ${["pending", "queued", "processing", "sent", "temp_failed", "perm_failed", "suppressed", "skipped", "bounced"].map(s => `<option ${MON.statusF === s ? "selected" : ""}>${s}</option>`).join("")}
        </select>
        <input id="recQ" placeholder="Search email…" value="${esc(MON.q || "")}">
        <button class="btn sm" id="recGo">Filter</button>
        <span class="small muted" style="margin-left:auto">${d.total} recipients</span>
      </div>
      ${rows.length ? `<div class="tbl-wrap"><table class="tbl">
        <thead><tr><th>Seq</th><th>Email</th><th>Status</th><th>Account</th><th>Subject</th><th>Invoice</th><th>Amount</th><th>Attempts</th><th>Sent at</th><th>Message ID</th><th>Last error</th></tr></thead>
        <tbody>${rows.map(r => `<tr>
          <td class="center mono-s">${r.seq}</td>
          <td>${esc(r.email)}</td>
          <td><span class="badge ${RS_BADGE[r.status] || "b-gray"}">${esc(r.status)}</span></td>
    <td class="small">${esc(r.account || "—")}</td>
          <td class="err-cell" title="${esc(r.subject || "")}">${esc((r.subject || "—").slice(0, 60))}</td>
          <td class="mono-s">${esc(r.invoice_number || "—")}</td>
          <td class="nowrap">${r.amount != null ? fmtMoney(r.amount) : "—"}</td>
          <td class="center">${r.attempts ?? 0}</td>
          <td class="nowrap small">${fmtDT(r.sent_at)}</td>
          <td class="mono-s" title="${esc(r.message_id || "")}">${r.message_id ? esc(r.message_id.slice(0, 14)) + "…" : "—"}</td>
          <td class="err-cell" title="${esc(r.last_error || "")}">${esc((r.last_error || "—").slice(0, 80))}</td>
        </tr>`).join("")}</tbody></table></div>` : `<div class="empty-state"><div class="e-ico">◷</div><p>No recipients materialized yet — they are created when the campaign starts.</p></div>`}
      <div class="pager" id="recPager"></div>
      </div></div>`;
    $("#recF").onchange = () => { MON.statusF = $("#recF").value; MON.page = 1; loadRecipients(box); };
    $("#recQ").onkeydown = e => { if (e.key === "Enter") { MON.q = $("#recQ").value.trim(); MON.page = 1; loadRecipients(box); } };
    $("#recGo").onclick = () => { MON.statusF = $("#recF").value; MON.q = $("#recQ").value.trim(); MON.page = 1; loadRecipients(box); };
    const pages = Math.max(1, Math.ceil(d.total / 50));
    $("#recPager").innerHTML = pages > 1 ?
      `<button class="btn sm secondary" ${MON.page <= 1 ? "disabled" : ""} data-pg="${MON.page - 1}">‹ Prev</button>
       <span class="small muted">Page ${MON.page} of ${pages}</span>
       <button class="btn sm secondary" ${MON.page >= pages ? "disabled" : ""} data-pg="${MON.page + 1}">Next ›</button>` : "";
    $$("#recPager [data-pg]").forEach(b => b.onclick = () => { MON.page = +b.dataset.pg; loadRecipients(box); });
  }

  /* ==================== CAMPAIGN PREVIEW MODAL ==================== */
  async function campaignPreview(cid, size) {
    let d;
    try { d = await POST(`/campaigns/${cid}/preview${size ? "?sample_size=" + size : ""}`, {}); }
    catch (e) { toast(e.message, "err", 6000); return; }
    const msgs = d.messages || [];
    const node = el("div", {},
      el("p", { class: "muted small" },
        `Real rendered messages for ${d.sample_size} sample recipient(s) — variables substituted, attachments planned. Nothing is sent.`),
      msgs.length ? msgs.map(m => el("details", { class: "pv-acc" },
        el("summary", {},
          `${m.recipient.email} — ${m.subject}`,
          m.warnings && m.warnings.length ? el("span", { class: "badge b-amber", style: "margin-left:8px" }, `${m.warnings.length} warning(s)`) : null),
        el("div", {},
          el("div", { class: "kv-inline" },
            el("span", { class: "badge b-blue" }, "from " + (m.from_account || "?")),
            m.raw_variables && m.raw_variables.invoice_number ? el("span", { class: "badge" }, esc(m.raw_variables.invoice_number)) : null,
            m.raw_variables && m.raw_variables.amount != null ? el("span", { class: "badge" }, fmtMoney(m.raw_variables.amount)) : null,
            m.raw_variables && m.raw_variables.shipping != null ? el("span", { class: "badge" }, "ship " + fmtMoney(m.raw_variables.shipping)) : null),
          m.warnings && m.warnings.length ? el("div", { class: "notice warn", html:
            `<span class="n-ico">⚠</span><div>${m.warnings.map(w => "• " + esc(w)).join("<br>")}</div>` }) : null,
          el("iframe", { class: "preview-frame", srcdoc: m.body_html })))) : el("p", { class: "muted" }, "No sample messages."),
      d.plan_summary ? el("p", { class: "muted small mt-2" }, "Plan summary: " + esc(JSON.stringify(d.plan_summary))) : null);
    openModal(`Preview campaign #${cid}`, node, { xw: true, buttons: [
      { label: "Close", kind: "secondary", onClick: (ev, close) => close() },
      { label: "Refresh sample", kind: "primary", onClick: (ev, close) => { close(); campaignPreview(cid, (size || 5)); } },
    ]});
  }

  /* ==================== 12-STEP WIZARD ==================== */
  async function openWizard(editId) {
    try { if (!OPT) await loadOptions(); } catch (e) { toast(e.message, "err", 6000); return; }
    editingId = editId || null;
    Object.assign(C, DEF());
    step = 1;
    if (editingId) {
      try {
        const c = await GET(`/campaigns/${editingId}`);
        Object.assign(C, DEF(), {
          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_pdf, attach_pdf_template_id: c.attach_pdf_template_id,
          attachment_id: c.attachment_id, variable_config: c.variable_config || {},
          account_ids: c.account_ids || [], rotation_strategy: c.rotation_strategy,
          rotation_weights: 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,
          rate_per_minute: c.rate_per_minute, max_retries: c.max_retries,
          retry_delay_seconds: c.retry_delay_seconds, unsubscribe_enabled: c.unsubscribe_enabled,
          preview_sample_size: c.preview_sample_size,
        });
      } catch (e) { toast(e.message, "err"); return; }
    }
    const wrap = el("div", {},
      el("div", { class: "wiz-steps", id: "wSteps" }),
      el("div", { class: "step-body", id: "wBody" }),
      el("div", { class: "wiz-foot" },
        el("button", { class: "btn secondary", id: "wCancel", onclick: () => closeModal() }, "Cancel"),
        el("div", { class: "flex", style: "gap:8px" },
          el("button", { class: "btn secondary", id: "wBack" }, "‹ Back"),
          el("button", { class: "btn", id: "wNext" }, "Next ›"))));
    openModal(editingId ? `Edit campaign #${editingId}` : "New campaign — 12-step wizard", wrap, { xw: true, onClose: () => stopWizardWatch() });
    $("#wBack").onclick = () => { if (step > 1) { step--; renderStep(); } };
    $("#wNext").onclick = next;
    renderStep();
  }
  function closeModal() { const x = $(".modal-back.open .x-btn"); if (x) x.click(); }
  function stopWizardWatch() {}

  function renderStepsBar() {
    $("#wSteps").innerHTML = STEPS.map((s, i) => {
      const n = i + 1;
      const cls = n === step ? "active" : (n < step ? "done" : "");
      return `<div class="wstep ${cls}" data-step="${n}" title="${esc(s)}"><span class="n">${n < step ? "✓" : n}</span>${esc(s)}</div>`;
    }).join("");
    $$("#wSteps .wstep.done").forEach(w => w.onclick = () => { step = +w.dataset.step; renderStep(); });
  }

  function numRow(label, key, attrs = "") {
    return `<div class="num-row"><label>${label}</label><input id="w_${key}" ${attrs} value="${esc(C[key] ?? "")}"></div>`;
  }

  function renderStep() {
    renderStepsBar();
    const b = $("#wBody");
    $("#wBack").style.visibility = step > 1 ? "visible" : "hidden";
    $("#wNext").textContent = step === 12 ? (editingId ? "Save changes" : "Create & start later") : "Next ›";
    const H = t => `<h3 style="margin:2px 0 4px">${t}</h3>`;
    const hint = t => `<p class="step-hint">${t}</p>`;

    if (step === 1) {
      b.innerHTML = H("Step 1 — Campaign name") + hint("A clear, unique name for this campaign. It appears in logs and exports.") +
        `<label class="lab">Campaign name</label>
         <input id="w_name" maxlength="250" placeholder="e.g. November invoice run — batch A" value="${esc(C.name)}">`;
    } else if (step === 2) {
      b.innerHTML = H("Step 2 — Recipient list") + hint(`Pick the list to send to. Max ${OPT.limits.max_recipients} recipients per campaign; only valid, non-suppressed rows are used.`) +
        pickList("lists", C.recipient_list_id, l => ({ v: l.id, main: l.name, sub: `${l.row_count} rows · ${l.valid_count} valid` }), "recipient_list_id", true);
    } else if (step === 3) {
      b.innerHTML = H("Step 3 — Subject lines") + hint(`Select subject line variations (up to ${OPT.limits.max_subjects}) and how they rotate across recipients.`) +
        pickList("subjects", C.subject_set_ids, s => ({ v: s.id, main: s.subject, sub: "" }), "subject_set_ids") +
        `<label class="lab mt-2">Subject rotation</label>
         <div class="radio-row">
           ${["round_robin", "random", "fixed_first"].map(m => `
             <label class="chk"><input type="radio" name="subjRot" value="${m}" ${C.subject_rotation === m ? "checked" : ""}>
             ${m === "round_robin" ? "Round robin" : m === "random" ? "Random shuffle" : "Fixed first subject"}</label>`).join("")}
         </div>`;
    } else if (step === 4) {
      b.innerHTML = H("Step 4 — HTML templates") + hint("Select one or more templates and how they sequence across recipients.") +
        pickList("templates", C.template_ids, t => ({ v: t.id, main: t.name, sub: "" }), "template_ids") +
        `<label class="lab mt-2">Template sequence mode</label>
         <div class="radio-row">
           ${["repeat", "stop", "random"].map(m => `
             <label class="chk"><input type="radio" name="tplMode" value="${m}" ${C.template_sequence_mode === m ? "checked" : ""}>
             ${m === "repeat" ? "Repeat from start" : m === "stop" ? "Stop after last" : "Random"}</label>`).join("")}
         </div>`;
    } else if (step === 5) {
      const am = C.attach_mode;
      b.innerHTML = H("Step 5 — Attachments") + hint("Choose what to attach. PDF mode renders a template to a unique Invoice PDF per recipient.") +
        `<div class="radio-row">
           ${[["none", "No attachment"], ["pdf", "Generated PDF invoice"], ["file", "Stored file attachment"], ["image", "Image attachment"]].map(([m, t]) => `
             <label class="chk"><input type="radio" name="attMode" value="${m}" ${am === m ? "checked" : ""}> ${t}</label>`).join("")}
         </div>
         <div id="attExtra"></div>`;
      bindAttExtra();
    } else if (step === 6) {
      const vc = C.variable_config;
      b.innerHTML = H("Step 6 — Variables & random amounts") + hint("Configure the invoice number format and random amount ranges used by %RandomInvoiceNumber%, %RandomTotal% and %RandomShippingCharge%.") +
        `<div class="grid cols-2">
           <div>
             <label class="lab">Invoice prefix</label>
             <input id="w_invoice_prefix" value="${esc(vc.invoice_prefix ?? "INV-")}">
             <label class="lab mt-1">Invoice format tokens</label>
             <input id="w_invoice_format" value="${esc(vc.invoice_format ?? "{prefix}{R4}-{R5}-{R6}-{seq4}")}">
             <p class="wcount">Tokens: {prefix} {R#} upper {r#} lower {d#} digits {seq4} {date} {hash}. Crypto-strong, collision-checked per campaign.</p>
           </div>
           <div>
             ${numRow("Amount min ($)", "amount_min", 'type="number" step="0.01" min="0"')}
             ${numRow("Amount max ($)", "amount_max", 'type="number" step="0.01" min="0"')}
             ${numRow("Shipping min ($)", "shipping_min", 'type="number" step="0.01" min="0"')}
             ${numRow("Shipping max ($)", "shipping_max", 'type="number" step="0.01" min="0"')}
           </div>
         </div>`;
    } else if (step === 7) {
      b.innerHTML = H("Step 7 — Gmail accounts") + hint(`Select up to ${OPT.limits.max_accounts} sending accounts. Only authorized, active accounts are shown.`) +
        (OPT.accounts.length ? pickList("accounts", C.account_ids, a => ({ v: a.id, main: a.email, sub: `${a.sends_today}/${a.daily_limit} today · ${a.sending_method}` }), "account_ids")
          : `<div class="notice warn"><span class="n-ico">⚠</span><div>No authorized Gmail accounts. Connect one on the Gmail Accounts page first.</div></div>`);
    } else if (step === 8) {
      const strat = C.rotation_strategy;
      b.innerHTML = H("Step 8 — Account rotation") + hint("How senders rotate. Weighted lets you favor specific accounts; per-account caps and cooldowns are enforced regardless.") +
        `<div class="radio-row">
           <label class="chk"><input type="radio" name="rotStrat" value="sequential" ${strat === "sequential" ? "checked" : ""}> Sequential</label>
           <label class="chk"><input type="radio" name="rotStrat" value="weighted" ${strat === "weighted" ? "checked" : ""}> Weighted</label>
         </div>
         <div id="rotExtra"></div>`;
      bindRotExtra();
    } else if (step === 9) {
      b.innerHTML = H("Step 9 — Limits & pacing") + hint("Conservative pacing protects your accounts. Sends are throttled and retried with exponential backoff.") +
        `<div class="grid cols-2">
           <div>
             ${numRow("Max sends per account", "max_per_account", 'type="number" min="1" max="2000"')}
             ${numRow("Interval between sends (s)", "send_interval_seconds", 'type="number" step="0.5" min="0.5" max="3600"')}
           </div>
           <div>
             ${numRow("Rate cap (per minute)", "rate_per_minute", 'type="number" min="1" max="600"')}
             ${numRow("Max retries", "max_retries", 'type="number" min="0" max="10"')}
             ${numRow("Retry delay (s)", "retry_delay_seconds", 'type="number" min="5" max="3600"')}
           </div>
         </div>`;
    } else if (step === 10) {
      b.innerHTML = H("Step 10 — Company phone") + hint("Which active phone number appears as %CompanyPhone%.") +
        (OPT.phones.length ? pickList("phones", C.phone_selection === "fixed" ? (C.variable_config.phone_id ?? null) : null, p => ({ v: p.id, main: p.phone, sub: p.label || "" }), "phone_fixed", true)
          : `<div class="notice warn"><span class="n-ico">⚠</span><div>No active phone numbers. %CompanyPhone% will be blank unless numbers are added on the Company Phones page.</div></div>`) +
        `<label class="lab mt-2">Selection rule</label>
         <div class="radio-row">
           ${["round_robin", "random", "fixed"].map(m => `
             <label class="chk"><input type="radio" name="phSel" value="${m}" ${C.phone_selection === m ? "checked" : ""}>
