// AdBlock-Maxxer — Twitch extras (ISOLATED world, twitch.tv).
//
// Injects two things directly into Twitch's own UI (no floating widget):
//   1. Pinned streamers — a "Pinned" section ABOVE "Followed Channels" in the left
//      sidebar. Live dot + viewer count, click to open, ✕ to unpin, ＋ to pin the
//      channel you're currently watching.
//   2. Stream latency — a small readout at the very top of the chat column on a
//      channel page, fed by twitch-latency-main.js (the MAIN-world player reader).
//
// Twitch is a React SPA with obfuscated, constantly re-rendering markup, so instead of
// watching every mutation we re-assert our nodes on a bounded 1.5s tick (cheap + robust):
// if Twitch wiped them on a re-render, we put them back. Honours the "twitchExtras" toggle
// and the per-site allowlist. Live status is fetched through the worker (no login).

(function () {
  "use strict";
  if (window.top !== window) return;
  if (window.__abmTwitchExtras) return;
  window.__abmTwitchExtras = true;

  const RESERVED = new Set([
    "directory", "videos", "settings", "u", "search", "subscriptions", "following",
    "wallet", "drops", "prime", "turbo", "friends", "downloads", "store", "jobs",
    "p", "popout", "team", "communities", "products", "bits", "moderator", "dashboard",
    "", "login", "signup",
  ]);

  let enabled = true;
  let allowed = true;
  let pinned = [];
  let latency = null;
  let statusMap = {}; // login -> {live, viewers, name}

  // fullscreen chat overlay state
  let overlayOn = false;       // the "twitchChatOverlay" setting
  let overlayMin = false;      // minimized (header only)
  let overlayPos = null;       // {x, y}
  let overlaySize = null;      // {w, h} from drag-resizing
  let overlayEl = null;
  let chatObserver = null;
  let mirrorSrc = null;        // the chat node we're currently observing (live container or VOD <ul>)
  let idleTimer = null;
  let forceQuality = false; // "twitchForceQuality" — keep the player at source res through ad-swaps

  function currentLogin() {
    const seg = location.pathname.split("/").filter(Boolean);
    if (!seg.length) return null;
    if (seg[0] === "popout" && seg[1]) return seg[1].toLowerCase();
    if (RESERVED.has(seg[0])) return null;
    return seg[0].toLowerCase();
  }
  function refreshAllow(list) {
    const h = location.hostname;
    allowed = !((list || []).some((d) => h === d || h.endsWith("." + d)));
  }
  function shouldShow() { return enabled && allowed; }
  function savePins(next) { chrome.storage.local.set({ pinnedStreamers: next }); }

  // ---- latency feed from the MAIN-world reader -----------------------------
  window.addEventListener("message", (e) => {
    if (e.source !== window || !e.data || e.data.__abmTwitch !== "latency") return;
    latency = typeof e.data.value === "number" ? e.data.value : null;
    paintLatency();
  });

  // ---- storage -------------------------------------------------------------
  chrome.storage.local.get(
    ["twitchExtras", "allowlist", "pinnedStreamers", "twitchChatOverlay", "twitchOverlayPos", "twitchOverlayMin", "twitchOverlaySize", "twitchForceQuality"],
    (r) => {
      if (r && r.twitchExtras === false) enabled = false;
      pinned = (r && Array.isArray(r.pinnedStreamers)) ? r.pinnedStreamers : [];
      overlayOn = !!(r && r.twitchChatOverlay);
      overlayPos = (r && r.twitchOverlayPos) || null;
      overlayMin = !!(r && r.twitchOverlayMin);
      overlaySize = (r && r.twitchOverlaySize) || null;
      forceQuality = !!(r && r.twitchForceQuality);
      refreshAllow(r && r.allowlist);
      applyForceQuality();
      syncDom();
      fetchStatus();
      updateOverlay();
    }
  );
  chrome.storage.onChanged.addListener((c) => {
    if (c.twitchExtras) { enabled = c.twitchExtras.newValue !== false; syncDom(); }
    if (c.allowlist) { refreshAllow(c.allowlist.newValue); syncDom(); updateOverlay(); }
    if (c.pinnedStreamers) {
      pinned = Array.isArray(c.pinnedStreamers.newValue) ? c.pinnedStreamers.newValue : [];
      renderSidebar(); fetchStatus();
    }
    if (c.twitchChatOverlay) { overlayOn = !!c.twitchChatOverlay.newValue; updateOverlay(); }
    if (c.twitchForceQuality) { forceQuality = !!c.twitchForceQuality.newValue; applyForceQuality(); }
  });

  // Force source quality (opt-in): set Twitch's default-quality pref to source AND tell the
  // MAIN-world player reader to re-assert top quality (it owns the player instance). The
  // backup-stream swap that hides ads can otherwise land you at a lower res.
  function applyForceQuality() {
    if (forceQuality) {
      try { localStorage.setItem("video-quality", '{"default":"chunked"}'); } catch (e) {}
    }
    try { window.postMessage({ __abmTwitch: "forceQuality", on: forceQuality }, "*"); } catch (e) {}
  }

  // ---- live status (via background → Twitch GQL) ---------------------------
  function fetchStatus() {
    if (!pinned.length || document.visibilityState !== "visible") return;
    try {
      chrome.runtime.sendMessage({ type: "twitchStatus", logins: pinned }, (resp) => {
        if (chrome.runtime.lastError || !resp || !resp.users) return;
        const m = {};
        for (const u of resp.users) m[u.login] = u;
        statusMap = m;
        renderSidebar();
      });
    } catch (e) {}
  }

  // ---- styles (blended with Twitch dark theme) -----------------------------
  function ensureStyle() {
    if (document.getElementById("abm-twitch-style")) return;
    const s = document.createElement("style");
    s.id = "abm-twitch-style";
    s.textContent = `
      #abm-pinned-section { padding: 4px 0 6px; }
      .abm-pin-head { display:flex; align-items:center; gap:6px; padding:6px 16px 4px;
        font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:#adadb8; }
      .abm-pin-head .abm-add { margin-left:auto; cursor:pointer; color:#bf94ff; font-size:16px;
        line-height:1; padding:0 5px; border-radius:4px; }
      .abm-pin-head .abm-add:hover { background:rgba(145,71,255,.2); }
      .abm-pin-row { display:flex; align-items:center; gap:9px; padding:5px 16px; cursor:pointer; color:#efeff1; font-size:13px; }
      .abm-pin-row:hover { background:rgba(255,255,255,.08); }
      .abm-pin-row .abm-dot { width:8px;height:8px;border-radius:50%;background:#53535f;flex:none; }
      .abm-pin-row.abm-live .abm-dot { background:#eb0400; }
      .abm-pin-row .abm-nm { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
      .abm-pin-row .abm-vc { color:#adadb8; font-size:12px; flex:none; }
      .abm-pin-row .abm-x { color:#6b6b75; opacity:0; padding:0 3px; flex:none; }
      .abm-pin-row:hover .abm-x { opacity:1; }
      .abm-pin-row .abm-x:hover { color:#eb0400; }
      #abm-chat-latency { display:flex; align-items:center; gap:6px; padding:5px 12px; font-size:12px;
        color:#bf94ff; background:rgba(145,71,255,.08); border-bottom:1px solid rgba(255,255,255,.08); }
      #abm-chat-latency b { color:#efeff1; font-weight:700; }
      #abm-chat-latency .abm-ll { color:#adadb8; }

      /* fullscreen chat overlay — transparent, draggable, minimizable, resizable (custom handles) */
      #abm-chat-overlay { position:fixed; z-index:2147483647; width:340px; height:300px;
        min-width:170px; min-height:74px; max-width:80vw; max-height:92vh;
        background:transparent; border-radius:10px; user-select:none;
        display:flex; flex-direction:column;
        font:13px/1.45 Inter,-apple-system,"Segoe UI",Roboto,sans-serif; }
      #abm-chat-overlay.abm-co-min { height:auto !important; min-height:0; }
      #abm-chat-overlay .abm-co-hd { flex:0 0 auto; display:flex; align-items:center; gap:8px; cursor:move;
        padding:5px 10px; font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
        color:#fff; background:rgba(0,0,0,.55); border-radius:8px 8px 0 0; }
      #abm-chat-overlay .abm-co-hd .abm-co-sp { flex:1; }
      #abm-chat-overlay .abm-co-hd .abm-co-btn { cursor:pointer; color:#cfcfd6; padding:0 5px; border-radius:4px; font-size:14px; }
      #abm-chat-overlay .abm-co-hd .abm-co-btn:hover { color:#fff; background:rgba(255,255,255,.18); }
      #abm-chat-overlay .abm-co-body { flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden; padding:6px 10px 8px;
        background:linear-gradient(180deg,rgba(0,0,0,.32),rgba(0,0,0,.16)); border-radius:0 0 10px 10px; }
      #abm-chat-overlay.abm-co-min .abm-co-body { display:none; }
      #abm-chat-overlay.abm-co-min .abm-co-hd { border-radius:8px; }
      /* drag-to-resize handles on every edge + corner */
      #abm-chat-overlay .abm-rz { position:absolute; z-index:2; }
      #abm-chat-overlay .abm-rz-n { top:-4px; left:0; right:0; height:9px; cursor:ns-resize; }
      #abm-chat-overlay .abm-rz-s { bottom:-4px; left:0; right:0; height:9px; cursor:ns-resize; }
      #abm-chat-overlay .abm-rz-w { top:0; bottom:0; left:-4px; width:9px; cursor:ew-resize; }
      #abm-chat-overlay .abm-rz-e { top:0; bottom:0; right:-4px; width:9px; cursor:ew-resize; }
      #abm-chat-overlay .abm-rz-nw { top:-4px; left:-4px; width:16px; height:16px; cursor:nwse-resize; z-index:3; }
      #abm-chat-overlay .abm-rz-ne { top:-4px; right:-4px; width:16px; height:16px; cursor:nesw-resize; z-index:3; }
      #abm-chat-overlay .abm-rz-sw { bottom:-4px; left:-4px; width:16px; height:16px; cursor:nesw-resize; z-index:3; }
      #abm-chat-overlay .abm-rz-se { bottom:-4px; right:-4px; width:16px; height:16px; cursor:nwse-resize; z-index:3; }
      #abm-chat-overlay.abm-co-min .abm-rz { display:none; }
      /* idle "ghost" mode — after a few seconds untouched, fade the chrome so only chat shows */
      #abm-chat-overlay .abm-co-hd { transition:opacity .35s ease; }
      #abm-chat-overlay .abm-co-body { transition:background .35s ease; }
      #abm-chat-overlay.abm-co-idle .abm-co-hd { opacity:0; }
      #abm-chat-overlay.abm-co-idle .abm-co-body { background:transparent; }
      #abm-chat-overlay .abm-co-line { padding:2px 0; color:#fff; word-wrap:break-word;
        text-shadow:0 1px 2px rgba(0,0,0,.95), 0 0 3px rgba(0,0,0,.9); transition:opacity .6s ease; }
      #abm-chat-overlay .abm-co-line.abm-fade { opacity:0 !important; }
      #abm-chat-overlay .abm-co-line .chat-author__display-name { font-weight:700; }
      #abm-chat-overlay .abm-co-body::-webkit-scrollbar { width:6px; }
      #abm-chat-overlay .abm-co-body::-webkit-scrollbar-thumb { background:rgba(255,255,255,.25); border-radius:3px; }
    `;
    (document.head || document.documentElement).appendChild(s);
  }

  // ---- sidebar: Pinned section above Followed Channels ---------------------
  function findFollowedSection(nav) {
    const secs = nav.querySelectorAll('[data-test-selector="side-nav-section"], .side-nav-section');
    for (const s of secs) {
      if (/follow/i.test((s.textContent || "").slice(0, 40))) return s;
    }
    // fallback: a heading whose text starts with "Followed"
    const heads = nav.querySelectorAll('h2,h3,[role="heading"],[data-a-target="side-nav-title"]');
    for (const h of heads) {
      if (/^\s*followed/i.test(h.textContent || "")) {
        return h.closest('[data-test-selector="side-nav-section"], .side-nav-section, div') || h.parentElement;
      }
    }
    return null;
  }

  function injectSidebar() {
    const nav = document.querySelector('#side-nav, [data-a-target="side-nav-bar"], nav.side-nav, .side-nav');
    let sec = document.getElementById("abm-pinned-section");
    const followed = nav ? findFollowedSection(nav) : null;
    if (!nav || !followed) { if (sec) sec.remove(); return; } // can't anchor (collapsed / not loaded yet)
    if (!sec) {
      sec = document.createElement("div");
      sec.id = "abm-pinned-section";
      sec.innerHTML =
        '<div class="abm-pin-head"><span>Pinned</span><span class="abm-add" title="Pin the channel you\'re watching">＋</span></div>' +
        '<div class="abm-pin-list"></div>';
      sec.querySelector(".abm-add").addEventListener("click", pinCurrent);
    }
    if (followed.parentNode && followed.previousElementSibling !== sec) {
      followed.parentNode.insertBefore(sec, followed);
    }
    renderSidebar();
  }

  function renderSidebar() {
    const sec = document.getElementById("abm-pinned-section");
    if (!sec) return;
    const list = sec.querySelector(".abm-pin-list");
    if (!list) return;
    const sorted = pinned.slice().sort((a, b) => {
      const la = statusMap[a] && statusMap[a].live ? 1 : 0;
      const lb = statusMap[b] && statusMap[b].live ? 1 : 0;
      return lb - la || a.localeCompare(b);
    });
    list.innerHTML = "";
    for (const login of sorted) {
      const s = statusMap[login] || {};
      const row = document.createElement("div");
      row.className = "abm-pin-row" + (s.live ? " abm-live" : "");
      const vc = s.live ? (s.viewers >= 1000 ? (s.viewers / 1000).toFixed(1) + "k" : s.viewers) : "";
      row.innerHTML =
        '<span class="abm-dot"></span>' +
        '<span class="abm-nm">' + (s.name || login) + "</span>" +
        '<span class="abm-vc">' + vc + "</span>" +
        '<span class="abm-x" title="Unpin">✕</span>';
      row.addEventListener("click", (e) => {
        if (e.target.classList.contains("abm-x")) return;
        location.assign("https://www.twitch.tv/" + login);
      });
      row.querySelector(".abm-x").addEventListener("click", (e) => {
        e.stopPropagation();
        pinned = pinned.filter((p) => p !== login);
        savePins(pinned); renderSidebar();
      });
      list.appendChild(row);
    }
  }

  function pinCurrent() {
    const login = currentLogin();
    if (!login || pinned.includes(login)) return;
    pinned = pinned.concat([login]);
    savePins(pinned); renderSidebar(); fetchStatus();
  }

  // ---- chat: latency readout at the top of the chat column -----------------
  function injectChat() {
    const header = document.querySelector(
      '.stream-chat-header, [data-test-selector="chat-room-header-label"], [data-a-target="chat-room-header"]'
    );
    let bar = document.getElementById("abm-chat-latency");
    if (!header) { if (bar) bar.remove(); return; }
    if (!bar) {
      bar = document.createElement("div");
      bar.id = "abm-chat-latency";
      bar.innerHTML = '<span>⚡</span><b id="abm-lat-val">—</b><span class="abm-ll" id="abm-lat-lbl">stream latency</span>';
    }
    const container =
      header.closest('.chat-room__content, [data-test-selector="chat-room-component-layout"], .stream-chat, section[data-test-selector="chat-room-component"]') ||
      header.parentElement;
    if (container && container.firstElementChild !== bar) {
      container.insertBefore(bar, container.firstElementChild);
    }
    paintLatency();
  }

  function paintLatency() {
    const v = document.getElementById("abm-lat-val");
    if (!v) return;
    v.textContent = latency == null ? "—" : latency.toFixed(1) + "s";
  }

  // ---- fullscreen chat overlay ---------------------------------------------
  // Mirrors live chat into a transparent, draggable, minimizable box. Lives INSIDE the
  // fullscreen element (anything outside it isn't rendered while fullscreen), so you can
  // read chat over the video. Off by default; toggle = "twitchChatOverlay".
  function chatSource() {
    // Live chat: messages append directly into this container.
    const live = document.querySelector(
      '.chat-scrollable-area__message-container, [data-test-selector="chat-scrollable-area__message-container"]'
    );
    if (live) return live;
    // VOD chat replay: messages append as <li> into the <ul> inside the message-list wrapper.
    const vod = document.querySelector(".video-chat__message-list-wrapper");
    if (vod) return vod.querySelector("ul") || vod;
    return null;
  }
  function fsRoot() {
    return document.fullscreenElement || document.webkitFullscreenElement || null;
  }
  function clampOverlay(p, host) {
    const w = overlayEl ? overlayEl.offsetWidth || 340 : 340;
    const h = 60;
    const W = host ? host.clientWidth : window.innerWidth;
    const H = host ? host.clientHeight : window.innerHeight;
    return { x: Math.min(Math.max(8, p.x), Math.max(8, W - w - 8)), y: Math.min(Math.max(8, p.y), Math.max(8, H - h - 8)) };
  }
  function placeOverlay() {
    if (!overlayEl) return;
    const host = overlayEl.parentElement;
    const p = clampOverlay(overlayPos || { x: 16, y: 70 }, host);
    overlayEl.style.left = p.x + "px";
    overlayEl.style.top = p.y + "px";
  }

  // "Ghost" idle: 5s after the last interaction, fade the header + body background so
  // only the chat text remains over the video. Any mouse activity (or a drag) wakes it.
  function wake() {
    if (!overlayEl) return;
    overlayEl.classList.remove("abm-co-idle");
    if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
    if (overlayMin) return; // minimized = just the bar; keep it visible
    idleTimer = setTimeout(() => { if (overlayEl) overlayEl.classList.add("abm-co-idle"); }, 5000);
  }

  // Resize by dragging any edge/corner. `dir` is some combination of n/s/e/w.
  function startResize(e, dir) {
    wake();
    if (e.button !== 0 || !overlayEl || overlayMin) return;
    e.preventDefault(); e.stopPropagation();
    const host = overlayEl.parentElement;
    const hostRect = host.getBoundingClientRect();
    const rect = overlayEl.getBoundingClientRect();
    const x0 = rect.left - hostRect.left, y0 = rect.top - hostRect.top;
    const w0 = rect.width, h0 = rect.height;
    const sx = e.clientX, sy = e.clientY;
    const minW = 170, minH = 74;
    const maxW = host.clientWidth * 0.95, maxH = host.clientHeight * 0.95;
    const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
    function move(ev) {
      let nx = x0, ny = y0, nw = w0, nh = h0;
      const dx = ev.clientX - sx, dy = ev.clientY - sy;
      if (dir.includes("e")) nw = clamp(w0 + dx, minW, maxW);
      if (dir.includes("s")) nh = clamp(h0 + dy, minH, maxH);
      if (dir.includes("w")) { nw = clamp(w0 - dx, minW, maxW); nx = x0 + (w0 - nw); }
      if (dir.includes("n")) { nh = clamp(h0 - dy, minH, maxH); ny = y0 + (h0 - nh); }
      nx = clamp(nx, 0, Math.max(0, host.clientWidth - nw));
      ny = clamp(ny, 0, Math.max(0, host.clientHeight - nh));
      overlayEl.style.left = nx + "px"; overlayEl.style.top = ny + "px";
      overlayEl.style.width = nw + "px"; overlayEl.style.height = nh + "px";
    }
    function up() {
      document.removeEventListener("mousemove", move, true);
      document.removeEventListener("mouseup", up, true);
      overlayPos = { x: parseFloat(overlayEl.style.left) || 0, y: parseFloat(overlayEl.style.top) || 0 };
      overlaySize = { w: overlayEl.offsetWidth, h: overlayEl.offsetHeight };
      chrome.storage.local.set({ twitchOverlayPos: overlayPos, twitchOverlaySize: overlaySize });
    }
    document.addEventListener("mousemove", move, true);
    document.addEventListener("mouseup", up, true);
  }
  function mirrorLine(node) {
    if (!overlayEl) return;
    const body = overlayEl.querySelector(".abm-co-body");
    if (!body) return;
    let clone;
    try { clone = node.cloneNode(true); } catch (e) { return; }
    clone.classList.add("abm-co-line");
    clone.setAttribute("data-abm-t", String(Date.now())); // when it appeared (for the 15s sweep)
    body.appendChild(clone);
    while (body.children.length > 80) body.removeChild(body.firstChild); // cap memory
    body.scrollTop = body.scrollHeight;
  }

  // Stale-chat cleanup: any line on screen >15s fades out and is removed, so a dead chat
  // doesn't leave a frozen wall of old text over the video. One sweep, deterministic.
  function sweepOverlay() {
    if (!overlayEl) return;
    const body = overlayEl.querySelector(".abm-co-body");
    if (!body) return;
    const now = Date.now();
    Array.from(body.children).forEach((ln) => {
      const t = parseInt(ln.getAttribute("data-abm-t") || "0", 10);
      if (t && now - t > 15000 && !ln.classList.contains("abm-fade")) {
        ln.classList.add("abm-fade");
        setTimeout(() => { if (ln.parentNode) ln.parentNode.removeChild(ln); }, 600);
      }
    });
  }
  function startMirror() {
    stopMirror();
    const src = chatSource();
    if (!src) return;
    mirrorSrc = src;
    // Seed with the last few existing lines. New messages arrive as direct children of `src`
    // for both live (message divs) and VOD replay (<li> rows), so seed from children directly.
    const existing = src.children;
    for (let i = Math.max(0, existing.length - 20); i < existing.length; i++) mirrorLine(existing[i]);
    chatObserver = new MutationObserver((muts) => {
      for (const m of muts) {
        for (const n of m.addedNodes) {
          if (n.nodeType === 1) mirrorLine(n);
        }
      }
    });
    chatObserver.observe(src, { childList: true });
  }
  function stopMirror() {
    if (chatObserver) { chatObserver.disconnect(); chatObserver = null; }
    mirrorSrc = null;
  }
  // Twitch swaps the chat container on re-render (and VOD replay rebuilds the <ul> on every
  // seek) — re-point the observer when the live source node changes or detaches.
  function ensureMirror() {
    if (!overlayEl) return;
    const src = chatSource();
    if (!src) return;
    if (src !== mirrorSrc || !mirrorSrc || !mirrorSrc.isConnected) startMirror();
  }
  function buildOverlay(parent) {
    // Already built: if Twitch detached us during a re-render, just re-attach the SAME node
    // (keeps the existing messages + their ages — never re-seed, or stale lines never expire).
    if (overlayEl) {
      if (overlayEl.parentElement !== parent) parent.appendChild(overlayEl);
      placeOverlay();
      return;
    }
    overlayEl = document.createElement("div");
    overlayEl.id = "abm-chat-overlay";
    if (overlayMin) overlayEl.classList.add("abm-co-min");
    overlayEl.innerHTML =
      '<div class="abm-co-hd"><span>Chat</span><span class="abm-co-sp"></span>' +
      '<span class="abm-co-btn abm-co-minbtn" title="Minimize / expand">' + (overlayMin ? "▢" : "—") + "</span></div>" +
      '<div class="abm-co-body"></div>';
    if (overlaySize && overlaySize.w && overlaySize.h) {
      overlayEl.style.width = overlaySize.w + "px";
      overlayEl.style.height = overlaySize.h + "px";
    }
    parent.appendChild(overlayEl);

    // Custom resize handles — grab any edge or corner (more reliable than CSS `resize`,
    // which misbehaves inside the fullscreen element).
    [["n", "abm-rz-n"], ["s", "abm-rz-s"], ["w", "abm-rz-w"], ["e", "abm-rz-e"],
     ["nw", "abm-rz-nw"], ["ne", "abm-rz-ne"], ["sw", "abm-rz-sw"], ["se", "abm-rz-se"]
    ].forEach(([dir, cls]) => {
      const h = document.createElement("div");
      h.className = "abm-rz " + cls;
      h.addEventListener("mousedown", (e) => startResize(e, dir));
      overlayEl.appendChild(h);
    });

    // wake on any pointer activity over the overlay
    overlayEl.addEventListener("mousemove", wake);
    overlayEl.addEventListener("mousedown", wake);

    const hd = overlayEl.querySelector(".abm-co-hd");
    hd.addEventListener("mousedown", (e) => {
      if (e.target.classList.contains("abm-co-btn")) return;
      const host = overlayEl.parentElement;
      const rect = overlayEl.getBoundingClientRect();
      const hostRect = host.getBoundingClientRect();
      const offX = e.clientX - rect.left, offY = e.clientY - rect.top;
      function move(ev) {
        overlayPos = clampOverlay({ x: ev.clientX - hostRect.left - offX, y: ev.clientY - hostRect.top - offY }, host);
        overlayEl.style.left = overlayPos.x + "px"; overlayEl.style.top = overlayPos.y + "px";
      }
      function up() {
        document.removeEventListener("mousemove", move, true);
        document.removeEventListener("mouseup", up, true);
        if (overlayPos) chrome.storage.local.set({ twitchOverlayPos: overlayPos });
      }
      document.addEventListener("mousemove", move, true);
      document.addEventListener("mouseup", up, true);
      e.preventDefault();
    });
    overlayEl.querySelector(".abm-co-minbtn").addEventListener("click", () => {
      overlayMin = !overlayMin;
      overlayEl.classList.toggle("abm-co-min", overlayMin);
      overlayEl.querySelector(".abm-co-minbtn").textContent = overlayMin ? "▢" : "—";
      chrome.storage.local.set({ twitchOverlayMin: overlayMin });
      wake();
    });

    placeOverlay();
    startMirror();
    wake();
  }
  function destroyOverlay(stop) {
    if (stop !== false) stopMirror();
    if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
    if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl);
    overlayEl = null;
  }
  function updateOverlay() {
    const root = fsRoot();
    // Only while fullscreen (windowed already shows chat) + setting on + feature allowed.
    if (overlayOn && shouldShow() && root) { ensureStyle(); buildOverlay(root); ensureMirror(); }
    else destroyOverlay();
  }
  document.addEventListener("fullscreenchange", updateOverlay);
  document.addEventListener("webkitfullscreenchange", updateOverlay);

  // ---- run -----------------------------------------------------------------
  function removeAll() {
    ["abm-pinned-section", "abm-chat-latency"].forEach((id) => {
      const e = document.getElementById(id);
      if (e) e.remove();
    });
  }
  function syncDom() {
    if (!shouldShow()) { removeAll(); destroyOverlay(); return; }
    ensureStyle();
    injectSidebar();
    injectChat();
    updateOverlay(); // re-assert the fullscreen overlay if Twitch re-rendered the player
    if (forceQuality) { try { window.postMessage({ __abmTwitch: "forceQuality", on: true }, "*"); } catch (e) {} }
  }

  // Bounded re-assert (Twitch re-renders wipe injected nodes; this puts them back).
  setInterval(syncDom, 1500);
  setInterval(sweepOverlay, 2000);
  setInterval(() => { if (shouldShow()) fetchStatus(); }, 60000);
  document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") fetchStatus(); });
})();
