// Read-only workflow viewer (v1.5).
//
// What changed vs v1:
//   - Top-to-bottom layout (reads like a flowchart, not a timeline)
//   - Plain-language narrative labels via window.WorkflowNarrative
//   - logic.if renders as a diamond with computed YES/NO labels
//     ("Yes — confidence ≥ 0.8" instead of true/false)
//   - logic.wait / cooldown / rate_limit render as pills
//   - logic.switch renders as a hexagon with case labels on each branch
//   - Diagram / Story toggle: the story view walks the DAG into a paragraph
//   - Inspector panel reads as "What it does · Inputs · Outputs · Settings"
//
// v2 will swap this for Rete.js v2 once edit + typed sockets are needed.
// The data model is identical, so the swap won't touch nodes.js or presets.

(function () {
  "use strict";

  const NS = "http://www.w3.org/2000/svg";

  const CATEGORY_COLORS = {
    trigger:  { fill: "#1e3a5f", stroke: "#3b82f6", text: "#dbeafe" },
    scrape:   { fill: "#1e4438", stroke: "#10b981", text: "#d1fae5" },
    enrich:   { fill: "#3a1f4f", stroke: "#a855f7", text: "#ede9fe" },
    ai:       { fill: "#4a2a1c", stroke: "#f97316", text: "#fed7aa" },
    outreach: { fill: "#4f1f3a", stroke: "#ec4899", text: "#fce7f3" },
    db:       { fill: "#26323e", stroke: "#64748b", text: "#cbd5e1" },
    logic:    { fill: "#3f3a1c", stroke: "#eab308", text: "#fef3c7" },
    notify:   { fill: "#1c3f3f", stroke: "#06b6d4", text: "#cffafe" },
    tracking: { fill: "#3f1c1c", stroke: "#ef4444", text: "#fecaca" },
    queue:    { fill: "#28323a", stroke: "#94a3b8", text: "#e2e8f0" },
    flow:     { fill: "#28323a", stroke: "#94a3b8", text: "#e2e8f0" },
    _default: { fill: "#2a2a2a", stroke: "#666",   text: "#ddd"     },
  };

  const NODE_W = 240;       // wider so narrative labels fit on one line for most cases
  const NODE_H = 76;
  const PORT_R = 4;
  const DIAMOND_W = 200;
  const DIAMOND_H = 130;
  const PILL_W = 200;
  const PILL_H = 52;
  const HEX_W = 240;
  const HEX_H = 100;


  function categoryOf(type) { return (type || "").split(".")[0] || "_default"; }
  function colorsOf(type) { return CATEGORY_COLORS[categoryOf(type)] || CATEGORY_COLORS._default; }

  // Decide which shape to draw for a node.
  // Returns the connection point on the node's edge for a given output/input slot.
  // For DOWN layout: outputs exit the bottom edge, inputs enter the top edge.




  // ── Inspector ────────────────────────────────────────────────────────

  // Map of preset_id → Set of "<nodeType>|<paramKey>" the engine actually
  // honors at runtime via readNodeParam(). All other params are visualization
  // only — editing them in a custom workflow won't change engine behavior.
  // Keep in sync with the engine modules' readNodeParam() calls.
  const WIRED_PARAMS = {
    signal_twitter:      new Set(["ai.classify_signal|min_confidence", "logic.if|right"]),
    signal_linkedin:     new Set(["ai.classify_signal|min_confidence", "logic.if|right"]),
    signal_reddit:       new Set(["ai.classify_signal|min_confidence", "logic.if|right"]),
    signal_hn:           new Set(["ai.classify_signal|min_confidence", "logic.if|right"]),
    top_tokens_ingest:   new Set(["scrape.coingecko|limit"]),
    enrichment_pipeline: new Set(["enrich.dequeue|batch_size"]),
    tg_group_rotation: new Set([
      "outreach.post_group|message_mode",
      "outreach.post_group|per_group_cooldown_hours",
    ]),
  };
  // A schedule override on trigger.cron is honored on EVERY wired preset
  // (gates scheduled cycles via shouldSkipForScheduleOverride).
  const SCHEDULE_PARAM = "trigger.cron|schedule";

  function isParamWired(presetId, nodeType, paramKey) {
    if (!presetId || !nodeType || !paramKey) return false;
    const key = `${nodeType}|${paramKey}`;
    if (key === SCHEDULE_PARAM) return true; // every wired preset honors schedule override
    return !!(WIRED_PARAMS[presetId] && WIRED_PARAMS[presetId].has(key));
  }

  function buildInspector(containerEl, workflow, registryByType, options = {}) {
    const editable = !!options.editable;
    const onSave = typeof options.onSave === "function" ? options.onSave : null;
    // Buffer of unsaved changes while the user edits. pendingOverrides stores
    // value-changes (additions / updates); pendingResets stores keys that
    // should be REMOVED from existing overrides (returning a parameter to
    // its preset default). Both are cleared on save.
    const pendingOverrides = {};
    const pendingResets = new Set();   // entries are "nodeId|paramKey"
    function hasPending() {
      if (pendingResets.size > 0) return true;
      for (const k of Object.keys(pendingOverrides)) {
        if (Object.keys(pendingOverrides[k] || {}).length > 0) return true;
      }
      return false;
    }
    function setPending(nodeId, paramKey, value, originalValue) {
      // A user edit cancels any pending reset on the same key
      pendingResets.delete(`${nodeId}|${paramKey}`);
      if (!pendingOverrides[nodeId]) pendingOverrides[nodeId] = {};
      // If user's input matches the original, drop it from the pending set —
      // this prevents the diff from carrying no-op edits.
      if (String(value) === String(originalValue ?? "")) {
        delete pendingOverrides[nodeId][paramKey];
        if (Object.keys(pendingOverrides[nodeId]).length === 0) delete pendingOverrides[nodeId];
      } else {
        pendingOverrides[nodeId][paramKey] = value;
      }
      updateSaveBtn();
    }
    // Reset request — clear an EXISTING override on save. UI should also
    // visually update the input to the original value.
    function setReset(nodeId, paramKey) {
      pendingResets.add(`${nodeId}|${paramKey}`);
      if (pendingOverrides[nodeId]) {
        delete pendingOverrides[nodeId][paramKey];
        if (Object.keys(pendingOverrides[nodeId]).length === 0) delete pendingOverrides[nodeId];
      }
      updateSaveBtn();
    }
    let saveBtnRef = null;
    function updateSaveBtn() {
      if (!saveBtnRef) return;
      const dirty = hasPending();
      saveBtnRef.disabled = !dirty;
      saveBtnRef.style.opacity = dirty ? "1" : "0.5";
      saveBtnRef.style.cursor = dirty ? "pointer" : "default";
    }

    const panel = document.createElement("div");
    panel.className = "wf-detail-panel";
    Object.assign(panel.style, {
      position: "absolute", top: "0", right: "0", width: "340px", height: "100%",
      background: "#1a1a1c", borderLeft: "1px solid #333", padding: "16px",
      color: "#e5e7eb", fontFamily: "system-ui, -apple-system, sans-serif", fontSize: "13px",
      overflowY: "auto", display: "none", zIndex: "2",
    });
    containerEl.appendChild(panel);

    // Build reverse index for "inputs come from"
    const incoming = {};
    for (const [src, byConn] of Object.entries(workflow.connections || {})) {
      for (const slots of Object.values(byConn)) {
        for (const slot of slots) for (const t of (slot || [])) {
          if (!incoming[t.node]) incoming[t.node] = [];
          incoming[t.node].push(src);
        }
      }
    }
    const outgoing = {};
    for (const [src, byConn] of Object.entries(workflow.connections || {})) {
      outgoing[src] = [];
      for (const slots of Object.values(byConn)) {
        for (const slot of slots) for (const t of (slot || [])) outgoing[src].push(t.node);
      }
    }
    const nodesById = {};
    for (const n of workflow.nodes) nodesById[n.id] = n;

    function nodeHumanName(id) {
      const n = nodesById[id];
      if (!n) return id;
      const def = registryByType[n.type];
      return (window.WorkflowNarrative && window.WorkflowNarrative.resolveNarrative)
        ? window.WorkflowNarrative.resolveNarrative(n, def || { label: n.type })
        : (n.name || n.type);
    }

    function show(node, def, focusFn) {
      panel.innerHTML = "";
      const c = colorsOf(node.type);
      const head = document.createElement("div");
      head.style.borderLeft = `3px solid ${c.stroke}`;
      head.style.paddingLeft = "10px";
      head.style.marginBottom = "12px";
      const narr = (window.WorkflowNarrative && window.WorkflowNarrative.resolveNarrative)
        ? window.WorkflowNarrative.resolveNarrative(node, def)
        : (node.name || def.label || node.type);
      head.innerHTML = `
        <div style="font-size:10px;text-transform:uppercase;color:${c.stroke};font-family:ui-monospace,monospace;letter-spacing:0.05em;">${escapeHtml(def.group || def.category || "")}</div>
        <div style="font-size:16px;font-weight:600;margin-top:4px;line-height:1.3">${escapeHtml(narr)}</div>
        <div style="font-size:11px;font-family:ui-monospace,monospace;opacity:0.5;margin-top:2px">${escapeHtml(node.type)}</div>
      `;
      panel.appendChild(head);

      // What it does
      if (def.description) {
        const sect = sectionEl("What it does");
        const d = document.createElement("div");
        d.style.color = "#cbd5e1"; d.style.lineHeight = "1.5";
        d.textContent = def.description;
        sect.appendChild(d);
        panel.appendChild(sect);
      }

      // Inputs come from
      const ins = (incoming[node.id] || []).map(nodeHumanName);
      if (ins.length) {
        const sect = sectionEl("Inputs come from");
        for (const id of incoming[node.id] || []) {
          const chip = chipEl("↑ " + nodeHumanName(id));
          chip.onclick = () => focusFn && focusFn(id);
          sect.appendChild(chip);
        }
        panel.appendChild(sect);
      }

      // Output goes to
      const outs = (outgoing[node.id] || []).map(nodeHumanName);
      if (outs.length) {
        const sect = sectionEl("Output goes to");
        for (const id of outgoing[node.id] || []) {
          const chip = chipEl("↓ " + nodeHumanName(id));
          chip.onclick = () => focusFn && focusFn(id);
          sect.appendChild(chip);
        }
        panel.appendChild(sect);
      }

      // Settings — read-only table on presets, editable form on customs.
      // The full registry parameter list (def.parameters) is iterated so
      // operators can see and SET params that aren't yet on the node, not
      // just edit ones that already have values.
      const currentParams = node.parameters || {};
      const originalParams = node._originalParameters || {};   // pre-override snapshot
      const overriddenParams = node._overriddenParameters || new Set();  // set of keys that came from overrides
      const defParams = def.parameters || [];

      // For read-only mode keep the original behaviour (only show populated keys).
      const readableKeys = Object.keys(currentParams).filter((k) =>
        currentParams[k] !== "" && currentParams[k] !== null && currentParams[k] !== undefined);

      if (defParams.length || readableKeys.length) {
        const sect = sectionEl(editable ? "Settings (editable)" : "Settings");

        if (editable) {
          // Render every registry-defined parameter as an editable control.
          // Iterate def.parameters so missing-on-node params are still authorable.
          const presetId = workflow?.source_preset_id || workflow?.id || null;
          const seen = new Set();
          for (const meta of defParams) {
            seen.add(meta.key);
            sect.appendChild(buildEditableParamRow(node, meta, currentParams, originalParams, overriddenParams, setPending, setReset, presetId));
          }
          // Catch any node parameters that aren't in def (legacy / hand-written
          // presets). Render them as text so the operator can still edit.
          for (const k of readableKeys) {
            if (seen.has(k)) continue;
            sect.appendChild(buildEditableParamRow(node, { key: k, label: k, type: "text" },
              currentParams, originalParams, overriddenParams, setPending, setReset, presetId));
          }
        } else {
          // Read-only table (presets)
          const tbl = document.createElement("table");
          tbl.style.width = "100%"; tbl.style.borderCollapse = "collapse";
          tbl.style.fontSize = "12px";
          for (const k of readableKeys) {
            const meta = defParams.find((p) => p.key === k) || { label: k };
            const tr = tbl.insertRow();
            const td1 = tr.insertCell();
            td1.textContent = meta.label || k;
            td1.style.color = "#9ca3af";
            td1.style.padding = "4px 8px 4px 0";
            td1.style.verticalAlign = "top";
            td1.style.width = "40%";
            const td2 = tr.insertCell();
            td2.textContent = String(currentParams[k]);
            td2.style.padding = "4px 0";
            td2.style.fontFamily = "ui-monospace, monospace";
            td2.style.color = "#e5e7eb";
            td2.style.wordBreak = "break-all";
          }
          sect.appendChild(tbl);
        }
        panel.appendChild(sect);
      }

      // Footer: Save (when editable) + Close
      const footer = document.createElement("div");
      footer.style.marginTop = "20px";
      footer.style.display = "flex";
      footer.style.gap = "8px";

      if (editable) {
        const saveBtn = document.createElement("button");
        saveBtn.textContent = "Save changes";
        Object.assign(saveBtn.style, {
          padding: "6px 14px", background: "#3b82f6", color: "#fff",
          border: "none", borderRadius: "4px", fontWeight: "500",
          cursor: "pointer", flex: "1",
        });
        saveBtn.disabled = true; saveBtn.style.opacity = "0.5"; saveBtn.style.cursor = "default";
        saveBtn.onclick = async () => {
          if (!hasPending() || !onSave) return;
          saveBtn.disabled = true; saveBtn.textContent = "Saving…";
          // Snapshot pending state. If save fails the buffer is preserved.
          const toSaveAdds = JSON.parse(JSON.stringify(pendingOverrides));
          const toSaveResets = Array.from(pendingResets);
          try {
            await onSave({ adds: toSaveAdds, resets: toSaveResets });
            // Clear buffers on success
            for (const k of Object.keys(pendingOverrides)) delete pendingOverrides[k];
            pendingResets.clear();
            saveBtn.textContent = "Saved ✓";
            setTimeout(() => { saveBtn.textContent = "Save changes"; updateSaveBtn(); }, 1200);
          } catch (err) {
            saveBtn.textContent = "Save failed";
            saveBtn.style.background = "#dc2626";
            setTimeout(() => {
              saveBtn.textContent = "Save changes";
              saveBtn.style.background = "#3b82f6";
              updateSaveBtn();
            }, 2400);
          }
        };
        saveBtnRef = saveBtn;
        footer.appendChild(saveBtn);
        updateSaveBtn();
      }

      const closeBtn = document.createElement("button");
      closeBtn.textContent = "Close";
      Object.assign(closeBtn.style, {
        padding: "6px 12px", background: "#27272a",
        border: "1px solid #3f3f46", color: "#e5e7eb", borderRadius: "4px", cursor: "pointer",
      });
      closeBtn.onclick = () => { panel.style.display = "none"; };
      footer.appendChild(closeBtn);
      panel.appendChild(footer);

      panel.style.display = "block";
    }

    // Build a row for one parameter. Renders an input matched to the param's
    // declared type (select / number / boolean / text). Marks the row with
    // a small "modified" dot when the value differs from the original snapshot.
    // When overridden, surfaces a Reset button that clears the override on save.
    function buildEditableParamRow(node, meta, currentParams, originalParams, overriddenParams, setPending, setReset, presetId) {
      const wrap = document.createElement("div");
      wrap.style.marginBottom = "10px";

      const labelRow = document.createElement("div");
      labelRow.style.display = "flex";
      labelRow.style.alignItems = "center";
      labelRow.style.gap = "6px";
      labelRow.style.marginBottom = "4px";
      const lbl = document.createElement("label");
      lbl.textContent = meta.label || meta.key;
      lbl.style.fontSize = "12px";
      lbl.style.color = "#9ca3af";
      lbl.style.fontWeight = "500";
      labelRow.appendChild(lbl);

      // Wired badge: green dot + tooltip when this param is honored by the
      // engine at runtime. Without it, edits here are visualization-only —
      // useful so users don't waste time tuning ornamental params.
      const wired = isParamWired(presetId, node.type, meta.key);
      if (wired) {
        const wiredDot = document.createElement("span");
        wiredDot.textContent = "●";
        wiredDot.title = "Wired — the engine reads this param at runtime";
        wiredDot.style.cssText = "color:#22c55e;font-size:9px;flex-shrink:0;line-height:1;";
        labelRow.appendChild(wiredDot);
      }

      const isOverridden = overriddenParams.has(meta.key);
      const dot = document.createElement("span");
      dot.style.cssText = "display:inline-block;width:6px;height:6px;border-radius:50%;background:#a855f7;flex-shrink:0;";
      dot.title = "Modified from preset default";
      dot.style.visibility = isOverridden ? "visible" : "hidden";
      labelRow.appendChild(dot);

      if (meta.required) {
        const req = document.createElement("span");
        req.textContent = "required";
        req.style.cssText = "font-size:9px;color:#ef4444;margin-left:auto;text-transform:uppercase;letter-spacing:0.05em;";
        labelRow.appendChild(req);
      } else if (isOverridden && typeof setReset === "function") {
        // Reset button — only shown for overridden params. Click reverts the
        // visible input to the original value AND queues a reset that drops
        // the override from the saved map on Save.
        const resetBtn = document.createElement("button");
        resetBtn.type = "button";
        resetBtn.textContent = "↺ Reset";
        resetBtn.title = "Revert to the preset's default value";
        resetBtn.style.cssText = "margin-left:auto;background:transparent;border:none;color:#a855f7;font-size:11px;cursor:pointer;padding:2px 6px;border-radius:3px;font-family:inherit;";
        resetBtn.onmouseover = () => { resetBtn.style.background = "rgba(168,85,247,0.12)"; };
        resetBtn.onmouseout  = () => { resetBtn.style.background = "transparent"; };
        resetBtn.onclick = () => {
          const orig = originalParams[meta.key];
          // Update the visible input to the original. The input ref is
          // captured below via `inputRef`, so it's defined by the time the
          // user can click this button.
          if (inputRef) {
            if (inputRef.tagName === "SELECT") inputRef.value = String(orig ?? "");
            else if (inputRef.type === "checkbox") inputRef.checked = !!orig;
            else inputRef.value = orig != null ? String(orig) : "";
          }
          // Hide the modified dot
          dot.style.visibility = "hidden";
          setReset(node.id, meta.key);
        };
        labelRow.appendChild(resetBtn);
      }
      wrap.appendChild(labelRow);

      const value = currentParams[meta.key];
      const originalValue = originalParams[meta.key];
      let input;
      let inputRef;
      const inputBase = "width:100%;padding:6px 10px;background:#0f0f10;border:1px solid #2a2a2e;border-radius:5px;color:#e5e7eb;font-size:12px;font-family:ui-monospace,monospace;box-sizing:border-box;";

      if (meta.type === "select" && Array.isArray(meta.options) && meta.options.length) {
        input = document.createElement("select");
        input.style.cssText = inputBase;
        for (const opt of meta.options) {
          const o = document.createElement("option");
          o.value = String(opt);
          o.textContent = String(opt);
          if (String(opt) === String(value ?? meta.default ?? "")) o.selected = true;
          input.appendChild(o);
        }
        input.onchange = () => setPending(node.id, meta.key, input.value, originalValue);
      } else if (meta.type === "boolean") {
        input = document.createElement("input");
        input.type = "checkbox";
        input.checked = !!value;
        input.style.cssText = "width:18px;height:18px;cursor:pointer;";
        input.onchange = () => setPending(node.id, meta.key, input.checked, originalValue);
      } else if (meta.type === "number") {
        input = document.createElement("input");
        input.type = "number";
        input.value = value != null ? String(value) : (meta.default != null ? String(meta.default) : "");
        input.style.cssText = inputBase;
        input.placeholder = meta.default != null ? String(meta.default) : "";
        input.oninput = () => setPending(node.id, meta.key, input.value === "" ? "" : Number(input.value), originalValue);
      } else {
        input = document.createElement("input");
        input.type = "text";
        input.value = value != null ? String(value) : "";
        input.style.cssText = inputBase;
        input.placeholder = meta.default != null ? String(meta.default) : (meta.label || meta.key);
        input.oninput = () => setPending(node.id, meta.key, input.value, originalValue);
      }
      inputRef = input;
      wrap.appendChild(input);
      return wrap;
    }

    function sectionEl(title) {
      const s = document.createElement("div");
      s.style.marginTop = "14px";
      const h = document.createElement("div");
      h.textContent = title.toUpperCase();
      h.style.fontSize = "10px";
      h.style.letterSpacing = "0.08em";
      h.style.color = "#6b7280";
      h.style.marginBottom = "6px";
      s.appendChild(h);
      return s;
    }
    function chipEl(label) {
      const c = document.createElement("div");
      c.textContent = label;
      Object.assign(c.style, {
        display: "inline-block", padding: "4px 10px", margin: "2px 4px 2px 0",
        background: "#27272a", border: "1px solid #3f3f46", borderRadius: "12px",
        fontSize: "12px", color: "#cbd5e1", cursor: "pointer",
      });
      return c;
    }

    return { show, hide() { panel.style.display = "none"; } };
  }

  // ── Story view ───────────────────────────────────────────────────────

  // Category → emoji icon (rendered inline on each step card)
  const STORY_ICONS = {
    trigger: "⚡",     // ⚡
    scrape:  "🔎",// 🔎
    enrich:  "🧬",// 🧬
    ai:      "🧠",// 🧠
    outreach:"✉️",// ✉
    db:      "💾",// 💾
    logic:   "◆",      // ◆
    notify:  "🔔",// 🔔
    tracking:"📊",// 📊
    queue:   "📋",// 📋
    flow:    "🔀",// 🔀
    _default:"·",
  };
  function iconFor(category) {
    return STORY_ICONS[category] || STORY_ICONS._default;
  }

  // Render the workflow as a vertical stack of step cards. Each card has a
  // colored left stripe (matches canvas), an icon, a verb chip, and the
  // narrative text. Conditional branches are indented with green (yes) or red
  // (else) left rules so the eye can see the structure without reading.
  function renderStory(containerEl, workflow, registry, focusFn) {
    containerEl.innerHTML = "";
    Object.assign(containerEl.style, {
      padding: "32px 48px 64px", background: "#0f0f10", color: "#e5e7eb",
      fontFamily: "system-ui, -apple-system, sans-serif",
      overflowY: "auto", height: "100%", boxSizing: "border-box",
    });

    // Header
    const head = document.createElement("div");
    head.style.marginBottom = "24px";
    head.style.maxWidth = "760px";
    const title = document.createElement("h2");
    title.textContent = workflow.name;
    title.style.fontSize = "22px"; title.style.fontWeight = "700"; title.style.margin = "0 0 6px";
    head.appendChild(title);
    if (workflow.description) {
      const desc = document.createElement("p");
      desc.textContent = workflow.description;
      desc.style.color = "#9ca3af"; desc.style.fontSize = "13px"; desc.style.lineHeight = "1.55"; desc.style.margin = "0";
      head.appendChild(desc);
    }
    containerEl.appendChild(head);

    if (!window.WorkflowNarrative) {
      const err = document.createElement("div");
      err.textContent = "workflow-narrative.js failed to load";
      err.style.color = "#ef4444";
      containerEl.appendChild(err);
      return;
    }
    const segments = window.WorkflowNarrative.walkStory(workflow, registry);

    // Map nodeId → workflow node (we need the node's type for category lookup)
    const nodeById = {};
    for (const n of (workflow.nodes || [])) nodeById[n.id] = n;
    function categoryOfSegment(seg) {
      const n = nodeById[seg.nodeId];
      return n ? categoryOf(n.type) : "_default";
    }

    // Verb chip per kind/marker. "WHEN" reads as the trigger, "IF" as the
    // diamond, "THEN" as a default action step, "ELSE" / "OTHERWISE" as the
    // false-branch separator.
    function verbForSegment(seg) {
      if (seg.kind === "trigger") return "WHEN";
      if (seg.kind === "if")      return "IF";
      if (seg.kind === "switch")  return "DEPENDING ON";
      if (seg.kind === "loop")    return "FOR EACH";
      if (seg.kind === "join")    return "↺";
      if (seg.marker === "else")  return "OTHERWISE";
      // Pick a more specific verb based on category for normal steps
      const cat = categoryOfSegment(seg);
      const verbByCat = { scrape: "SEARCH", enrich: "LOOK UP", ai: "DECIDE",
                          outreach: "SEND",  db: "SAVE",      notify: "NOTIFY",
                          tracking: "TRACK", queue: "QUEUE",  flow: "ROUTE" };
      return verbByCat[cat] || "THEN";
    }

    // Track current branch: when we see an "if", remember its indent + state.
    // Subsequent segments at higher indent are inside the branch and get
    // a colored left rule. "else" marker flips state to red.
    let branchIndent = -1;
    let branchState = null; // 'yes' | 'else' | null

    const list = document.createElement("div");
    list.style.maxWidth = "760px";

    for (const seg of segments) {
      // Have we exited the current branch?
      if (branchIndent >= 0 && seg.indent <= branchIndent && seg.kind !== "if" && seg.marker !== "else") {
        branchIndent = -1; branchState = null;
      }

      const cat = categoryOfSegment(seg);
      const c = colorsOf((nodeById[seg.nodeId] || {}).type || "");
      const verb = verbForSegment(seg);

      // Visual: outer wrapper handles indent + branch rule; card sits inside
      const wrap = document.createElement("div");
      wrap.style.display = "flex"; wrap.style.alignItems = "stretch";
      wrap.style.position = "relative";
      wrap.style.marginLeft = (seg.indent * 28) + "px";
      wrap.style.marginTop = "6px";

      // Branch rule on the left for nested cards
      if (seg.indent > 0 && branchIndent >= 0 && branchState) {
        const ruleColor = branchState === "yes" ? "#10b981" : "#ef4444";
        const rule = document.createElement("div");
        Object.assign(rule.style, {
          width: "2px", background: ruleColor, marginRight: "12px", borderRadius: "1px",
          opacity: "0.7",
        });
        wrap.appendChild(rule);
      }

      // The card itself
      const card = document.createElement("div");
      card.dataset.nodeId = seg.nodeId;
      Object.assign(card.style, {
        display: "flex", alignItems: "center", flex: "1",
        background: "#1a1a1c", border: "1px solid #2a2a2e", borderRadius: "8px",
        padding: "12px 14px", cursor: "pointer", minWidth: "0",
        transition: "border-color 120ms, transform 120ms",
        gap: "12px",
      });
      // Colored left stripe (4px)
      const stripe = document.createElement("div");
      Object.assign(stripe.style, {
        width: "4px", alignSelf: "stretch", background: c.stroke, borderRadius: "2px",
        flexShrink: "0", marginRight: "2px",
      });
      card.appendChild(stripe);

      // Icon (emoji)
      const icon = document.createElement("div");
      icon.textContent = iconFor(cat);
      Object.assign(icon.style, {
        fontSize: "18px", lineHeight: "1", width: "24px", textAlign: "center", flexShrink: "0",
        opacity: "0.95",
      });
      card.appendChild(icon);

      // Verb chip
      const verbEl = document.createElement("div");
      verbEl.textContent = verb;
      Object.assign(verbEl.style, {
        fontFamily: "ui-monospace, SFMono-Regular, monospace",
        fontSize: "10px", letterSpacing: "0.08em", fontWeight: "600",
        color: c.stroke, flexShrink: "0", minWidth: "92px",
      });
      card.appendChild(verbEl);

      // Narrative text
      const text = document.createElement("div");
      text.textContent = seg.text;
      Object.assign(text.style, {
        color: "#e5e7eb", fontSize: "14px", lineHeight: "1.4",
        flex: "1", minWidth: "0", whiteSpace: "nowrap",
        overflow: "hidden", textOverflow: "ellipsis",
      });
      card.appendChild(text);

      // Hover state + click: highlight on diagram
      card.onmouseover = () => { card.style.borderColor = c.stroke; card.style.transform = "translateX(2px)"; };
      card.onmouseout  = () => { card.style.borderColor = "#2a2a2e"; card.style.transform = "translateX(0)"; };
      card.onclick = () => focusFn && focusFn(seg.nodeId);

      // Style overrides per kind
      if (seg.kind === "if" || seg.kind === "switch") {
        card.style.background = "#2a2818";  // warmer yellow tint
        card.style.borderColor = "#52431e";
      }
      if (seg.marker === "else") {
        card.style.background = "#2a1818";
        card.style.borderColor = "#52262a";
        // Otherwise marker is decorative — narrative is empty, hide the text
        text.textContent = "";
      }

      wrap.appendChild(card);
      list.appendChild(wrap);

      // Update branch tracking AFTER rendering the gate node
      if (seg.kind === "if") {
        branchIndent = seg.indent; branchState = "yes";
      } else if (seg.marker === "else") {
        branchState = "else";
      }
    }

    containerEl.appendChild(list);
  }

  // ── Activity view ────────────────────────────────────────────────────
  //
  // Replaces the legacy Diagram view. Shows what this workflow has
  // actually DONE — a list of recent runs with stats per cycle (items
  // processed, items kept, errors, duration). Aggregated header gives
  // 7-day totals so the user gets a quick "is this producing?" answer.
  //
  // Data source: options.recentRuns (passed by renderAutomationView).
  // For presets without their own runs (presets are templates; runs
  // belong to user customs), shows an empty-state explaining that.
  function renderActivity(containerEl, workflow, options) {
    containerEl.innerHTML = "";
    const recentRuns = Array.isArray(options.recentRuns) ? options.recentRuns : [];
    const fetchRunOutputs = typeof options.fetchRunOutputs === "function" ? options.fetchRunOutputs : null;

    // Header — title + aggregate stats
    const head = document.createElement("div");
    head.style.maxWidth = "760px"; head.style.marginBottom = "18px";
    const title = document.createElement("h2");
    title.textContent = "Activity";
    Object.assign(title.style, { color: "#e5e7eb", fontSize: "18px", margin: "0 0 4px 0" });
    head.appendChild(title);
    const subtitle = document.createElement("p");
    subtitle.textContent = "What this workflow has produced over time.";
    Object.assign(subtitle.style, { color: "#9ca3af", fontSize: "13px", margin: "0 0 12px 0" });
    head.appendChild(subtitle);

    // Aggregate stats
    if (recentRuns.length > 0) {
      let totalRuns = recentRuns.length;
      let totalErrors = 0;
      let totalProcessed = 0;
      let totalKept = 0;
      let lastRunAt = null;
      for (const r of recentRuns) {
        if (r.status === "error") totalErrors++;
        totalProcessed += Number(r.items_processed) || 0;
        totalKept      += Number(r.items_kept) || 0;
        if (!lastRunAt || (r.finished_at && r.finished_at > lastRunAt)) lastRunAt = r.finished_at || r.started_at;
      }
      const successPct = totalRuns > 0 ? Math.round(((totalRuns - totalErrors) / totalRuns) * 100) : null;
      const stats = document.createElement("div");
      Object.assign(stats.style, {
        display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(140px,1fr))", gap: "8px",
        padding: "12px 14px", background: "rgba(99,102,241,0.04)",
        border: "1px solid rgba(99,102,241,0.18)", borderRadius: "8px",
      });
      const stat = (label, value, sub) => {
        const cell = document.createElement("div");
        cell.style.display = "flex"; cell.style.flexDirection = "column"; cell.style.gap = "2px";
        const l = document.createElement("div");
        Object.assign(l.style, { fontSize: "10px", color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: "600" });
        l.textContent = label;
        const v = document.createElement("div");
        Object.assign(v.style, { fontSize: "18px", fontWeight: "600", color: "#e5e7eb", fontVariantNumeric: "tabular-nums" });
        v.textContent = String(value);
        cell.appendChild(l); cell.appendChild(v);
        if (sub) {
          const s = document.createElement("div");
          Object.assign(s.style, { fontSize: "10px", color: "#71717a", fontFamily: "ui-monospace, monospace" });
          s.textContent = sub;
          cell.appendChild(s);
        }
        return cell;
      };
      stats.appendChild(stat("Runs", totalRuns, successPct !== null ? `${successPct}% ok` : ""));
      stats.appendChild(stat("Items kept", totalKept, totalProcessed > 0 ? `of ${totalProcessed}` : ""));
      stats.appendChild(stat("Errors", totalErrors, totalErrors > 0 ? "needs attention" : ""));
      stats.appendChild(stat("Last run", lastRunAt ? _activityFmtSince(lastRunAt) : "—", "ago"));
      head.appendChild(stats);
    }
    containerEl.appendChild(head);

    if (recentRuns.length === 0) {
      const empty = document.createElement("div");
      Object.assign(empty.style, {
        padding: "24px", textAlign: "center", color: "#9ca3af", fontSize: "13px",
        border: "1px dashed #2a2a2e", borderRadius: "8px", maxWidth: "760px",
      });
      const isCustom = String(workflow.id || "").startsWith("custom_") || workflow.kind === "custom";
      empty.textContent = isCustom
        ? "No runs yet. The engine picks this up on its next scheduled cycle."
        : "Presets don't run directly — clone this preset to start tracking activity.";
      containerEl.appendChild(empty);
      return;
    }

    // Run log — most recent first. Each row is a card showing status,
    // when, what got produced, and (on click) full error / stats.
    const log = document.createElement("div");
    log.style.maxWidth = "760px"; log.style.display = "flex"; log.style.flexDirection = "column"; log.style.gap = "6px";
    const sorted = [...recentRuns].sort((a, b) => {
      const ta = new Date(a.started_at || 0).getTime();
      const tb = new Date(b.started_at || 0).getTime();
      return tb - ta;
    });
    for (const r of sorted) log.appendChild(renderActivityRow(r, fetchRunOutputs));
    containerEl.appendChild(log);
  }

  function renderActivityRow(r, fetchRunOutputs) {
    const palette = {
      success: { bar: "#10b981", icon: "✓", color: "#a7f3d0" },
      error:   { bar: "#ef4444", icon: "✗", color: "#fca5a5" },
      skipped: { bar: "#94a3b8", icon: "·", color: "#cbd5e1" },
      running: { bar: "#3b82f6", icon: "▶", color: "#93c5fd" },
    };
    const p = palette[r.status] || palette.skipped;
    const when = _activityFmtSince(r.finished_at || r.started_at);

    const row = document.createElement("div");
    Object.assign(row.style, {
      background: "#161618", border: "1px solid #26262a",
      borderLeft: "3px solid " + p.bar, borderRadius: "8px",
      padding: "10px 12px", display: "flex", flexDirection: "column", gap: "4px",
      cursor: r.id ? "pointer" : "default",
      transition: "border-color 120ms",
    });

    // Title row: icon + status + when + counts
    const top = document.createElement("div");
    Object.assign(top.style, { display: "flex", alignItems: "center", gap: "10px", flexWrap: "wrap" });
    const iconEl = document.createElement("span");
    iconEl.textContent = p.icon;
    Object.assign(iconEl.style, { color: p.bar, fontSize: "14px", fontWeight: "600", width: "16px", textAlign: "center" });
    top.appendChild(iconEl);
    const statusEl = document.createElement("span");
    statusEl.textContent = (r.status || "—") + (r.triggered_by === "manual" ? " (manual)" : "");
    Object.assign(statusEl.style, { color: p.color, fontSize: "12px", fontWeight: "600", textTransform: "capitalize" });
    top.appendChild(statusEl);
    const whenEl = document.createElement("span");
    whenEl.textContent = `${when} ago`;
    Object.assign(whenEl.style, { color: "#9ca3af", fontSize: "11px", fontFamily: "ui-monospace, monospace" });
    top.appendChild(whenEl);
    if (Number.isFinite(Number(r.duration_ms))) {
      const dur = document.createElement("span");
      dur.textContent = `· ${(Number(r.duration_ms) / 1000).toFixed(1)}s`;
      Object.assign(dur.style, { color: "#6b7280", fontSize: "11px", fontFamily: "ui-monospace, monospace" });
      top.appendChild(dur);
    }
    // Counts spacer + numbers
    const spacer = document.createElement("span");
    spacer.style.flex = "1";
    top.appendChild(spacer);
    if (r.status === "success") {
      const kept = Number(r.items_kept) || 0;
      const proc = Number(r.items_processed) || 0;
      const counts = document.createElement("span");
      counts.textContent = proc > 0 ? `${kept} kept of ${proc} processed` : (kept > 0 ? `${kept} kept` : "nothing kept");
      Object.assign(counts.style, { color: "#86efac", fontSize: "11px", fontFamily: "ui-monospace, monospace", fontWeight: "500" });
      top.appendChild(counts);
    } else if (r.status === "skipped") {
      const reason = document.createElement("span");
      reason.textContent = "skipped";
      Object.assign(reason.style, { color: "#94a3b8", fontSize: "11px", fontFamily: "ui-monospace, monospace" });
      top.appendChild(reason);
    }
    row.appendChild(top);

    // Error preview (truncated; full text on hover via title attr)
    if (r.status === "error" && r.error) {
      const err = document.createElement("div");
      Object.assign(err.style, {
        marginTop: "4px", padding: "6px 10px",
        background: "rgba(239,68,68,0.06)", border: "1px solid rgba(239,68,68,0.25)",
        borderRadius: "5px",
        fontSize: "11px", color: "#fca5a5",
        fontFamily: "ui-monospace, monospace",
        whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
      });
      err.textContent = "⚠ " + String(r.error).split("\n")[0].slice(0, 200);
      err.title = String(r.error);
      row.appendChild(err);
    }

    // Expand affordance + drill-down panel. Click row → fetch produced
    // outputs (signals + outreach events) for this run; toggle visibility.
    // Skips fetch when r.id is missing (legacy runs from before v56).
    if (r.id) {
      const expandHint = document.createElement("div");
      Object.assign(expandHint.style, {
        fontSize: "10px", color: "#71717a",
        fontFamily: "ui-monospace, monospace", marginTop: "2px",
      });
      expandHint.textContent = "▸ click to see what this run produced";
      row.appendChild(expandHint);

      const panel = document.createElement("div");
      Object.assign(panel.style, {
        display: "none", marginTop: "8px",
        paddingTop: "8px", borderTop: "1px solid #26262a",
        flexDirection: "column", gap: "8px",
      });
      let loaded = false;
      row.appendChild(panel);

      row.onmouseover = () => { row.style.borderColor = "#3a3a3e"; row.style.borderLeftColor = p.bar; };
      row.onmouseout  = () => { row.style.borderColor = "#26262a"; row.style.borderLeftColor = p.bar; };
      row.onclick = async (ev) => {
        // Ignore clicks on inner buttons / links inside the panel
        if (ev.target.closest("a, button")) return;
        const wasOpen = panel.style.display !== "none";
        panel.style.display = wasOpen ? "none" : "flex";
        expandHint.textContent = wasOpen ? "▸ click to see what this run produced" : "▾ outputs";
        if (wasOpen || loaded) return;
        loaded = true;
        // Loading state
        const loading = document.createElement("div");
        Object.assign(loading.style, { fontSize: "11px", color: "#9ca3af" });
        loading.textContent = "Loading…";
        panel.appendChild(loading);
        try {
          if (typeof fetchRunOutputs !== "function") {
            throw new Error("fetchRunOutputs callback not provided");
          }
          const fresh = await fetchRunOutputs(r.id);
          panel.innerHTML = "";
          panel.appendChild(renderRunOutputsPanel(fresh));
        } catch (err) {
          panel.innerHTML = "";
          const errEl = document.createElement("div");
          Object.assign(errEl.style, { fontSize: "11px", color: "#fca5a5" });
          errEl.textContent = "Failed to load: " + (err?.message || "unknown");
          panel.appendChild(errEl);
        }
      };
    }

    return row;
  }

  // Renders the per-run outputs panel — what this run produced. Lists
  // signals + outreach events with concise per-row formatting.
  function renderRunOutputsPanel(data) {
    const wrap = document.createElement("div");
    Object.assign(wrap.style, { display: "flex", flexDirection: "column", gap: "8px" });

    const signals = data?.signals || [];
    const outreachEvents = data?.outreachEvents || [];

    if (signals.length === 0 && outreachEvents.length === 0) {
      const empty = document.createElement("div");
      Object.assign(empty.style, { fontSize: "11px", color: "#9ca3af", fontStyle: "italic" });
      empty.textContent = "This run didn't produce any tracked signals or outreach events.";
      wrap.appendChild(empty);
      return wrap;
    }

    if (signals.length > 0) {
      const head = document.createElement("div");
      Object.assign(head.style, {
        fontSize: "10px", color: "#9ca3af", textTransform: "uppercase",
        letterSpacing: "0.04em", fontWeight: "600", marginBottom: "2px",
      });
      head.textContent = `📍 Signals discovered (${signals.length})`;
      wrap.appendChild(head);
      for (const s of signals.slice(0, 20)) wrap.appendChild(renderSignalLine(s));
      if (signals.length > 20) {
        const more = document.createElement("div");
        Object.assign(more.style, { fontSize: "10px", color: "#71717a", marginTop: "2px" });
        more.textContent = `+${signals.length - 20} more · open Signals view to see all`;
        wrap.appendChild(more);
      }
    }

    if (outreachEvents.length > 0) {
      const head = document.createElement("div");
      Object.assign(head.style, {
        fontSize: "10px", color: "#9ca3af", textTransform: "uppercase",
        letterSpacing: "0.04em", fontWeight: "600", marginBottom: "2px", marginTop: "6px",
      });
      head.textContent = `✉ Outreach events (${outreachEvents.length})`;
      wrap.appendChild(head);
      for (const e of outreachEvents.slice(0, 20)) wrap.appendChild(renderOutreachLine(e));
      if (outreachEvents.length > 20) {
        const more = document.createElement("div");
        Object.assign(more.style, { fontSize: "10px", color: "#71717a", marginTop: "2px" });
        more.textContent = `+${outreachEvents.length - 20} more · open Outreach queue to see all`;
        wrap.appendChild(more);
      }
    }

    return wrap;
  }

  function renderSignalLine(s) {
    const platformEmoji = ({ twitter: "𝕏", linkedin: "💼", reddit: "🔻", hackernews: "🔶", telegram: "✈️" })[s.platform] || "·";
    const conf = Number(s.ai_confidence) || 0;
    const confColor = conf >= 0.85 ? "#10b981" : conf >= 0.7 ? "#fbbf24" : "#9ca3af";
    const row = document.createElement("div");
    Object.assign(row.style, {
      display: "flex", alignItems: "center", gap: "8px",
      padding: "5px 8px", borderRadius: "5px",
      background: "rgba(255,255,255,0.02)",
      fontSize: "11px",
    });
    const platEl = document.createElement("span");
    platEl.textContent = platformEmoji;
    Object.assign(platEl.style, { fontSize: "12px", width: "14px", textAlign: "center" });
    row.appendChild(platEl);
    const handleEl = document.createElement("span");
    Object.assign(handleEl.style, { color: "#cbd5e1", fontWeight: "500", fontFamily: "ui-monospace, monospace", whiteSpace: "nowrap" });
    handleEl.textContent = s.source_handle ? `@${s.source_handle}` : (s.source_company || "—");
    row.appendChild(handleEl);
    const textEl = document.createElement("span");
    Object.assign(textEl.style, {
      color: "#9ca3af", flex: "1", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
    });
    textEl.textContent = (s.source_text || "").slice(0, 120);
    textEl.title = s.source_text || "";
    row.appendChild(textEl);
    const confEl = document.createElement("span");
    Object.assign(confEl.style, { color: confColor, fontFamily: "ui-monospace, monospace", flexShrink: "0" });
    confEl.textContent = `${Math.round(conf * 100)}%`;
    row.appendChild(confEl);
    if (s.source_url) {
      const linkEl = document.createElement("a");
      linkEl.href = s.source_url;
      linkEl.target = "_blank";
      linkEl.textContent = "↗";
      Object.assign(linkEl.style, { color: "#a5b4fc", textDecoration: "none", marginLeft: "4px" });
      row.appendChild(linkEl);
    }
    return row;
  }

  function renderOutreachLine(e) {
    const platformEmoji = ({ twitter: "𝕏", linkedin: "💼", telegram: "✈️", email: "📧", smtp: "📧" })[e.platform] || "·";
    const eventColor = /reply/i.test(e.event_type) ? "#86efac"
                     : /accept/i.test(e.event_type) ? "#86efac"
                     : /fail|error/i.test(e.event_type) ? "#fca5a5"
                     : /skip/i.test(e.event_type) ? "#94a3b8"
                     : "#cbd5e1";
    const row = document.createElement("div");
    Object.assign(row.style, {
      display: "flex", alignItems: "center", gap: "8px",
      padding: "5px 8px", borderRadius: "5px",
      background: "rgba(255,255,255,0.02)",
      fontSize: "11px",
    });
    const platEl = document.createElement("span");
    platEl.textContent = platformEmoji;
    Object.assign(platEl.style, { fontSize: "12px", width: "14px", textAlign: "center" });
    row.appendChild(platEl);
    const evEl = document.createElement("span");
    Object.assign(evEl.style, {
      color: eventColor, fontWeight: "500",
      fontFamily: "ui-monospace, monospace",
      fontSize: "10px", textTransform: "uppercase", letterSpacing: "0.04em",
      flexShrink: "0", minWidth: "100px",
    });
    evEl.textContent = e.event_type || "—";
    row.appendChild(evEl);
    const targetEl = document.createElement("span");
    Object.assign(targetEl.style, { color: "#cbd5e1", flex: "1", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" });
    const targetName = e.contact_name || e.company_name || (e.detail || "").slice(0, 80) || "—";
    targetEl.textContent = targetName;
    targetEl.title = e.detail || targetName;
    row.appendChild(targetEl);
    return row;
  }

  function _activityFmtSince(ts) {
    if (!ts) return "—";
    const ms = Date.now() - new Date(ts).getTime();
    const sec = Math.max(0, Math.round(ms / 1000));
    if (sec < 60) return `${sec}s`;
    if (sec < 3600) return `${Math.round(sec / 60)}m`;
    const h = Math.floor(sec / 3600);
    if (h < 24) return `${h}h`;
    return `${Math.floor(h / 24)}d`;
  }

  // ── Main render ──────────────────────────────────────────────────────

  async function render(containerEl, workflow, registry, options = {}) {
    // No elkjs guard here any more: the DAG canvas this viewer once drew was
    // removed (see the story/activity split below), so nothing in this file
    // needs a layout engine. The guard was the only surviving reference to
    // window.ELK and it made a 1.5 MB vendored bundle look load-bearing.
    const registryByType = {};
    for (const n of (registry?.nodes || [])) registryByType[n.type] = n;

    containerEl.innerHTML = "";
    containerEl.style.position = "relative";
    containerEl.style.background = "#0f0f10";
    containerEl.style.overflow = "hidden";

    // Toolbar
    const toolbar = document.createElement("div");
    Object.assign(toolbar.style, {
      position: "absolute", top: "12px", left: "12px", zIndex: "3",
      display: "flex", gap: "0", background: "#1a1a1c",
      border: "1px solid #2a2a2e", borderRadius: "6px", overflow: "hidden",
      fontFamily: "ui-monospace, monospace", fontSize: "12px",
    });
    function makeToggle(label, active, onClick) {
      const b = document.createElement("button");
      b.textContent = label;
      Object.assign(b.style, {
        background: active ? "#3b82f6" : "transparent", color: active ? "#fff" : "#cbd5e1",
        border: "none", padding: "6px 12px", cursor: "pointer", fontFamily: "inherit", fontSize: "inherit",
      });
      b.onclick = onClick;
      return b;
    }
    // Mode replaces the legacy Story / Diagram toggle. The DAG canvas is
    // gone — users found it redundant with the story narrative + the
    // node parameter editor. The right-hand toggle now switches to
    // Activity (per-workflow run history showing what each cycle
    // actually produced).
    let mode = "story";
    const storyBtn = makeToggle("Story", true, () => setMode("story"));
    const activityBtn = makeToggle("Activity", false, () => setMode("activity"));
    toolbar.appendChild(storyBtn);
    toolbar.appendChild(activityBtn);
    containerEl.appendChild(toolbar);

    // Activity area (hidden initially — Story is the default landing view)
    const activityArea = document.createElement("div");
    Object.assign(activityArea.style, {
      position: "absolute", inset: "0", display: "none",
      overflow: "auto", padding: "60px 24px 24px",
    });
    containerEl.appendChild(activityArea);

    // Story area (default visible)
    const storyArea = document.createElement("div");
    Object.assign(storyArea.style, {
      position: "absolute", inset: "0", display: "block",
      overflow: "auto", padding: "60px 24px 24px",
    });
    containerEl.appendChild(storyArea);

    const inspector = buildInspector(containerEl, workflow, registryByType, options);

    // Map node id → workflow node + its registry def, so the story
    // click handler can open the inspector directly without going
    // through a diagram canvas.
    const nodeById = {};
    const defByNodeId = {};
    for (const n of (workflow.nodes || [])) {
      nodeById[n.id] = n;
      defByNodeId[n.id] = registryByType[n.type] || { label: n.type };
    }

    // Click a story step → open the inspector directly. The user
    // explicitly asked for this (was: switch-to-diagram-and-highlight).
    // Inspector overlays on whichever mode is active so they don't lose
    // context.
    function openInspectorForNode(nodeId) {
      const node = nodeById[nodeId];
      const def = defByNodeId[nodeId];
      if (!node) return;
      inspector.show(node, def, openInspectorForNode);
    }
    // Kept for backwards compat with internal callers below.
    const focusNode = openInspectorForNode;

    function setMode(next) {
      mode = next;
      activityBtn.style.background = mode === "activity" ? "#3b82f6" : "transparent";
      activityBtn.style.color = mode === "activity" ? "#fff" : "#cbd5e1";
      storyBtn.style.background = mode === "story" ? "#3b82f6" : "transparent";
      storyBtn.style.color = mode === "story" ? "#fff" : "#cbd5e1";
      if (mode === "activity") {
        activityArea.style.display = "block";
        storyArea.style.display = "none";
        renderActivity(activityArea, workflow, options);
      } else {
        activityArea.style.display = "none";
        storyArea.style.display = "block";
        renderStory(storyArea, workflow, registry, focusNode);
      }
    }

    // Diagram canvas removed (Phase: cockpit reframe). The DAG was
    // redundant with the story narrative — and clicking a story step
    // now opens the inspector directly. ELK + canvas drawing skipped
    // entirely; the toolbar's second button is "Activity" instead.

    // Story view is the landing mode — render it now so the initial paint
    // shows narrative cards, not an empty container.
    renderStory(storyArea, workflow, registry, focusNode);

    return {
      destroy() { containerEl.innerHTML = ""; },
      focus: focusNode,
    };
  }

  function escapeHtml(s) {
    return String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
  }

  window.WorkflowViewer = { render };
})();
