// dashboard-workflows-hub.js — extracted verbatim from dashboard.js (classic script,
// shared global scope). Loaded before dashboard.js. Pure move, no logic change.

// ── #workflows hub (2026-05-30 from-scratch reorg, Phase B) ──────
//
// Internal tab strip [Library | My workflows | Runs] over the unified
// Workflows surface. The Mine tab reuses the legacy compositions list
// (the existing renderWorkflowsHome body); Library is a clean landing
// for discovery; Runs is a stub awaiting per-run history aggregation.
//
// Sub-routes (`#workflows/library`, `#workflows/mine`, `#workflows/runs`)
// drive which tab opens via the parseHash matcher.
async function renderWorkflowsHub({ tab } = {}) {
  const activeTab = tab || "library";
  const wrapper = el("div", {
    class: "workflows-hub",
    style: "padding:16px;max-width:1100px;margin:0 auto;",
  });
  setContentNode(wrapper);

  wrapper.appendChild(
    el("div", { class: "page-header", style: "margin-bottom:10px;" }, [
      el("h1", { style: "margin:0;font-size:18px;" }, "Workflows"),
      el(
        "p",
        { class: "muted", style: "margin:4px 0 0;font-size:12px;" },
        "Chained DAGs — multi-step recipes that orchestrate actions across platforms. Single-platform actions live in Automations.",
      ),
    ]),
  );

  const tabs = [
    { id: "library", icon: "📚", label: "Library", route: "workflows/library" },
    { id: "mine", icon: "🛠", label: "My workflows", route: "workflows/mine" },
    { id: "runs", icon: "📜", label: "Runs", route: "workflows/runs" },
  ];
  wrapper.appendChild(_renderHubTabStrip(tabs, activeTab));

  const content = el("div", { class: "hub-content" });
  wrapper.appendChild(content);

  if (activeTab === "library") {
    _buildWorkflowsLibraryView(content);
  } else if (activeTab === "mine") {
    await _buildWorkflowsMineBody(content);
  } else if (activeTab === "runs") {
    await _buildWorkflowsRunsBody(content);
  } else {
    content.appendChild(el("div", { class: "muted" }, `Unknown tab: ${activeTab}`));
  }
}

// Library tab — discovery landing. Two big CTAs (Browse templates /
// Start from scratch) plus a brief framing paragraph. The templates
// picker modal + the n8n canvas are both pre-existing — this view just
// surfaces them as the primary entry points.
function _buildWorkflowsLibraryView(root) {
  root.appendChild(
    el(
      "div",
      {
        style:
          "background:#1a1a22;border:1px solid #2a2a38;border-radius:10px;padding:24px;margin-bottom:14px;",
      },
      [
        el("h2", { style: "margin:0 0 6px;font-size:16px;color:#fff;" }, "Pick a starting point"),
        el(
          "p",
          { class: "muted", style: "margin:0 0 18px;font-size:13px;line-height:1.55;" },
          "Browse the preset gallery (23 ready-to-clone DAGs covering outreach, intel, content amplification, and event promotion) or start with a blank canvas. Either path lands you in the n8n-style editor, where you wire nodes together and save.",
        ),
        el("div", { style: "display:flex;gap:10px;flex-wrap:wrap;" }, [
          el(
            "button",
            {
              class: "btn btn-primary",
              style: "padding:10px 16px;",
              onclick: () => _openTemplatesPickerModal(),
            },
            "📚 Browse templates",
          ),
          el(
            "button",
            {
              class: "btn btn-secondary",
              style: "padding:10px 16px;",
              onclick: () => {
                dashboardState.composerDraft = _composerEmptyDraft();
                navigate("workflows/canvas");
              },
            },
            "+ Start from scratch",
          ),
        ]),
      ],
    ),
  );

  // Secondary hint pointing at the Mine tab if the user already has stuff
  root.appendChild(
    el(
      "p",
      {
        class: "muted",
        style: "font-size:12px;text-align:center;margin:14px 0 0;",
      },
      [
        "Looking for what you've already built? ",
        el(
          "a",
          {
            href: "#workflows/mine",
            style: "color:#93c5fd;text-decoration:none;",
            onclick: (e) => {
              e.preventDefault();
              navigate("workflows/mine");
            },
          },
          "Open My workflows →",
        ),
      ],
    ),
  );
}

