/**
 * SX Block Editor — a Ghost/Koenig-style block editor that stores sx source.
 *
 * Matches Koenig's UX: single hover + button on empty paragraphs, slash
 * commands, full card edit modes, inline format toolbar, keyboard shortcuts,
 * drag-drop uploads, oEmbed/bookmark metadata fetching.
 *
 * Usage:
 *   var handle = SxEditor.mount('sx-editor', {
 *     initialSx: '(<> (p "Hello") (h2 "Title"))',
 *     csrfToken: '...',
 *     uploadUrls: { image: '/upload/image', media: '/upload/media', file: '/upload/file' },
 *     oembedUrl: '/api/oembed',
 *     onChange: function(sx) { ... }
 *   });
 *   handle.getSx();   // current sx source
 *   handle.destroy();  // teardown
 */
(function () {
  "use strict";

  // =========================================================================
  // Constants
  // =========================================================================

  var TEXT_TAGS = { p: true, h2: true, h3: true, blockquote: true };
  var LIST_TAGS = { ul: true, ol: true };
  var INLINE_MAP = {
    bold: "strong", italic: "em", strikethrough: "s", underline: "u", code: "code"
  };

  // Card menu items grouped like Koenig
  var CARD_MENU = [
    { section: "Primary", items: [
      { type: "image",    icon: "fa-regular fa-image",       label: "Image",   desc: "Upload, or embed with /image [url]", slash: ["image", "img"] },
      { type: "gallery",  icon: "fa-regular fa-images",      label: "Gallery", desc: "Create an image gallery", slash: ["gallery"] },
      { type: "video",    icon: "fa-solid fa-video",         label: "Video",   desc: "Upload and play a video", slash: ["video"] },
      { type: "audio",    icon: "fa-solid fa-headphones",    label: "Audio",   desc: "Upload and play audio", slash: ["audio"] },
      { type: "file",     icon: "fa-regular fa-file",        label: "File",    desc: "Upload a downloadable file", slash: ["file"] },
    ]},
    { section: "Text", items: [
      { type: "html",     icon: "fa-solid fa-code",          label: "HTML",    desc: "Insert raw HTML", slash: ["html"] },
      { type: "markdown", icon: "fa-brands fa-markdown",     label: "Markdown",desc: "Insert markdown content", slash: ["markdown", "md"] },
      { type: "hr",       icon: "fa-solid fa-minus",         label: "Divider", desc: "Insert a dividing line", slash: ["divider", "hr"] },
      { type: "callout",  icon: "fa-regular fa-comment-dots",label: "Callout", desc: "Info box that stands out", slash: ["callout"] },
      { type: "toggle",   icon: "fa-solid fa-caret-down",    label: "Toggle",  desc: "Collapsible content", slash: ["toggle"] },
      { type: "code",     icon: "fa-solid fa-terminal",      label: "Code",    desc: "Insert a code block", slash: ["code", "codeblock"] },
    ]},
    { section: "Embed", items: [
      { type: "youtube",  icon: "fa-brands fa-youtube",      label: "YouTube", desc: "Embed a YouTube video", slash: ["youtube", "yt"] },
      { type: "twitter",  icon: "fa-brands fa-x-twitter",    label: "X (Twitter)", desc: "Embed a tweet", slash: ["twitter", "tweet", "x"] },
      { type: "vimeo",    icon: "fa-brands fa-vimeo-v",      label: "Vimeo",   desc: "Embed a Vimeo video", slash: ["vimeo"] },
      { type: "spotify",  icon: "fa-brands fa-spotify",      label: "Spotify", desc: "Embed a Spotify track or playlist", slash: ["spotify"] },
      { type: "codepen",  icon: "fa-brands fa-codepen",      label: "CodePen", desc: "Embed a CodePen", slash: ["codepen"] },
      { type: "bookmark", icon: "fa-solid fa-bookmark",      label: "Bookmark",desc: "Embed a link as a visual bookmark", slash: ["bookmark"] },
      { type: "embed",    icon: "fa-solid fa-link",          label: "Other...",desc: "Embed any URL via oEmbed", slash: ["embed", "oembed"] },
      { type: "button",   icon: "fa-solid fa-square",        label: "Button",  desc: "Add a button", slash: ["button", "cta"] },
    ]},
  ];

  // Build flat list for slash matching
  var ALL_CARD_ITEMS = [];
  for (var s = 0; s < CARD_MENU.length; s++) {
    for (var j = 0; j < CARD_MENU[s].items.length; j++) {
      ALL_CARD_ITEMS.push(CARD_MENU[s].items[j]);
    }
  }

  // =========================================================================
  // Helpers
  // =========================================================================

  function el(tag, attrs, children) {
    var node = document.createElement(tag);
    if (attrs) {
      for (var k in attrs) {
        if (k === "className") node.className = attrs[k];
        else if (k.indexOf("on") === 0) node.addEventListener(k.slice(2).toLowerCase(), attrs[k]);
        else node.setAttribute(k, attrs[k]);
      }
    }
    if (children != null) {
      if (typeof children === "string") node.textContent = children;
      else if (Array.isArray(children)) {
        for (var i = 0; i < children.length; i++) {
          if (children[i] != null) node.appendChild(
            typeof children[i] === "string" ? document.createTextNode(children[i]) : children[i]
          );
        }
      } else {
        node.appendChild(typeof children === "string" ? document.createTextNode(children) : children);
      }
    }
    return node;
  }

  function escSx(s) {
    return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
  }

  function escHtml(s) {
    var d = document.createElement("div");
    d.textContent = s;
    return d.innerHTML;
  }

  // Void elements that have no closing tag
  var VOID_ELEMENTS = {
    area:1, base:1, br:1, col:1, embed:1, hr:1, img:1, input:1,
    link:1, meta:1, param:1, source:1, track:1, wbr:1
  };
  // Boolean HTML attributes
  var BOOLEAN_ATTRS = {
    async:1, autofocus:1, autoplay:1, checked:1, controls:1,
    default:1, defer:1, disabled:1, formnovalidate:1, hidden:1,
    inert:1, ismap:1, loop:1, multiple:1, muted:1, nomodule:1,
    novalidate:1, open:1, playsinline:1, readonly:1, required:1,
    reversed:1, selected:1
  };

  /**
   * Convert an HTML string to sx source using the browser's DOM parser.
   */
  function htmlToSx(html) {
    if (!html || !html.trim()) return '""';
    var doc = new DOMParser().parseFromString("<body>" + html + "</body>", "text/html");
    var body = doc.body;
    // Collect non-whitespace-only root nodes
    var roots = [];
    for (var i = 0; i < body.childNodes.length; i++) {
      var n = body.childNodes[i];
      if (n.nodeType === 3 && !n.textContent.trim()) continue; // skip ws-only text at root
      roots.push(n);
    }
    if (!roots.length) return '""';
    if (roots.length === 1) return nodeToSx(roots[0]);
    var parts = [];
    for (var i = 0; i < roots.length; i++) parts.push(nodeToSx(roots[i]));
    return "(<> " + parts.join(" ") + ")";
  }

  function nodeToSx(node) {
    if (node.nodeType === 3) {
      return '"' + escSx(node.textContent) + '"';
    }
    if (node.nodeType === 8) return ""; // comment
    if (node.nodeType !== 1) return "";
    var tag = node.tagName.toLowerCase();
    var parts = [tag];
    // Attributes
    for (var i = 0; i < node.attributes.length; i++) {
      var a = node.attributes[i];
      if (BOOLEAN_ATTRS[a.name]) {
        parts.push(":" + a.name + " true");
      } else {
        parts.push(':' + a.name + ' "' + escSx(a.value) + '"');
      }
    }
    if (VOID_ELEMENTS[tag]) return "(" + parts.join(" ") + ")";
    // Children
    var children = [];
    for (var i = 0; i < node.childNodes.length; i++) {
      var s = nodeToSx(node.childNodes[i]);
      if (s) children.push(s);
    }
    if (children.length) return "(" + parts.join(" ") + " " + children.join(" ") + ")";
    return "(" + parts.join(" ") + ")";
  }

  /**
   * Render sx children expressions back to an HTML string (for the HTML card textarea).
   */
  function sxChildrenToHtml(childrenSx) {
    if (!childrenSx || !childrenSx.trim()) return "";
    try {
      var rendered = Sx.render(childrenSx);
      if (rendered instanceof Node) {
        var div = document.createElement("div");
        div.appendChild(rendered);
        return div.innerHTML;
      }
      return String(rendered);
    } catch (e) {
      return childrenSx;
    }
  }

  /**
   * Serialize a parsed sx expression (from Sx.parse) back to sx source string.
   * Used to capture children of ~kg-html/~kg-md cards from the parsed tree.
   */
  function serializeExpr(expr) {
    if (typeof expr === "string") return '"' + escSx(expr) + '"';
    if (expr && expr.constructor === Sx.Keyword) return ":" + expr.name;
    if (expr && expr.name !== undefined && expr.constructor !== Sx.Keyword) return expr.name;
    if (Array.isArray(expr)) {
      var parts = [];
      for (var i = 0; i < expr.length; i++) parts.push(serializeExpr(expr[i]));
      return "(" + parts.join(" ") + ")";
    }
    if (expr === true) return "true";
    if (expr === false) return "false";
    if (expr === null || expr === undefined) return "nil";
    return String(expr);
  }

  function closestBlock(node, container) {
    while (node && node !== container) {
      if (node.hasAttribute && node.hasAttribute("data-sx-block")) return node;
      node = node.parentNode;
    }
    return null;
  }

  function getBlockIndex(container, block) {
    var blocks = container.querySelectorAll("[data-sx-block]");
    for (var i = 0; i < blocks.length; i++) {
      if (blocks[i] === block) return i;
    }
    return -1;
  }

  function focusEnd(editable) {
    if (!editable) return;
    editable.focus();
    var range = document.createRange();
    range.selectNodeContents(editable);
    range.collapse(false);
    var sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
  }

  function formatFileSize(bytes) {
    if (!bytes) return "";
    var kb = bytes / 1024;
    if (kb < 1024) return Math.round(kb) + " KB";
    return (kb / 1024).toFixed(1) + " MB";
  }

  function formatDuration(seconds) {
    if (!seconds) return "0:00";
    var m = Math.floor(seconds / 60);
    var s = Math.floor(seconds % 60);
    return m + ":" + (s < 10 ? "0" : "") + s;
  }

  // =========================================================================
  // DOM → SX serialization
  // =========================================================================

  function serializeInline(node) {
    var parts = [];
    for (var i = 0; i < node.childNodes.length; i++) {
      var child = node.childNodes[i];
      if (child.nodeType === 3) {
        var text = child.textContent;
        if (text) parts.push('"' + escSx(text) + '"');
      } else if (child.nodeType === 1) {
        var tag = child.tagName.toLowerCase();
        if (tag === "br") {
          parts.push('"\\n"');
        } else if (tag === "a") {
          var href = child.getAttribute("href") || "";
          var inner = serializeInline(child);
          parts.push('(a :href "' + escSx(href) + '" ' + inner + ')');
        } else if (tag === "strong" || tag === "b") {
          parts.push("(strong " + serializeInline(child) + ")");
        } else if (tag === "em" || tag === "i") {
          parts.push("(em " + serializeInline(child) + ")");
        } else if (tag === "s" || tag === "strike" || tag === "del") {
          parts.push("(s " + serializeInline(child) + ")");
        } else if (tag === "u") {
          parts.push("(u " + serializeInline(child) + ")");
        } else if (tag === "code") {
          parts.push("(code " + serializeInline(child) + ")");
        } else if (tag === "sub") {
          parts.push("(sub " + serializeInline(child) + ")");
        } else if (tag === "sup") {
          parts.push("(sup " + serializeInline(child) + ")");
        } else if (tag === "span") {
          parts.push(serializeInline(child));
        } else {
          parts.push('"' + escSx(child.textContent || "") + '"');
        }
      }
    }
    return parts.join(" ") || '""';
  }

  function serializeList(blockEl) {
    var tag = blockEl.getAttribute("data-sx-tag");
    var lis = blockEl.querySelectorAll("[data-sx-li]");
    var items = [];
    for (var i = 0; i < lis.length; i++) {
      items.push("(li " + serializeInline(lis[i]) + ")");
    }
    return "(" + tag + " " + items.join(" ") + ")";
  }

  function serializeBlocks(container) {
    var blocks = container.querySelectorAll("[data-sx-block]");
    var parts = [];
    for (var i = 0; i < blocks.length; i++) {
      var block = blocks[i];
      var tag = block.getAttribute("data-sx-tag");

      if (block.hasAttribute("data-sx-card")) {
        parts.push(serializeCard(block));
      } else if (tag === "hr") {
        parts.push("(hr)");
      } else if (tag === "pre") {
        var codeEl = block.querySelector("textarea, code");
        var code = codeEl ? (codeEl.value !== undefined ? codeEl.value : codeEl.textContent) : "";
        var lang = block.getAttribute("data-sx-lang") || "";
        var langAttr = lang ? ' :class "language-' + escSx(lang) + '"' : "";
        parts.push('(pre (code' + langAttr + ' "' + escSx(code) + '"))');
      } else if (LIST_TAGS[tag]) {
        parts.push(serializeList(block));
      } else if (TEXT_TAGS[tag]) {
        var editable = block.querySelector("[contenteditable]");
        if (editable) {
          parts.push("(" + tag + " " + serializeInline(editable) + ")");
        }
      }
    }
    if (parts.length === 0) return '(<> (p ""))';
    if (parts.length === 1) return parts[0];
    return "(<>\n  " + parts.join("\n  ") + ")";
  }

  // Cards whose children are positional sx args (not kwargs)
  var CHILDREN_CARDS = { "kg-html": true, "kg-md": true };

  function serializeCard(block) {
    var cardType = block.getAttribute("data-sx-card");
    var attrsJson = block.getAttribute("data-sx-attrs") || "{}";
    var attrs;
    try { attrs = JSON.parse(attrsJson); } catch (e) { attrs = {}; }

    // Serialize caption as inline sx from the contenteditable
    var captionEl = block.querySelector("[data-sx-caption]");
    if (captionEl) {
      var captionHtml = captionEl.innerHTML.trim();
      if (captionHtml && captionEl.textContent.trim()) {
        attrs.caption = serializeInline(captionEl);
      } else {
        delete attrs.caption;
      }
    }

    var parts = ["(~" + cardType];
    for (var k in attrs) {
      if (attrs[k] === null || attrs[k] === undefined || attrs[k] === false) continue;
      if (k === "caption") {
        // Caption is already an sx expression string
        parts.push(":caption " + attrs[k]);
        continue;
      }
      if (k === "_childrenSx") continue; // handled below
      if (attrs[k] === true) {
        parts.push(":" + k + " true");
      } else {
        parts.push(':' + k + ' "' + escSx(String(attrs[k])) + '"');
      }
    }

    // Append children for cards like ~kg-html, ~kg-md
    if (CHILDREN_CARDS[cardType] && attrs._childrenSx) {
      parts.push(attrs._childrenSx);
    }

    parts.push(")");
    return parts.join(" ");
  }

  // =========================================================================
  // SX → DOM deserialization
  // =========================================================================

  function deserializeSx(source) {
    if (!source || !source.trim()) return [];
    var expr;
    try { expr = Sx.parse(source); } catch (e) {
      console.error("sx-editor: parse error", e);
      return [];
    }

    var blocks = [];
    if (Array.isArray(expr) && expr[0] && expr[0].name === "<>") {
      for (var i = 1; i < expr.length; i++) {
        var b = exprToBlock(expr[i]);
        if (b) blocks.push(b);
      }
    } else {
      var b = exprToBlock(expr);
      if (b) blocks.push(b);
    }
    return blocks;
  }

  function exprToBlock(expr) {
    if (!Array.isArray(expr) || !expr[0]) return null;
    var head = expr[0];
    var tag = head.name || (typeof head === "string" ? head : "");

    if (TEXT_TAGS[tag]) {
      return createTextBlock(tag, renderInlineToHtml(expr.slice(1)));
    }
    if (LIST_TAGS[tag]) {
      return createListBlock(tag, expr.slice(1));
    }
    if (tag === "hr") return createHrBlock();
    if (tag === "pre") {
      var codeExpr = expr[1];
      var code = "", lang = "";
      if (Array.isArray(codeExpr) && codeExpr[0] && codeExpr[0].name === "code") {
        for (var i = 1; i < codeExpr.length; i++) {
          if (codeExpr[i] && codeExpr[i].name === "class") {
            var cls = codeExpr[i + 1] || "";
            var m = cls.match(/language-(\w+)/);
            if (m) lang = m[1];
            i++;
          } else if (typeof codeExpr[i] === "string") {
            code = codeExpr[i];
          }
        }
      }
      return createCodeBlock(code, lang);
    }
    if (tag.charAt(0) === "~") {
      var cardType = tag.slice(1);
      var args = expr.slice(1);
      var attrs = extractKwargs(args);
      // For children-based cards, capture positional children as sx source
      if (CHILDREN_CARDS[cardType]) {
        var children = extractChildren(args);
        if (children.length) {
          var childParts = [];
          for (var ci = 0; ci < children.length; ci++) childParts.push(serializeExpr(children[ci]));
          attrs._childrenSx = childParts.join(" ");
        }
      }
      // Convert caption from parsed sx expression back to sx source string
      if (attrs.caption !== undefined && attrs.caption !== null) {
        attrs.caption = serializeExpr(attrs.caption);
      }
      return createCardBlock(cardType, attrs);
    }
    return null;
  }

  function renderInlineToHtml(exprs) {
    var html = "";
    for (var i = 0; i < exprs.length; i++) {
      var e = exprs[i];
      if (typeof e === "string") {
        html += escHtml(e);
      } else if (Array.isArray(e) && e[0]) {
        var t = e[0].name || "";
        if (t === "a") {
          var kw = extractKwargs(e.slice(1));
          var rest = extractChildren(e.slice(1));
          html += '<a href="' + escHtml(kw.href || "") + '">' + renderInlineToHtml(rest) + "</a>";
        } else if (t === "strong" || t === "em" || t === "s" || t === "u" || t === "code" || t === "sub" || t === "sup") {
          html += "<" + t + ">" + renderInlineToHtml(e.slice(1)) + "</" + t + ">";
        } else {
          html += escHtml(sxToString(e));
        }
      }
    }
    return html;
  }

  function extractKwargs(args) {
    var kw = {};
    for (var i = 0; i < args.length; i++) {
      if (args[i] && typeof args[i] === "object" && args[i].name && args[i].constructor &&
          args[i].constructor === Sx.Keyword) {
        kw[args[i].name] = args[i + 1];
        i++;
      }
    }
    return kw;
  }

  function extractChildren(args) {
    var children = [];
    for (var i = 0; i < args.length; i++) {
      if (args[i] && typeof args[i] === "object" && args[i].name && args[i].constructor &&
          args[i].constructor === Sx.Keyword) {
        i++;
      } else {
        children.push(args[i]);
      }
    }
    return children;
  }

  function sxToString(expr) {
    if (typeof expr === "string") return expr;
    if (Array.isArray(expr)) return expr.map(sxToString).join("");
    return String(expr);
  }

  // =========================================================================
  // Block creation
  // =========================================================================

  function createTextBlock(tag, htmlContent) {
    var wrapper = el("div", {
      className: "sx-block sx-block-text",
      "data-sx-block": "true",
      "data-sx-tag": tag
    });

    var editable = el("div", {
      contenteditable: "true",
      className: "sx-block-content sx-editable" + (tag === "h2" || tag === "h3" ? " sx-heading" : "") +
                 (tag === "blockquote" ? " sx-quote" : ""),
      "data-placeholder": tag === "p" ? "Type / for commands..." :
                          tag === "blockquote" ? "Type a quote..." :
                          "Type a heading...",
      "data-block-type": tag
    });
    editable.innerHTML = htmlContent || "";

    wrapper.appendChild(editable);
    return wrapper;
  }

  function createListBlock(tag, items) {
    var wrapper = el("div", {
      className: "sx-block sx-block-list",
      "data-sx-block": "true",
      "data-sx-tag": tag
    });

    var listEl = el("div", {
      className: "sx-block-content sx-list-content",
      "data-list-type": tag
    });

    if (items && items.length) {
      for (var i = 0; i < items.length; i++) {
        var item = items[i];
        if (Array.isArray(item) && item[0] && item[0].name === "li") {
          var li = el("div", {
            contenteditable: "true",
            className: "sx-list-item sx-editable",
            "data-sx-li": "true",
            "data-placeholder": "List item..."
          });
          li.innerHTML = renderInlineToHtml(item.slice(1));
          listEl.appendChild(li);
        }
      }
    }
    if (!listEl.children.length) {
      var li = el("div", {
        contenteditable: "true",
        className: "sx-list-item sx-editable",
        "data-sx-li": "true",
        "data-placeholder": "List item..."
      });
      listEl.appendChild(li);
    }

    wrapper.appendChild(listEl);
    return wrapper;
  }

  function createHrBlock() {
    var wrapper = el("div", {
      className: "sx-block sx-block-hr",
      "data-sx-block": "true",
      "data-sx-tag": "hr"
    });
    wrapper.appendChild(el("hr", { className: "sx-hr" }));
    return wrapper;
  }

  function createCodeBlock(code, lang) {
    var wrapper = el("div", {
      className: "sx-block sx-block-code",
      "data-sx-block": "true",
      "data-sx-tag": "pre",
      "data-sx-lang": lang || ""
    });

    var header = el("div", { className: "sx-code-header" });
    var langInput = el("input", {
      type: "text",
      className: "sx-code-lang",
      placeholder: "Language",
      value: lang || ""
    });
    langInput.addEventListener("input", function () {
      wrapper.setAttribute("data-sx-lang", langInput.value);
    });
    header.appendChild(langInput);

    var textarea = el("textarea", {
      className: "sx-code-textarea",
      spellcheck: "false",
      placeholder: "Paste or type code..."
    }, code || "");

    function autoResize() {
      textarea.style.height = "auto";
      textarea.style.height = textarea.scrollHeight + "px";
    }
    textarea.addEventListener("input", autoResize);
    setTimeout(autoResize, 0);

    wrapper.appendChild(header);
    wrapper.appendChild(textarea);
    return wrapper;
  }

  // =========================================================================
  // Card blocks with edit/preview modes
  // =========================================================================

  /**
   * Create a card block. Cards have two modes:
   * - Preview mode: rendered ~kg-* component output
   * - Edit mode: card-specific editing UI
   *
   * Click to enter edit mode, click outside to return to preview.
   */
  function createCardBlock(cardType, attrs) {
    attrs = attrs || {};
    var wrapper = el("div", {
      className: "sx-block sx-block-card",
      "data-sx-block": "true",
      "data-sx-tag": "card",
      "data-sx-card": cardType,
      "data-sx-attrs": JSON.stringify(attrs)
    });

    var preview = el("div", { className: "sx-card-preview" });
    var editPanel = el("div", { className: "sx-card-edit", style: "display:none" });

    // Render preview
    renderCardPreview(cardType, attrs, preview);

    // Build edit UI based on card type
    buildCardEditUI(cardType, attrs, editPanel, wrapper, preview);

    // Card toolbar (delete, width controls)
    var toolbar = el("div", { className: "sx-card-toolbar" });
    var deleteBtn = el("button", {
      type: "button", className: "sx-card-tool-btn", title: "Delete card"
    });
    deleteBtn.innerHTML = '<i class="fa-solid fa-trash-can"></i>';
    toolbar.appendChild(deleteBtn);

    wrapper.appendChild(toolbar);
    wrapper.appendChild(preview);
    wrapper.appendChild(editPanel);

    // Click preview → enter edit mode
    preview.addEventListener("click", function (e) {
      if (e.target.closest("[data-sx-caption]")) return; // caption is always editable
      wrapper.classList.add("sx-card-editing");
      preview.style.display = "none";
      editPanel.style.display = "block";
      // Focus first input in edit panel
      var firstInput = editPanel.querySelector("input, textarea, [contenteditable]");
      if (firstInput) firstInput.focus();
    });

    // Add caption for applicable card types
    var captionTypes = {
      "kg-image": true, "kg-gallery": true, "kg-embed": true,
      "kg-bookmark": true, "kg-video": true, "kg-file": true
    };
    if (captionTypes[cardType]) {
      var captionEl = el("div", {
        contenteditable: "true",
        className: "sx-card-caption sx-editable",
        "data-sx-caption": "true",
        "data-placeholder": "Type caption for image (optional)"
      });
      if (attrs.caption) {
        // Caption is an sx expression string — render to HTML for the contenteditable
        captionEl.innerHTML = sxChildrenToHtml(attrs.caption);
      }
      wrapper.appendChild(captionEl);
    }

    return wrapper;
  }

  function renderCardPreview(cardType, attrs, container) {
    container.innerHTML = "";
    try {
      var sxSource = buildCardSx(cardType, attrs);
      var dom = Sx.render(sxSource);
      container.appendChild(dom);
    } catch (e) {
      container.innerHTML = '<div class="sx-card-fallback">[' + escHtml(cardType) + ' card]</div>';
    }
  }

  function buildCardSx(cardType, attrs) {
    var parts = ["(~" + cardType];
    for (var k in attrs) {
      if (attrs[k] === null || attrs[k] === undefined || attrs[k] === false || attrs[k] === "") continue;
      if (k === "caption") {
        // Caption is an sx expression, not a quoted string
        parts.push(":caption " + attrs[k]);
        continue;
      }
      if (k === "_childrenSx") continue; // handled below
      if (attrs[k] === true) {
        parts.push(":" + k + " true");
      } else {
        parts.push(':' + k + ' "' + escSx(String(attrs[k])) + '"');
      }
    }
    // Append children for ~kg-html, ~kg-md
    if (CHILDREN_CARDS[cardType] && attrs._childrenSx) {
      parts.push(attrs._childrenSx);
    }
    parts.push(")");
    return parts.join(" ");
  }

  function updateCardAttrs(wrapper, newAttrs) {
    wrapper.setAttribute("data-sx-attrs", JSON.stringify(newAttrs));
  }

  function exitCardEdit(wrapper) {
    var preview = wrapper.querySelector(".sx-card-preview");
    var editPanel = wrapper.querySelector(".sx-card-edit");
    var attrs;
    try { attrs = JSON.parse(wrapper.getAttribute("data-sx-attrs") || "{}"); } catch (e) { attrs = {}; }
    var cardType = wrapper.getAttribute("data-sx-card");

    wrapper.classList.remove("sx-card-editing");
    renderCardPreview(cardType, attrs, preview);
    preview.style.display = "block";
    editPanel.style.display = "none";
  }

  // =========================================================================
  // Card edit UIs
  // =========================================================================

  function buildCardEditUI(cardType, attrs, editPanel, wrapper, previewEl) {
    switch (cardType) {
      case "kg-image":   buildImageEditUI(attrs, editPanel, wrapper); break;
      case "kg-gallery":  buildGalleryEditUI(attrs, editPanel, wrapper); break;
      case "kg-html":    buildHtmlEditUI(attrs, editPanel, wrapper); break;
      case "kg-md":      buildMarkdownEditUI(attrs, editPanel, wrapper); break;
      case "kg-embed":   buildEmbedEditUI(attrs, editPanel, wrapper); break;
      case "kg-bookmark": buildBookmarkEditUI(attrs, editPanel, wrapper); break;
      case "kg-callout": buildCalloutEditUI(attrs, editPanel, wrapper); break;
      case "kg-toggle":  buildToggleEditUI(attrs, editPanel, wrapper); break;
      case "kg-button":  buildButtonEditUI(attrs, editPanel, wrapper); break;
      case "kg-audio":   buildAudioEditUI(attrs, editPanel, wrapper); break;
      case "kg-video":   buildVideoEditUI(attrs, editPanel, wrapper); break;
      case "kg-file":    buildFileEditUI(attrs, editPanel, wrapper); break;
      default:           buildGenericEditUI(attrs, editPanel, wrapper); break;
    }
  }

  // -- Image card edit UI --
  function buildImageEditUI(attrs, panel, wrapper) {
    if (attrs.src) {
      // Has image — show it with controls
      var img = el("img", { src: attrs.src, className: "sx-edit-img-preview" });
      panel.appendChild(img);

      var controls = el("div", { className: "sx-edit-controls" });

      var altRow = makeInputRow("Alt text", attrs.alt || "", function (v) {
        attrs.alt = v; updateCardAttrs(wrapper, attrs);
      });
      controls.appendChild(altRow);

      var widthRow = makeSelectRow("Width", attrs.width || "", [
        { value: "", label: "Normal" },
        { value: "wide", label: "Wide" },
        { value: "full", label: "Full" },
      ], function (v) {
        attrs.width = v; updateCardAttrs(wrapper, attrs);
      });
      controls.appendChild(widthRow);

      var hrefRow = makeInputRow("Link URL", attrs.href || "", function (v) {
        attrs.href = v; updateCardAttrs(wrapper, attrs);
      });
      controls.appendChild(hrefRow);

      var replaceBtn = el("button", {
        type: "button", className: "sx-edit-btn sx-edit-btn-sm"
      }, "Replace image");
      replaceBtn.addEventListener("click", function () {
        triggerFileUpload(wrapper, "image", function (url) {
          attrs.src = url;
          updateCardAttrs(wrapper, attrs);
          img.src = url;
        });
      });
      controls.appendChild(replaceBtn);

      panel.appendChild(controls);
    } else {
      // No image yet — show upload area
      buildUploadArea(panel, wrapper, "image", "Drop image here or click to upload", function (url) {
        attrs.src = url;
        updateCardAttrs(wrapper, attrs);
        panel.innerHTML = "";
        buildImageEditUI(attrs, panel, wrapper);
      });
    }
  }

  // -- Gallery card edit UI --
  function buildGalleryEditUI(attrs, panel, wrapper) {
    var images = attrs.images || [];
    panel.innerHTML = "";

    if (images.length) {
      var grid = el("div", { className: "sx-edit-gallery-grid" });
      // We store images as a stringified JSON in the `images` attr for now
      var imgList;
      try { imgList = typeof images === "string" ? JSON.parse(images) : images; } catch (e) { imgList = []; }

      for (var i = 0; i < imgList.length; i++) {
        (function (idx) {
          var thumb = el("div", { className: "sx-edit-gallery-thumb" });
          var img = el("img", { src: imgList[idx].src || imgList[idx] });
          var removeBtn = el("button", {
            type: "button", className: "sx-edit-gallery-remove", title: "Remove"
          });
          removeBtn.innerHTML = "&times;";
          removeBtn.addEventListener("click", function () {
            imgList.splice(idx, 1);
            attrs.images = imgList;
            updateCardAttrs(wrapper, attrs);
            buildGalleryEditUI(attrs, panel, wrapper);
          });
          thumb.appendChild(img);
          thumb.appendChild(removeBtn);
          grid.appendChild(thumb);
        })(i);
      }
      panel.appendChild(grid);
    }

    var addBtn = el("button", {
      type: "button", className: "sx-edit-btn"
    }, "+ Add images");
    addBtn.addEventListener("click", function () {
      triggerFileUpload(wrapper, "image", function (url) {
        var imgList;
        try { imgList = typeof attrs.images === "string" ? JSON.parse(attrs.images) : (attrs.images || []); } catch (e) { imgList = []; }
        imgList.push({ src: url, alt: "" });
        attrs.images = imgList;
        updateCardAttrs(wrapper, attrs);
        buildGalleryEditUI(attrs, panel, wrapper);
      }, true);
    });
    panel.appendChild(addBtn);
  }

  // -- HTML card edit UI --
  function buildHtmlEditUI(attrs, panel, wrapper) {
    // Show HTML in textarea; convert HTML↔sx children for storage
    var currentHtml = attrs._childrenSx ? sxChildrenToHtml(attrs._childrenSx) : "";
    var textarea = el("textarea", {
      className: "sx-edit-html-textarea",
      placeholder: "Paste HTML here...",
      spellcheck: "false"
    }, currentHtml);

    function autoResize() {
      textarea.style.height = "auto";
      textarea.style.height = Math.max(120, textarea.scrollHeight) + "px";
    }

    textarea.addEventListener("input", function () {
      attrs._childrenSx = htmlToSx(textarea.value);
      updateCardAttrs(wrapper, attrs);
      autoResize();
    });
    setTimeout(autoResize, 0);

    panel.appendChild(textarea);
  }

  // -- Markdown card edit UI (read-only rendered view, edit as HTML) --
  function buildMarkdownEditUI(attrs, panel, wrapper) {
    var currentHtml = attrs._childrenSx ? sxChildrenToHtml(attrs._childrenSx) : "";
    var textarea = el("textarea", {
      className: "sx-edit-html-textarea",
      placeholder: "Markdown content (as HTML)...",
      spellcheck: "false"
    }, currentHtml);

    function autoResize() {
      textarea.style.height = "auto";
      textarea.style.height = Math.max(120, textarea.scrollHeight) + "px";
    }

    textarea.addEventListener("input", function () {
      attrs._childrenSx = htmlToSx(textarea.value);
      updateCardAttrs(wrapper, attrs);
      autoResize();
    });
    setTimeout(autoResize, 0);

    panel.appendChild(textarea);
  }

  // -- Embed card edit UI --
  function buildEmbedEditUI(attrs, panel, wrapper) {
    if (attrs.html) {
      // Already have embed HTML
      var previewDiv = el("div", { className: "sx-edit-embed-preview" });
      previewDiv.innerHTML = attrs.html;
      panel.appendChild(previewDiv);

      if (attrs.url) {
        var urlDisplay = el("div", { className: "sx-edit-url-display" }, attrs.url);
        panel.appendChild(urlDisplay);
      }

      var reloadBtn = el("button", {
        type: "button", className: "sx-edit-btn sx-edit-btn-sm"
      }, "Change URL");
      reloadBtn.addEventListener("click", function () {
        attrs.html = "";
        attrs.url = "";
        updateCardAttrs(wrapper, attrs);
        panel.innerHTML = "";
        buildEmbedEditUI(attrs, panel, wrapper);
      });
      panel.appendChild(reloadBtn);
    } else {
      // URL input for oEmbed lookup
      var urlInput = el("input", {
        type: "url",
        className: "sx-edit-input sx-edit-url-input",
        placeholder: "Paste URL to embed...",
        value: attrs.url || ""
      });

      var status = el("div", { className: "sx-edit-status" });

      urlInput.addEventListener("keydown", function (e) {
        if (e.key === "Enter") {
          e.preventDefault();
          fetchOembed(wrapper, urlInput.value, attrs, panel, status);
        }
      });

      var fetchBtn = el("button", {
        type: "button", className: "sx-edit-btn"
      }, "Embed");
      fetchBtn.addEventListener("click", function () {
        fetchOembed(wrapper, urlInput.value, attrs, panel, status);
      });

      panel.appendChild(urlInput);
      panel.appendChild(fetchBtn);
      panel.appendChild(status);
    }
  }

  // -- Bookmark card edit UI --
  function buildBookmarkEditUI(attrs, panel, wrapper) {
    if (attrs.url && attrs.title) {
      // Already fetched — show editable fields
      var controls = el("div", { className: "sx-edit-controls" });

      controls.appendChild(makeInputRow("URL", attrs.url || "", function (v) {
        attrs.url = v; updateCardAttrs(wrapper, attrs);
      }));
      controls.appendChild(makeInputRow("Title", attrs.title || "", function (v) {
        attrs.title = v; updateCardAttrs(wrapper, attrs);
      }));
      controls.appendChild(makeInputRow("Description", attrs.description || "", function (v) {
        attrs.description = v; updateCardAttrs(wrapper, attrs);
      }));
      controls.appendChild(makeInputRow("Author", attrs.author || "", function (v) {
        attrs.author = v; updateCardAttrs(wrapper, attrs);
      }));
      controls.appendChild(makeInputRow("Publisher", attrs.publisher || "", function (v) {
        attrs.publisher = v; updateCardAttrs(wrapper, attrs);
      }));
      controls.appendChild(makeInputRow("Thumbnail URL", attrs.thumbnail || "", function (v) {
        attrs.thumbnail = v; updateCardAttrs(wrapper, attrs);
      }));
      controls.appendChild(makeInputRow("Icon URL", attrs.icon || "", function (v) {
        attrs.icon = v; updateCardAttrs(wrapper, attrs);
      }));

      var refetchBtn = el("button", {
        type: "button", className: "sx-edit-btn sx-edit-btn-sm"
      }, "Re-fetch metadata");
      refetchBtn.addEventListener("click", function () {
        attrs.title = ""; attrs.description = ""; attrs.author = ""; attrs.publisher = "";
        attrs.thumbnail = ""; attrs.icon = "";
        updateCardAttrs(wrapper, attrs);
        panel.innerHTML = "";
        buildBookmarkEditUI(attrs, panel, wrapper);
      });
      controls.appendChild(refetchBtn);

      panel.appendChild(controls);
    } else {
      // URL input for metadata fetch
      var urlInput = el("input", {
        type: "url",
        className: "sx-edit-input sx-edit-url-input",
        placeholder: "Paste URL for bookmark...",
        value: attrs.url || ""
      });
      var status = el("div", { className: "sx-edit-status" });

      urlInput.addEventListener("keydown", function (e) {
        if (e.key === "Enter") {
          e.preventDefault();
          fetchBookmark(wrapper, urlInput.value, attrs, panel, status);
        }
      });

      var fetchBtn = el("button", {
        type: "button", className: "sx-edit-btn"
      }, "Add bookmark");
      fetchBtn.addEventListener("click", function () {
        fetchBookmark(wrapper, urlInput.value, attrs, panel, status);
      });

      panel.appendChild(urlInput);
      panel.appendChild(fetchBtn);
      panel.appendChild(status);
    }
  }

  // -- Callout card edit UI --
  function buildCalloutEditUI(attrs, panel, wrapper) {
    var colors = ["grey", "white", "blue", "green", "yellow", "red", "pink", "purple"];
    var colorMap = {
      grey: "#f1f1f1", white: "#ffffff", blue: "#e8f0fe", green: "#e6f4ea",
      yellow: "#fef7cd", red: "#fce8e6", pink: "#fce4ec", purple: "#f3e8fd"
    };

    var colorRow = el("div", { className: "sx-edit-color-row" });
    for (var i = 0; i < colors.length; i++) {
      (function (color) {
        var swatch = el("button", {
          type: "button",
          className: "sx-edit-color-swatch" + (attrs.color === color ? " active" : ""),
          style: "background:" + colorMap[color],
          title: color
        });
        swatch.addEventListener("click", function () {
          attrs.color = color;
          updateCardAttrs(wrapper, attrs);
          // Update active state
          var swatches = colorRow.querySelectorAll(".sx-edit-color-swatch");
          for (var j = 0; j < swatches.length; j++) swatches[j].classList.remove("active");
          swatch.classList.add("active");
        });
        colorRow.appendChild(swatch);
      })(colors[i]);
    }
    panel.appendChild(colorRow);

    var emojiInput = el("input", {
      type: "text",
      className: "sx-edit-input sx-edit-emoji-input",
      placeholder: "Emoji",
      value: attrs.emoji || "",
      maxlength: "4"
    });
    emojiInput.addEventListener("input", function () {
      attrs.emoji = emojiInput.value;
      updateCardAttrs(wrapper, attrs);
    });
    panel.appendChild(emojiInput);

    var contentArea = el("div", {
      contenteditable: "true",
      className: "sx-editable sx-edit-callout-content",
      "data-placeholder": "Callout text..."
    });
    contentArea.textContent = attrs.content || "";
    contentArea.addEventListener("input", function () {
      attrs.content = contentArea.textContent;
      updateCardAttrs(wrapper, attrs);
    });
    panel.appendChild(contentArea);
  }

  // -- Toggle card edit UI --
  function buildToggleEditUI(attrs, panel, wrapper) {
    var headingInput = el("input", {
      type: "text",
      className: "sx-edit-input",
      placeholder: "Toggle heading...",
      value: attrs.heading || ""
    });
    headingInput.addEventListener("input", function () {
      attrs.heading = headingInput.value;
      updateCardAttrs(wrapper, attrs);
    });
    panel.appendChild(el("label", { className: "sx-edit-label" }, "Heading"));
    panel.appendChild(headingInput);

    var contentArea = el("div", {
      contenteditable: "true",
      className: "sx-editable sx-edit-toggle-content",
      "data-placeholder": "Toggle content..."
    });
    contentArea.textContent = attrs.content || "";
    contentArea.addEventListener("input", function () {
      attrs.content = contentArea.textContent;
      updateCardAttrs(wrapper, attrs);
    });
    panel.appendChild(el("label", { className: "sx-edit-label" }, "Content"));
    panel.appendChild(contentArea);
  }

  // -- Button card edit UI --
  function buildButtonEditUI(attrs, panel, wrapper) {
    var controls = el("div", { className: "sx-edit-controls" });

    controls.appendChild(makeInputRow("Button text", attrs.text || "", function (v) {
      attrs.text = v; updateCardAttrs(wrapper, attrs);
    }));
    controls.appendChild(makeInputRow("Button URL", attrs.url || "", function (v) {
      attrs.url = v; updateCardAttrs(wrapper, attrs);
    }));

    var alignRow = makeSelectRow("Alignment", attrs.alignment || "center", [
      { value: "left", label: "Left" },
      { value: "center", label: "Center" },
    ], function (v) {
      attrs.alignment = v; updateCardAttrs(wrapper, attrs);
    });
    controls.appendChild(alignRow);

    panel.appendChild(controls);
  }

  // -- Audio card edit UI --
  function buildAudioEditUI(attrs, panel, wrapper) {
    if (attrs.src) {
      var audio = el("audio", { src: attrs.src, controls: "true", className: "sx-edit-audio-player" });
      panel.appendChild(audio);

      var controls = el("div", { className: "sx-edit-controls" });
      controls.appendChild(makeInputRow("Title", attrs.title || "", function (v) {
        attrs.title = v; updateCardAttrs(wrapper, attrs);
      }));

      var replaceBtn = el("button", {
        type: "button", className: "sx-edit-btn sx-edit-btn-sm"
      }, "Replace audio");
      replaceBtn.addEventListener("click", function () {
        triggerFileUpload(wrapper, "media", function (url) {
          attrs.src = url;
          updateCardAttrs(wrapper, attrs);
          audio.src = url;
        });
      });
      controls.appendChild(replaceBtn);
      panel.appendChild(controls);
    } else {
      buildUploadArea(panel, wrapper, "media", "Drop audio file here or click to upload", function (url, file) {
        attrs.src = url;
        attrs.title = attrs.title || (file ? file.name.replace(/\.[^.]+$/, "") : "");
        updateCardAttrs(wrapper, attrs);
        panel.innerHTML = "";
        buildAudioEditUI(attrs, panel, wrapper);
      });
    }
  }

  // -- Video card edit UI --
  function buildVideoEditUI(attrs, panel, wrapper) {
    if (attrs.src) {
      var video = el("video", {
        src: attrs.src, controls: "true", className: "sx-edit-video-player"
      });
      panel.appendChild(video);

      var controls = el("div", { className: "sx-edit-controls" });
      controls.appendChild(makeSelectRow("Width", attrs.width || "", [
        { value: "", label: "Normal" },
        { value: "wide", label: "Wide" },
        { value: "full", label: "Full" },
      ], function (v) {
        attrs.width = v; updateCardAttrs(wrapper, attrs);
      }));

      var loopLabel = el("label", { className: "sx-edit-checkbox-label" });
      var loopCheck = el("input", { type: "checkbox" });
      loopCheck.checked = !!attrs.loop;
      loopCheck.addEventListener("change", function () {
        attrs.loop = loopCheck.checked;
        updateCardAttrs(wrapper, attrs);
      });
      loopLabel.appendChild(loopCheck);
      loopLabel.appendChild(document.createTextNode(" Loop"));
      controls.appendChild(loopLabel);

      var replaceBtn = el("button", {
        type: "button", className: "sx-edit-btn sx-edit-btn-sm"
      }, "Replace video");
      replaceBtn.addEventListener("click", function () {
        triggerFileUpload(wrapper, "media", function (url) {
          attrs.src = url;
          updateCardAttrs(wrapper, attrs);
          video.src = url;
        });
      });
      controls.appendChild(replaceBtn);
      panel.appendChild(controls);
    } else {
      buildUploadArea(panel, wrapper, "media", "Drop video file here or click to upload", function (url) {
        attrs.src = url;
        updateCardAttrs(wrapper, attrs);
        panel.innerHTML = "";
        buildVideoEditUI(attrs, panel, wrapper);
      });
    }
  }

  // -- File card edit UI --
  function buildFileEditUI(attrs, panel, wrapper) {
    if (attrs.src) {
      var controls = el("div", { className: "sx-edit-controls" });
      controls.appendChild(makeInputRow("Filename", attrs.filename || "", function (v) {
        attrs.filename = v; updateCardAttrs(wrapper, attrs);
      }));
      controls.appendChild(makeInputRow("Title", attrs.title || "", function (v) {
        attrs.title = v; updateCardAttrs(wrapper, attrs);
      }));
      if (attrs.filesize) {
        controls.appendChild(el("div", { className: "sx-edit-info" }, "Size: " + attrs.filesize));
      }

      var replaceBtn = el("button", {
        type: "button", className: "sx-edit-btn sx-edit-btn-sm"
      }, "Replace file");
      replaceBtn.addEventListener("click", function () {
        triggerFileUpload(wrapper, "file", function (url, file) {
          attrs.src = url;
          if (file) {
            attrs.filename = file.name;
            attrs.title = attrs.title || file.name;
            attrs.filesize = formatFileSize(file.size);
          }
          updateCardAttrs(wrapper, attrs);
          panel.innerHTML = "";
          buildFileEditUI(attrs, panel, wrapper);
        });
      });
      controls.appendChild(replaceBtn);
      panel.appendChild(controls);
    } else {
      buildUploadArea(panel, wrapper, "file", "Drop file here or click to upload", function (url, file) {
        attrs.src = url;
        if (file) {
          attrs.filename = file.name;
          attrs.title = file.name;
          attrs.filesize = formatFileSize(file.size);
        }
        updateCardAttrs(wrapper, attrs);
        panel.innerHTML = "";
        buildFileEditUI(attrs, panel, wrapper);
      });
    }
  }

  // -- Generic card edit UI (fallback) --
  function buildGenericEditUI(attrs, panel, wrapper) {
    var controls = el("div", { className: "sx-edit-controls" });
    for (var k in attrs) {
      (function (key) {
        controls.appendChild(makeInputRow(key, String(attrs[key] || ""), function (v) {
          attrs[key] = v; updateCardAttrs(wrapper, attrs);
        }));
      })(k);
    }
    panel.appendChild(controls);
  }

  // =========================================================================
  // Shared edit UI helpers
  // =========================================================================

  function makeInputRow(label, value, onChange) {
    var row = el("div", { className: "sx-edit-row" });
    var lbl = el("label", { className: "sx-edit-label" }, label);
    var input = el("input", {
      type: "text", className: "sx-edit-input", value: value
    });
    input.addEventListener("input", function () { onChange(input.value); });
    row.appendChild(lbl);
    row.appendChild(input);
    return row;
  }

  function makeSelectRow(label, value, options, onChange) {
    var row = el("div", { className: "sx-edit-row" });
    var lbl = el("label", { className: "sx-edit-label" }, label);
    var select = el("select", { className: "sx-edit-select" });
    for (var i = 0; i < options.length; i++) {
      var opt = el("option", { value: options[i].value }, options[i].label);
      if (options[i].value === value) opt.selected = true;
      select.appendChild(opt);
    }
    select.addEventListener("change", function () { onChange(select.value); });
    row.appendChild(lbl);
    row.appendChild(select);
    return row;
  }

  function buildUploadArea(panel, wrapper, uploadType, message, onUploaded) {
    var area = el("div", { className: "sx-upload-area" });
    var icon = el("div", { className: "sx-upload-icon" });
    icon.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i>';
    var msg = el("div", { className: "sx-upload-msg" }, message);
    var progress = el("div", { className: "sx-upload-progress", style: "display:none" });

    area.appendChild(icon);
    area.appendChild(msg);
    area.appendChild(progress);

    // Click to upload
    area.addEventListener("click", function () {
      triggerFileUpload(wrapper, uploadType, onUploaded);
    });

    // Drag and drop
    area.addEventListener("dragover", function (e) {
      e.preventDefault();
      area.classList.add("sx-upload-dragover");
    });
    area.addEventListener("dragleave", function () {
      area.classList.remove("sx-upload-dragover");
    });
    area.addEventListener("drop", function (e) {
      e.preventDefault();
      area.classList.remove("sx-upload-dragover");
      var files = e.dataTransfer.files;
      if (files.length) {
        progress.style.display = "block";
        progress.textContent = "Uploading...";
        doUpload(wrapper, uploadType, files[0], function (url) {
          onUploaded(url, files[0]);
        }, function (err) {
          progress.textContent = "Error: " + err;
          setTimeout(function () { progress.style.display = "none"; }, 3000);
        });
      }
    });

    panel.appendChild(area);
  }

  // =========================================================================
  // File upload
  // =========================================================================

  function triggerFileUpload(wrapper, uploadType, onUploaded, multi) {
    var input = document.createElement("input");
    input.type = "file";
    if (multi) input.multiple = true;
    if (uploadType === "image") input.accept = "image/jpeg,image/png,image/gif,image/webp,image/svg+xml";
    else if (uploadType === "media") input.accept = "audio/*,video/*";

    input.addEventListener("change", function () {
      if (!input.files || !input.files.length) return;
      for (var i = 0; i < input.files.length; i++) {
        (function (file) {
          doUpload(wrapper, uploadType, file, function (url) {
            onUploaded(url, file);
          }, function (err) {
            console.error("Upload error:", err);
          });
        })(input.files[i]);
      }
    });
    input.click();
  }

  function doUpload(wrapper, uploadType, file, onSuccess, onError) {
    // Find the editor instance
    var editor = findEditor(wrapper);
    if (!editor || !editor._opts.uploadUrls) {
      if (onError) onError("Upload not configured");
      return;
    }

    var urlMap = { image: "image", media: "media", file: "file" };
    var url = editor._opts.uploadUrls[urlMap[uploadType] || uploadType];
    if (!url) {
      if (onError) onError("Upload URL not configured for " + uploadType);
      return;
    }

    var fd = new FormData();
    fd.append("file", file);

    fetch(url, {
      method: "POST",
      body: fd,
      headers: { "X-CSRFToken": editor._opts.csrfToken || "" }
    })
    .then(function (r) {
      if (!r.ok) throw new Error("Upload failed (" + r.status + ")");
      return r.json();
    })
    .then(function (data) {
      // Ghost returns { images: [{url}] } or { media: [{url}] } or { files: [{url}] }
      var resultUrl = null;
      if (data.images && data.images[0]) resultUrl = data.images[0].url;
      else if (data.media && data.media[0]) resultUrl = data.media[0].url;
      else if (data.files && data.files[0]) resultUrl = data.files[0].url;
      else if (data.url) resultUrl = data.url;

      if (!resultUrl) throw new Error("No URL in upload response");
      onSuccess(resultUrl);
      fireChange(editor);
    })
    .catch(function (e) {
      if (onError) onError(e.message);
    });
  }

  function findEditor(node) {
    while (node) {
      if (node._sxEditor) return node._sxEditor;
      if (node.classList && node.classList.contains("sx-editor")) return node._sxEditor;
      node = node.parentNode;
    }
    return null;
  }

  // =========================================================================
  // oEmbed / bookmark metadata fetching
  // =========================================================================

  function fetchOembed(wrapper, url, attrs, panel, status) {
    if (!url) { status.textContent = "Please enter a URL"; return; }
    var editor = findEditor(wrapper);
    if (!editor || !editor._opts.oembedUrl) {
      status.textContent = "oEmbed not configured";
      return;
    }

    status.textContent = "Fetching embed...";

    fetch(editor._opts.oembedUrl + "?url=" + encodeURIComponent(url) + "&type=embed")
    .then(function (r) {
      if (!r.ok) throw new Error("oEmbed lookup failed (" + r.status + ")");
      return r.json();
    })
    .then(function (data) {
      if (data.html) {
        attrs.html = data.html;
        attrs.url = url;
        if (data.title) attrs.title = data.title;
        updateCardAttrs(wrapper, attrs);
        panel.innerHTML = "";
        buildEmbedEditUI(attrs, panel, wrapper);
        fireChange(editor);
      } else {
        status.textContent = "No embed HTML returned. Try bookmark instead.";
      }
    })
    .catch(function (e) {
      status.textContent = "Error: " + e.message;
    });
  }

  function fetchBookmark(wrapper, url, attrs, panel, status) {
    if (!url) { status.textContent = "Please enter a URL"; return; }
    var editor = findEditor(wrapper);
    if (!editor || !editor._opts.oembedUrl) {
      status.textContent = "oEmbed not configured";
      return;
    }

    status.textContent = "Fetching metadata...";

    fetch(editor._opts.oembedUrl + "?url=" + encodeURIComponent(url) + "&type=bookmark")
    .then(function (r) {
      if (!r.ok) throw new Error("Metadata fetch failed (" + r.status + ")");
      return r.json();
    })
    .then(function (data) {
      attrs.url = url;
      attrs.title = data.metadata && data.metadata.title || data.title || "";
      attrs.description = data.metadata && data.metadata.description || data.description || "";
      attrs.author = data.metadata && data.metadata.author || data.author || "";
      attrs.publisher = data.metadata && data.metadata.publisher || data.publisher || "";
      attrs.thumbnail = data.metadata && data.metadata.thumbnail || data.thumbnail || "";
      attrs.icon = data.metadata && data.metadata.icon || data.icon || "";
      updateCardAttrs(wrapper, attrs);
      panel.innerHTML = "";
      buildBookmarkEditUI(attrs, panel, wrapper);
      fireChange(editor);
    })
    .catch(function (e) {
      status.textContent = "Error: " + e.message;
    });
  }

  // =========================================================================
  // Plus button (Koenig-style: single floating + on empty paragraphs)
  // =========================================================================

  function createPlusButton(editor) {
    var plusContainer = el("div", { className: "sx-plus-container" });
    plusContainer.style.display = "none";

    var plusBtn = el("button", {
      type: "button", className: "sx-plus-btn", title: "Add a card"
    });
    plusBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M7 0v14M0 7h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';

    plusContainer.appendChild(plusBtn);
    editor._root.appendChild(plusContainer);

    plusBtn.addEventListener("click", function (e) {
      e.stopPropagation();
      if (editor._plusMenuOpen) {
        closePlusMenu(editor);
      } else {
        showPlusMenu(editor, plusContainer);
      }
    });

    return plusContainer;
  }

  function positionPlusButton(editor) {
    var plus = editor._plusContainer;
    var block = editor._activeBlock;

    if (!block) {
      plus.style.display = "none";
      return;
    }

    var editable = block.querySelector("[contenteditable]");
    var isEmpty = editable && editable.textContent.trim() === "" && !editable.querySelector("img");
    var tag = block.getAttribute("data-sx-tag");
    var isTextBlock = TEXT_TAGS[tag];

    if (!isTextBlock || !isEmpty) {
      plus.style.display = "none";
      return;
    }

    var containerRect = editor._root.getBoundingClientRect();
    var blockRect = block.getBoundingClientRect();

    plus.style.display = "flex";
    plus.style.top = (blockRect.top - containerRect.top + (blockRect.height / 2) - 14) + "px";
    plus.style.left = "-40px";
  }

  function showPlusMenu(editor, plusContainer) {
    closePlusMenu(editor);

    var menu = el("div", { className: "sx-plus-menu" });

    for (var s = 0; s < CARD_MENU.length; s++) {
      var section = CARD_MENU[s];
      var sectionEl = el("div", { className: "sx-plus-menu-section" });
      sectionEl.appendChild(el("div", { className: "sx-plus-menu-heading" }, section.section));

      for (var j = 0; j < section.items.length; j++) {
        (function (item) {
          var row = el("button", {
            type: "button", className: "sx-plus-menu-item"
          });
          row.innerHTML = '<span class="sx-plus-menu-icon"><i class="' + item.icon + '"></i></span>' +
                          '<span class="sx-plus-menu-label">' + escHtml(item.label) + '</span>' +
                          '<span class="sx-plus-menu-desc">' + escHtml(item.desc) + '</span>';
          row.addEventListener("click", function (e) {
            e.stopPropagation();
            closePlusMenu(editor);
            insertBlock(editor, item.type);
          });
          sectionEl.appendChild(row);
        })(section.items[j]);
      }
      menu.appendChild(sectionEl);
    }

    // Text block types at the bottom
    var textSection = el("div", { className: "sx-plus-menu-section" });
    textSection.appendChild(el("div", { className: "sx-plus-menu-heading" }, "Text"));
    var textItems = [
      { type: "p",          icon: "fa-solid fa-paragraph", label: "Paragraph" },
      { type: "h2",         icon: "fa-solid fa-heading",   label: "Heading 2" },
      { type: "h3",         icon: "fa-solid fa-heading",   label: "Heading 3" },
      { type: "blockquote", icon: "fa-solid fa-quote-left",label: "Quote" },
      { type: "ul",         icon: "fa-solid fa-list-ul",   label: "Bulleted List" },
      { type: "ol",         icon: "fa-solid fa-list-ol",   label: "Numbered List" },
    ];
    for (var t = 0; t < textItems.length; t++) {
      (function (item) {
        var row = el("button", {
          type: "button", className: "sx-plus-menu-item"
        });
        row.innerHTML = '<span class="sx-plus-menu-icon"><i class="' + item.icon + '"></i></span>' +
                        '<span class="sx-plus-menu-label">' + escHtml(item.label) + '</span>';
        row.addEventListener("click", function (e) {
          e.stopPropagation();
          closePlusMenu(editor);
          convertBlock(editor, item.type);
        });
        textSection.appendChild(row);
      })(textItems[t]);
    }
    menu.appendChild(textSection);

    plusContainer.appendChild(menu);
    editor._plusMenuOpen = true;

    // Rotate the + to X
    plusContainer.querySelector(".sx-plus-btn").classList.add("sx-plus-btn-open");

    setTimeout(function () {
      document.addEventListener("click", editor._closePlusHandler = function () {
        closePlusMenu(editor);
      }, { once: true });
    }, 0);
  }

  function closePlusMenu(editor) {
    if (editor._plusMenuOpen) {
      var menu = editor._plusContainer.querySelector(".sx-plus-menu");
      if (menu) menu.remove();
      editor._plusMenuOpen = false;
      var btn = editor._plusContainer.querySelector(".sx-plus-btn");
      if (btn) btn.classList.remove("sx-plus-btn-open");
    }
  }

  // =========================================================================
  // Slash commands
  // =========================================================================

  function handleSlashCommand(editor, e) {
    var editable = e.target;
    if (!editable.hasAttribute || !editable.hasAttribute("contenteditable")) return;
    var block = closestBlock(editable, editor._container);
    if (!block) return;

    var text = editable.textContent;

    // If text starts with "/" show slash menu
    if (text.charAt(0) === "/") {
      var query = text.slice(1).toLowerCase();
      showSlashMenu(editor, block, editable, query);
    } else {
      closeSlashMenu(editor);
    }
  }

  function showSlashMenu(editor, block, editable, query) {
    closeSlashMenu(editor);

    // Filter matching items
    var matches = [];
    for (var i = 0; i < ALL_CARD_ITEMS.length; i++) {
      var item = ALL_CARD_ITEMS[i];
      if (!query) {
        matches.push(item);
      } else {
        // Match against label and slash commands
        var matchFound = item.label.toLowerCase().indexOf(query) !== -1;
        if (!matchFound && item.slash) {
          for (var s = 0; s < item.slash.length; s++) {
            if (item.slash[s].indexOf(query) !== -1) { matchFound = true; break; }
          }
        }
        if (matchFound) matches.push(item);
      }
    }

    if (matches.length === 0) return;

    var menu = el("div", { className: "sx-slash-menu" });

    for (var i = 0; i < matches.length && i < 8; i++) {
      (function (item) {
        var row = el("button", {
          type: "button", className: "sx-slash-menu-item"
        });
        row.innerHTML = '<span class="sx-slash-menu-icon"><i class="' + item.icon + '"></i></span>' +
                        '<span class="sx-slash-menu-label">' + escHtml(item.label) + '</span>' +
                        '<span class="sx-slash-menu-desc">' + escHtml(item.desc) + '</span>';
        row.addEventListener("mousedown", function (e) {
          e.preventDefault();
          e.stopPropagation();
          closeSlashMenu(editor);
          // Clear the slash text
          editable.textContent = "";
          insertBlock(editor, item.type);
        });
        menu.appendChild(row);
      })(matches[i]);
    }

    // Position below the block
    var blockRect = block.getBoundingClientRect();
    var containerRect = editor._root.getBoundingClientRect();
    menu.style.top = (blockRect.bottom - containerRect.top + 4) + "px";
    menu.style.left = "0px";

    editor._root.appendChild(menu);
    editor._slashMenu = menu;
    editor._slashBlock = block;
  }

  function closeSlashMenu(editor) {
    if (editor._slashMenu) {
      editor._slashMenu.remove();
      editor._slashMenu = null;
      editor._slashBlock = null;
    }
  }

  // =========================================================================
  // Block insertion & conversion
  // =========================================================================

  function insertBlock(editor, type) {
    var block;
    var container = editor._container;
    var refBlock = editor._activeBlock;

    if (TEXT_TAGS[type]) {
      block = createTextBlock(type, "");
    } else if (LIST_TAGS[type]) {
      block = createListBlock(type, []);
    } else if (type === "hr") {
      block = createHrBlock();
    } else if (type === "code") {
      block = createCodeBlock("", "");
    } else if (type === "image") {
      // Create empty image card — edit mode opens automatically
      block = createCardBlock("kg-image", {});
      insertBlockNode(editor, block, refBlock);
      // Open edit mode immediately
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "gallery") {
      block = createCardBlock("kg-gallery", {});
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "video") {
      block = createCardBlock("kg-video", {});
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "audio") {
      block = createCardBlock("kg-audio", {});
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "file") {
      block = createCardBlock("kg-file", {});
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "callout") {
      block = createCardBlock("kg-callout", { color: "grey", emoji: "", content: "" });
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "toggle") {
      block = createCardBlock("kg-toggle", { heading: "", content: "" });
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "html") {
      block = createCardBlock("kg-html", { _childrenSx: '""' });
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "markdown") {
      block = createCardBlock("kg-md", { _childrenSx: '""' });
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "button") {
      block = createCardBlock("kg-button", { url: "", text: "Click here", alignment: "center" });
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "bookmark") {
      block = createCardBlock("kg-bookmark", {});
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    } else if (type === "embed" || type === "youtube" || type === "vimeo" ||
               type === "twitter" || type === "spotify" || type === "codepen") {
      block = createCardBlock("kg-embed", {});
      insertBlockNode(editor, block, refBlock);
      block.querySelector(".sx-card-preview").click();
      return;
    }

    if (!block) return;
    insertBlockNode(editor, block, refBlock);

    var editable = block.querySelector("[contenteditable]");
    if (editable) editable.focus();
    else {
      var ta = block.querySelector("textarea");
      if (ta) ta.focus();
    }
  }

  function insertBlockNode(editor, block, refBlock) {
    var container = editor._container;
    if (refBlock && refBlock.parentNode === container) {
      refBlock.parentNode.insertBefore(block, refBlock.nextSibling);
    } else {
      container.appendChild(block);
    }
    editor._activeBlock = block;
    fireChange(editor);
  }

  function convertBlock(editor, newType) {
    var block = editor._activeBlock;
    if (!block) return;

    var tag = block.getAttribute("data-sx-tag");

    // If same type, do nothing
    if (tag === newType) return;

    // For text blocks, change the tag
    if (TEXT_TAGS[tag] && TEXT_TAGS[newType]) {
      var editable = block.querySelector("[contenteditable]");
      var html = editable ? editable.innerHTML : "";
      block.setAttribute("data-sx-tag", newType);
      if (editable) {
        editable.className = "sx-block-content sx-editable" +
          (newType === "h2" || newType === "h3" ? " sx-heading" : "") +
          (newType === "blockquote" ? " sx-quote" : "");
        editable.setAttribute("data-placeholder",
          newType === "p" ? "Type / for commands..." :
          newType === "blockquote" ? "Type a quote..." :
          "Type a heading...");
        editable.setAttribute("data-block-type", newType);
        editable.focus();
      }
      fireChange(editor);
      return;
    }

    // For text→list conversion
    if (TEXT_TAGS[tag] && LIST_TAGS[newType]) {
      var editable = block.querySelector("[contenteditable]");
      var html = editable ? editable.innerHTML : "";
      var newBlock = createListBlock(newType, []);
      var firstLi = newBlock.querySelector("[data-sx-li]");
      if (firstLi) firstLi.innerHTML = html;
      block.parentNode.replaceChild(newBlock, block);
      editor._activeBlock = newBlock;
      var li = newBlock.querySelector("[contenteditable]");
      if (li) focusEnd(li);
      fireChange(editor);
      return;
    }

    // For list→text conversion
    if (LIST_TAGS[tag] && TEXT_TAGS[newType]) {
      var firstLi = block.querySelector("[data-sx-li]");
      var html = firstLi ? firstLi.innerHTML : "";
      var newBlock = createTextBlock(newType, html);
      block.parentNode.replaceChild(newBlock, block);
      editor._activeBlock = newBlock;
      var ed = newBlock.querySelector("[contenteditable]");
      if (ed) focusEnd(ed);
      fireChange(editor);
      return;
    }
  }

  // =========================================================================
  // Inline formatting toolbar
  // =========================================================================

  function createFormatBar() {
    var bar = el("div", { className: "sx-format-bar" });
    bar.style.display = "none";

    var commands = [
      { cmd: "bold",          label: "B",  cls: "sx-fmt-bold",   shortcut: "Ctrl+B" },
      { cmd: "italic",        label: "I",  cls: "sx-fmt-italic", shortcut: "Ctrl+I" },
      { cmd: "h2",            label: "H2", cls: "",              shortcut: "Ctrl+Alt+2" },
      { cmd: "h3",            label: "H3", cls: "",              shortcut: "Ctrl+Alt+3" },
      { cmd: "blockquote",    label: "❝",  cls: "",              shortcut: "" },
      { cmd: "link",          label: '<i class="fa-solid fa-link"></i>', cls: "", shortcut: "Ctrl+K" },
    ];

    for (var i = 0; i < commands.length; i++) {
      (function (c) {
        var btn = el("button", {
          type: "button",
          className: "sx-format-btn" + (c.cls ? " " + c.cls : ""),
          title: c.cmd + (c.shortcut ? " (" + c.shortcut + ")" : "")
        });
        btn.innerHTML = c.label;
        btn.addEventListener("mousedown", function (e) {
          e.preventDefault();
          applyFormat(c.cmd);
        });
        bar.appendChild(btn);
      })(commands[i]);
    }

    document.body.appendChild(bar);
    return bar;
  }

  function applyFormat(cmd) {
    if (cmd === "link") {
      var sel = window.getSelection();
      if (!sel.rangeCount) return;
      // Check if already linked
      var anchor = sel.anchorNode;
      while (anchor && anchor.tagName !== "A") anchor = anchor.parentNode;
      if (anchor && anchor.tagName === "A") {
        document.execCommand("unlink", false, null);
      } else {
        var url = prompt("Link URL:");
        if (url) document.execCommand("createLink", false, url);
      }
    } else if (cmd === "h2" || cmd === "h3" || cmd === "blockquote") {
      // Block-level format change: find the current block and convert it
      var sel = window.getSelection();
      if (!sel.anchorNode) return;
      var node = sel.anchorNode;
      while (node && !(node.hasAttribute && node.hasAttribute("data-sx-block"))) {
        node = node.parentNode;
      }
      if (!node) return;
      var currentTag = node.getAttribute("data-sx-tag");
      // Toggle: if already this type, revert to p
      var newType = currentTag === cmd ? "p" : cmd;

      var editable = node.querySelector("[contenteditable]");
      if (editable) {
        node.setAttribute("data-sx-tag", newType);
        editable.className = "sx-block-content sx-editable" +
          (newType === "h2" || newType === "h3" ? " sx-heading" : "") +
          (newType === "blockquote" ? " sx-quote" : "");
        editable.setAttribute("data-placeholder",
          newType === "p" ? "Type / for commands..." :
          newType === "blockquote" ? "Type a quote..." :
          "Type a heading...");
        editable.setAttribute("data-block-type", newType);
      }
    } else if (cmd === "code") {
      var sel = window.getSelection();
      if (sel.rangeCount) {
        var range = sel.getRangeAt(0);
        var code = document.createElement("code");
        try {
          range.surroundContents(code);
        } catch (ex) {
          document.execCommand("insertHTML", false, "<code>" + escHtml(sel.toString()) + "</code>");
        }
      }
    } else {
      document.execCommand(cmd, false, null);
    }
  }

  function updateFormatBar(editor) {
    var bar = editor._formatBar;
    var sel = window.getSelection();
    if (!sel.rangeCount || sel.isCollapsed) {
      bar.style.display = "none";
      return;
    }

    var anchor = sel.anchorNode;
    var inEditor = false;
    var node = anchor;
    while (node) {
      if (node === editor._container) { inEditor = true; break; }
      node = node.parentNode;
    }
    if (!inEditor) {
      bar.style.display = "none";
      return;
    }

    var range = sel.getRangeAt(0);
    var rect = range.getBoundingClientRect();
    if (rect.width === 0) {
      bar.style.display = "none";
      return;
    }

    bar.style.display = "flex";
    bar.style.top = (rect.top + window.scrollY - bar.offsetHeight - 8) + "px";
    bar.style.left = (rect.left + window.scrollX + rect.width / 2 - bar.offsetWidth / 2) + "px";
  }

  // =========================================================================
  // Keyboard handling
  // =========================================================================

  function handleKeydown(editor, e) {
    var block = closestBlock(e.target, editor._container);
    if (!block) return;

    var tag = block.getAttribute("data-sx-tag");

    // Keyboard shortcuts
    var mod = e.metaKey || e.ctrlKey;
    if (mod && !e.shiftKey && !e.altKey) {
      if (e.key === "b" || e.key === "B") {
        e.preventDefault(); applyFormat("bold"); return;
      }
      if (e.key === "i" || e.key === "I") {
        e.preventDefault(); applyFormat("italic"); return;
      }
      if (e.key === "k" || e.key === "K") {
        e.preventDefault(); applyFormat("link"); return;
      }
    }
    if (mod && e.altKey) {
      if (e.key === "2") { e.preventDefault(); applyFormat("h2"); return; }
      if (e.key === "3") { e.preventDefault(); applyFormat("h3"); return; }
    }

    // Escape closes slash menu and card edit
    if (e.key === "Escape") {
      closeSlashMenu(editor);
      closePlusMenu(editor);
      // Exit card edit mode if in one
      var editingCard = editor._container.querySelector(".sx-card-editing");
      if (editingCard) exitCardEdit(editingCard);
      return;
    }

    // Slash menu keyboard navigation
    if (editor._slashMenu) {
      if (e.key === "Escape") {
        e.preventDefault();
        closeSlashMenu(editor);
        return;
      }
    }

    // Enter in text block → new paragraph
    if (e.key === "Enter" && !e.shiftKey) {
      if (TEXT_TAGS[tag]) {
        e.preventDefault();
        // Split at cursor or insert new p after
        insertBlock(editor, "p");
        return;
      }
      // Enter in list item → new list item or exit list
      if (LIST_TAGS[tag]) {
        var li = e.target;
        if (li.hasAttribute("data-sx-li") && li.textContent.trim() === "") {
          // Empty list item → exit list, create new paragraph
          e.preventDefault();
          if (block.querySelectorAll("[data-sx-li]").length <= 1) {
            // Only one empty item → convert whole list to paragraph
            var newBlock = createTextBlock("p", "");
            block.parentNode.replaceChild(newBlock, block);
            editor._activeBlock = newBlock;
            var ed = newBlock.querySelector("[contenteditable]");
            if (ed) ed.focus();
          } else {
            // Remove this item and add paragraph after list
            li.remove();
            var newBlock = createTextBlock("p", "");
            block.parentNode.insertBefore(newBlock, block.nextSibling);
            editor._activeBlock = newBlock;
            var ed = newBlock.querySelector("[contenteditable]");
            if (ed) ed.focus();
          }
          fireChange(editor);
          return;
        }
        if (li.hasAttribute("data-sx-li")) {
          e.preventDefault();
          var newLi = el("div", {
            contenteditable: "true",
            className: "sx-list-item sx-editable",
            "data-sx-li": "true",
            "data-placeholder": "List item..."
          });
          li.parentNode.insertBefore(newLi, li.nextSibling);
          newLi.focus();
          fireChange(editor);
          return;
        }
      }
    }

    // Backspace in empty text block → delete block
    if (e.key === "Backspace") {
      var editable = e.target;
      if (editable.hasAttribute("contenteditable") &&
          editable.textContent.trim() === "" &&
          !editable.querySelector("img")) {

        // In list, remove item
        if (editable.hasAttribute("data-sx-li")) {
          var items = block.querySelectorAll("[data-sx-li]");
          if (items.length > 1) {
            e.preventDefault();
            var prevItem = editable.previousElementSibling;
            editable.remove();
            if (prevItem) focusEnd(prevItem);
            fireChange(editor);
            return;
          }
        }

        var allBlocks = editor._container.querySelectorAll("[data-sx-block]");
        if (allBlocks.length > 1) {
          e.preventDefault();
          removeBlock(editor, block);
          return;
        }
      }
    }

    // Arrow up at start of block → focus previous block
    if (e.key === "ArrowUp") {
      var editable = e.target;
      if (editable.hasAttribute("contenteditable")) {
        var sel = window.getSelection();
        if (sel.rangeCount) {
          var range = sel.getRangeAt(0);
          if (range.startOffset === 0 && range.collapsed) {
            var prev = block.previousElementSibling;
            while (prev && !prev.hasAttribute("data-sx-block")) prev = prev.previousElementSibling;
            if (prev) {
              e.preventDefault();
              var prevEditable = prev.querySelector("[contenteditable]:last-child") ||
                                 prev.querySelector("[contenteditable]");
              if (prevEditable) focusEnd(prevEditable);
              editor._activeBlock = prev;
              positionPlusButton(editor);
            }
          }
        }
      }
    }

    // Arrow down at end of block → focus next block
    if (e.key === "ArrowDown") {
      var editable = e.target;
      if (editable.hasAttribute("contenteditable")) {
        var sel = window.getSelection();
        if (sel.rangeCount) {
          var range = sel.getRangeAt(0);
          var atEnd = range.collapsed && range.startContainer.nodeType === 3 &&
                      range.startOffset === range.startContainer.textContent.length;
          if (!atEnd) atEnd = range.collapsed && range.startOffset === editable.childNodes.length;
          if (atEnd) {
            var next = block.nextElementSibling;
            while (next && !next.hasAttribute("data-sx-block")) next = next.nextElementSibling;
            if (next) {
              e.preventDefault();
              var nextEditable = next.querySelector("[contenteditable]");
              if (nextEditable) {
                nextEditable.focus();
                var r = document.createRange();
                r.setStart(nextEditable, 0);
                r.collapse(true);
                sel.removeAllRanges();
                sel.addRange(r);
              }
              editor._activeBlock = next;
              positionPlusButton(editor);
            }
          }
        }
      }
    }
  }

  function removeBlock(editor, block) {
    var prev = block.previousElementSibling;
    while (prev && !prev.hasAttribute("data-sx-block")) prev = prev.previousElementSibling;
    block.remove();

    if (prev) {
      var editable = prev.querySelector("[contenteditable]:last-child") ||
                     prev.querySelector("[contenteditable]");
      if (editable) focusEnd(editable);
      editor._activeBlock = prev;
    } else {
      var first = editor._container.querySelector("[data-sx-block]");
      if (first) {
        var editable = first.querySelector("[contenteditable]");
        if (editable) editable.focus();
        editor._activeBlock = first;
      }
    }
    positionPlusButton(editor);
    fireChange(editor);
  }

  // =========================================================================
  // Change handler
  // =========================================================================

  function fireChange(editor) {
    if (editor._changeTimeout) clearTimeout(editor._changeTimeout);
    editor._changeTimeout = setTimeout(function () {
      var sx = serializeBlocks(editor._container);
      if (editor._opts.onChange) editor._opts.onChange(sx);
    }, 150);
  }

  // =========================================================================
  // Mount
  // =========================================================================

  function mount(elementId, opts) {
    opts = opts || {};
    var root = document.getElementById(elementId);
    if (!root) {
      console.error("sx-editor: element not found: #" + elementId);
      return null;
    }

    // Clear any previous mount
    root.innerHTML = "";
    root.className = (root.className || "").replace(/\bsx-editor\b/g, "").trim() + " sx-editor";

    var container = el("div", { className: "sx-blocks-container" });
    root.appendChild(container);

    var editor = {
      _root: root,
      _container: container,
      _opts: opts,
      _formatBar: createFormatBar(),
      _plusContainer: null,
      _plusMenuOpen: false,
      _closePlusHandler: null,
      _slashMenu: null,
      _slashBlock: null,
      _activeBlock: null,
      _changeTimeout: null
    };

    // Store editor reference on root for findEditor()
    root._sxEditor = editor;

    // Create floating + button
    editor._plusContainer = createPlusButton(editor);

    // Load initial content
    var blocks = [];
    if (opts.initialSx) {
      blocks = deserializeSx(opts.initialSx);
    }
    if (blocks.length === 0) {
      blocks = [createTextBlock("p", "")];
    }

    for (var i = 0; i < blocks.length; i++) {
      container.appendChild(blocks[i]);
    }

    // Track active block
    container.addEventListener("focusin", function (e) {
      var block = closestBlock(e.target, container);
      if (block) {
        editor._activeBlock = block;
        positionPlusButton(editor);
      }
    });

    container.addEventListener("click", function (e) {
      var block = closestBlock(e.target, container);
      if (block) {
        editor._activeBlock = block;
        positionPlusButton(editor);
      }
    });

    // Click on container background → focus last block or add new paragraph
    container.addEventListener("click", function (e) {
      if (e.target === container) {
        // Clicked empty area below blocks → focus last block or create new one
        var lastBlock = container.querySelector("[data-sx-block]:last-child");
        if (lastBlock) {
          var editable = lastBlock.querySelector("[contenteditable]");
          if (editable && editable.textContent.trim() === "") {
            editable.focus();
          } else {
            // Add a new paragraph at the end
            var newBlock = createTextBlock("p", "");
            container.appendChild(newBlock);
            newBlock.querySelector("[contenteditable]").focus();
            editor._activeBlock = newBlock;
            positionPlusButton(editor);
            fireChange(editor);
          }
        }
      }
    });

    // Event delegation
    container.addEventListener("input", function (e) {
      handleSlashCommand(editor, e);
      fireChange(editor);
    });

    container.addEventListener("keydown", function (e) {
      handleKeydown(editor, e);
    });

    // Click outside card → exit edit mode
    document.addEventListener("click", function (e) {
      var editingCards = editor._container.querySelectorAll(".sx-card-editing");
      for (var i = 0; i < editingCards.length; i++) {
        if (!editingCards[i].contains(e.target)) {
          exitCardEdit(editingCards[i]);
          fireChange(editor);
        }
      }
    });

    // Delete card button
    container.addEventListener("click", function (e) {
      var deleteBtn = e.target.closest(".sx-card-tool-btn");
      if (deleteBtn) {
        var block = closestBlock(deleteBtn, container);
        if (block) removeBlock(editor, block);
      }
    });

    document.addEventListener("selectionchange", function () {
      updateFormatBar(editor);
    });

    // Drag-drop on the whole editor
    container.addEventListener("dragover", function (e) {
      e.preventDefault();
      container.classList.add("sx-drag-over");
    });
    container.addEventListener("dragleave", function (e) {
      if (!container.contains(e.relatedTarget)) {
        container.classList.remove("sx-drag-over");
      }
    });
    container.addEventListener("drop", function (e) {
      container.classList.remove("sx-drag-over");
      var files = e.dataTransfer && e.dataTransfer.files;
      if (!files || !files.length) return;

      e.preventDefault();

      // Find drop position
      var dropBlock = closestBlock(e.target, container);
      editor._activeBlock = dropBlock || container.querySelector("[data-sx-block]:last-child");

      for (var i = 0; i < files.length; i++) {
        (function (file) {
          var type = file.type;
          if (type.startsWith("image/")) {
            var block = createCardBlock("kg-image", {});
            insertBlockNode(editor, block, editor._activeBlock);
            doUpload(block, "image", file, function (url) {
              var attrs = { src: url, alt: "" };
              updateCardAttrs(block, attrs);
              block.setAttribute("data-sx-attrs", JSON.stringify(attrs));
              renderCardPreview("kg-image", attrs, block.querySelector(".sx-card-preview"));
              fireChange(editor);
            }, function (err) {
              console.error("Upload error:", err);
            });
          } else if (type.startsWith("video/")) {
            var block = createCardBlock("kg-video", {});
            insertBlockNode(editor, block, editor._activeBlock);
            doUpload(block, "media", file, function (url) {
              var attrs = { src: url };
              updateCardAttrs(block, attrs);
              block.setAttribute("data-sx-attrs", JSON.stringify(attrs));
              renderCardPreview("kg-video", attrs, block.querySelector(".sx-card-preview"));
              fireChange(editor);
            }, function (err) {
              console.error("Upload error:", err);
            });
          } else if (type.startsWith("audio/")) {
            var block = createCardBlock("kg-audio", {});
            insertBlockNode(editor, block, editor._activeBlock);
            doUpload(block, "media", file, function (url) {
              var attrs = { src: url, title: file.name.replace(/\.[^.]+$/, "") };
              updateCardAttrs(block, attrs);
              block.setAttribute("data-sx-attrs", JSON.stringify(attrs));
              renderCardPreview("kg-audio", attrs, block.querySelector(".sx-card-preview"));
              fireChange(editor);
            }, function (err) {
              console.error("Upload error:", err);
            });
          } else {
            var block = createCardBlock("kg-file", {});
            insertBlockNode(editor, block, editor._activeBlock);
            doUpload(block, "file", file, function (url) {
              var attrs = { src: url, filename: file.name, title: file.name, filesize: formatFileSize(file.size) };
              updateCardAttrs(block, attrs);
              block.setAttribute("data-sx-attrs", JSON.stringify(attrs));
              renderCardPreview("kg-file", attrs, block.querySelector(".sx-card-preview"));
              fireChange(editor);
            }, function (err) {
              console.error("Upload error:", err);
            });
          }
        })(files[i]);
      }
    });

    // Set min-height and cursor for clicking below content
    container.style.minHeight = "300px";
    container.style.cursor = "text";

    // Focus first editable
    var firstEditable = container.querySelector("[contenteditable]");
    if (firstEditable) firstEditable.focus();
    editor._activeBlock = container.querySelector("[data-sx-block]");
    positionPlusButton(editor);

    return {
      getSx: function () {
        return serializeBlocks(container);
      },
      destroy: function () {
        if (editor._formatBar) editor._formatBar.remove();
        if (editor._plusContainer) editor._plusContainer.remove();
        root._sxEditor = null;
        root.innerHTML = "";
        root.className = root.className.replace(/\bsx-editor\b/, "").trim();
      }
    };
  }

  // =========================================================================
  // Export
  // =========================================================================

  var VERSION = "2026-03-02b-exorcism";

  window.SxEditor = {
    VERSION: VERSION,
    mount: mount,
    _serializeBlocks: serializeBlocks,
    _serializeInline: serializeInline,
    _deserializeSx: deserializeSx
  };

  console.log("[sx-editor] v" + VERSION + " init");

})();
