(() => {
  const chatStates = new Map();
  const anchors = {
    "top-left": [0, 0],
    "top-center": [-50, 0],
    "top-right": [-100, 0],
    "center-left": [0, -50],
    center: [-50, -50],
    "center-right": [-100, -50],
    "bottom-left": [0, -100],
    "bottom-center": [-50, -100],
    "bottom-right": [-100, -100]
  };

  function finite(value, fallback) {
    const number = Number(value);
    return Number.isFinite(number) ? number : fallback;
  }

  function applyBox(element, config) {
    const [translateX, translateY] = anchors[config.anchor] || anchors["top-left"];
    element.style.left = `${finite(config.x, 0)}%`;
    element.style.top = `${finite(config.y, 0)}%`;
    element.style.width = `${finite(config.width, 100)}%`;
    element.style.height = `${finite(config.height, 100)}%`;
    element.style.opacity = `${finite(config.opacity, 1)}`;
    element.style.transform = `translate(${translateX}%, ${translateY}%) translateZ(0)`;
  }

  function shrinkText(box, content, preferredSize, minimumSize, scale) {
    let size = preferredSize;
    const fit = () => {
      content.style.fontSize = `${size * scale}px`;
      if (size <= minimumSize || (content.scrollWidth <= box.clientWidth && content.scrollHeight <= box.clientHeight)) return;
      size = Math.max(minimumSize, size - Math.max(1, preferredSize / 24));
      requestAnimationFrame(fit);
    };
    requestAnimationFrame(fit);
  }

  function restartMediaElement(media) {
    if (!media) return false;
    const startAt = Math.max(0, finite(media.dataset.startAt, 0));
    try { media.currentTime = startAt; } catch {}
    const playback = media.play?.();
    playback?.catch?.(() => {});
    return true;
  }

  function buildMedia(module, values, options, renderType) {
    const media = document.createElement(renderType);
    media.className = `lumi-overlay-${renderType}`;
    media.src = values.url;
    media.dataset.originalUrl = values.url;
    media.dataset.moduleId = module.id;
    media.dataset.startAt = `${finite(values.startAt, 0)}`;
    media.preload = "auto";
    media.playsInline = true;
    media.loop = values.playBehavior === "loop";
    media.controls = values.showControls === true || values.playBehavior === "manual";
    media.muted = values.muted === true;
    media.volume = Math.min(1, Math.max(0, finite(values.volume, 1)));
    media.playbackRate = Math.min(4, Math.max(0.25, finite(values.playbackRate, 1)));
    media.autoplay = !options.editor && values.playBehavior !== "manual";
    media.referrerPolicy = "no-referrer";
    media.style.pointerEvents = !options.editor && media.controls ? "auto" : "none";
    if (renderType === "video") media.style.objectFit = values.fit || "contain";
    media.addEventListener("loadedmetadata", () => {
      const startAt = Math.max(0, finite(values.startAt, 0));
      if (startAt && startAt < finite(media.duration, Infinity)) {
        try { media.currentTime = startAt; } catch {}
      }
      media.playbackRate = Math.min(4, Math.max(0.25, finite(values.playbackRate, 1)));
      if (media.autoplay) media.play().catch(() => {});
    }, { once: true });
    return media;
  }

  function webFrameLayout(values) {
    const left = Math.min(95, Math.max(0, finite(values.cropLeft, 0)));
    const right = Math.min(95, Math.max(0, finite(values.cropRight, 0)));
    const top = Math.min(95, Math.max(0, finite(values.cropTop, 0)));
    const bottom = Math.min(95, Math.max(0, finite(values.cropBottom, 0)));
    const zoom = Math.min(5, Math.max(0.1, finite(values.zoom, 1)));
    const visibleWidth = Math.max(5, 100 - left - right);
    const visibleHeight = Math.max(5, 100 - top - bottom);
    return {
      width: (10000 * zoom) / visibleWidth,
      height: (10000 * zoom) / visibleHeight,
      left: (-100 * left * zoom) / visibleWidth,
      top: (-100 * top * zoom) / visibleHeight
    };
  }

  function buildWebsite(module, values, options) {
    const frame = document.createElement("iframe");
    const sourceUrl = values.renderUrl || values.url;
    const injectedDocument = Boolean(values.renderUrl);
    frame.className = "lumi-overlay-frame";
    frame.src = sourceUrl;
    frame.dataset.originalUrl = sourceUrl;
    frame.dataset.moduleId = module.id;
    let sandbox = "allow-scripts allow-forms allow-popups allow-presentation";
    try {
      if (!injectedDocument && new URL(sourceUrl, window.location.href).origin !== window.location.origin) sandbox += " allow-same-origin";
    } catch {}
    frame.sandbox = sandbox;
    frame.referrerPolicy = "no-referrer";
    frame.setAttribute("allow", "autoplay; fullscreen");
    frame.setAttribute("allowtransparency", "true");
    frame.style.pointerEvents = options.editor ? "none" : (values.allowPointerEvents ? "auto" : "none");

    const layout = webFrameLayout(values);
    const root = document.createElement("div");
    root.className = "lumi-overlay-web-root";
    const shadow = root.attachShadow({ mode: "open" });
    const style = document.createElement("style");
    style.textContent = `
      :host { display: block; position: relative; width: 100%; height: 100%; overflow: hidden; background-color: rgba(0, 0, 0, 0); }
      .lumi-overlay-frame {
        position: absolute; display: block; border: 0; background-color: rgba(0, 0, 0, 0);
        width: ${layout.width}%; height: ${layout.height}%;
        left: ${layout.left}%; top: ${layout.top}%;
      }
    `;
    shadow.append(style, frame);
    return root;
  }

  function normalizedChannel(value) {
    return String(value || "").trim().toLowerCase().replace(/^#/, "");
  }

  function chatAccepts(values, payload) {
    if (!(values.platforms || []).includes(payload.platform) || chatUserBlocked(values, payload)) return false;
    const filters = Array.isArray(values.channels) ? values.channels : [];
    if (!filters.length) return true;
    const aliases = [payload.channel?.id, payload.channel?.name, payload.channel?.key]
      .map(normalizedChannel)
      .filter(Boolean);
    return filters.some((entry) => {
      const raw = String(entry || "").trim();
      const separator = raw.indexOf(":");
      const platform = separator > 0 ? raw.slice(0, separator).trim().toLowerCase() : "";
      const channel = normalizedChannel(separator > 0 ? raw.slice(separator + 1) : raw);
      return (!platform || platform === payload.platform) && (channel === "*" || aliases.includes(channel));
    });
  }

  function normalizedIdentity(value) {
    return String(value || "").trim().toLowerCase().replace(/^@/, "");
  }

  function chatUserBlocked(values, payload) {
    return (values.blockedUsers || []).some((entry) => {
      const raw = String(entry || "").trim();
      const separator = raw.indexOf(":");
      if (separator < 1) return false;
      const platform = raw.slice(0, separator).toLowerCase();
      const blocked = raw.slice(separator + 1).split("|").map(normalizedIdentity).filter(Boolean);
      const aliases = platform === "lumi"
        ? [payload.author?.lumi?.id, payload.author?.lumi?.username]
        : platform === payload.platform
          ? [payload.author?.id, payload.author?.username, payload.author?.name]
          : [];
      const normalizedAliases = aliases.map(normalizedIdentity).filter(Boolean);
      return blocked.some((value) => normalizedAliases.includes(value));
    });
  }

  function chatMessageElement(payload, values, { animate = true } = {}) {
    const row = document.createElement("article");
    row.className = `message platform-${payload.platform}`;
    row.dataset.messageId = payload.id;
    row.dataset.platform = payload.platform;

    if (values.showAvatars && payload.author?.avatar) {
      const avatar = document.createElement("img");
      avatar.className = "avatar";
      avatar.src = payload.author.avatar;
      avatar.alt = "";
      avatar.referrerPolicy = "no-referrer";
      avatar.addEventListener("error", () => avatar.remove(), { once: true });
      row.appendChild(avatar);
    }

    const content = document.createElement("div");
    content.className = "content";
    const meta = document.createElement("div");
    meta.className = "meta";
    if (values.showPlatform) {
      const platform = document.createElement("img");
      platform.className = "platform";
      platform.src = `/icons/platforms/${["twitch", "youtube", "discord"].includes(payload.platform) ? payload.platform : "lumi"}.svg`;
      platform.alt = payload.platform === "youtube" ? "YouTube" : payload.platform[0].toUpperCase() + payload.platform.slice(1);
      platform.title = platform.alt;
      meta.appendChild(platform);
    }
    const author = document.createElement("span");
    author.className = "author";
    author.textContent = payload.author?.name || "Viewer";
    author.style.color = payload.author?.color || values.usernameColor || "#67e8f9";
    meta.appendChild(author);
    if (values.showBadges) {
      for (const badgeData of payload.author?.badges || []) {
        const badge = document.createElement("img");
        badge.className = "badge";
        if (badgeData.image) {
          badge.src = badgeData.image;
        } else {
          const label = String(badgeData.label || "").toLowerCase();
          const kind = /owner|broadcaster/.test(label) ? "owner" : /moderator|mod|staff/.test(label) ? "moderator" : /member|subscriber|sub|vip/.test(label) ? "member" : "generic";
          badge.src = `/icons/badges/${kind}.svg`;
        }
        badge.alt = badgeData.label || "Badge";
        badge.title = badge.alt;
        badge.referrerPolicy = "no-referrer";
        meta.appendChild(badge);
      }
    }
    if (values.showTimestamp) {
      const timestamp = document.createElement("time");
      timestamp.className = "timestamp";
      timestamp.dateTime = new Date(payload.timestamp).toISOString();
      timestamp.textContent = new Date(payload.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
      meta.appendChild(timestamp);
    }
    const messageText = String(payload.text || "");
    if (messageText) {
      const text = document.createElement("div");
      text.className = "text";
      let cursor = 0;
      const emotes = [...(payload.emotes || [])].sort((a, b) => a.start - b.start);
      for (const emoteData of emotes) {
        if (emoteData.start < cursor || emoteData.start >= messageText.length) continue;
        text.append(document.createTextNode(messageText.slice(cursor, emoteData.start)));
        const emote = document.createElement("img");
        emote.className = "emote";
        emote.src = emoteData.image;
        emote.alt = emoteData.label || messageText.slice(emoteData.start, Math.min(messageText.length, emoteData.end + 1)) || "Emote";
        emote.referrerPolicy = "no-referrer";
        text.appendChild(emote);
        cursor = Math.min(messageText.length, emoteData.end + 1);
      }
      text.append(document.createTextNode(messageText.slice(cursor)));
      content.appendChild(text);
    }
    if (payload.media?.length) {
      const mediaRow = document.createElement("div");
      mediaRow.className = "media-row";
      for (const mediaData of payload.media) {
        const media = document.createElement(mediaData.type === "video" ? "video" : "img");
        media.className = "media";
        media.src = mediaData.url;
        media.setAttribute("aria-label", mediaData.alt || "Animated image");
        media.referrerPolicy = "no-referrer";
        if (media.tagName === "VIDEO") {
          media.autoplay = true;
          media.loop = true;
          media.muted = true;
          media.playsInline = true;
        } else media.alt = mediaData.alt || "Animated image";
        media.addEventListener("error", () => media.remove(), { once: true });
        mediaRow.appendChild(media);
      }
      content.appendChild(mediaRow);
    }
    content.prepend(meta);
    row.appendChild(content);

    if (animate && values.inAnimation !== "none" && Number(values.animationDurationMs) > 0) {
      row.classList.add("enter", `enter-${values.inAnimation}`);
      requestAnimationFrame(() => requestAnimationFrame(() => row.classList.add("enter-active")));
      setTimeout(() => row.classList.remove("enter", "enter-active", `enter-${values.inAnimation}`), Number(values.animationDurationMs));
    }
    return row;
  }

  function removeChatMessage(moduleId, messageId) {
    const state = chatStates.get(moduleId);
    if (!state) return;
    state.messages = state.messages.filter((message) => message.id !== messageId);
    const row = state.list?.querySelector?.(`[data-message-id="${CSS.escape(messageId)}"]`);
    if (!row) return;
    const values = state.values || {};
    if (values.outAnimation !== "none" && Number(values.animationDurationMs) > 0) {
      row.classList.add("leave", `leave-${values.outAnimation}`);
      requestAnimationFrame(() => row.classList.add("leave-active"));
      setTimeout(() => row.remove(), Number(values.animationDurationMs));
    } else row.remove();
  }

  function scheduleChatRemoval(moduleId, payload, values) {
    if (!(Number(values.messageTimeoutSeconds) > 0)) return;
    const remaining = Math.max(0, payload.timestamp + Number(values.messageTimeoutSeconds) * 1000 - Date.now());
    setTimeout(() => removeChatMessage(moduleId, payload.id), remaining);
  }

  const previewChatMessages = [
    { platform: "twitch", text: "Hi chat!", author: { name: "CozyViewer", color: "#f472b6", avatar: "/icons/avatars/preview-pink.svg", badges: [{ label: "Subscriber", image: null }] } },
    { platform: "discord", text: "The badges, avatar, and service icon move together.", author: { name: "PackMember", color: "#a78bfa", avatar: "/icons/avatars/preview-purple.svg", badges: [{ label: "Moderator", image: null }] } },
    { platform: "youtube", text: "Short message", author: { name: "Lumi Fan", color: "#fb7185", avatar: "/icons/avatars/preview-red.svg", badges: [{ label: "Member", image: null }] } },
    { platform: "twitch", text: "This longer sample wraps naturally so you can compare it with compact one-line messages.", author: { name: "StreamFriend", color: "#22d3ee", avatar: "/icons/avatars/preview-cyan.svg", badges: [] } },
    { platform: "discord", text: "Animated arrivals and departures repeat in this preview.", author: { name: "Lumi", color: "#67e8f9", avatar: "/icons/platforms/lumi.svg", badges: [{ label: "Owner", image: null }] } },
    { platform: "youtube", text: "Looks good!", author: { name: "ChatRegular", color: "#fbbf24", avatar: "/icons/avatars/preview-gold.svg", badges: [] } }
  ];

  function nextPreviewMessage(moduleId, state) {
    const allowed = previewChatMessages.filter((message) => (state.values.platforms || []).includes(message.platform));
    const samples = allowed.length ? allowed : previewChatMessages;
    const sample = samples[state.previewIndex % samples.length];
    state.previewIndex += 1;
    return {
      ...sample,
      id: `${moduleId}-preview-${state.previewIndex}-${Date.now()}`,
      preview: true,
      timestamp: Date.now(),
      channel: { name: sample.platform === "discord" ? "#community" : "Live chat" },
      author: { ...sample.author }
    };
  }

  function schedulePreviewChat(moduleId, state) {
    clearTimeout(state.previewTimer);
    const delay = Math.max(1400, Number(state.values.animationDurationMs || 0) * 2 + 500);
    state.previewTimer = setTimeout(() => {
      if (!state.editor || !state.list?.isConnected) {
        state.previewTimer = null;
        return;
      }
      const payload = nextPreviewMessage(moduleId, state);
      state.messages.push(payload);
      const row = chatMessageElement(payload, state.values);
      if (state.values.newestAt === "top") state.list.prepend(row);
      else state.list.appendChild(row);
      const visibleLimit = Math.max(1, Math.min(Number(state.values.maxMessages || 8), 5));
      while (state.messages.length > visibleLimit) removeChatMessage(moduleId, state.messages[0].id);
      schedulePreviewChat(moduleId, state);
    }, delay);
  }

  function buildChat(module, values, options) {
    const root = document.createElement("div");
    root.className = "lumi-overlay-chat-root";
    const shadow = root.attachShadow({ mode: "open" });
    const style = document.createElement("style");
    const chatCustomCss = values.customCss || "";
    const alignment = values.horizontalAlign === "right" ? "flex-end" : values.horizontalAlign === "center" ? "center" : "flex-start";
    const dock = Boolean(options.chatDock);
    style.textContent = `
      :host { display:block; width:100%; ${dock ? "min-height:100%; height:auto; overflow:visible;" : "height:100%; overflow:hidden;"} box-sizing:border-box; background:${values.background}; color:${values.color}; font:${values.fontWeight} ${values.fontSize}px/${values.lineHeight} ${values.fontFamily}; }
      .chat { width:100%; ${dock ? "min-height:100vh; height:auto; overflow:visible;" : "height:100%; overflow:hidden;"} box-sizing:border-box; display:flex; flex-direction:column; align-items:${alignment}; justify-content:${values.newestAt === "bottom" ? "flex-end" : "flex-start"}; gap:${values.gap}px; padding:${values.padding}px; text-align:${values.horizontalAlign}; }
      .message { display:inline-flex; align-items:flex-start; align-self:${alignment}; gap:.55em; box-sizing:border-box; width:fit-content; max-width:100%; padding:.55em .7em; border-radius:${values.borderRadius}px; background:${values.messageBackground}; overflow:hidden; transition:opacity ${values.animationDurationMs}ms ease, transform ${values.animationDurationMs}ms ease; }
      .avatar { width:1.85em; height:1.85em; flex:0 0 auto; border-radius:50%; object-fit:cover; }
      .content { min-width:0; max-width:100%; flex:0 1 auto; }
      .meta { display:flex; align-items:center; justify-content:${alignment}; gap:.4em; min-width:0; margin-bottom:.12em; font-size:.78em; line-height:1.15; }
      .author { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:800; }
      .platform { flex:0 0 auto; width:1.05em; height:1.05em; border-radius:.24em; object-fit:contain; }
      .badge { flex:0 0 auto; width:1.1em; height:1.1em; border-radius:.2em; object-fit:contain; }
      .timestamp { flex:0 0 auto; opacity:.65; font-size:.85em; }
      .text { overflow-wrap:anywhere; white-space:pre-wrap; }
      .emote { display:inline-block; width:auto; height:1.35em; margin:0 .08em; vertical-align:-.3em; object-fit:contain; }
      .media-row { display:flex; flex-wrap:wrap; justify-content:${alignment}; gap:.3em; margin-top:.2em; max-width:100%; }
      .media { display:block; width:auto; height:${Math.max(1.6, Number(values.lineHeight || 1.25) * 2)}em; max-width:100%; border-radius:.25em; object-fit:contain; }
      .enter-fade, .leave-active { opacity:0; }
      .enter-slide-left { opacity:0; transform:translateX(-1.5em); }
      .enter-slide-right { opacity:0; transform:translateX(1.5em); }
      .enter-slide-up { opacity:0; transform:translateY(-1em); }
      .enter-slide-down { opacity:0; transform:translateY(1em); }
      .enter-scale { opacity:0; transform:scale(.92); }
      .enter-active { opacity:1; transform:none; }
      .leave-slide-left.leave-active { transform:translateX(-1.5em); }
      .leave-slide-right.leave-active { transform:translateX(1.5em); }
      .leave-slide-up.leave-active { transform:translateY(-1em); }
      .leave-slide-down.leave-active { transform:translateY(1em); }
      .leave-scale.leave-active { transform:scale(.92); }
      ${chatCustomCss}
      ${dock ? ":host { width:100% !important; min-height:100% !important; height:auto !important; overflow:visible !important; } .chat { width:100% !important; min-height:100vh !important; height:auto !important; overflow:visible !important; } .message, .message * { animation:none !important; transition:none !important; } .message { transform:none !important; opacity:1 !important; }" : ""}
    `;
    const list = document.createElement("div");
    list.className = "chat";
    list.setAttribute("role", "log");
    list.setAttribute("aria-live", "off");
    shadow.append(style, list);

    let state = chatStates.get(module.id);
    if (!state) state = { messages: [], previewIndex: 0 };
    state.values = values;
    state.list = list;
    state.editor = Boolean(options.editor);
    if (options.editor && !state.messages.length) {
      const initialCount = Math.max(1, Math.min(Number(values.maxMessages || 8), 5));
      for (let index = 0; index < initialCount; index += 1) state.messages.push(nextPreviewMessage(module.id, state));
    }
    state.messages = state.messages.slice(-Number(values.maxMessages || 8));
    const acceptedMessages = state.messages.filter((message) => message.preview
      ? (values.platforms || []).includes(message.platform)
      : chatAccepts(values, message));
    const display = values.newestAt === "top" ? [...acceptedMessages].reverse() : acceptedMessages;
    for (const message of display) list.appendChild(chatMessageElement(message, values, { animate: false }));
    chatStates.set(module.id, state);
    if (options.editor) schedulePreviewChat(module.id, state);
    else clearTimeout(state.previewTimer);
    return root;
  }

  function handleChatMessage(root, payload) {
    if (!payload?.id || !payload?.platform || (!payload?.text && !payload?.media?.length)) return 0;
    let accepted = 0;
    root?.querySelectorAll?.('.lumi-overlay-module[data-render-type="chat"]').forEach((box) => {
      const moduleId = box.dataset.moduleId;
      const state = chatStates.get(moduleId);
      if (!state?.list || !chatAccepts(state.values, payload) || state.messages.some((message) => message.id === payload.id)) return;
      state.messages.push(payload);
      const row = chatMessageElement(payload, state.values);
      if (state.values.newestAt === "top") state.list.prepend(row);
      else state.list.appendChild(row);
      while (state.messages.length > Number(state.values.maxMessages || 8)) removeChatMessage(moduleId, state.messages[0].id);
      scheduleChatRemoval(moduleId, payload, state.values);
      accepted += 1;
    });
    return accepted;
  }

  function findSourceContent(root, moduleId, selector) {
    const box = root?.querySelector?.(`.lumi-overlay-module[data-module-id="${CSS.escape(moduleId)}"]`);
    return box?.querySelector?.(selector) || box?.querySelector?.(".lumi-overlay-web-root")?.shadowRoot?.querySelector?.(selector) || null;
  }

  function buildModule(module, options = {}) {
    const renderType = module.renderType || module.type;
    const values = module.config || {};
    const scale = finite(options.scale, 1);
    const box = document.createElement("section");
    box.className = `lumi-overlay-module lumi-overlay-module-${renderType}`;
    if (options.editor) box.classList.add("is-editor-source");
    box.dataset.moduleId = module.id;
    box.dataset.moduleType = module.type;
    box.dataset.renderType = renderType;
    box.dataset.moduleName = module.name || "Source";
    applyBox(box, values);
    if (values.eventOnly && !options.editor) {
      box.hidden = true;
      box.dataset.eventOnly = "true";
    }

    if (renderType === "text") {
      box.classList.add("lumi-overlay-text", `overflow-${values.overflow || "wrap"}`);
      const content = document.createElement("span");
      content.className = "lumi-overlay-text-content";
      content.textContent = values.text || "";
      const horizontal = values.horizontalAlign || values.align || "left";
      const vertical = values.verticalAlign || "center";
      box.style.textAlign = horizontal;
      box.style.justifyContent = horizontal === "center" ? "center" : horizontal === "right" ? "flex-end" : "flex-start";
      box.style.alignItems = vertical === "center" ? "center" : vertical === "bottom" ? "flex-end" : "flex-start";
      box.style.color = values.color || "#ffffff";
      box.style.background = values.background || "transparent";
      box.style.padding = `${finite(values.padding, 0) * scale}px`;
      content.style.fontWeight = `${finite(values.fontWeight, 700)}`;
      content.style.fontSize = `${finite(values.fontSize, 48) * scale}px`;
      box.appendChild(content);
      if (values.overflow === "shrink") shrinkText(box, content, finite(values.fontSize, 48), 8, scale);
    } else if (renderType === "chat") {
      box.dataset.renderType = "chat";
      box.appendChild(buildChat(module, values, options));
    } else if (renderType === "image" && values.url) {
      const image = document.createElement("img");
      image.className = "lumi-overlay-image";
      image.alt = "";
      image.src = values.url;
      image.referrerPolicy = "no-referrer";
      image.style.objectFit = values.fit || "contain";
      box.appendChild(image);
    } else if ((renderType === "video" || renderType === "audio") && values.url) {
      const media = buildMedia(module, values, options, renderType);
      box.appendChild(media);
      if (renderType === "audio" && options.editor) {
        const placeholder = document.createElement("span");
        placeholder.className = "lumi-overlay-audio-placeholder";
        placeholder.textContent = "Audio source";
        box.appendChild(placeholder);
      }
    } else if (renderType === "web" && values.url) {
      box.appendChild(buildWebsite(module, values, options));
    }

    if (options.editor) {
      const tag = document.createElement("span");
      tag.className = "overlay-canvas-source-label";
      tag.textContent = module.name || "Source";
      box.appendChild(tag);
      for (const direction of ["nw", "n", "ne", "e", "se", "s", "sw", "w"]) {
        const handle = document.createElement("button");
        handle.type = "button";
        handle.tabIndex = -1;
        handle.className = `overlay-resize-handle handle-${direction}`;
        handle.dataset.resizeHandle = direction;
        handle.setAttribute("aria-label", `Resize ${module.name || "source"} from ${direction}`);
        box.appendChild(handle);
      }
    }
    return box;
  }

  function render(root, state, options = {}) {
    const canvas = options.canvas || state?.canvas || { width: 1920, height: 1080 };
    const layout = stageLayout(root, canvas, options.scale);
    const stage = document.createElement("div");
    stage.className = "lumi-overlay-stage";
    stage.style.width = `${layout.width}px`;
    stage.style.height = `${layout.height}px`;
    stage.style.transform = `scale(${layout.scaleX}, ${layout.scaleY})`;
    if (options.editor) {
      const editorScale = Math.max(0.01, Math.min(layout.scaleX, layout.scaleY));
      const handleSize = window.matchMedia?.("(max-width: 700px)").matches ? 14 : 10;
      stage.style.setProperty("--lumi-editor-handle-size", `${handleSize / editorScale}px`);
      stage.style.setProperty("--lumi-editor-outline-size", `${2 / editorScale}px`);
      stage.style.setProperty("--lumi-editor-label-font-size", `${11.2 / editorScale}px`);
      stage.style.setProperty("--lumi-editor-label-max-width", `${192 / editorScale}px`);
      stage.style.setProperty("--lumi-editor-label-padding-y", `${3.2 / editorScale}px`);
      stage.style.setProperty("--lumi-editor-label-padding-x", `${6.4 / editorScale}px`);
      stage.style.setProperty("--lumi-editor-label-radius", `${4 / editorScale}px`);
    }
    const elements = new Map();
    if (state?.enabled !== false && state?.scene) {
      for (const module of state.scene.modules || []) {
        if (module.enabled === false) continue;
        const element = buildModule(module, { ...options, canvas, scale: 1 });
        elements.set(module.id, element);
        stage.appendChild(element);
      }
    }
    root.replaceChildren(stage);
    return elements;
  }

  function renderChatDock(root, module) {
    if (!root) return null;
    if (!module || (module.renderType || module.type) !== "chat") {
      root.replaceChildren();
      return null;
    }
    const wrapper = document.createElement("section");
    wrapper.className = "lumi-overlay-module lumi-overlay-chat-dock";
    wrapper.dataset.moduleId = module.id;
    wrapper.dataset.moduleType = module.type;
    wrapper.dataset.renderType = "chat";
    wrapper.appendChild(buildChat({
      ...module,
      config: { ...(module.config || {}), inAnimation: "none", outAnimation: "none", animationDurationMs: 0 }
    }, { chatDock: true }));
    root.replaceChildren(wrapper);
    return wrapper;
  }

  function stageLayout(root, canvas, scaleOverride) {
    const width = Math.max(1, finite(canvas?.width, 1920));
    const height = Math.max(1, finite(canvas?.height, 1080));
    const override = finite(scaleOverride, 0);
    return {
      width,
      height,
      scaleX: override > 0 ? override : (root?.clientWidth ? root.clientWidth / width : 1),
      scaleY: override > 0 ? override : (root?.clientHeight ? root.clientHeight / height : 1)
    };
  }

  window.LumiOverlayRenderer = { anchors, applyBox, buildModule, findSourceContent, handleChatMessage, render, renderChatDock, restartMediaElement, stageLayout };
})();