// Runs tab — v1 implementation: per-workflow run-rollup view sorted by
// last_run_at descending. Uses the existing compositions-list payload
// which already includes total_runs / total_audience_reached /
// total_replies / last_run_at per workflow. Click a row to drill into
// that workflow's individual runs (composition detail).
//
// Future (Phase C): a flat per-run feed across all workflows would
// require an aggregation endpoint joining custom_workflow_runs +
// composition_runs. Today's rollup answers "which workflows have been
// firing recently?" — the highest-frequency user question.
async function _buildWorkflowsRunsBody(root) {
  root.appendChild(
    el(
      "p",
      { class: "muted", style: "margin:0 0 14px;font-size:12px;" },
      "Per-workflow run rollup — most recently active first. Click a workflow to see its individual run history + canvas.",
    ),
  );

  const resp = await api("compositions-list").catch(() => null);
  if (!resp || resp.error) {
    root.appendChild(
      el("div", { class: "empty-state", style: "padding:32px;text-align:center;" }, [
        el("div", { style: "font-size:32px;margin-bottom:8px;" }, "⚠️"),
        el("div", { style: "color:#aaa;font-size:13px;" }, "Could not load workflows"),
        el(
          "div",
          { class: "muted", style: "font-size:12px;margin-top:6px;" },
          resp?.error || "Try refreshing.",
        ),
      ]),
    );
    return;
  }

  // Sort by recency. Workflows that have NEVER run go to the bottom.
  const all = (resp.compositions || []).slice();
  const ever = all.filter((w) => w.last_run_at);
  const never = all.filter((w) => !w.last_run_at);
  ever.sort((a, b) => new Date(b.last_run_at) - new Date(a.last_run_at));

  // Aggregate strip at top — how many ran today / this week, total
  // replies / reach. Helps the user feel the pulse.
  const dayMs = 24 * 60 * 60 * 1000;
  const now = Date.now();
  const ran24h = ever.filter((w) => now - new Date(w.last_run_at) <= dayMs).length;
  const ran7d = ever.filter((w) => now - new Date(w.last_run_at) <= 7 * dayMs).length;
  const totalRuns = ever.reduce((s, w) => s + (w.total_runs || 0), 0);
  const totalReached = ever.reduce((s, w) => s + (w.total_audience_reached || 0), 0);
  const totalReplies = ever.reduce((s, w) => s + (w.total_replies || 0), 0);
  const replyRate = totalReached > 0 ? (100 * totalReplies) / totalReached : 0;

  root.appendChild(
    el(
      "div",
      {
        style:
          "display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:18px;",
      },
      [
        _runsStatTile("Workflows", String(all.length), `${ever.length} have run`),
        _runsStatTile("Ran 24h", String(ran24h), `${ran7d} in 7d`),
        _runsStatTile("Total runs", String(totalRuns), `${ever.length} workflows`),
        _runsStatTile("Reached", String(totalReached), `${totalReplies} replied`),
        _runsStatTile(
          "Reply rate",
          `${replyRate.toFixed(1)}%`,
          totalReached > 0 ? "across all" : "—",
        ),
      ],
    ),
  );

  if (ever.length === 0 && never.length === 0) {
    root.appendChild(
      el("div", { class: "empty-state", style: "padding:32px;text-align:center;" }, [
        el("div", { style: "font-size:32px;margin-bottom:8px;" }, "📜"),
        el("div", { style: "color:#aaa;font-size:13px;margin-bottom:6px;" }, "No workflows yet"),
        el(
          "div",
          { class: "muted", style: "font-size:12px;" },
          "Build one in the Library tab — runs appear here as they fire.",
        ),
      ]),
    );
    return;
  }

  // Per-workflow run-rollup rows.
  if (ever.length > 0) {
    root.appendChild(
      el(
        "h3",
        { style: "margin:18px 0 8px;font-size:13px;color:#fff;" },
        `Recently active (${ever.length})`,
      ),
    );
    const list = el("div", { style: "display:flex;flex-direction:column;gap:6px;" });
    for (const w of ever) list.appendChild(_renderWorkflowRunRollupRow(w));
    root.appendChild(list);
  }

  if (never.length > 0) {
    root.appendChild(
      el(
        "h3",
        { style: "margin:24px 0 8px;font-size:13px;color:#fff;" },
        `Never run (${never.length})`,
      ),
    );
    root.appendChild(
      el(
        "p",
        { class: "muted", style: "margin:0 0 8px;font-size:11px;" },
        "These workflows are configured but haven't fired yet. Open and hit Run, or schedule a trigger.",
      ),
    );
    const list = el("div", { style: "display:flex;flex-direction:column;gap:6px;" });
    for (const w of never) list.appendChild(_renderWorkflowRunRollupRow(w));
    root.appendChild(list);
  }
}

function _runsStatTile(label, value, sub) {
  return el(
    "div",
    {
      style: "background:#1a1a22;border:1px solid #2a2a38;border-radius:8px;padding:12px 14px;",
    },
    [
      el(
        "div",
        {
          style:
            "font-size:10px;color:#888;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;",
        },
        label,
      ),
      el("div", { style: "font-size:20px;color:#fff;font-weight:600;line-height:1.1;" }, value),
      el("div", { class: "muted", style: "font-size:10px;margin-top:2px;" }, sub),
    ],
  );
}

function _renderWorkflowRunRollupRow(w) {
  const ts = w.last_run_at ? new Date(w.last_run_at) : null;
  const timeLabel = ts ? _shortRelativeTime(ts) : "never";
  const reach = w.total_audience_reached || 0;
  const replies = w.total_replies || 0;
  const replyRate = reach > 0 ? (100 * replies) / reach : null;
  const status = w.status || "—";

  return el(
    "div",
    {
      style:
        "background:#1a1a22;border:1px solid #2a2a38;border-radius:8px;padding:12px 14px;display:flex;align-items:center;gap:14px;cursor:pointer;transition:border-color 120ms,background 120ms;",
      onmouseenter: (e) => {
        e.currentTarget.style.borderColor = "#3a3a4e";
        e.currentTarget.style.background = "#1f1f28";
      },
      onmouseleave: (e) => {
        e.currentTarget.style.borderColor = "#2a2a38";
        e.currentTarget.style.background = "#1a1a22";
      },
      onclick: () => {
        // Open the workflow's canvas — same target as clicking a workflow row in My workflows.
        dashboardState.composerDraft = { ...w };
        navigate("workflows/canvas");
      },
    },
    [
      el("div", { style: "flex:1;min-width:0;" }, [
        el("div", { style: "display:flex;align-items:center;gap:8px;margin-bottom:4px;" }, [
          el(
            "span",
            {
              style:
                "font-size:13px;color:#fff;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
            },
            w.name || `Workflow #${w.id}`,
          ),
          _renderConfigStatusPill(status),
        ]),
        el(
          "div",
          { class: "muted", style: "font-size:11px;" },
          `${w.total_runs || 0} run${(w.total_runs || 0) === 1 ? "" : "s"} · last ${timeLabel}${reach > 0 ? ` · ${reach} reached` : ""}${replies > 0 ? ` · ${replies} replied` : ""}`,
        ),
      ]),
      // Right side — reply rate or "never run" indicator
      replyRate != null
        ? el("div", { style: "text-align:right;flex-shrink:0;" }, [
            el(
              "div",
              {
                style: `font-size:15px;font-weight:600;color:${replyRate >= 10 ? "#4ade80" : replyRate >= 3 ? "#fbbf24" : "#aaa"};line-height:1.1;`,
              },
              `${replyRate.toFixed(1)}%`,
            ),
            el(
              "div",
              {
                class: "muted",
                style: "font-size:10px;text-transform:uppercase;letter-spacing:0.4px;",
              },
              "reply rate",
            ),
          ])
        : el("div", { class: "muted", style: "font-size:11px;flex-shrink:0;" }, timeLabel),
    ],
  );
}

// Compact relative time formatter: 2m / 14h / 3d / 2w / Apr 5
function _shortRelativeTime(d) {
  if (!d || isNaN(d.getTime())) return "—";
  const diffMs = Date.now() - d.getTime();
  const sec = Math.floor(diffMs / 1000);
  if (sec < 60) return `${sec}s ago`;
  const min = Math.floor(sec / 60);
  if (min < 60) return `${min}m ago`;
  const hr = Math.floor(min / 60);
  if (hr < 24) return `${hr}h ago`;
  const day = Math.floor(hr / 24);
  if (day < 14) return `${day}d ago`;
  if (day < 60) return `${Math.floor(day / 7)}w ago`;
  return d.toLocaleDateString([], { month: "short", day: "numeric" });
}

// Phase E — track tiles: one per starter-template track. Click → opens the
// templates picker modal pre-filtered to that track. Falls back to a plain
// "Browse all templates" button if track metadata isn't available yet.
async function _buildBuildPageTracksStrip() {
  const { tracks } = await _ensureCompositionTemplates().catch(() => ({ tracks: [] }));
  if (!Array.isArray(tracks) || tracks.length === 0) {
    // Fallback: simple "Browse templates" CTA
    return el(
      "div",
      {
        style:
          "padding: 14px; background: #0a0a0a; border: 1px solid #222; border-radius: 8px; margin-bottom: 16px;",
      },
      [
        el(
          "button",
          {
            class: "btn btn-secondary",
            type: "button",
            onclick: () => _openTemplatesPickerModal(),
          },
          "📚 Browse all templates",
        ),
      ],
    );
  }
  const tileRow = el("div", {
    style:
      "display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px;",
  });
  for (const t of tracks) {
    const tile = el(
      "button",
      {
        type: "button",
        class: "btn",
        style: [
          "display:flex",
          "flex-direction:column",
          "align-items:flex-start",
          "padding:14px 14px",
          "gap:6px",
          "background:#0f0f10",
          "border:1px solid #26262a",
          "border-radius:8px",
          "cursor:pointer",
          "text-align:left",
          "transition:transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease",
        ].join(";"),
        onmouseover: (e) => {
          e.currentTarget.style.borderColor = "#5adada";
          e.currentTarget.style.transform = "translateY(-1px)";
          e.currentTarget.style.boxShadow = "0 4px 16px -6px rgba(90,218,218,0.3)";
        },
        onmouseout: (e) => {
          e.currentTarget.style.borderColor = "#26262a";
          e.currentTarget.style.transform = "translateY(0)";
          e.currentTarget.style.boxShadow = "none";
        },
        onclick: () => _openTemplatesPickerModal(null, t.id),
      },
      [
        el("div", { style: "font-size: 22px; line-height: 1;" }, t.icon || "📦"),
        el("div", { style: "font-size: 13px; font-weight: 600; color: #e5e7eb;" }, t.label || t.id),
        el(
          "div",
          { class: "muted", style: "font-size: 10px; color: #888; line-height: 1.4;" },
          t.description || "",
        ),
      ],
    );
    tileRow.appendChild(tile);
  }
  return el("div", { style: "margin-bottom: 20px;" }, [
    el(
      "div",
      {
        class: "row",
        style: "justify-content: space-between; align-items: baseline; margin-bottom: 8px;",
      },
      [
        el(
          "div",
          { style: "font-size: 13px; font-weight: 600; color: #ccc;" },
          "📚 Start from a template",
        ),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            onclick: () => _openTemplatesPickerModal(),
          },
          "View all →",
        ),
      ],
    ),
    tileRow,
  ]);
}

async function renderWorkflowComposer() {
  // Use whatever the user staged on dashboardState (set by the list page or
  // by a fresh "+ New workflow" click).
  if (!dashboardState.composerDraft) dashboardState.composerDraft = _composerEmptyDraft();
  const draft = dashboardState.composerDraft;
  // Pre-load source catalog so resolver cards render with their labels
  // populated on first paint.
  await _ensureSourceCatalog();

  const root = el("div", { style: "max-width: 900px; margin: 0 auto; padding: 24px;" });

  const header = el(
    "div",
    {
      class: "row",
      style: "justify-content: space-between; align-items: center; margin-bottom: 24px;",
    },
    [
      el("div", { style: "flex: 1;" }, [
        el("input", {
          type: "text",
          value: draft.name,
          placeholder: "Workflow name",
          style:
            "background: transparent; border: none; border-bottom: 1px dashed #444; color: #eee; font-size: 20px; font-weight: 600; padding: 4px 0; width: 100%; outline: none;",
          oninput: (e) => {
            draft.name = e.target.value;
          },
        }),
        el(
          "textarea",
          {
            placeholder: "Description (optional)",
            rows: 1,
            style:
              "background: transparent; border: none; color: #888; font-size: 12px; padding: 4px 0; width: 100%; resize: none; outline: none; margin-top: 4px;",
            oninput: (e) => {
              draft.description = e.target.value;
            },
          },
          draft.description || "",
        ),
      ]),
      el(
        "button",
        {
          class: "btn btn-secondary btn-tiny",
          type: "button",
          onclick: () => navigate("workflows"),
        },
        "← Back",
      ),
    ],
  );
  root.appendChild(header);

  // Phase 9: edit-lock banner — fetched once on wizard mount. If the
  // composition has a running run, edits are blocked server-side (409);
  // we surface it here so the user knows before they try.
  if (draft.id) {
    api("composition-runs", { id: draft.id, limit: 1 })
      .then((rr) => {
        const running = (rr?.runs || []).some((r) => r.status === "running");
        if (running) {
          const lockBanner = el(
            "div",
            {
              style:
                "padding: 8px 12px; background: #2a1a1a; border: 1px solid #5a3a3a; border-radius: 4px; font-size: 11px; margin-bottom: 14px; color: #faa;",
            },
            "🔒 Composition has a running run — edits will be blocked. Wait for it to finish, then refresh.",
          );
          root.insertBefore(lockBanner, root.firstChild?.nextSibling || null);
        }
      })
      .catch(() => {});
  }

  // Stepper crumb
  const stepBar = el(
    "div",
    {
      class: "row",
      style:
        "gap: 8px; margin-bottom: 24px; padding: 8px 12px; background: #111; border-radius: 6px;",
    },
    [
      el("div", {}, "1. WHO"),
      el("span", { class: "muted" }, "→"),
      el("div", {}, "2. WHERE"),
      el("span", { class: "muted" }, "→"),
      el("div", {}, "3. WHAT"),
      el("span", { class: "muted" }, "→"),
      el("div", {}, "4. REVIEW"),
    ],
  );
  root.appendChild(stepBar);

  // Step 1 — WHO
  const stepWho = el("div", { class: "card", style: "margin-bottom: 16px;" });
  stepWho.appendChild(el("div", { class: "card-title" }, "1. WHO — audience"));

  // Phase 4: trigger picker (manual / schedule / event)
  const triggerSection = await _composerTriggerSection(draft);
  stepWho.appendChild(triggerSection);

  const audienceCountBadge = el(
    "div",
    { class: "muted", style: "font-size: 12px; margin-bottom: 12px;" },
    "Total: —",
  );

  // Phase 7: live count is debounced (400ms) + cached (30s by audience-hash).
  // Cuts request volume on every keystroke; same answer as the old direct fetch.
  const audienceForm = _composerWhoForm(draft, () => {
    audienceCountBadge.textContent = "Computing…";
    _composerDebouncedAudienceCount(draft.audience, draft.channels, (r) => {
      if (!r) {
        audienceCountBadge.textContent = "Could not compute";
        return;
      }
      const reachable = r.reachable || {};
      const reachableLine = ["telegram", "twitter", "linkedin", "email"]
        .map((p) => `${p}: ${reachable[p] || 0}`)
        .join(" · ");
      // Phase 20: audience freshness — % of contacts never previously targeted
      const fresh = r.freshness?.fresh_pct;
      const freshSuffix =
        fresh != null && r.freshness.total > 0 ? ` · 🌱 ${Math.round(fresh * 100)}% fresh` : "";
      audienceCountBadge.textContent =
        `Total: ${r.total || 0} · ${reachableLine}` +
        (draft.channels.length > 0 ? ` · usable via channels: ${r.usableViaChannels || 0}` : "") +
        freshSuffix;
      // Color-hint when freshness is alarmingly low
      if (fresh != null && r.freshness.total >= 10 && fresh < 0.3) {
        audienceCountBadge.style.color = "#daaa5a";
        audienceCountBadge.title = `Audience freshness: ${Math.round(fresh * 100)}% (most contacts have already been targeted by other compositions). Consider bumping the recency exclusion days, or use combine=exclude with crm_excluded_from_composition.`;
      } else {
        audienceCountBadge.style.color = "";
        audienceCountBadge.title = "";
      }
      // Stash latest count on the draft so the REVIEW step's cap simulator
      // can read it without a second round-trip.
      draft._lastCount = r;
    });
  });
  stepWho.appendChild(audienceCountBadge);
  stepWho.appendChild(audienceForm);
  root.appendChild(stepWho);

  // Step 2 — WHERE (Phase 1: minimal — pick one platform + account, fallback chain in Phase 2)
  const stepWhere = el("div", { class: "card", style: "margin-bottom: 16px;" });
  stepWhere.appendChild(el("div", { class: "card-title" }, "2. WHERE — channel"));
  const accountsResp = await api("accounts-all").catch(() => ({}));
  const accounts = accountsResp?.accounts || [];
  stepWhere.appendChild(
    _composerWhereForm(draft, accounts, () =>
      audienceForm.querySelector(".composer-recount")?.click(),
    ),
  );
  root.appendChild(stepWhere);

  // Step 3 — WHAT (Phase 1: send_dm only, with template + voice preset stub)
  const stepWhat = el("div", { class: "card", style: "margin-bottom: 16px;" });
  stepWhat.appendChild(el("div", { class: "card-title" }, "3. WHAT — play sequence"));
  stepWhat.appendChild(_composerWhatForm(draft));
  root.appendChild(stepWhat);

  // Step 4 — REVIEW + Save/Run
  const stepReview = el("div", { class: "card", style: "margin-bottom: 16px;" });
  stepReview.appendChild(el("div", { class: "card-title" }, "4. REVIEW"));

  // Phase 17: pacing panel — per-composition daily cap so one workflow
  // doesn't dominate an account's budget.
  stepReview.appendChild(_composerRenderPacingPanel(draft));

  // Phase 11: canary toggle (sits on draft.audience.canary so it travels
  // with the composition body across saves).
  stepReview.appendChild(_composerRenderCanaryPanel(draft));

  // Phase 9: sandbox list panel
  stepReview.appendChild(_composerRenderSandboxPanel());

  // Phase 7: daily-cap simulator (lazy — clicked refresh re-renders)
  const capSlot = el("div", {});
  const renderCapSim = () => {
    capSlot.innerHTML = "";
    const sim = _composerBuildCapSimulator(
      draft._lastCount?.total || 0,
      draft._lastCount?.reachable || {},
      draft.channels,
    );
    if (sim) capSlot.appendChild(sim);
  };
  capSlot.appendChild(
    el(
      "div",
      { class: "muted", style: "font-size: 11px;" },
      "Daily-cap simulator appears after the audience count loads.",
    ),
  );
  setTimeout(renderCapSim, 1500); // initial after WHO refresh runs
  stepReview.appendChild(capSlot);

  const reviewArea = el(
    "pre",
    {
      style:
        "white-space: pre-wrap; background: #0c0c0c; padding: 10px; border-radius: 4px; font-size: 11px; color: #aaa; max-height: 300px; overflow: auto; margin-top: 10px;",
    },
    "(click Dry-run to see resolved audience + simulated plays)",
  );
  const reviewActions = el(
    "div",
    { class: "row", style: "gap: 8px; margin-top: 12px; flex-wrap: wrap;" },
    [
      el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          onclick: async () => {
            if (draft.audience.resolvers.length === 0) {
              toast("Pick an audience first (WHO step)", "err");
              return;
            }
            if (draft.channels.length === 0) {
              toast("Pick a channel first (WHERE step)", "err");
              return;
            }
            if ((draft.plays || []).length === 0) {
              toast("Add at least one play (WHAT step)", "err");
              return;
            }
            // Save (or update) the draft, then preview against the saved row.
            const saved = await _composerSaveDraft(draft);
            if (!saved) return;
            reviewArea.textContent = "Running dry-run…";
            const r = await api("composition-preview", { id: saved.id }).catch((e) => ({
              error: e?.message,
            }));
            if (r?.error) {
              reviewArea.textContent = `ERROR: ${r.error}`;
              return;
            }
            const lines = [
              `Audience total: ${r.audienceTotal || 0}`,
              `Showing first ${r.audienceSize || 0} contacts (max 5 in dry-run):`,
              "",
              ...(r.preview || []),
            ];
            reviewArea.textContent = lines.join("\n");
          },
        },
        "🔍 Dry-run",
      ),
      el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          onclick: async () => {
            const saved = await _composerSaveDraft(draft);
            if (saved) toast(`Saved as draft (#${saved.id})`, "ok");
          },
        },
        "💾 Save draft",
      ),
      // Phase 7: re-run the cap simulator with the latest reachability counts
      el(
        "button",
        {
          class: "btn btn-secondary btn-tiny",
          type: "button",
          onclick: () => renderCapSim(),
          title: "Recompute the daily-cap simulator with the latest audience numbers.",
        },
        "↻ Cap sim",
      ),
      // Phase 9: sandbox run — fires plays for real but only against the
      // user's sandbox contact list. Use to receive your own DM and judge it
      // before unleashing on the real audience.
      el(
        "button",
        {
          class: "btn btn-secondary btn-tiny",
          type: "button",
          title: "Run plays for real, but only against the sandbox contact list (set above).",
          onclick: async () => {
            const sandboxIds = _composerGetSandboxIds();
            if (sandboxIds.length === 0) {
              toast("Sandbox list is empty — set IDs in the panel above first", "err");
              return;
            }
            if (
              !confirm(
                `🧪 Run for real on ${sandboxIds.length} sandbox contact${sandboxIds.length === 1 ? "" : "s"}? This sends actual messages.`,
              )
            )
              return;
            const saved = await _composerSaveDraft(draft);
            if (!saved) return;
            const r = await api("composition-run", {
              id: saved.id,
              body: { audienceOverrideContactIds: sandboxIds },
            }).catch((e) => ({ error: e?.message }));
            if (r?.error) {
              toast(`Sandbox run failed: ${r.error}`, "err");
              return;
            }
            toast(
              `🧪 Sandbox run · ${r.playsSucceeded || 0}/${r.playsExecuted || 0} plays succeeded`,
              "ok",
              5000,
            );
          },
        },
        "🧪 Run on sandbox",
      ),
      // Phase 7: save the current composition body as a personal template
      el(
        "button",
        {
          class: "btn btn-secondary btn-tiny",
          type: "button",
          onclick: async () => {
            const saved = await _composerSaveDraft(draft);
            if (!saved) return;
            const name = prompt("Name for this template:", saved.name || "My template");
            if (!name) return;
            const r = await api("composition-save-as-template", {
              body: { compositionId: saved.id, name, jtbd: "reach", icon: "🧩" },
            }).catch((e) => ({ error: e?.message }));
            if (r?.error) {
              toast(`Save-template failed: ${r.error}`, "err");
              return;
            }
            // Bust the cached templates list so the picker shows the new one.
            try {
              _composerTemplates = null;
            } catch {}
            toast(`📚 Saved as personal template "${name}"`, "ok", 3500);
          },
        },
        "📚 Save as template",
      ),
      el(
        "button",
        {
          class: "btn btn-primary",
          type: "button",
          onclick: async () => {
            // Phase 8: pre-flight warnings — surface red flags before
            // committing to a real run. User can override with "Activate
            // anyway" if they understand the risk.
            const warnings = _composerPreflightWarnings(draft);
            if (warnings.length > 0) {
              const proceed = await _composerShowPreflightModal(warnings);
              if (!proceed) return;
            }
            const saved = await _composerSaveDraft(draft);
            if (!saved) return;
            const r1 = await api("composition-activate", { id: saved.id }).catch(() => ({}));
            if (r1?.error) {
              toast(`Activate failed: ${r1.error}`, "err");
              return;
            }
            const r2 = await api("composition-run", { id: saved.id, body: {} }).catch((e) => ({
              error: e?.message,
            }));
            if (r2?.error) {
              toast(`Run failed: ${r2.error}`, "err");
              return;
            }
            toast(
              `▶ Activated & ran · ${r2.playsSucceeded || 0}/${r2.playsExecuted || 0} plays succeeded`,
              "ok",
              5000,
            );
            navigate("workflows");
          },
        },
        "▶ Activate & run now",
      ),
    ],
  );
  stepReview.appendChild(reviewArea);
  stepReview.appendChild(reviewActions);
  root.appendChild(stepReview);

  setContentNode(root);

  // Kick off an initial audience count so the badge is populated.
  setTimeout(() => audienceForm.querySelector(".composer-recount")?.click(), 50);
}
