☰

e621 extras

Downvotes-to-blacklist manager, post tree viewer, hover zoom and tag scripts for regular members of e621 and e926.

As of 16. 10. 2026. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        e621 extras
// @version     1.5.3
// @description Downvotes-to-blacklist manager, post tree viewer, hover zoom and tag scripts for regular members of e621 and e926.
// @namespace   e621-extras
// @author      Converter Script
// @match       https://e621.net/*
// @match       https://e926.net/*
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_listValues
// @grant       GM_deleteValue
// @grant       GM_xmlhttpRequest
// @grant       GM_registerMenuCommand
// @grant       GM_openInTab
// @run-at      document-end
// ==/UserScript==

console.log("Script start:",performance.now());const e=!0,t=e=>e,o="passthrough";let s,c={createHTML:t,createScript:t,createScriptURL:t},i=!1;const r=()=>{try{void 0!==window.isSecureContext&&window.isSecureContext&&window.trustedTypes&&window.trustedTypes.createPolicy&&(i=!0,trustedTypes.defaultPolicy?(l("TT Default Policy exists"),c=window.trustedTypes.createPolicy("default",c),s=trustedTypes.defaultPolicy,l(`Created custom passthrough policy, in case the default policy is too restrictive: Use Policy '${o}' in var 'TTP':`,c)):s=c=window.trustedTypes.createPolicy("default",c),l("Trusted-Type Policies: TTP:",c,"TTP_default:",s))}catch(e){l(e)}},l=(...e)=>{console.log(...e)};r();

(function() {
    // #region Logging
	
	  const SCRIPT_NAME = "e621 extras";
	  const _log = (...args) => {};
	  const _warn = (...args) => console.warn(`[${typeof SCRIPT_NAME === 'string' ? SCRIPT_NAME : '[USERSCRIPT_CONVERTED]'}]`, ...args);
	  const _error = (...args) => {
	    let e = args[0];
	    console.error(`[${typeof SCRIPT_NAME === 'string' ? SCRIPT_NAME : '[USERSCRIPT_CONVERTED]'}]`, ...args);
	  }
	  
    // #endregion
    // #region Unified Polyfill
	
// #region Messaging implementation
		
		function createEventBus(
		  scopeId,
		  type = "page", // "page" or "iframe"
		  { allowedOrigin = "*", children = [], parentWindow = null } = {}
		) {
		  if (!scopeId) throw new Error("createEventBus requires a scopeId");
		
		  const handlers = {};
		
		  function handleIncoming(ev) {
		    if (allowedOrigin !== "*" && ev.origin !== allowedOrigin) return;
		
		    const msg = ev.data;
		    if (!msg || msg.__eventBus !== true || msg.scopeId !== scopeId) return;
		
		    const { event, payload } = msg;
		
		    // PAGE: if it's an INIT from an iframe, adopt it
		    if (type === "page" && event === "__INIT__") {
		      const win = ev.source;
		      if (win && !children.includes(win)) {
		        children.push(win);
		      }
		      return;
		    }
		
		    (handlers[event] || []).forEach((fn) =>
		      fn(payload, { origin: ev.origin, source: ev.source })
		    );
		  }
		
		  window.addEventListener("message", handleIncoming);
		
		  function emitTo(win, event, payload) {
		    const envelope = {
		      __eventBus: true,
		      scopeId,
		      event,
		      payload,
		    };
		    win.postMessage(envelope, allowedOrigin);
		  }
		
		  // IFRAME: announce to page on startup
		  if (type === "iframe") {
		    setTimeout(() => {
		      const pw = parentWindow || window.parent;
		      if (pw && pw.postMessage) {
		        emitTo(pw, "__INIT__", null);
		      }
		    }, 0);
		  }
		
		  return {
		    on(event, fn) {
		      handlers[event] = handlers[event] || [];
		      handlers[event].push(fn);
		    },
		    off(event, fn) {
		      if (!handlers[event]) return;
		      handlers[event] = handlers[event].filter((h) => h !== fn);
		    },
		    /**
		     * Emits an event.
		     * @param {string} event - The event name.
		     * @param {any} payload - The event payload.
		     * @param {object} [options] - Emission options.
		     * @param {Window} [options.to] - A specific window to target. If provided, message is ONLY sent to the target.
		     */
		    emit(event, payload, { to } = {}) {
		      // If a specific target window is provided, send only to it and DO NOT dispatch locally.
		      // This prevents a port from receiving its own messages.
		      if (to) {
		        if (to && typeof to.postMessage === "function") {
		          emitTo(to, event, payload);
		        }
		        return; // Exit after targeted send.
		      }
		
		      // For broadcast messages (no 'to' target), dispatch locally first.
		      (handlers[event] || []).forEach((fn) =>
		        fn(payload, { origin: location.origin, source: window })
		      );
		
		      // Then propagate the broadcast to other windows.
		      if (type === "page") {
		        children.forEach((win) => emitTo(win, event, payload));
		      } else {
		        const pw = parentWindow || window.parent;
		        if (pw && pw.postMessage) {
		          emitTo(pw, event, payload);
		        }
		      }
		    },
		  };
		}
		
		function createRuntime(type = "background", bus) {
		  let nextId = 1;
		  const pending = {};
		  const msgListeners = [];
		
		  let nextPortId = 1;
		  const ports = {};
		  const onConnectListeners = [];
		
		  function parseArgs(args) {
		    let target, message, options, callback;
		    const arr = [...args];
		    if (arr.length === 0) {
		      throw new Error("sendMessage requires at least one argument");
		    }
		    if (arr.length === 1) {
		      return { message: arr[0] };
		    }
		    // last object could be options
		    if (
		      arr.length &&
		      typeof arr[arr.length - 1] === "object" &&
		      !Array.isArray(arr[arr.length - 1])
		    ) {
		      options = arr.pop();
		    }
		    // last function is callback
		    if (arr.length && typeof arr[arr.length - 1] === "function") {
		      callback = arr.pop();
		    }
		    if (
		      arr.length === 2 &&
		      (typeof arr[0] === "string" || typeof arr[0] === "number")
		    ) {
		      [target, message] = arr;
		    } else {
		      [message] = arr;
		    }
		    return { target, message, options, callback };
		  }
		
		  if (type === "background") {
		    bus.on("__REQUEST__", ({ id, message }, { source }) => {
		      let responded = false,
		        isAsync = false;
		      function sendResponse(resp) {
		        if (responded) return;
		        responded = true;
		        // Target the response directly back to the window that sent the request.
		        bus.emit("__RESPONSE__", { id, response: resp }, { to: source });
		      }
		      const results = msgListeners
		        .map((fn) => {
		          try {
		            // msg, sender, sendResponse
		            const ret = fn(message, { id, tab: { id: source } }, sendResponse);
		            if (ret === true || (ret && typeof ret.then === "function")) {
		              isAsync = true;
		              return ret;
		            }
		            return ret;
		          } catch (e) {
		            _error(e);
		          }
		        })
		        .filter((r) => r !== undefined);
		
		      const promises = results.filter((r) => r && typeof r.then === "function");
		      if (!isAsync && promises.length === 0) {
		        const out = results.length === 1 ? results[0] : results;
		        sendResponse(out);
		      } else if (promises.length) {
		        Promise.all(promises).then((vals) => {
		          if (!responded) {
		            const out = vals.length === 1 ? vals[0] : vals;
		            sendResponse(out);
		          }
		        });
		      }
		    });
		  }
		
		  if (type !== "background") {
		    bus.on("__RESPONSE__", ({ id, response }) => {
		      const entry = pending[id];
		      if (!entry) return;
		      entry.resolve(response);
		      if (entry.callback) entry.callback(response);
		      delete pending[id];
		    });
		  }
		
		  function sendMessage(...args) {
		    // Background should be able to send message to itself
		    // if (type === "background") {
		    //   throw new Error("Background cannot sendMessage to itself");
		    // }
		    const { target, message, callback } = parseArgs(args);
		    const id = nextId++;
		    const promise = new Promise((resolve) => {
		      pending[id] = { resolve, callback };
		      bus.emit("__REQUEST__", { id, message });
		    });
		    return promise;
		  }
		
		  bus.on("__PORT_CONNECT__", ({ portId, name }, { source }) => {
		    if (type !== "background") return;
		    const backgroundPort = makePort("background", portId, name, source);
		    ports[portId] = backgroundPort;
		
		    onConnectListeners.forEach((fn) => fn(backgroundPort));
		
		    // send back a CONNECT_ACK so the client can
		    // start listening on its end:
		    bus.emit("__PORT_CONNECT_ACK__", { portId, name }, { to: source });
		  });
		
		  // Clients handle the ACK and finalize their Port object by learning the remote window.
		  bus.on("__PORT_CONNECT_ACK__", ({ portId, name }, { source }) => {
		    if (type === "background") return; // ignore
		    const p = ports[portId];
		    if (!p) return;
		    // Call the port's internal finalize method to complete the handshake
		    if (p._finalize) {
		      p._finalize(source);
		    }
		  });
		
		  // Any port message travels via "__PORT_MESSAGE__"
		  bus.on("__PORT_MESSAGE__", (envelope, { source }) => {
		    const { portId } = envelope;
		    const p = ports[portId];
		    if (!p) return;
		    p._receive(envelope, source);
		  });
		
		  // Any port disconnect:
		  bus.on("__PORT_DISCONNECT__", ({ portId }) => {
		    const p = ports[portId];
		    if (!p) return;
		    p._disconnect();
		    delete ports[portId];
		  });
		
		  // Refactored makePort to correctly manage internal state and the connection handshake.
		  function makePort(side, portId, name, remoteWindow) {
		    let onMessageHandlers = [];
		    let onDisconnectHandlers = [];
		    let buffer = [];
		    // Unique instance ID for this port instance
		    const instanceId = Math.random().toString(36).slice(2) + Date.now();
		    // These state variables are part of the closure and are updated by _finalize
		    let _ready = side === "background";
		
		    function _drainBuffer() {
		      buffer.forEach((m) => _post(m));
		      buffer = [];
		    }
		
		    function _post(msg) {
		      // Always use the 'to' parameter for port messages, making them directional.
		      // Include senderInstanceId
		      bus.emit(
		        "__PORT_MESSAGE__",
		        { portId, msg, senderInstanceId: instanceId },
		        { to: remoteWindow }
		      );
		    }
		
		    function postMessage(msg) {
		      if (!_ready) {
		        buffer.push(msg);
		      } else {
		        _post(msg);
		      }
		    }
		
		    function _receive(envelope, source) {
		      // envelope: { msg, senderInstanceId }
		      if (envelope.senderInstanceId === instanceId) return; // Don't dispatch to self
		      onMessageHandlers.forEach((fn) =>
		        fn(envelope.msg, { id: portId, tab: { id: source } })
		      );
		    }
		
		    function disconnect() {
		      // Also use the 'to' parameter for disconnect messages
		      bus.emit("__PORT_DISCONNECT__", { portId }, { to: remoteWindow });
		      _disconnect();
		      delete ports[portId];
		    }
		
		    function _disconnect() {
		      onDisconnectHandlers.forEach((fn) => fn());
		      onMessageHandlers = [];
		      onDisconnectHandlers = [];
		    }
		
		    // This function is called on the client port when the ACK is received from background.
		    // It updates the port's state, completing the connection.
		    function _finalize(win) {
		      remoteWindow = win; // <-- This is the crucial part: learn the destination
		      _ready = true;
		      _drainBuffer();
		    }
		
		    return {
		      name,
		      sender: {
		        id: portId,
		      },
		      onMessage: {
		        addListener(fn) {
		          onMessageHandlers.push(fn);
		        },
		        removeListener(fn) {
		          onMessageHandlers = onMessageHandlers.filter((x) => x !== fn);
		        },
		      },
		      onDisconnect: {
		        addListener(fn) {
		          onDisconnectHandlers.push(fn);
		        },
		        removeListener(fn) {
		          onDisconnectHandlers = onDisconnectHandlers.filter((x) => x !== fn);
		        },
		      },
		      postMessage,
		      disconnect,
		      // Internal methods used by the runtime
		      _receive,
		      _disconnect,
		      _finalize, // Expose the finalizer for the ACK handler
		    };
		  }
		
		  function connect(connectInfo = {}) {
		    if (type === "background") {
		      throw new Error("Background must use onConnect, not connect()");
		    }
		    const name = connectInfo.name || "";
		    const portId = nextPortId++;
		    // create the client side port
		    // remoteWindow is initially null; it will be set by _finalize upon ACK.
		    const clientPort = makePort("client", portId, name, null);
		    ports[portId] = clientPort;
		
		    // fire the connect event across the bus
		    bus.emit("__PORT_CONNECT__", { portId, name });
		    return clientPort;
		  }
		
		  function onConnect(fn) {
		    if (type !== "background") {
		      throw new Error("connect event only fires in background");
		    }
		    onConnectListeners.push(fn);
		  }
		
		  return {
		    // rpc:
		    sendMessage,
		    onMessage: {
		      addListener(fn) {
		        msgListeners.push(fn);
		      },
		      removeListener(fn) {
		        const i = msgListeners.indexOf(fn);
		        if (i >= 0) msgListeners.splice(i, 1);
		      },
		    },
		
		    // port API:
		    connect,
		    onConnect: {
		      addListener(fn) {
		        onConnect(fn);
		      },
		      removeListener(fn) {
		        const i = onConnectListeners.indexOf(fn);
		        if (i >= 0) onConnectListeners.splice(i, 1);
		      },
		    },
		  };
		}
		
		
// #region Abstraction layer Handle postmesage for
			(function () {
			  const pendingRequests = new Map(); // requestId -> { resolve, reject, timeout }
			  let nextRequestId = 1;
			
			  window.addEventListener("message", async (event) => {
			    const { type, requestId, method, args } = event.data;
			
			    if (type === "abstraction-request") {
			      try {
			        let result;
			
			        switch (method) {
			          case "_storageSet":
			            result = await _storageSet(args[0]);
			            break;
			          case "_storageGet":
			            result = await _storageGet(args[0]);
			            break;
			          case "_storageRemove":
			            result = await _storageRemove(args[0]);
			            break;
			          case "_storageClear":
			            result = await _storageClear();
			            break;
			          case "_cookieList":
			            result = await _cookieList(args[0]);
			            break;
			          case "_cookieSet":
			            result = await _cookieSet(args[0]);
			            break;
			          case "_cookieDelete":
			            result = await _cookieDelete(args[0]);
			            break;
			          case "_fetch":
			            result = await _fetch(args[0], args[1]);
			            break;
			          case "_registerMenuCommand":
			            result = _registerMenuCommand(args[0], args[1]);
			            break;
			          case "_openTab":
			            result = _openTab(args[0], args[1]);
			            break;
			          case "_initStorage":
			            result = await _initStorage();
			            break;
			          default:
			            throw new Error(`Unknown abstraction method: ${method}`);
			        }
			
			        event.source.postMessage({
			          type: "abstraction-response",
			          requestId,
			          success: true,
			          result,
			        });
			      } catch (error) {
			        event.source.postMessage({
			          type: "abstraction-response",
			          requestId,
			          success: false,
			          error: {
			            message: error.message,
			            stack: error.stack,
			          },
			        });
			      }
			    }
			  });
			
			  _log("[PostMessage Handler] Abstraction layer message handler initialized");
			})();
			
			
// #endregion
// #region Abstraction Layer Userscript Target
			
			async function _storageSet(items) {
			  try {
			    for (const key in items) {
			      if (items.hasOwnProperty(key)) {
			        await GM_setValue(key, items[key]);
			      }
			    }
			    return Promise.resolve();
			  } catch (e) {
			    _error("GM_setValue error:", e);
			    return Promise.reject(e);
			  }
			}
			
			async function _storageGet(keys) {
			  if (!keys) {
			    keys = null;
			  }
			  if (
			    Array.isArray(keys) &&
			    (keys.length === 0 || [null, undefined].includes(keys[0]))
			  ) {
			    keys = null;
			  }
			  try {
			    const results = {};
			    let keyList = [];
			    let defaults = {};
			    let requestedKeys = [];
			
			    if (keys === null) {
			      keyList = await GM_listValues();
			      requestedKeys = [...keyList];
			    } else if (typeof keys === "string") {
			      keyList = [keys];
			      requestedKeys = [keys];
			    } else if (Array.isArray(keys)) {
			      keyList = keys;
			      requestedKeys = [...keys];
			    } else if (typeof keys === "object" && keys !== null) {
			      keyList = Object.keys(keys);
			      requestedKeys = [...keyList];
			      defaults = keys;
			    } else {
			      _error("_storageGet error: Invalid keys format", keys);
			      return Promise.reject(new Error("Invalid keys format for get"));
			    }
			
			    for (const key of keyList) {
			      const defaultValue = defaults.hasOwnProperty(key)
			        ? defaults[key]
			        : undefined;
			      const storedValue = await GM_getValue(key, defaultValue);
			      results[key] = storedValue;
			    }
			
			    const finalResult = {};
			    for (const key of requestedKeys) {
			      if (results.hasOwnProperty(key)) {
			        finalResult[key] = results[key];
			      } else if (defaults.hasOwnProperty(key)) {
			        finalResult[key] = defaults[key];
			      }
			    }
			
			    return Promise.resolve(finalResult);
			  } catch (e) {
			    _error("GM_getValue/GM_listValues error:", e);
			    return Promise.reject(e);
			  }
			}
			
			async function _storageRemove(keysToRemove) {
			  try {
			    let keyList = [];
			    if (typeof keysToRemove === "string") {
			      keyList = [keysToRemove];
			    } else if (Array.isArray(keysToRemove)) {
			      keyList = keysToRemove;
			    } else {
			      _error("_storageRemove error: Invalid keys format", keysToRemove);
			      return Promise.reject(new Error("Invalid keys format for remove"));
			    }
			
			    for (const key of keyList) {
			      await GM_deleteValue(key);
			    }
			    return Promise.resolve();
			  } catch (e) {
			    _error("GM_deleteValue error:", e);
			    return Promise.reject(e);
			  }
			}
			
			async function _storageClear() {
			  try {
			    const keys = await GM_listValues();
			    await Promise.all(keys.map((key) => GM_deleteValue(key)));
			    return Promise.resolve();
			  } catch (e) {
			    _error("GM_listValues/GM_deleteValue error during clear:", e);
			    return Promise.reject(e);
			  }
			}
			
			async function _cookieList(details) {
			  return new Promise((resolve, reject) => {
			    if (typeof GM_cookie === "undefined" || !GM_cookie.list) {
			      return reject(new Error("GM_cookie.list is not available."));
			    }
			    GM_cookie.list(details, (cookies, error) => {
			      if (error) {
			        return reject(new Error(error));
			      }
			      resolve(cookies);
			    });
			  });
			}
			
			async function _cookieSet(details) {
			  return new Promise((resolve, reject) => {
			    if (typeof GM_cookie === "undefined" || !GM_cookie.set) {
			      return reject(new Error("GM_cookie.set is not available."));
			    }
			    GM_cookie.set(details, (error) => {
			      if (error) {
			        return reject(new Error(error));
			      }
			      resolve();
			    });
			  });
			}
			
			async function _cookieDelete(details) {
			  return new Promise((resolve, reject) => {
			    if (typeof GM_cookie === "undefined" || !GM_cookie.delete) {
			      return reject(new Error("GM_cookie.delete is not available."));
			    }
			    GM_cookie.delete(details, (error) => {
			      if (error) {
			        return reject(new Error(error));
			      }
			      resolve();
			    });
			  });
			}
			
			async function _fetch(url, options = {}) {
			  return new Promise((resolve, reject) => {
			    try {
			      GM_xmlhttpRequest({
			        method: options.method || "GET",
			        url: url,
			        headers: options.headers || {},
			        data: options.body,
			        responseType: options.responseType,
			        timeout: options.timeout || 0,
			        binary:
			          options.responseType === "blob" ||
			          options.responseType === "arraybuffer",
			        onload: function (response) {
			          const responseHeaders = {};
			          if (response.responseHeaders) {
			            response.responseHeaders
			              .trim()
			              .split("\\r\\n")
			              .forEach((header) => {
			                const parts = header.match(/^([^:]+):\s*(.*)$/);
			                if (parts && parts.length === 3) {
			                  responseHeaders[parts[1].toLowerCase()] = parts[2];
			                }
			              });
			          }
			
			          const mockResponse = {
			            ok: response.status >= 200 && response.status < 300,
			            status: response.status,
			            statusText:
			              response.statusText ||
			              (response.status >= 200 && response.status < 300 ? "OK" : ""),
			            url: response.finalUrl || url,
			            headers: new Headers(responseHeaders),
			            text: () => Promise.resolve(response.responseText),
			            json: () => {
			              try {
			                return Promise.resolve(JSON.parse(response.responseText));
			              } catch (e) {
			                return Promise.reject(new SyntaxError("Could not parse JSON"));
			              }
			            },
			            blob: () => {
			              if (response.response instanceof Blob) {
			                return Promise.resolve(response.response);
			              }
			              return Promise.reject(
			                new Error("Requires responseType:'blob' in GM_xmlhttpRequest")
			              );
			            },
			            arrayBuffer: () => {
			              if (response.response instanceof ArrayBuffer) {
			                return Promise.resolve(response.response);
			              }
			              return Promise.reject(
			                new Error(
			                  "Requires responseType:'arraybuffer' in GM_xmlhttpRequest"
			                )
			              );
			            },
			            clone: function () {
			              const cloned = { ...this };
			              cloned.text = () => Promise.resolve(response.responseText);
			              cloned.json = () => this.json();
			              cloned.blob = () => this.blob();
			              cloned.arrayBuffer = () => this.arrayBuffer();
			              return cloned;
			            },
			          };
			
			          if (mockResponse.ok) {
			            resolve(mockResponse);
			          } else {
			            const error = new Error(`HTTP error! status: ${response.status}`);
			            error.response = mockResponse;
			            reject(error);
			          }
			        },
			        onerror: function (response) {
			          reject(
			            new Error(
			              `GM_xmlhttpRequest network error: ${
			                response.statusText || "Unknown Error"
			              }`
			            )
			          );
			        },
			        onabort: function () {
			          reject(new Error("GM_xmlhttpRequest aborted"));
			        },
			        ontimeout: function () {
			          reject(new Error("GM_xmlhttpRequest timed out"));
			        },
			      });
			    } catch (e) {
			      _error("_fetch (GM_xmlhttpRequest) error:", e);
			      reject(e);
			    }
			  });
			}
			
			function _registerMenuCommand(name, func) {
			  if (typeof GM_registerMenuCommand === "function") {
			    try {
			      GM_registerMenuCommand(name, func);
			    } catch (e) {
			      _error("GM_registerMenuCommand failed:", e);
			    }
			  } else {
			    _warn("GM_registerMenuCommand not available.");
			  }
			}
			
			function _openTab(url, active) {
			  if (typeof GM_openInTab === "function") {
			    try {
			      GM_openInTab(url, { loadInBackground: !active });
			    } catch (e) {
			      _error("GM_openInTab failed:", e);
			    }
			  } else {
			    _warn("GM_openInTab not available, using window.open as fallback.");
			    try {
			      window.open(url);
			    } catch (e) {
			      _error("window.open fallback failed:", e);
			    }
			  }
			}
			
			async function _initStorage() {
			  return Promise.resolve();
			}
			
			
			const EXTENSION_ASSETS_MAP = {
			  "popup/popup.html": "<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n  <meta charset=\"UTF-8\">\r\n  <title>e621 extras</title>\r\n  <style>\n* {\r\n  box-sizing: border-box;\r\n  margin: 0;\r\n  padding: 0;\r\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\r\n  -webkit-font-smoothing: antialiased;\r\n}\r\n\r\nbody {\r\n  width: 290px;\r\n  background-color: #000000;\r\n  color: #ffffff;\r\n  padding: 14px;\r\n  user-select: none;\r\n}\r\n\r\n/* 1. Remove arrows from numeric input */\r\ninput::-webkit-outer-spin-button,\r\ninput::-webkit-inner-spin-button {\r\n  -webkit-appearance: none;\r\n  margin: 0;\r\n}\r\n\r\ninput[type=\"number\"] {\r\n  -moz-appearance: textfield;\r\n  appearance: textfield;\r\n}\r\n\r\n.container {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 12px;\r\n}\r\n\r\n.header {\r\n  display: flex;\r\n  align-items: center;\r\n  padding-bottom: 2px;\r\n}\r\n\r\n.title {\r\n  font-size: 13px;\r\n  font-weight: 600;\r\n  letter-spacing: 0.8px;\r\n  text-transform: uppercase;\r\n}\r\n\r\n/* Tabs */\r\n.tabs {\r\n  display: flex;\r\n  gap: 4px;\r\n  border-bottom: 1px solid #222222;\r\n  padding-bottom: 8px;\r\n}\r\n\r\n.tab-btn {\r\n  flex: 1;\r\n  background: #000000;\r\n  color: #666666;\r\n  border: 1px solid #222222;\r\n  padding: 6px 0;\r\n  font-size: 10px;\r\n  font-weight: 600;\r\n  letter-spacing: 0.5px;\r\n  cursor: pointer;\r\n  transition: all 0.15s ease;\r\n  text-align: center;\r\n}\r\n\r\n.tab-btn:hover {\r\n  color: #cccccc;\r\n  background-color: #0a0a0a;\r\n}\r\n\r\n.tab-btn.active {\r\n  background-color: #ffffff;\r\n  color: #000000;\r\n  border-color: #ffffff;\r\n}\r\n\r\n.tab-content {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 10px;\r\n}\r\n\r\n.section {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 8px;\r\n}\r\n\r\n.hidden {\r\n  display: none !important;\r\n}\r\n\r\n.btn {\r\n  width: 100%;\r\n  background: #000000;\r\n  color: #ffffff;\r\n  border: 1px solid #272727;\r\n  padding: 8px 12px;\r\n  font-size: 12px;\r\n  letter-spacing: 0.3px;\r\n  cursor: pointer;\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  transition: background-color 0.15s ease, border-color 0.15s ease;\r\n}\r\n\r\n.btn:hover:not(:disabled) {\r\n  background-color: #141414;\r\n  border-color: #444444;\r\n}\r\n\r\n.btn:active:not(:disabled) {\r\n  background-color: #222222;\r\n}\r\n\r\n.btn:disabled {\r\n  color: #444444;\r\n  border-color: #181818;\r\n  cursor: not-allowed;\r\n}\r\n\r\n.btn-sub {\r\n  padding-left: 20px;\r\n  color: #aaaaaa;\r\n}\r\n\r\n.setting-row {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  background: #0a0a0a;\r\n  border: 1px solid #222222;\r\n  padding: 10px 12px;\r\n}\r\n\r\n.setting-block {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 8px;\r\n  background: #0a0a0a;\r\n  border: 1px solid #222222;\r\n  padding: 10px 12px;\r\n}\r\n\r\n.setting-info {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 2px;\r\n}\r\n\r\n.setting-label {\r\n  font-size: 12px;\r\n  color: #ffffff;\r\n}\r\n\r\n.setting-desc {\r\n  font-size: 10px;\r\n  color: #666666;\r\n}\r\n\r\n.btn-toggle-switch {\r\n  background: transparent;\r\n  border: none;\r\n  cursor: pointer;\r\n  padding: 0;\r\n}\r\n\r\n.input-inline-group {\r\n  display: flex;\r\n  gap: 6px;\r\n  align-items: center;\r\n}\r\n\r\n.input-minimal {\r\n  flex: 1;\r\n  background: #000000;\r\n  border: 1px solid #272727;\r\n  color: #ffffff;\r\n  padding: 6px 10px;\r\n  font-size: 12px;\r\n  outline: none;\r\n  transition: border-color 0.15s;\r\n}\r\n\r\n.input-minimal:focus {\r\n  border-color: #666666;\r\n}\r\n\r\n.btn-inline {\r\n  width: auto;\r\n  padding: 6px 14px;\r\n}\r\n\r\n.badge {\r\n  display: inline-block;\r\n  width: 36px;\r\n  text-align: center;\r\n  font-size: 10px;\r\n  border: 1px solid #444444;\r\n  padding: 2px 0;\r\n  text-transform: uppercase;\r\n  color: #ffffff;\r\n  background-color: transparent;\r\n  font-weight: 500;\r\n}\r\n\r\n.badge.on {\r\n  background-color: #ffffff;\r\n  color: #000000;\r\n  border-color: #ffffff;\r\n}\r\n\r\n.footer {\r\n  border-top: 1px solid #222222;\r\n  padding-top: 8px;\r\n}\r\n\r\n.status-msg {\r\n  font-size: 11px;\r\n  color: #888888;\r\n  line-height: 1.4;\r\n  min-height: 16px;\r\n  word-break: break-word;\r\n}\r\n\r\n/* 2. Screen styles for third-party pages */\r\n.offline-view {\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  justify-content: center;\r\n  min-height: 160px;\r\n  text-align: center;\r\n  gap: 14px;\r\n  padding: 8px 0;\r\n}\r\n\r\n.offline-text {\r\n  font-size: 12px;\r\n  color: #888888;\r\n  line-height: 1.4;\r\n}\r\n\r\n.offline-links {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 8px;\r\n  width: 100%;\r\n}\r\n\r\n.offline-link {\r\n  display: block;\r\n  text-decoration: none;\r\n  color: #ffffff;\r\n  background-color: #0a0a0a;\r\n  border: 1px solid #222222;\r\n  padding: 8px 0;\r\n  font-size: 12px;\r\n  letter-spacing: 0.5px;\r\n  transition: background-color 0.15s, border-color 0.15s;\r\n}\r\n\r\n.offline-link:hover {\r\n  background-color: #141414;\r\n  border-color: #444444;\r\n}\n</style>\r\n</head>\r\n<body>\r\n  <!-- Shown on non-e621 pages -->\r\n  <div id=\"offlineView\" class=\"offline-view hidden\">\r\n    <p class=\"offline-text\">extension works on these webpages:</p>\r\n    <div class=\"offline-links\">\r\n      <a href=\"https://e621.net\" target=\"_blank\" class=\"offline-link\">e621.net</a>\r\n      <a href=\"https://e926.net\" target=\"_blank\" class=\"offline-link\">e926.net</a>\r\n    </div>\r\n  </div>\r\n\r\n  <!-- Main UI -->\r\n  <div id=\"mainView\" class=\"container\">\r\n    <header class=\"header\">\r\n      <h1 class=\"title\">e621 extras</h1>\r\n    </header>\r\n\r\n    <nav class=\"tabs\">\r\n      <button class=\"tab-btn active\" data-tab=\"main\">MAIN</button>\r\n      <button class=\"tab-btn\" data-tab=\"blck\">BLCK</button>\r\n      <button class=\"tab-btn\" data-tab=\"tagscr\">TAGSCR</button>\r\n      <button class=\"tab-btn\" data-tab=\"hz\">HZ</button>\r\n    </nav>\r\n\r\n    <!-- Tab 1: MAIN -->\r\n    <main id=\"tab-main\" class=\"tab-content\">\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Hide Thumbnail Tooltips</span>\r\n          <span class=\"setting-desc\">Suppress browser tooltip on thumbnails</span>\r\n        </div>\r\n        <button id=\"btnHideTooltipsToggle\" class=\"btn-toggle-switch\">\r\n          <span id=\"hideTooltipsBadge\" class=\"badge\">OFF</span>\r\n        </button>\r\n      </div>\r\n\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Tracking FPS</span>\r\n          <span class=\"setting-desc\">0 = Monitor Hz (or limit for weak PCs)</span>\r\n        </div>\r\n        <input type=\"number\" id=\"inputTrackingFps\" class=\"input-minimal\" placeholder=\"0\" min=\"0\" max=\"360\" style=\"width: 58px; flex: none; text-align: center;\" />\r\n      </div>\r\n\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Show Uploader</span>\r\n          <span class=\"setting-desc\">Under status in post details</span>\r\n        </div>\r\n        <button id=\"btnShowUploader\" class=\"btn-toggle-switch\">\r\n          <span id=\"showUploaderBadge\" class=\"badge on\">ON</span>\r\n        </button>\r\n      </div>\r\n\r\n      <div class=\"setting-block\">\r\n        <span class=\"setting-label\">Show Post Tree</span>\r\n        <div class=\"input-inline-group\">\r\n          <input type=\"number\" id=\"inputTreePostId\" class=\"input-minimal\" placeholder=\"post id\" />\r\n          <button id=\"btnShowTreePopup\" class=\"btn btn-inline\">Show</button>\r\n        </div>\r\n      </div>\r\n    </main>\r\n\r\n    <!-- Tab 2: BLCK -->\r\n    <main id=\"tab-blck\" class=\"tab-content hidden\">\r\n      <div class=\"section\">\r\n        <button id=\"btnAutoUpdate\" class=\"btn\" title=\"Keep the blacklist in sync with your downvotes automatically\">\r\n          <span>Auto Update</span>\r\n          <span id=\"autoUpdateState\" class=\"badge\">OFF</span>\r\n        </button>\r\n        <button id=\"btnSyncApi\" class=\"btn btn-sub hidden\" title=\"Re-fetch downvotes and update the blacklist\">↳ Sync via API</button>\r\n      </div>\r\n\r\n      <div id=\"manualActions\" class=\"section\">\r\n        <button id=\"btnBackup\" class=\"btn\" title=\"Save the current blacklist in the extension\">Backup Current Blacklist</button>\r\n        <button id=\"btnLoadDownvotes\" class=\"btn\" title=\"Fetch your downvoted posts from the site into the extension\">Load Downvotes</button>\r\n        <button id=\"btnUpload\" class=\"btn\" title=\"Add the loaded downvotes to your blacklist as id:N rules\">Upload to Blacklist</button>\r\n        <button id=\"btnUnload\" class=\"btn\" title=\"Remove those id:N rules from your blacklist\">Unload from Blacklist</button>\r\n      </div>\r\n    </main>\r\n\r\n    <!-- Tab 3: TAGSCR -->\r\n    <main id=\"tab-tagscr\" class=\"tab-content hidden\">\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Tag Script Preview (Ctrl)</span>\r\n          <span class=\"setting-desc\">View active tag scripts</span>\r\n        </div>\r\n        <button id=\"btnCtrlPreviewToggle\" class=\"btn-toggle-switch\">\r\n          <span id=\"ctrlPreviewBadge\" class=\"badge on\">ON</span>\r\n        </button>\r\n      </div>\r\n\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Sticky Preview (Ctrl)</span>\r\n          <span class=\"setting-desc\">Toggle with Ctrl instead of holding</span>\r\n        </div>\r\n        <button id=\"btnCtrlStickyToggle\" class=\"btn-toggle-switch\">\r\n          <span id=\"ctrlStickyBadge\" class=\"badge\">OFF</span>\r\n        </button>\r\n      </div>\r\n\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Queued Tag Scripts</span>\r\n          <span class=\"setting-desc\">Queue edits during hourly limits</span>\r\n        </div>\r\n        <button id=\"btnQueueToggle\" class=\"btn-toggle-switch\">\r\n          <span id=\"queueBadge\" class=\"badge\">OFF</span>\r\n        </button>\r\n      </div>\r\n\r\n      <div id=\"queueBlock\" class=\"setting-block hidden\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Tag Script Queue</span>\r\n          <span id=\"queueStatusText\" class=\"setting-desc\">0 posts queued</span>\r\n        </div>\r\n        <div class=\"input-inline-group\">\r\n          <a id=\"btnOpenTimeouted\" href=\"https://e621.net/posts/timeouted\" target=\"_blank\" class=\"btn btn-inline\">View Queue</a>\r\n          <button id=\"btnProcessQueue\" class=\"btn btn-inline\">Process Now</button>\r\n        </div>\r\n      </div>\r\n    </main>\r\n\r\n    <!-- Tab 4: HZ -->\r\n    <main id=\"tab-hz\" class=\"tab-content hidden\">\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Hover Zoom (Shift)</span>\r\n          <span class=\"setting-desc\">Hold Shift while hovering</span>\r\n        </div>\r\n        <button id=\"btnHzToggle\" class=\"btn-toggle-switch\">\r\n          <span id=\"hzBadge\" class=\"badge on\">ON</span>\r\n        </button>\r\n      </div>\r\n\r\n      <div class=\"setting-row\">\r\n        <div class=\"setting-info\">\r\n          <span class=\"setting-label\">Load Sample on Hover</span>\r\n          <span class=\"setting-desc\">Load sample image instead of full</span>\r\n        </div>\r\n        <button id=\"btnHzSampleToggle\" class=\"btn-toggle-switch\">\r\n          <span id=\"hzSampleBadge\" class=\"badge on\">ON</span>\r\n        </button>\r\n      </div>\r\n    </main>\r\n\r\n    <footer class=\"footer\">\r\n      <div id=\"statusMessage\" class=\"status-msg\">Ready</div>\r\n    </footer>\r\n  </div>\r\n\r\n  <script>\n/* Shared by content scripts, popup and background — single source of truth for keys and message names. */\r\nglobalThis.E621_CONSTANTS = Object.freeze({\r\n  SCHEMA_VERSION: 1,\r\n\r\n  STORAGE_KEYS: Object.freeze({\r\n    SCHEMA_VERSION: 'e621_schema_version',\r\n    DOWNVOTES: 'e621_downvoted_posts',\r\n    BLACKLIST_BACKUP: 'e621_clean_blacklist_backup',\r\n    BLACKLIST_LOCKED: 'e621_load_blacklist_disabled',\r\n    AUTO_UPDATE: 'e621_auto_update_enabled',\r\n    SHOW_UPLOADER: 'e621_extra_show_uploader',\r\n    ACTIVE_POPUP_TAB: 'e621_active_popup_tab',\r\n\r\n    /* Hover Zoom keys */\r\n    HZ_ENABLED: 'e621_hz_enabled',\r\n    HZ_LOAD_SAMPLE: 'e621_hz_load_sample',\r\n    HZ_TAGS: 'e621_hz_tags',\r\n    HZ_TIME: 'e621_hz_time',\r\n    HZ_AUDIO: 'e621_hz_audio',\r\n    HZ_DELAY: 'e621_hz_delay',\r\n    HZ_STICKY: 'e621_hz_sticky',\r\n\r\n    /* Queued / Timeouted Tag Scripts keys */\r\n    QUEUE_TAG_SCRIPTS_ENABLED: 'e621_queue_tag_scripts_enabled',\r\n    TAG_SCRIPT_QUEUE: 'e621_tag_script_queue',\r\n    QUEUE_TIMEOUT_UNTIL: 'e621_queue_timeout_until',\r\n    QUEUE_SLOT_TIMESTAMPS: 'e621_queue_slot_timestamps',\r\n\r\n    /* Preview & Tooltip suppression & Performance */\r\n    CTRL_PREVIEW_ENABLED: 'e621_ctrl_preview_enabled',\r\n    CTRL_PREVIEW_STICKY: 'e621_ctrl_preview_sticky',\r\n    CTRL_STICKY_ACTIVE: 'e621_ctrl_sticky_active',\r\n    HIDE_THUMB_TOOLTIPS: 'e621_hide_thumb_tooltips',\r\n    TRACKING_FPS: 'e621_tracking_fps'\r\n  }),\r\n\r\n  ACTIONS: Object.freeze({\r\n    BACKUP_BLACKLIST: 'backupBlacklist',\r\n    LOAD_DOWNVOTES: 'loadDownvotes',\r\n    UPLOAD_DOWNVOTES: 'uploadDownvotes',\r\n    UNLOAD_DOWNVOTES: 'unloadDownvotes',\r\n    SYNC_API: 'syncApi',\r\n    TOGGLE_AUTO_UPDATE: 'toggleAutoUpdate',\r\n    SHOW_POST_TREE: 'showPostTree',\r\n    FETCH_IMAGE: 'fetchImageAsDataUrl',\r\n    PROCESS_TAG_QUEUE: 'processTagQueue',\r\n    CLEAR_TAG_QUEUE: 'clearTagQueue',\r\n    ENQUEUE_TAG_SCRIPT: 'enqueueTagScript',\r\n    DEQUEUE_TAG_SCRIPT: 'dequeueTagScript',\r\n    SAVE_IMAGE: 'saveImage',\r\n    GET_IMAGE: 'getImage',\r\n    DELETE_IMAGE: 'deleteImage',\r\n    CLEAR_IMAGES: 'clearImages',\r\n    SET_CSRF_TOKEN: 'setCsrfToken'\r\n  })\r\n});\n</script>\r\n  <script>\ndocument.addEventListener('DOMContentLoaded', async () => {\r\n  const { STORAGE_KEYS: KEYS, ACTIONS } = E621_CONSTANTS;\r\n  const $ = (id) => document.getElementById(id);\r\n\r\n  const mainView = $('mainView');\r\n  const offlineView = $('offlineView');\r\n  const statusMsg = $('statusMessage');\r\n  const tabBtns = document.querySelectorAll('.tab-btn');\r\n  const tabs = {\r\n    main: $('tab-main'),\r\n    blck: $('tab-blck'),\r\n    tagscr: $('tab-tagscr'),\r\n    hz: $('tab-hz')\r\n  };\r\n\r\n  const autoUpdateState = $('autoUpdateState');\r\n  const btnSyncApi = $('btnSyncApi');\r\n  const manualActions = $('manualActions');\r\n  const btnBackup = $('btnBackup');\r\n  const showUploaderBadge = $('showUploaderBadge');\r\n  const inputTreePostId = $('inputTreePostId');\r\n\r\n  const setStatus = (text) => { statusMsg.textContent = text; };\r\n  const setButtonsDisabled = (disabled) =>\r\n    document.querySelectorAll('.btn').forEach(b => { b.disabled = disabled; });\r\n\r\n  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });\r\n  const isE621 = tab?.url && /^https:\\/\\/(e621|e926)\\.net\\//.test(tab.url);\r\n\r\n  if (!isE621) {\r\n    mainView.classList.add('hidden');\r\n    offlineView.classList.remove('hidden');\r\n    return;\r\n  }\r\n\r\n  function showTab(name) {\r\n    tabBtns.forEach(b => b.classList.toggle('active', b.dataset.tab === name));\r\n    Object.entries(tabs).forEach(([key, node]) => node.classList.toggle('hidden', key !== name));\r\n  }\r\n\r\n  tabBtns.forEach(btn => btn.addEventListener('click', () => {\r\n    showTab(btn.dataset.tab);\r\n    chrome.storage.local.set({ [KEYS.ACTIVE_POPUP_TAB]: btn.dataset.tab });\r\n  }));\r\n\r\n  const postMatch = tab.url.match(/\\/posts\\/(\\d+)/);\r\n  if (postMatch) inputTreePostId.value = postMatch[1];\r\n\r\n  const btnHzToggle = $('btnHzToggle');\r\n  const hzBadge = $('hzBadge');\r\n  const btnHzSampleToggle = $('btnHzSampleToggle');\r\n  const hzSampleBadge = $('hzSampleBadge');\r\n\r\n  const btnCtrlPreviewToggle = $('btnCtrlPreviewToggle');\r\n  const ctrlPreviewBadge = $('ctrlPreviewBadge');\r\n  const btnCtrlStickyToggle = $('btnCtrlStickyToggle');\r\n  const ctrlStickyBadge = $('ctrlStickyBadge');\r\n  const inputTrackingFps = $('inputTrackingFps');\r\n  const btnHideTooltipsToggle = $('btnHideTooltipsToggle');\r\n  const hideTooltipsBadge = $('hideTooltipsBadge');\r\n\r\n  const btnQueueToggle = $('btnQueueToggle');\r\n  const queueBadge = $('queueBadge');\r\n  const queueBlock = $('queueBlock');\r\n  const queueStatusText = $('queueStatusText');\r\n  const btnProcessQueue = $('btnProcessQueue');\r\n  const btnOpenTimeouted = $('btnOpenTimeouted');\r\n\r\n  if (btnOpenTimeouted && tab.url) {\r\n    const isE926 = /^https:\\/\\/e926\\.net/i.test(tab.url);\r\n    btnOpenTimeouted.href = isE926 ? 'https://e926.net/posts/timeouted' : 'https://e621.net/posts/timeouted';\r\n  }\r\n\r\n  function formatTime(ms) {\r\n    const totalSec = Math.ceil(ms / 1000);\r\n    const mins = Math.floor(totalSec / 60);\r\n    const secs = totalSec % 60;\r\n    return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;\r\n  }\r\n\r\n  async function refreshUI() {\r\n    const data = await chrome.storage.local.get([\r\n      KEYS.AUTO_UPDATE,\r\n      KEYS.BLACKLIST_LOCKED,\r\n      KEYS.SHOW_UPLOADER,\r\n      KEYS.HZ_ENABLED,\r\n      KEYS.HZ_LOAD_SAMPLE,\r\n      KEYS.CTRL_PREVIEW_ENABLED,\r\n      KEYS.CTRL_PREVIEW_STICKY,\r\n      KEYS.TRACKING_FPS,\r\n      KEYS.HIDE_THUMB_TOOLTIPS,\r\n      KEYS.QUEUE_TAG_SCRIPTS_ENABLED,\r\n      KEYS.TAG_SCRIPT_QUEUE,\r\n      KEYS.QUEUE_TIMEOUT_UNTIL,\r\n      KEYS.QUEUE_SLOT_TIMESTAMPS\r\n    ]);\r\n\r\n    const isAuto = Boolean(data[KEYS.AUTO_UPDATE]);\r\n    const isLocked = Boolean(data[KEYS.BLACKLIST_LOCKED]);\r\n\r\n    const hzEnabled = data[KEYS.HZ_ENABLED] !== false;\r\n    hzBadge.textContent = hzEnabled ? 'ON' : 'OFF';\r\n    hzBadge.classList.toggle('on', hzEnabled);\r\n\r\n    const hzSample = data[KEYS.HZ_LOAD_SAMPLE] !== false;\r\n    hzSampleBadge.textContent = hzSample ? 'ON' : 'OFF';\r\n    hzSampleBadge.classList.toggle('on', hzSample);\r\n\r\n    const ctrlPreview = data[KEYS.CTRL_PREVIEW_ENABLED] !== false;\r\n    ctrlPreviewBadge.textContent = ctrlPreview ? 'ON' : 'OFF';\r\n    ctrlPreviewBadge.classList.toggle('on', ctrlPreview);\r\n\r\n    const ctrlSticky = Boolean(data[KEYS.CTRL_PREVIEW_STICKY]);\r\n    ctrlStickyBadge.textContent = ctrlSticky ? 'ON' : 'OFF';\r\n    ctrlStickyBadge.classList.toggle('on', ctrlSticky);\r\n\r\n    if (inputTrackingFps && document.activeElement !== inputTrackingFps) {\r\n      inputTrackingFps.value = Number(data[KEYS.TRACKING_FPS]) || 0;\r\n    }\r\n\r\n    const hideTooltips = Boolean(data[KEYS.HIDE_THUMB_TOOLTIPS]);\r\n    hideTooltipsBadge.textContent = hideTooltips ? 'ON' : 'OFF';\r\n    hideTooltipsBadge.classList.toggle('on', hideTooltips);\r\n\r\n    const queueEnabled = Boolean(data[KEYS.QUEUE_TAG_SCRIPTS_ENABLED]);\r\n    queueBadge.textContent = queueEnabled ? 'ON' : 'OFF';\r\n    queueBadge.classList.toggle('on', queueEnabled);\r\n    queueBlock.classList.toggle('hidden', !queueEnabled);\r\n\r\n    const queue = data[KEYS.TAG_SCRIPT_QUEUE] || {};\r\n    const count = Object.keys(queue).length;\r\n    const slots = data[KEYS.QUEUE_SLOT_TIMESTAMPS] || [];\r\n    const timeoutUntil = Number(data[KEYS.QUEUE_TIMEOUT_UNTIL]) || 0;\r\n    const now = Date.now();\r\n\r\n    const nextSlot = slots[0] || timeoutUntil;\r\n    const isTimeout = nextSlot > now;\r\n\r\n    if (isTimeout && count > 0) {\r\n      const nextMs = Math.max(0, nextSlot - now);\r\n      const allSlotIdx = Math.min(count - 1, slots.length - 1);\r\n      const allSlot = slots[allSlotIdx] || nextSlot;\r\n      const allMs = Math.max(0, allSlot - now);\r\n\r\n      if (count > 1) {\r\n        queueStatusText.textContent = `${count} queued (next: ${formatTime(nextMs)}, all: ${formatTime(allMs)})`;\r\n      } else {\r\n        queueStatusText.textContent = `1 queued (ready in ${formatTime(nextMs)})`;\r\n      }\r\n    } else {\r\n      queueStatusText.textContent = `${count} posts queued (ready)`;\r\n    }\r\n\r\n    setButtonsDisabled(false);\r\n    autoUpdateState.textContent = isAuto ? 'ON' : 'OFF';\r\n    autoUpdateState.classList.toggle('on', isAuto);\r\n    btnSyncApi.classList.toggle('hidden', !isAuto);\r\n    manualActions.classList.toggle('hidden', isAuto);\r\n    if (!isAuto) btnBackup.disabled = isLocked;\r\n\r\n    const showUploader = data[KEYS.SHOW_UPLOADER] !== false;\r\n    showUploaderBadge.textContent = showUploader ? 'ON' : 'OFF';\r\n    showUploaderBadge.classList.toggle('on', showUploader);\r\n  }\r\n\r\n  btnHzToggle?.addEventListener('click', async () => {\r\n    const data = await chrome.storage.local.get([KEYS.HZ_ENABLED]);\r\n    await chrome.storage.local.set({ [KEYS.HZ_ENABLED]: data[KEYS.HZ_ENABLED] === false });\r\n    await refreshUI();\r\n  });\r\n\r\n  btnHzSampleToggle?.addEventListener('click', async () => {\r\n    const data = await chrome.storage.local.get([KEYS.HZ_LOAD_SAMPLE]);\r\n    const current = data[KEYS.HZ_LOAD_SAMPLE] !== false;\r\n    await chrome.storage.local.set({ [KEYS.HZ_LOAD_SAMPLE]: !current });\r\n    await refreshUI();\r\n  });\r\n\r\n  btnCtrlPreviewToggle?.addEventListener('click', async () => {\r\n    const data = await chrome.storage.local.get([KEYS.CTRL_PREVIEW_ENABLED]);\r\n    const current = data[KEYS.CTRL_PREVIEW_ENABLED] !== false;\r\n    await chrome.storage.local.set({ [KEYS.CTRL_PREVIEW_ENABLED]: !current });\r\n    if (current) {\r\n      await chrome.storage.local.set({ [KEYS.CTRL_STICKY_ACTIVE]: false });\r\n    }\r\n    await refreshUI();\r\n  });\r\n\r\n  btnCtrlStickyToggle?.addEventListener('click', async () => {\r\n    const data = await chrome.storage.local.get([KEYS.CTRL_PREVIEW_STICKY]);\r\n    const current = Boolean(data[KEYS.CTRL_PREVIEW_STICKY]);\r\n    await chrome.storage.local.set({ [KEYS.CTRL_PREVIEW_STICKY]: !current });\r\n    if (current) {\r\n      await chrome.storage.local.set({ [KEYS.CTRL_STICKY_ACTIVE]: false });\r\n    }\r\n    await refreshUI();\r\n  });\r\n\r\n  inputTrackingFps?.addEventListener('change', async () => {\r\n    let val = parseInt(inputTrackingFps.value, 10);\r\n    if (isNaN(val) || val < 0) val = 0;\r\n    if (val > 360) val = 360;\r\n    inputTrackingFps.value = val;\r\n    await chrome.storage.local.set({ [KEYS.TRACKING_FPS]: val });\r\n  });\r\n\r\n  inputTrackingFps?.addEventListener('keydown', (e) => {\r\n    if (e.key === 'Enter') inputTrackingFps.blur();\r\n  });\r\n\r\n  btnHideTooltipsToggle?.addEventListener('click', async () => {\r\n    const data = await chrome.storage.local.get([KEYS.HIDE_THUMB_TOOLTIPS]);\r\n    const current = Boolean(data[KEYS.HIDE_THUMB_TOOLTIPS]);\r\n    await chrome.storage.local.set({ [KEYS.HIDE_THUMB_TOOLTIPS]: !current });\r\n    await refreshUI();\r\n  });\r\n\r\n  btnQueueToggle?.addEventListener('click', async () => {\r\n    const data = await chrome.storage.local.get([KEYS.QUEUE_TAG_SCRIPTS_ENABLED]);\r\n    await chrome.storage.local.set({ [KEYS.QUEUE_TAG_SCRIPTS_ENABLED]: !data[KEYS.QUEUE_TAG_SCRIPTS_ENABLED] });\r\n    await refreshUI();\r\n  });\r\n\r\n  async function sendAction(action) {\r\n    const enabling = action === ACTIONS.TOGGLE_AUTO_UPDATE && autoUpdateState.textContent === 'OFF';\r\n    setStatus(enabling ? 'Syncing API & enabling...' : 'Processing...');\r\n    setButtonsDisabled(true);\r\n\r\n    try {\r\n      let response = null;\r\n\r\n      try {\r\n        response = await chrome.tabs.sendMessage(tab.id, { action });\r\n      } catch (err) {}\r\n\r\n      if (!response && window.parent && window.parent !== window) {\r\n        response = await new Promise((resolve) => {\r\n          const channel = new MessageChannel();\r\n          channel.port1.onmessage = (e) => resolve(e.data);\r\n          window.parent.postMessage({ fromE621Popup: true, action }, '*', [channel.port2]);\r\n          setTimeout(() => resolve(null), 5000);\r\n        });\r\n      }\r\n\r\n      setStatus(response?.status || (response?.ok === false ? 'Failed' : 'Done'));\r\n    } catch (err) {\r\n      const noReceiver = /Receiving end does not exist|message (channel|port) closed/i.test(err.message);\r\n      setStatus(noReceiver ? 'Error: Refresh the page' : `Error: ${err.message}`);\r\n    } finally {\r\n      await refreshUI();\r\n    }\r\n  }\r\n\r\n  $('btnAutoUpdate').addEventListener('click', () => sendAction(ACTIONS.TOGGLE_AUTO_UPDATE));\r\n  btnSyncApi.addEventListener('click', () => sendAction(ACTIONS.SYNC_API));\r\n  btnBackup.addEventListener('click', () => sendAction(ACTIONS.BACKUP_BLACKLIST));\r\n  $('btnLoadDownvotes').addEventListener('click', () => sendAction(ACTIONS.LOAD_DOWNVOTES));\r\n  $('btnUpload').addEventListener('click', () => sendAction(ACTIONS.UPLOAD_DOWNVOTES));\r\n  $('btnUnload').addEventListener('click', () => sendAction(ACTIONS.UNLOAD_DOWNVOTES));\r\n  btnProcessQueue?.addEventListener('click', () => sendAction(ACTIONS.PROCESS_TAG_QUEUE));\r\n\r\n  $('btnShowUploader').addEventListener('click', async () => {\r\n    const data = await chrome.storage.local.get([KEYS.SHOW_UPLOADER]);\r\n    await chrome.storage.local.set({ [KEYS.SHOW_UPLOADER]: data[KEYS.SHOW_UPLOADER] === false });\r\n    await refreshUI();\r\n  });\r\n\r\n  $('btnShowTreePopup').addEventListener('click', async () => {\r\n    const id = Number(inputTreePostId.value.trim());\r\n    if (!Number.isInteger(id) || id <= 0) return setStatus('Enter a valid post ID');\r\n    try {\r\n      await chrome.tabs.sendMessage(tab.id, { action: ACTIONS.SHOW_POST_TREE, postId: id });\r\n      window.close();\r\n    } catch (err) {\r\n      setStatus('Error: Refresh the page');\r\n    }\r\n  });\r\n\r\n  const stored = await chrome.storage.local.get([KEYS.ACTIVE_POPUP_TAB]);\r\n  const validTabs = ['main', 'blck', 'tagscr', 'hz'];\r\n  let initialTab = 'main';\r\n  if (stored[KEYS.ACTIVE_POPUP_TAB] === 'extra') {\r\n    initialTab = 'tagscr';\r\n  } else if (validTabs.includes(stored[KEYS.ACTIVE_POPUP_TAB])) {\r\n    initialTab = stored[KEYS.ACTIVE_POPUP_TAB];\r\n  }\r\n  showTab(initialTab);\r\n\r\n  await refreshUI();\r\n\r\n  const liveTimer = setInterval(refreshUI, 1000);\r\n  window.addEventListener('unload', () => clearInterval(liveTimer));\r\n});\n</script>\r\n</body>\r\n</html>"
			};
			
// #endregion
// #endregion
// #region Polyfill Implementation
		function buildPolyfill({ isBackground = false, isOtherPage = false } = {}) {
		  // Generate a unique context ID for this polyfill instance
		  const contextType = isBackground
		    ? "background"
		    : isOtherPage
		      ? "options"
		      : "content";
		  const contextId = `${contextType}_${Math.random()
		    .toString(36)
		    .substring(2, 15)}`;
		
		  const IS_IFRAME = "false" === "true";
		  const BUS = (function () {
		    if (globalThis.__BUS) {
		      return globalThis.__BUS;
		    }
		    globalThis.__BUS = createEventBus(
		      "e621-extras",
		      IS_IFRAME ? "iframe" : "page",
		    );
		    return globalThis.__BUS;
		  })();
		  const RUNTIME = createRuntime(isBackground ? "background" : "tab", BUS);
		  const createNoopListeners = () => ({
		    addListener: (callback) => {
		      _log("addListener", callback);
		    },
		    removeListener: (callback) => {
		      _log("removeListener", callback);
		    },
		  });
		  // TODO: Stub
		  const storageChangeListeners = new Set();
		  function broadcastStorageChange(changes, areaName) {
		    storageChangeListeners.forEach((listener) => {
		      listener(changes, areaName);
		    });
		  }
		
		  let REQ_PERMS = [];
		
  // #region Chrome polyfill
			  let chrome = {
			    extension: {
			      isAllowedIncognitoAccess: () => Promise.resolve(true),
			      sendMessage: (...args) => _messagingHandler.sendMessage(...args),
			    },
			    permissions: {
			      // TODO: Remove origin permission means exclude from origin in startup (when checking for content scripts)
			      request: (permissions, callback) => {
			        _log("permissions.request", permissions, callback);
			        if (Array.isArray(permissions)) {
			          REQ_PERMS = [...REQ_PERMS, ...permissions];
			        }
			        if (typeof callback === "function") {
			          callback(permissions);
			        }
			        return Promise.resolve(permissions);
			      },
			      contains: (permissions, callback) => {
			        if (typeof callback === "function") {
			          callback(true);
			        }
			        return Promise.resolve(true);
			      },
			      getAll: () => {
			        return Promise.resolve({
			          permissions: EXTENSION_PERMISSIONS,
			          origins: ORIGIN_PERMISSIONS,
			        });
			      },
			      onAdded: createNoopListeners(),
			      onRemoved: createNoopListeners(),
			    },
			    i18n: {
			      getUILanguage: () => {
			        return USED_LOCALE || "en";
			      },
			      getMessage: (key, substitutions = []) => {
			        if (typeof substitutions === "string") {
			          substitutions = [substitutions];
			        }
			        if (typeof LOCALE_KEYS !== "undefined" && LOCALE_KEYS[key]) {
			          return LOCALE_KEYS[key].message?.replace(
			            /\$(\d+)/g,
			            (match, p1) => substitutions[p1 - 1] || match,
			          );
			        }
			        return key;
			      },
			    },
			    alarms: {
			      onAlarm: createNoopListeners(),
			      create: () => {
			        _log("alarms.create", arguments);
			      },
			      get: () => {
			        _log("alarms.get", arguments);
			      },
			    },
			    runtime: {
			      ...RUNTIME,
			      onInstalled: createNoopListeners(),
			      onStartup: createNoopListeners(),
			      // TODO: Postmessage to parent to open options page or call openOptionsPage
			      openOptionsPage: () => {
			        // const url = chrome.runtime.getURL(OPTIONS_PAGE_PATH);
			        // console.log("openOptionsPage", _openTab, url, EXTENSION_ASSETS_MAP);
			        // _openTab(url);
			        if (typeof openOptionsPage === "function") {
			          openOptionsPage();
			        } else if (window.parent) {
			          window.parent.postMessage({ type: "openOptionsPage" }, "*");
			        } else {
			          _warn("openOptionsPage not available.");
			        }
			      },
			      getManifest: () => {
			        // The manifest object will be injected into the scope where buildPolyfill is called
			        if (typeof INJECTED_MANIFEST !== "undefined") {
			          return JSON.parse(JSON.stringify(INJECTED_MANIFEST)); // Return deep copy
			        }
			        _warn("INJECTED_MANIFEST not found for chrome.runtime.getManifest");
			        return { name: "Unknown", version: "0.0", manifest_version: 2 };
			      },
			      getURL: (path) => {
			        if (!path) return "";
			        if (path.startsWith("/")) {
			          path = path.substring(1);
			        }
			
			        if (typeof _createAssetUrl === "function") {
			          return _createAssetUrl(path);
			        }
			
			        _warn(
			          `chrome.runtime.getURL fallback for '${path}'. Assets may not be available.`,
			        );
			        // Attempt a relative path resolution (highly context-dependent and likely wrong)
			        try {
			          if (window.location.protocol.startsWith("http")) {
			            return new URL(path, window.location.href).toString();
			          }
			        } catch (e) {
			          /* ignore error, fallback */
			        }
			        return path;
			      },
			      id: "polyfilled-extension-" + Math.random().toString(36).substring(2, 15),
			      lastError: null,
			      setUninstallURL: () => {},
			      setUpdateURL: () => {},
			      getPlatformInfo: async () => {
			        const platform = {
			          os: "unknown",
			          arch: "unknown",
			          nacl_arch: "unknown",
			        };
			
			        if (typeof navigator !== "undefined") {
			          const userAgent = navigator.userAgent.toLowerCase();
			          if (userAgent.includes("mac")) platform.os = "mac";
			          else if (userAgent.includes("win")) platform.os = "win";
			          else if (userAgent.includes("linux")) platform.os = "linux";
			          else if (userAgent.includes("android")) platform.os = "android";
			          else if (userAgent.includes("ios")) platform.os = "ios";
			
			          if (userAgent.includes("x86_64") || userAgent.includes("amd64")) {
			            platform.arch = "x86-64";
			          } else if (userAgent.includes("i386") || userAgent.includes("i686")) {
			            platform.arch = "x86-32";
			          } else if (userAgent.includes("arm")) {
			            platform.arch = "arm";
			          }
			        }
			
			        return platform;
			      },
			      getBrowserInfo: async () => {
			        const info = {
			          name: "unknown",
			          version: "unknown",
			          buildID: "unknown",
			        };
			
			        if (typeof navigator !== "undefined") {
			          const userAgent = navigator.userAgent;
			          if (userAgent.includes("Chrome")) {
			            info.name = "Chrome";
			            const match = userAgent.match(/Chrome\/([0-9.]+)/);
			            if (match) info.version = match[1];
			          } else if (userAgent.includes("Firefox")) {
			            info.name = "Firefox";
			            const match = userAgent.match(/Firefox\/([0-9.]+)/);
			            if (match) info.version = match[1];
			          } else if (userAgent.includes("Safari")) {
			            info.name = "Safari";
			            const match = userAgent.match(/Version\/([0-9.]+)/);
			            if (match) info.version = match[1];
			          }
			        }
			
			        return info;
			      },
			    },
			    storage: {
			      local: {
			        get: function (keys, callback) {
			          if (typeof _storageGet !== "function")
			            throw new Error("_storageGet not defined");
			
			          const promise = _storageGet(keys);
			
			          if (typeof callback === "function") {
			            promise
			              .then((result) => {
			                try {
			                  callback(result);
			                } catch (e) {
			                  _error("Error in storage.get callback:", e);
			                }
			              })
			              .catch((error) => {
			                _error("Storage.get error:", error);
			                callback({});
			              });
			            return;
			          }
			
			          return promise;
			        },
			        set: function (items, callback) {
			          if (typeof _storageSet !== "function")
			            throw new Error("_storageSet not defined");
			
			          const promise = _storageSet(items).then((result) => {
			            broadcastStorageChange(items, "local");
			            return result;
			          });
			
			          if (typeof callback === "function") {
			            promise
			              .then((result) => {
			                try {
			                  callback(result);
			                } catch (e) {
			                  _error("Error in storage.set callback:", e);
			                }
			              })
			              .catch((error) => {
			                _error("Storage.set error:", error);
			                callback();
			              });
			            return;
			          }
			
			          return promise;
			        },
			        remove: function (keys, callback) {
			          if (typeof _storageRemove !== "function")
			            throw new Error("_storageRemove not defined");
			
			          const promise = _storageRemove(keys).then((result) => {
			            const changes = {};
			            const keyList = Array.isArray(keys) ? keys : [keys];
			            keyList.forEach((key) => {
			              changes[key] = { oldValue: undefined, newValue: undefined };
			            });
			            broadcastStorageChange(changes, "local");
			            return result;
			          });
			
			          if (typeof callback === "function") {
			            promise
			              .then((result) => {
			                try {
			                  callback(result);
			                } catch (e) {
			                  _error("Error in storage.remove callback:", e);
			                }
			              })
			              .catch((error) => {
			                _error("Storage.remove error:", error);
			                callback();
			              });
			            return;
			          }
			
			          return promise;
			        },
			        clear: function (callback) {
			          if (typeof _storageClear !== "function")
			            throw new Error("_storageClear not defined");
			
			          const promise = _storageClear().then((result) => {
			            broadcastStorageChange({}, "local");
			            return result;
			          });
			
			          if (typeof callback === "function") {
			            promise
			              .then((result) => {
			                try {
			                  callback(result);
			                } catch (e) {
			                  _error("Error in storage.clear callback:", e);
			                }
			              })
			              .catch((error) => {
			                _error("Storage.clear error:", error);
			                callback();
			              });
			            return;
			          }
			
			          return promise;
			        },
			        onChanged: {
			          addListener: (callback) => {
			            storageChangeListeners.add(callback);
			          },
			          removeListener: (callback) => {
			            storageChangeListeners.delete(callback);
			          },
			        },
			      },
			      sync: {
			        get: function (keys, callback) {
			          _warn("chrome.storage.sync polyfill maps to local");
			          return chrome.storage.local.get(keys, callback);
			        },
			        set: function (items, callback) {
			          _warn("chrome.storage.sync polyfill maps to local");
			
			          const promise = chrome.storage.local.set(items).then((result) => {
			            broadcastStorageChange(items, "sync");
			            return result;
			          });
			
			          if (typeof callback === "function") {
			            promise
			              .then((result) => {
			                try {
			                  callback(result);
			                } catch (e) {
			                  _error("Error in storage.sync.set callback:", e);
			                }
			              })
			              .catch((error) => {
			                _error("Storage.sync.set error:", error);
			                callback();
			              });
			            return;
			          }
			
			          return promise;
			        },
			        remove: function (keys, callback) {
			          _warn("chrome.storage.sync polyfill maps to local");
			
			          const promise = chrome.storage.local.remove(keys).then((result) => {
			            const changes = {};
			            const keyList = Array.isArray(keys) ? keys : [keys];
			            keyList.forEach((key) => {
			              changes[key] = { oldValue: undefined, newValue: undefined };
			            });
			            broadcastStorageChange(changes, "sync");
			            return result;
			          });
			
			          if (typeof callback === "function") {
			            promise
			              .then((result) => {
			                try {
			                  callback(result);
			                } catch (e) {
			                  _error("Error in storage.sync.remove callback:", e);
			                }
			              })
			              .catch((error) => {
			                _error("Storage.sync.remove error:", error);
			                callback();
			              });
			            return;
			          }
			
			          return promise;
			        },
			        clear: function (callback) {
			          _warn("chrome.storage.sync polyfill maps to local");
			
			          const promise = chrome.storage.local.clear().then((result) => {
			            broadcastStorageChange({}, "sync");
			            return result;
			          });
			
			          if (typeof callback === "function") {
			            promise
			              .then((result) => {
			                try {
			                  callback(result);
			                } catch (e) {
			                  _error("Error in storage.sync.clear callback:", e);
			                }
			              })
			              .catch((error) => {
			                _error("Storage.sync.clear error:", error);
			                callback();
			              });
			            return;
			          }
			
			          return promise;
			        },
			        onChanged: {
			          addListener: (callback) => {
			            storageChangeListeners.add(callback);
			          },
			          removeListener: (callback) => {
			            storageChangeListeners.delete(callback);
			          },
			        },
			      },
			      onChanged: {
			        addListener: (callback) => {
			          storageChangeListeners.add(callback);
			        },
			        removeListener: (callback) => {
			          storageChangeListeners.delete(callback);
			        },
			      },
			      managed: {
			        get: function (keys, callback) {
			          _warn("chrome.storage.managed polyfill is read-only empty.");
			
			          const promise = Promise.resolve({});
			
			          if (typeof callback === "function") {
			            promise.then((result) => {
			              try {
			                callback(result);
			              } catch (e) {
			                _error("Error in storage.managed.get callback:", e);
			              }
			            });
			            return;
			          }
			
			          return promise;
			        },
			      },
			    },
			    cookies: (function () {
			      const cookieChangeListeners = new Set();
			      function broadcastCookieChange(changeInfo) {
			        cookieChangeListeners.forEach((listener) => {
			          try {
			            listener(changeInfo);
			          } catch (e) {
			            _error("Error in cookies.onChanged listener:", e);
			          }
			        });
			      }
			
			      function handlePromiseCallback(promise, callback) {
			        if (typeof callback === "function") {
			          promise
			            .then((result) => callback(result))
			            .catch((error) => {
			              // chrome.runtime.lastError = { message: error.message }; // TODO: Implement lastError
			              _error(error);
			              callback(); // Call with undefined on error
			            });
			          return;
			        }
			        return promise;
			      }
			
			      return {
			        get: function (details, callback) {
			          if (typeof _cookieList !== "function") {
			            return handlePromiseCallback(
			              Promise.reject(new Error("_cookieList not defined")),
			              callback,
			            );
			          }
			          const promise = _cookieList({
			            url: details.url,
			            name: details.name,
			            storeId: details.storeId,
			            partitionKey: details.partitionKey,
			          }).then((cookies) => {
			            if (!cookies || cookies.length === 0) {
			              return null;
			            }
			            // Sort by path length (longest first), then creation time (earliest first, if available)
			            cookies.sort((a, b) => {
			              const pathLenDiff = (b.path || "").length - (a.path || "").length;
			              if (pathLenDiff !== 0) return pathLenDiff;
			              return (a.creationTime || 0) - (b.creationTime || 0);
			            });
			            return cookies[0];
			          });
			          return handlePromiseCallback(promise, callback);
			        },
			
			        getAll: function (details, callback) {
			          if (typeof _cookieList !== "function") {
			            return handlePromiseCallback(
			              Promise.reject(new Error("_cookieList not defined")),
			              callback,
			            );
			          }
			          if (details.partitionKey) {
			            _warn(
			              "cookies.getAll: partitionKey is not fully supported in this environment.",
			            );
			          }
			          const promise = _cookieList(details);
			          return handlePromiseCallback(promise, callback);
			        },
			
			        set: function (details, callback) {
			          const promise = (async () => {
			            if (
			              typeof _cookieSet !== "function" ||
			              typeof _cookieList !== "function"
			            ) {
			              throw new Error("_cookieSet or _cookieList not defined");
			            }
			            if (details.partitionKey) {
			              _warn(
			                "cookies.set: partitionKey is not fully supported in this environment.",
			              );
			            }
			
			            const getDetails = {
			              url: details.url,
			              name: details.name,
			              storeId: details.storeId,
			            };
			            const oldCookies = await _cookieList(getDetails);
			            const oldCookie = oldCookies && oldCookies[0];
			
			            if (oldCookie) {
			              broadcastCookieChange({
			                cause: "overwrite",
			                cookie: oldCookie,
			                removed: true,
			              });
			            }
			
			            await _cookieSet(details);
			            const newCookies = await _cookieList(getDetails);
			            const newCookie = newCookies && newCookies[0];
			
			            if (newCookie) {
			              broadcastCookieChange({
			                cause: "explicit",
			                cookie: newCookie,
			                removed: false,
			              });
			            }
			            return newCookie || null;
			          })();
			          return handlePromiseCallback(promise, callback);
			        },
			
			        remove: function (details, callback) {
			          const promise = (async () => {
			            if (
			              typeof _cookieDelete !== "function" ||
			              typeof _cookieList !== "function"
			            ) {
			              throw new Error("_cookieDelete or _cookieList not defined");
			            }
			            const oldCookies = await _cookieList(details);
			            const oldCookie = oldCookies && oldCookies[0];
			
			            if (!oldCookie) return null; // Nothing to remove
			
			            await _cookieDelete(details);
			
			            broadcastCookieChange({
			              cause: "explicit",
			              cookie: oldCookie,
			              removed: true,
			            });
			
			            return {
			              url: details.url,
			              name: details.name,
			              storeId: details.storeId || "0",
			              partitionKey: details.partitionKey,
			            };
			          })();
			          return handlePromiseCallback(promise, callback);
			        },
			
			        getAllCookieStores: function (callback) {
			          const promise = Promise.resolve([
			            { id: "0", tabIds: [1] }, // Mock store for the current context
			          ]);
			          return handlePromiseCallback(promise, callback);
			        },
			
			        getPartitionKey: function (details, callback) {
			          _warn(
			            "chrome.cookies.getPartitionKey is not supported in this environment.",
			          );
			          const promise = Promise.resolve({ partitionKey: {} }); // Return empty partition key
			          return handlePromiseCallback(promise, callback);
			        },
			
			        onChanged: {
			          addListener: (callback) => {
			            if (typeof callback === "function") {
			              cookieChangeListeners.add(callback);
			            }
			          },
			          removeListener: (callback) => {
			            cookieChangeListeners.delete(callback);
			          },
			        },
			      };
			    })(),
			    tabs: {
			      query: async (queryInfo) => {
			        _warn("chrome.tabs.query polyfill only returns current tab info.");
			        const dummyId = Math.floor(Math.random() * 1000) + 1;
			        return [
			          {
			            id: dummyId,
			            url: CURRENT_LOCATION,
			            active: true,
			            windowId: 1,
			            status: "complete",
			          },
			        ];
			      },
			      create: async ({ url, active = true }) => {
			        _log(`[Polyfill tabs.create] URL: ${url}`);
			        if (typeof _openTab !== "function")
			          throw new Error("_openTab not defined");
			        _openTab(url, active);
			        const dummyId = Math.floor(Math.random() * 1000) + 1001;
			        return Promise.resolve({
			          id: dummyId,
			          url: url,
			          active,
			          windowId: 1,
			        });
			      },
			      sendMessage: async (tabId, message) => {
			        _warn(
			          `chrome.tabs.sendMessage polyfill (to tab ${tabId}) redirects to runtime.sendMessage (current context).`,
			        );
			        return chrome.runtime.sendMessage(message);
			      },
			      onActivated: createNoopListeners(),
			      onUpdated: createNoopListeners(),
			      onRemoved: createNoopListeners(),
			      onReplaced: createNoopListeners(),
			      onCreated: createNoopListeners(),
			      onMoved: createNoopListeners(),
			      onDetached: createNoopListeners(),
			      onAttached: createNoopListeners(),
			    },
			    windows: {
			      onFocusChanged: createNoopListeners(),
			      onCreated: createNoopListeners(),
			      onRemoved: createNoopListeners(),
			      onFocused: createNoopListeners(),
			      onFocus: createNoopListeners(),
			      onBlur: createNoopListeners(),
			      onFocused: createNoopListeners(),
			    },
			    notifications: {
			      create: async (notificationId, options) => {
			        try {
			          let id = notificationId;
			          let notificationOptions = options;
			
			          if (typeof notificationId === "object" && notificationId !== null) {
			            notificationOptions = notificationId;
			            id = "notification_" + Math.random().toString(36).substring(2, 15);
			          } else if (typeof notificationId === "string" && options) {
			            id = notificationId;
			            notificationOptions = options;
			          } else {
			            throw new Error("Invalid parameters for notifications.create");
			          }
			
			          if (!notificationOptions || typeof notificationOptions !== "object") {
			            throw new Error("Notification options must be an object");
			          }
			
			          const {
			            title,
			            message,
			            iconUrl,
			            type = "basic",
			          } = notificationOptions;
			
			          if (!title || !message) {
			            throw new Error("Notification must have title and message");
			          }
			
			          if ("Notification" in window) {
			            if (Notification.permission === "granted") {
			              const notification = new Notification(title, {
			                body: message,
			                icon: iconUrl,
			                tag: id,
			              });
			
			              _log(`[Notifications] Created notification: ${id}`);
			              return id;
			            } else if (Notification.permission === "default") {
			              const permission = await Notification.requestPermission();
			              if (permission === "granted") {
			                const notification = new Notification(title, {
			                  body: message,
			                  icon: iconUrl,
			                  tag: id,
			                });
			                _log(
			                  `[Notifications] Created notification after permission: ${id}`,
			                );
			                return id;
			              } else {
			                _warn("[Notifications] Permission denied for notifications");
			                return id;
			              }
			            } else {
			              _warn("[Notifications] Notifications are blocked");
			              return id;
			            }
			          } else {
			            _warn(
			              "[Notifications] Native notifications not supported, using console fallback",
			            );
			            _log(`[Notification] ${title}: ${message}`);
			            return id;
			          }
			        } catch (error) {
			          _error("[Notifications] Error creating notification:", error.message);
			          throw error;
			        }
			      },
			      clear: async (notificationId) => {
			        _log(`[Notifications] Clear notification: ${notificationId}`);
			        // For native notifications, there's no direct way to clear by ID
			        // This is a limitation of the Web Notifications API
			        return true;
			      },
			      getAll: async () => {
			        _warn("[Notifications] getAll not fully supported in polyfill");
			        return {};
			      },
			      getPermissionLevel: async () => {
			        if ("Notification" in window) {
			          const permission = Notification.permission;
			          return { level: permission === "granted" ? "granted" : "denied" };
			        }
			        return { level: "denied" };
			      },
			    },
			    contextMenus: {
			      create: (createProperties, callback) => {
			        try {
			          if (!createProperties || typeof createProperties !== "object") {
			            throw new Error("Context menu create properties must be an object");
			          }
			
			          const { id, title, contexts = ["page"], onclick } = createProperties;
			          const menuId =
			            id || `menu_${Math.random().toString(36).substring(2, 15)}`;
			
			          if (!title || typeof title !== "string") {
			            throw new Error("Context menu must have a title");
			          }
			
			          // Store menu items for potential use
			          if (!window._polyfillContextMenus) {
			            window._polyfillContextMenus = new Map();
			          }
			
			          window._polyfillContextMenus.set(menuId, {
			            id: menuId,
			            title,
			            contexts,
			            onclick,
			            enabled: createProperties.enabled !== false,
			          });
			
			          _log(
			            `[ContextMenus] Created context menu item: ${title} (${menuId})`,
			          );
			
			          // Try to register a menu command as fallback
			          if (typeof _registerMenuCommand === "function") {
			            try {
			              _registerMenuCommand(
			                title,
			                onclick ||
			                  (() => {
			                    _log(`Context menu clicked: ${title}`);
			                  }),
			              );
			            } catch (e) {
			              _warn(
			                "[ContextMenus] Failed to register as menu command:",
			                e.message,
			              );
			            }
			          }
			
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			
			          return menuId;
			        } catch (error) {
			          _error("[ContextMenus] Error creating context menu:", error.message);
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			          throw error;
			        }
			      },
			      update: (id, updateProperties, callback) => {
			        try {
			          if (
			            !window._polyfillContextMenus ||
			            !window._polyfillContextMenus.has(id)
			          ) {
			            throw new Error(`Context menu item not found: ${id}`);
			          }
			
			          const menuItem = window._polyfillContextMenus.get(id);
			          Object.assign(menuItem, updateProperties);
			
			          _log(`[ContextMenus] Updated context menu item: ${id}`);
			
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			        } catch (error) {
			          _error("[ContextMenus] Error updating context menu:", error.message);
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			        }
			      },
			      remove: (menuItemId, callback) => {
			        try {
			          if (
			            window._polyfillContextMenus &&
			            window._polyfillContextMenus.has(menuItemId)
			          ) {
			            window._polyfillContextMenus.delete(menuItemId);
			            _log(`[ContextMenus] Removed context menu item: ${menuItemId}`);
			          } else {
			            _warn(
			              `[ContextMenus] Context menu item not found for removal: ${menuItemId}`,
			            );
			          }
			
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			        } catch (error) {
			          _error("[ContextMenus] Error removing context menu:", error.message);
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			        }
			      },
			      removeAll: (callback) => {
			        try {
			          if (window._polyfillContextMenus) {
			            const count = window._polyfillContextMenus.size;
			            window._polyfillContextMenus.clear();
			            _log(`[ContextMenus] Removed all ${count} context menu items`);
			          }
			
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			        } catch (error) {
			          _error(
			            "[ContextMenus] Error removing all context menus:",
			            error.message,
			          );
			          if (callback && typeof callback === "function") {
			            setTimeout(() => callback(), 0);
			          }
			        }
			      },
			      onClicked: {
			        addListener: (callback) => {
			          if (!window._polyfillContextMenuListeners) {
			            window._polyfillContextMenuListeners = new Set();
			          }
			          window._polyfillContextMenuListeners.add(callback);
			          _log("[ContextMenus] Added click listener");
			        },
			        removeListener: (callback) => {
			          if (window._polyfillContextMenuListeners) {
			            window._polyfillContextMenuListeners.delete(callback);
			            _log("[ContextMenus] Removed click listener");
			          }
			        },
			      },
			    },
			  };
			
			  const tc = (fn) => {
			    try {
			      fn();
			    } catch (e) {}
			  };
			  const loggingProxyHandler = (_key) => ({
			    get(target, key, receiver) {
			      tc(() => _log(`[${contextType}] [CHROME - ${_key}] Getting ${key}`));
			      return Reflect.get(target, key, receiver);
			    },
			    set(target, key, value, receiver) {
			      tc(() =>
			        _log(`[${contextType}] [CHROME - ${_key}] Setting ${key} to ${value}`),
			      );
			      return Reflect.set(target, key, value, receiver);
			    },
			    has(target, key) {
			      tc(() =>
			        _log(`[${contextType}] [CHROME - ${_key}] Checking if ${key} exists`),
			      );
			      return Reflect.has(target, key);
			    },
			  });
			  chrome = Object.fromEntries(
			    Object.entries(chrome).map(([key, value]) => [
			      key,
			      new Proxy(value, loggingProxyHandler(key)),
			    ]),
			  );
			
			  // Alias browser to chrome for common Firefox pattern
			  const browser = new Proxy(chrome, loggingProxyHandler);
			
			  const oldGlobalThis = globalThis;
			  const oldWindow = window;
			  const oldSelf = self;
			  const oldGlobal = globalThis;
			  const __globalsStorage = {};
			
			  const TO_MODIFY = [oldGlobalThis, oldWindow, oldSelf, oldGlobal];
			  const set = (k, v) => {
			    __globalsStorage[k] = v;
			    TO_MODIFY.forEach((target) => {
			      target[k] = v;
			    });
			  };
			  const proxyHandler = {
			    get(target, key, receiver) {
			      const fns = [
			        () => __globalsStorage[key],
			        () => Reflect.get(target, key, target),
			        () => target[key],
			      ];
			      const out = fns
			        .map((f) => {
			          try {
			            let out = f();
			            return out;
			          } catch (e) {
			            return undefined;
			          }
			        })
			        .find((f) => f !== undefined);
			      if (typeof out === "function") {
			        return out.bind(target);
			      }
			      return out;
			    },
			    set(target, key, value, receiver) {
			      try {
			        tc(() => _log(`[${contextType}] Setting ${key} to ${value}`));
			        set(key, value);
			        return Reflect.set(target, key, value, receiver);
			      } catch (e) {
			        _error("Error setting", key, value, e);
			        try {
			          target[key] = value;
			          return true;
			        } catch (e) {
			          _error("Error setting", key, value, e);
			        }
			        return false;
			      }
			    },
			    has(target, key) {
			      try {
			        return key in __globalsStorage || key in target;
			      } catch (e) {
			        _error("Error has", key, e);
			        try {
			          return key in __globalsStorage || key in target;
			        } catch (e) {
			          _error("Error has", key, e);
			        }
			        return false;
			      }
			    },
			    getOwnPropertyDescriptor(target, key) {
			      try {
			        if (key in __globalsStorage) {
			          return {
			            configurable: true,
			            enumerable: true,
			            writable: true,
			            value: __globalsStorage[key],
			          };
			        }
			        // fall back to the real globalThis
			        const desc = Reflect.getOwnPropertyDescriptor(target, key);
			        // ensure it's configurable so the with‑scope binding logic can override it
			        if (desc && !desc.configurable) {
			          desc.configurable = true;
			        }
			        return desc;
			      } catch (e) {
			        _error("Error getOwnPropertyDescriptor", key, e);
			        return {
			          configurable: true,
			          enumerable: true,
			          writable: true,
			          value: undefined,
			        };
			      }
			    },
			
			    defineProperty(target, key, descriptor) {
			      try {
			        // Normalize descriptor to avoid mixed accessor & data attributes
			        const hasAccessor = "get" in descriptor || "set" in descriptor;
			
			        if (hasAccessor) {
			          // Build a clean descriptor without value/writable when accessors present
			          const normalized = {
			            configurable:
			              "configurable" in descriptor ? descriptor.configurable : true,
			            enumerable:
			              "enumerable" in descriptor ? descriptor.enumerable : false,
			          };
			          if ("get" in descriptor) normalized.get = descriptor.get;
			          if ("set" in descriptor) normalized.set = descriptor.set;
			
			          // Store accessor references for inspection but avoid breaking invariants
			          set(key, {
			            get: descriptor.get,
			            set: descriptor.set,
			          });
			
			          return Reflect.defineProperty(target, key, normalized);
			        }
			
			        // Data descriptor path
			        set(key, descriptor.value);
			        return Reflect.defineProperty(target, key, descriptor);
			      } catch (e) {
			        _error("Error defineProperty", key, descriptor, e);
			        return false;
			      }
			    },
			  };
			
			  // Create proxies once proxyHandler is defined
			  const proxyWindow = new Proxy(oldWindow, proxyHandler);
			  const proxyGlobalThis = new Proxy(oldGlobalThis, proxyHandler);
			  const proxyGlobal = new Proxy(oldGlobal, proxyHandler);
			  const proxySelf = new Proxy(oldSelf, proxyHandler);
			
			  // Seed storage with core globals so lookups succeed inside `with` blocks
			  Object.assign(__globalsStorage, {
			    chrome,
			    browser,
			    window: proxyWindow,
			    globalThis: proxyGlobalThis,
			    global: proxyGlobal,
			    self: proxySelf,
			    document: oldWindow.document,
			  });
			
			  const __globals = {
			    chrome,
			    browser,
			    window: proxyWindow,
			    globalThis: proxyGlobalThis,
			    global: proxyGlobal,
			    self: proxySelf,
			    __globals: __globalsStorage,
			  };
			
			  __globals.contextId = contextId;
			  __globals.contextType = contextType;
			  __globals.module = undefined;
			  __globals.amd = undefined;
			  __globals.define = undefined;
			  __globals.importScripts = (...args) => {
			    _log("importScripts", args);
			  };
			
			  return __globals;
			}
			
			
			if (typeof window !== 'undefined') {
			    window.buildPolyfill = buildPolyfill;
			}
			
  // #endregion
// #endregion
    // #endregion
   // #region Background Script Environment
	
	const START_BACKGROUND_SCRIPT = (function(){
	  const backgroundPolyfill = buildPolyfill({ isBackground: true });
	  const scriptName = "e621 extras";
	  const debug = "[e621 extras]";
	  _log(debug + ' Executing background scripts...');
	
	  function executeBackgroundScripts(){
	    with(backgroundPolyfill){
	      // BG: background.js
	/* Service worker: manages tag script queue, IndexedDB image storage,
	 * and background tag script execution without requiring open tabs. */
	importScripts('scripts/constants.js');
	
	const { ACTIONS, STORAGE_KEYS: KEYS } = E621_CONSTANTS;
	const ALLOWED_HOST = /^(?:[a-z0-9-]+\.)*e(?:621|926)\.net$/i;
	const BASE64_CHUNK = 0x8000;
	const HOURLY_EDIT_LIMIT = 150;
	const ONE_HOUR_MS = 60 * 60 * 1000;
	const CLOCK_BUFFER_MS = 3000;
	
	// ---------- IndexedDB image store ----------
	const DB_NAME = 'e621_extras_db';
	const DB_VERSION = 1;
	const STORE_NAME = 'post_images';
	
	function openDatabase() {
	  return new Promise((resolve, reject) => {
	    const req = indexedDB.open(DB_NAME, DB_VERSION);
	    req.onupgradeneeded = () => {
	      const db = req.result;
	      if (!db.objectStoreNames.contains(STORE_NAME)) {
	        db.createObjectStore(STORE_NAME, { keyPath: 'postId' });
	      }
	    };
	    req.onsuccess = () => resolve(req.result);
	    req.onerror = () => reject(req.error);
	  });
	}
	
	async function idbSaveImage(postId, dataUrl) {
	  if (!dataUrl) return;
	  const db = await openDatabase();
	  return new Promise((resolve, reject) => {
	    const tx = db.transaction(STORE_NAME, 'readwrite');
	    tx.objectStore(STORE_NAME).put({ postId: Number(postId), dataUrl, savedAt: Date.now() });
	    tx.oncomplete = () => resolve();
	    tx.onerror = () => reject(tx.error);
	  });
	}
	
	async function idbGetImage(postId) {
	  const db = await openDatabase();
	  return new Promise((resolve, reject) => {
	    const tx = db.transaction(STORE_NAME, 'readonly');
	    const req = tx.objectStore(STORE_NAME).get(Number(postId));
	    req.onsuccess = () => resolve(req.result?.dataUrl || null);
	    req.onerror = () => reject(req.error);
	  });
	}
	
	async function idbDeleteImage(postId) {
	  const db = await openDatabase();
	  return new Promise((resolve, reject) => {
	    const tx = db.transaction(STORE_NAME, 'readwrite');
	    tx.objectStore(STORE_NAME).delete(Number(postId));
	    tx.oncomplete = () => resolve();
	    tx.onerror = () => reject(tx.error);
	  });
	}
	
	async function idbClearImages() {
	  const db = await openDatabase();
	  return new Promise((resolve, reject) => {
	    const tx = db.transaction(STORE_NAME, 'readwrite');
	    tx.objectStore(STORE_NAME).clear();
	    tx.oncomplete = () => resolve();
	    tx.onerror = () => reject(tx.error);
	  });
	}
	
	// ---------- Image fetch helpers ----------
	function toBase64(bytes) {
	  let binary = '';
	  for (let i = 0; i < bytes.length; i += BASE64_CHUNK) {
	    binary += String.fromCharCode.apply(null, bytes.subarray(i, i + BASE64_CHUNK));
	  }
	  return btoa(binary);
	}
	
	async function fetchImageAsDataUrl(rawUrl) {
	  const url = new URL(rawUrl);
	  if (url.protocol !== 'https:' || !ALLOWED_HOST.test(url.hostname)) {
	    throw new Error('Host not allowed');
	  }
	  const res = await fetch(url.href);
	  if (!res.ok) throw new Error(`HTTP ${res.status}`);
	  const type = res.headers.get('content-type') || '';
	  if (!type.startsWith('image/')) throw new Error('Not an image');
	  const bytes = new Uint8Array(await res.arrayBuffer());
	  return `data:${type};base64,${toBase64(bytes)}`;
	}
	
	// ---------- Queue synchronization mutex ----------
	let queueTail = Promise.resolve();
	function runQueueOp(task) {
	  const next = queueTail.then(task);
	  queueTail = next.catch(err => console.warn('[background] queueOp error:', err));
	  return next;
	}
	
	// ---------- CSRF token management ----------
	let cachedCsrfToken = null;
	async function getCsrfToken(forceFresh = false) {
	  if (cachedCsrfToken && !forceFresh) return cachedCsrfToken;
	  try {
	    const res = await fetch('https://e621.net/posts', { credentials: 'include' });
	    if (!res.ok) return null;
	    const html = await res.text();
	    const match = html.match(/<meta\s+name=["']csrf-token["']\s+content=["']([^"']+)["']/i);
	    if (match?.[1]) {
	      cachedCsrfToken = match[1];
	      return cachedCsrfToken;
	    }
	  } catch (err) {
	    console.warn('[background] Failed to fetch CSRF token:', err);
	  }
	  return null;
	}
	
	// ---------- Queue state operations ----------
	function mergeDiffs(oldDiff = '', newDiff = '') {
	  const tokenMap = new Map();
	  const parse = (str) => String(str || '').split(/\s+/).filter(Boolean);
	
	  parse(oldDiff).forEach(token => {
	    const key = token.startsWith('-') ? token.slice(1).toLowerCase() : token.toLowerCase();
	    tokenMap.set(key, token);
	  });
	
	  parse(newDiff).forEach(token => {
	    const key = token.startsWith('-') ? token.slice(1).toLowerCase() : token.toLowerCase();
	    tokenMap.set(key, token);
	  });
	
	  return Array.from(tokenMap.values()).join(' ');
	}
	
	async function enqueuePost(postId, editData) {
	  const id = Number(postId);
	  const data = await chrome.storage.local.get([KEYS.TAG_SCRIPT_QUEUE]);
	  const queue = data[KEYS.TAG_SCRIPT_QUEUE] || {};
	  const existing = queue[id];
	
	  queue[id] = {
	    id,
	    changes: existing ? mergeDiffs(existing.changes, editData.changes) : editData.changes,
	    reason: editData.reason || existing?.reason || '',
	    queuedAt: existing?.queuedAt || Date.now(),
	    previewUrl: editData.previewUrl || existing?.previewUrl || '',
	    score: editData.score !== undefined ? editData.score : (existing?.score ?? 0),
	    favCount: editData.favCount !== undefined ? editData.favCount : (existing?.favCount ?? 0),
	    commentCount: editData.commentCount !== undefined ? editData.commentCount : (existing?.commentCount ?? 0),
	    rating: editData.rating || existing?.rating || 's',
	    fileExt: editData.fileExt || existing?.fileExt || 'jpg',
	    tags: editData.tags !== undefined ? editData.tags : (existing?.tags || ''),
	    flags: editData.flags !== undefined ? editData.flags : (existing?.flags || ''),
	    hasParent: editData.hasParent !== undefined ? editData.hasParent : (existing?.hasParent ?? false),
	    hasChildren: editData.hasChildren !== undefined ? editData.hasChildren : (existing?.hasChildren ?? false)
	  };
	
	  await chrome.storage.local.set({ [KEYS.TAG_SCRIPT_QUEUE]: queue });
	  return queue;
	}
	
	async function dequeuePost(postId) {
	  const id = Number(postId);
	  const data = await chrome.storage.local.get([KEYS.TAG_SCRIPT_QUEUE]);
	  const queue = data[KEYS.TAG_SCRIPT_QUEUE] || {};
	  if (queue[id]) {
	    delete queue[id];
	    await chrome.storage.local.set({ [KEYS.TAG_SCRIPT_QUEUE]: queue });
	    await idbDeleteImage(id);
	    if (Object.keys(queue).length === 0) {
	      await chrome.storage.local.set({
	        [KEYS.QUEUE_SLOT_TIMESTAMPS]: [],
	        [KEYS.QUEUE_TIMEOUT_UNTIL]: 0
	      });
	    }
	  }
	  return queue;
	}
	
	async function clearTagQueue() {
	  await chrome.storage.local.set({
	    [KEYS.TAG_SCRIPT_QUEUE]: {},
	    [KEYS.QUEUE_SLOT_TIMESTAMPS]: [],
	    [KEYS.QUEUE_TIMEOUT_UNTIL]: 0
	  });
	  await idbClearImages();
	}
	
	// ---------- Background Queue Processor ----------
	function isHourlyLimitResponse(status, errorText) {
	  if (status === 429) return true;
	  if (status === 403) {
	    const msg = String(errorText || '').toLowerCase();
	    return msg.includes('hourly limit') || msg.includes('limit exceeded') || msg.includes('throttl');
	  }
	  return false;
	}
	
	async function verifyHourlyLimitExceeded() {
	  try {
	    const meRes = await fetch('https://e621.net/users/me.json', { credentials: 'include' });
	    if (!meRes.ok) return { isLimit: false, nextAvailableMs: 0, slots: [] };
	    const me = await meRes.json();
	    if (!me?.id) return { isLimit: false, nextAvailableMs: 0, slots: [] };
	
	    const url = `https://e621.net/post_versions.json?search[updater_id]=${me.id}&limit=${HOURLY_EDIT_LIMIT}`;
	    const vRes = await fetch(url, { credentials: 'include' });
	    if (!vRes.ok) return { isLimit: false, nextAvailableMs: 0, slots: [] };
	    const list = await vRes.json();
	    const versions = Array.isArray(list) ? list : (list?.post_versions || []);
	    const now = Date.now();
	
	    const recentTimestamps = [];
	    for (const item of versions) {
	      const dateStr = item?.updated_at || item?.created_at;
	      if (!dateStr) continue;
	      const t = new Date(dateStr).getTime();
	      if (!Number.isNaN(t) && (now - t) < ONE_HOUR_MS) {
	        recentTimestamps.push(t);
	      }
	    }
	
	    if (recentTimestamps.length < HOURLY_EDIT_LIMIT) {
	      return { isLimit: false, nextAvailableMs: 0, slots: [] };
	    }
	
	    recentTimestamps.sort((a, b) => a - b);
	    const slots = recentTimestamps.map(t => t + ONE_HOUR_MS + CLOCK_BUFFER_MS);
	
	    const data = await chrome.storage.local.get([KEYS.TAG_SCRIPT_QUEUE]);
	    const queue = data[KEYS.TAG_SCRIPT_QUEUE] || {};
	    const queueLen = Math.max(Object.keys(queue).length, slots.length);
	    while (slots.length < queueLen) {
	      const baseIdx = Math.max(0, slots.length - HOURLY_EDIT_LIMIT);
	      slots.push((slots[baseIdx] || now) + ONE_HOUR_MS);
	    }
	
	    const nextAvailableMs = Math.max(1000, slots[0] - now);
	    await chrome.storage.local.set({ [KEYS.QUEUE_SLOT_TIMESTAMPS]: slots });
	    return { isLimit: true, nextAvailableMs, slots };
	  } catch (err) {
	    console.warn('[background] verifyHourlyLimitExceeded error:', err);
	    return { isLimit: false, nextAvailableMs: 0, slots: [] };
	  }
	}
	
	async function processQueueInBackground() {
	  const data = await chrome.storage.local.get([
	    KEYS.QUEUE_TAG_SCRIPTS_ENABLED,
	    KEYS.TAG_SCRIPT_QUEUE,
	    KEYS.QUEUE_TIMEOUT_UNTIL,
	    KEYS.QUEUE_SLOT_TIMESTAMPS
	  ]);
	
	  if (!data[KEYS.QUEUE_TAG_SCRIPTS_ENABLED]) {
	    return { total: 0, updated: 0, errors: 0, disabled: true };
	  }
	
	  const queue = data[KEYS.TAG_SCRIPT_QUEUE] || {};
	  const items = Object.values(queue);
	  if (!items.length) {
	    await chrome.storage.local.set({
	      [KEYS.QUEUE_SLOT_TIMESTAMPS]: [],
	      [KEYS.QUEUE_TIMEOUT_UNTIL]: 0
	    });
	    return { total: 0, updated: 0, errors: 0 };
	  }
	
	  const now = Date.now();
	  let slots = data[KEYS.QUEUE_SLOT_TIMESTAMPS] || [];
	  const scheduledTime = slots[0] || Number(data[KEYS.QUEUE_TIMEOUT_UNTIL]) || 0;
	
	  if (scheduledTime > now) {
	    chrome.alarms.create('tagQueueCheck', { when: scheduledTime + 500 });
	    return { total: items.length, updated: 0, errors: 0, hitLimit: true };
	  }
	
	  let csrf = await getCsrfToken();
	  let updated = 0;
	  let errors = 0;
	  let hitLimit = false;
	
	  for (let i = 0; i < items.length; i++) {
	    const item = items[i];
	    const currentSlots = (await chrome.storage.local.get([KEYS.QUEUE_SLOT_TIMESTAMPS]))[KEYS.QUEUE_SLOT_TIMESTAMPS] || [];
	    const nextSlot = currentSlots[0] || 0;
	
	    if (nextSlot > Date.now()) {
	      hitLimit = true;
	      await chrome.storage.local.set({ [KEYS.QUEUE_TIMEOUT_UNTIL]: nextSlot });
	      chrome.alarms.create('tagQueueCheck', { when: nextSlot + 500 });
	      break;
	    }
	
	    try {
	      let res = await fetch(`https://e621.net/posts/${item.id}.json`, {
	        method: 'PATCH',
	        headers: {
	          'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
	          'X-CSRF-Token': csrf || ''
	        },
	        body: new URLSearchParams({
	          'post[tag_string_diff]': item.changes,
	          'post[edit_reason]': item.reason || ''
	        }).toString(),
	        credentials: 'include'
	      });
	
	      if (res.status === 403) {
	        csrf = await getCsrfToken(true);
	        res = await fetch(`https://e621.net/posts/${item.id}.json`, {
	          method: 'PATCH',
	          headers: {
	            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
	            'X-CSRF-Token': csrf || ''
	          },
	          body: new URLSearchParams({
	            'post[tag_string_diff]': item.changes,
	            'post[edit_reason]': item.reason || ''
	          }).toString(),
	          credentials: 'include'
	        });
	      }
	
	      if (!res.ok) {
	        let errorText = '';
	        try {
	          const jsonErr = await res.json();
	          errorText = jsonErr.reason || jsonErr.message || JSON.stringify(jsonErr);
	        } catch (_) {
	          errorText = await res.text().catch(() => '');
	        }
	
	        if (isHourlyLimitResponse(res.status, errorText)) {
	          const check = await verifyHourlyLimitExceeded();
	          const delay = check.isLimit ? check.nextAvailableMs : (res.status === 429 ? 60000 : 30000);
	          const resumeAt = Date.now() + delay;
	          await chrome.storage.local.set({ [KEYS.QUEUE_TIMEOUT_UNTIL]: resumeAt });
	          chrome.alarms.create('tagQueueCheck', { when: resumeAt + 500 });
	          hitLimit = true;
	          break;
	        }
	
	        // Post cannot be edited or tag is locked — dequeue to avoid blocking the queue
	        await dequeuePost(item.id);
	        errors++;
	        continue;
	      }
	
	      // Success
	      await dequeuePost(item.id);
	      const remainingSlots = (await chrome.storage.local.get([KEYS.QUEUE_SLOT_TIMESTAMPS]))[KEYS.QUEUE_SLOT_TIMESTAMPS] || [];
	      if (remainingSlots.length > 0) {
	        remainingSlots.shift();
	        await chrome.storage.local.set({ [KEYS.QUEUE_SLOT_TIMESTAMPS]: remainingSlots });
	      }
	      updated++;
	    } catch (err) {
	      console.warn(`[background] Error processing queued post #${item.id}:`, err);
	      errors++;
	    }
	  }
	
	  if (!hitLimit) {
	    const rem = (await chrome.storage.local.get([KEYS.TAG_SCRIPT_QUEUE]))[KEYS.TAG_SCRIPT_QUEUE] || {};
	    if (Object.keys(rem).length === 0) {
	      await chrome.storage.local.set({
	        [KEYS.QUEUE_SLOT_TIMESTAMPS]: [],
	        [KEYS.QUEUE_TIMEOUT_UNTIL]: 0
	      });
	    }
	  }
	
	  return { total: items.length, updated, errors, hitLimit };
	}
	
	// ---------- Message Router ----------
	chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
	  if (sender.id !== chrome.runtime.id) return false;
	
	  switch (msg?.action) {
	    case ACTIONS.FETCH_IMAGE:
	      fetchImageAsDataUrl(msg.url)
	        .then(dataUrl => sendResponse({ ok: true, dataUrl }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.SAVE_IMAGE:
	      idbSaveImage(msg.postId, msg.dataUrl)
	        .then(() => sendResponse({ ok: true }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.GET_IMAGE:
	      idbGetImage(msg.postId)
	        .then(dataUrl => sendResponse({ ok: true, dataUrl }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.DELETE_IMAGE:
	      idbDeleteImage(msg.postId)
	        .then(() => sendResponse({ ok: true }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.CLEAR_IMAGES:
	      idbClearImages()
	        .then(() => sendResponse({ ok: true }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.ENQUEUE_TAG_SCRIPT:
	      runQueueOp(() => enqueuePost(msg.postId, msg.editData))
	        .then(queue => sendResponse({ ok: true, queue }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.DEQUEUE_TAG_SCRIPT:
	      runQueueOp(() => dequeuePost(msg.postId))
	        .then(queue => sendResponse({ ok: true, queue }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.CLEAR_TAG_QUEUE:
	      runQueueOp(() => clearTagQueue())
	        .then(() => sendResponse({ ok: true }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.PROCESS_TAG_QUEUE:
	      runQueueOp(() => processQueueInBackground())
	        .then(res => sendResponse({ ok: true, ...res }))
	        .catch(err => sendResponse({ ok: false, error: err.message }));
	      return true;
	
	    case ACTIONS.SET_CSRF_TOKEN:
	      if (msg.csrfToken) cachedCsrfToken = msg.csrfToken;
	      sendResponse({ ok: true });
	      return false;
	
	    default:
	      return false;
	  }
	});
	
	// Periodic alarm: triggers background processing even when all e621 tabs are closed
	chrome.alarms.onAlarm.addListener(async (alarm) => {
	  if (alarm.name === 'tagQueueCheck') {
	    await runQueueOp(() => processQueueInBackground());
	  }
	});
	
	function ensureAlarm() {
	  chrome.alarms.get('tagQueueCheck', (alarm) => {
	    if (!alarm) {
	      chrome.alarms.create('tagQueueCheck', { periodInMinutes: 1 });
	    }
	  });
	}
	
	chrome.runtime.onInstalled.addListener(ensureAlarm);
	chrome.runtime.onStartup.addListener(ensureAlarm);
	ensureAlarm();
	    }
	  }
	
	  executeBackgroundScripts.call(backgroundPolyfill);
	
	  _log(debug + ' Background scripts execution complete.');
	});
	
	setTimeout(() => {
	  // Wait for things to be defined
	  START_BACKGROUND_SCRIPT();
	}, 10);
	_log("START_BACKGROUND_SCRIPT", START_BACKGROUND_SCRIPT);
	// End background script environment
	
	
   // #endregion
    // #region Orchestration Logic
	// Other globals currently defined at this spot: SCRIPT_NAME, _log, _warn, _error
	const INJECTED_MANIFEST = {"manifest_version":3,"name":"e621 extras","version":"1.5.3","description":"Downvotes-to-blacklist manager, post tree viewer, hover zoom and tag scripts for regular members of e621 and e926.","permissions":["storage","alarms","unlimitedStorage"],"optional_permissions":[],"content_scripts":[{"matches":["https://e621.net/*","https://e926.net/*"],"css":["styles/extras.css"],"js":["scripts/constants.js","scripts/core.js","scripts/selectors.js","scripts/utils.js","scripts/net.js","scripts/ui/dom.js","scripts/ui/layout.js","scripts/ui/thumbnail.js","scripts/ui/dtext.js","scripts/ui/tooltips.js","scripts/blacklist.js","scripts/uploader.js","scripts/user_edits.js","scripts/tree/data.js","scripts/tree/export.js","scripts/tree/modal.js","scripts/hover_zoom/data.js","scripts/hover_zoom/index.js","scripts/tag_script/engine.js","scripts/tag_script/queue.js","scripts/tag_script/timeouted_page.js","scripts/tag_script/logger.js","scripts/tag_script/storage.js","scripts/tag_script/runner.js","scripts/tag_script/preview.js","scripts/tag_script/index.js","scripts/content.js"],"run_at":"document_end"}],"options_ui":{},"browser_action":{},"page_action":{},"action":{"default_popup":"popup/popup.html","default_title":"e621 extras"},"icons":{},"web_accessible_resources":[],"background":{"service_worker":"background.js"},"_id":"e621-extras"};
	const CONTENT_SCRIPT_CONFIGS_FOR_MATCHING = [
	  {
	    "matches": [
	      "https://e621.net/*",
	      "https://e926.net/*"
	    ]
	  }
	];
	const OPTIONS_PAGE_PATH = null;
	const POPUP_PAGE_PATH = "popup/popup.html";
	const EXTENSION_ICON = null;
	const extensionCssData = {    "styles/extras.css": "/* ===== toast ===== */\r\n.e621x-toast {\r\n  position: fixed; bottom: 24px; right: 24px; z-index: 999999;\r\n  background: #000; color: #fff; border: 1px solid #444;\r\n  padding: 10px 16px; font: 12px/1.4 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\r\n  letter-spacing: 0.3px; pointer-events: none; transition: opacity 0.3s;\r\n}\r\n.e621x-toast--error { background: #1a1a1a; border-color: #ff4444; }\r\n.e621x-toast.is-hiding { opacity: 0; }\r\n\r\n/* ===== thumbnail card (size/fit come from --e621x-* variables set on the container) ===== */\r\n.e621x-thumb {\r\n  margin: 0 !important; float: none !important; align-self: center !important;\r\n  width: var(--e621x-w, 160px) !important; height: var(--e621x-h, 210px) !important;\r\n  background: var(--color-section, #152f56) !important;\r\n  display: flex !important; flex-direction: column !important;\r\n  position: relative !important; overflow: visible !important;\r\n  z-index: 2; box-sizing: border-box !important;\r\n  transition: transform 0.15s ease, box-shadow 0.15s ease;\r\n}\r\n.e621x-thumb:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6); }\r\n.e621x-thumb .thm-link {\r\n  flex: 1 1 auto !important; width: 100% !important; min-height: 0 !important;\r\n  position: relative !important; display: flex !important;\r\n  align-items: center !important; justify-content: center !important; overflow: hidden !important;\r\n}\r\n.e621x-thumb picture { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; overflow: hidden; }\r\n.e621x-thumb .thm-img-thumb { width: 100%; height: 100%; object-fit: var(--e621x-fit, cover); object-position: center; display: block; }\r\n.e621x-thumb .thm-desc {\r\n  width: 100% !important; max-width: 100% !important; box-sizing: border-box !important;\r\n  margin: 0 !important; align-self: stretch !important; flex-shrink: 0 !important;\r\n  border-radius: 0 0 4px 4px !important;\r\n}\r\n.e621x-pin {\r\n  position: absolute; left: 50%; transform: translateX(-50%);\r\n  width: 18px; height: 18px; border-radius: 50%; box-sizing: border-box;\r\n  background: #152f56; border: 2px solid #3b6aa0; color: #fff;\r\n  font-size: 10px; font-weight: bold; display: flex; align-items: center; justify-content: center;\r\n  z-index: 10; pointer-events: none;\r\n}\r\n.e621x-pin--top { top: -16px; }\r\n.e621x-pin--bottom { bottom: -16px; }\r\n\r\n/* ===== post tree modal ===== */\r\n.e621x-tree-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.75); z-index: 1000000; backdrop-filter: blur(2px); }\r\n.e621x-tree-dialog {\r\n  position: fixed !important; inset: 40px; margin: 0; padding: 0;\r\n  background: var(--color-section, #152f56); border: 1px solid var(--color-section-border, #2a4c7e);\r\n  border-radius: 4px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); z-index: 1000001;\r\n  display: flex; flex-direction: column; overflow: hidden;\r\n}\r\n.e621x-tree-titlebar { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; border-bottom: 1px solid var(--color-section-border, #2a4c7e); }\r\n.e621x-tree-toolbar { display: flex; gap: 8px; align-items: center; }\r\n.e621x-tree-close { font-size: 20px; line-height: 1; padding: 2px 8px; cursor: pointer; }\r\n.e621x-tree-content { flex: 1; overflow: auto; position: relative; padding: 0; width: 100%; box-sizing: border-box; }\r\n.e621x-tree-message { margin: 40px auto; text-align: center; color: #888; }\r\n.e621x-tree-message--error { color: #ff5555; }\r\n.e621x-tree { position: relative; display: flex; flex-direction: column; align-items: center; width: 100%; min-width: 100%; margin: 0 auto; padding: 30px 24px 0; box-sizing: border-box; }\r\n.e621x-tree-lines { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 1; overflow: visible; }\r\n.e621x-tree-line { stroke: rgba(255, 255, 255, 0.22); stroke-width: 1.5; fill: none; transition: stroke 0.15s ease, stroke-width 0.15s ease, opacity 0.15s ease; }\r\n.e621x-tree-line.is-active { stroke: #3b82f6; stroke-width: 2.5; }\r\n.e621x-tree-line.is-dim { opacity: 0.12; }\r\n.e621x-tree-tier { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 100%; margin-bottom: 50px; position: relative; z-index: 2; box-sizing: border-box; }\r\n.e621x-tree-divider { display: flex; align-items: center; width: 100%; margin-bottom: 24px; user-select: none; }\r\n.e621x-tree-divider-line { flex: 1; height: 1px; background: rgba(255, 255, 255, 0.08); }\r\n.e621x-tree-divider-label { padding: 2px 14px; margin: 0 16px; font-size: 10px; font-weight: 700; letter-spacing: 1.2px; text-transform: uppercase; color: #8a9ba8; background: #0c1a2f; border: 1px solid #24416b; border-radius: 12px; }\r\n.e621x-tree-cards { display: flex; flex-flow: row wrap; justify-content: center; align-items: flex-start; gap: 36px 24px; width: 100%; max-width: 100%; box-sizing: border-box; }\r\n.e621x-tree-spacer { width: 100%; height: 60px; flex-shrink: 0; pointer-events: none; }\r\n\r\n/* ===== \"show post tree\" button / notices ===== */\r\n.e621x-notice-row { display: flex !important; align-items: center !important; flex-wrap: wrap !important; gap: 8px !important; }\r\n.e621x-notice-inline { display: inline-flex !important; align-items: center !important; gap: 6px !important; min-height: 32px !important; line-height: normal !important; margin: 0 !important; vertical-align: middle !important; }\r\n.e621x-tree-btn { margin-left: 8px; vertical-align: middle; }\r\n\r\n/* ===== tag script panel ===== */\r\n#e621x-tag-script-ui { display: none; margin-top: 0.5em; flex-direction: column; gap: 6px; }\r\n#e621x-tag-script-ui.is-visible { display: flex; }\r\n.e621x-ts-input { width: 100%; min-width: 0; box-sizing: border-box; font-size: 12px; padding: 4px 6px; }\r\n.e621x-ts-grow { flex: 1 1 0; min-width: 0; width: auto; }\r\n.e621x-ts-all { padding: 4px 10px; font-size: 12px; cursor: pointer; flex-shrink: 0; }\r\n.e621x-ts-btn { padding: 4px 6px; font-size: 12px; cursor: pointer; white-space: nowrap; flex-shrink: 0; }\r\n.e621x-ts-row { display: flex; gap: 4px; align-items: center; width: 100%; box-sizing: border-box; }\r\n.e621x-ts-row--spread { justify-content: space-between; padding: 0px 0; font-size: 11px; }\r\n.e621x-ts-label { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; color: #b4c7d9; }\r\n\r\n/* ===== Hover Zoom ===== */\r\n.e621x-zoom {\r\n  position: fixed;\r\n  z-index: 10000002;\r\n  pointer-events: none;\r\n  padding: 0 !important;\r\n  margin: 0 !important;\r\n  line-height: 0 !important;\r\n  font-size: 0 !important;\r\n  border: 1px solid rgba(255, 255, 255, 0.45);\r\n  background: transparent !important;\r\n  border-radius: 4px;\r\n  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.9);\r\n  overflow: hidden;\r\n  box-sizing: content-box !important;\r\n  display: block;\r\n}\r\n\r\n.e621x-zoom.is-hidden {\r\n  display: none !important;\r\n}\r\n\r\n.e621x-zoom-media {\r\n  display: block !important;\r\n  margin: 0 !important;\r\n  padding: 0 !important;\r\n  border: none !important;\r\n  outline: none !important;\r\n}\r\n\r\n/* ===== Queued Tag Scripts (3px Clean Yellow Outline outside thumbnail) ===== */\r\n.e621x-thumb-queued {\r\n  outline: 3px solid #ffff00 !important;\r\n  outline-offset: 0 !important;\r\n  box-shadow: none !important;\r\n  position: relative !important;\r\n  z-index: 5 !important;\r\n}\r\n\r\n/* ===== Successfully Updated Tag Scripts (3px Clean Green Outline) ===== */\r\n.e621x-thumb-updated {\r\n  outline: 3px solid #22c55e !important;\r\n  outline-offset: 0 !important;\r\n  box-shadow: none !important;\r\n  position: relative !important;\r\n  z-index: 5 !important;\r\n}\r\n\r\n/* ===== Locked Tag Scripts (3px Clean Red Outline) ===== */\r\n.e621x-thumb-locked {\r\n  outline: 3px solid #ef4444 !important;\r\n  outline-offset: 0 !important;\r\n  box-shadow: none !important;\r\n  position: relative !important;\r\n  z-index: 5 !important;\r\n}\r\n\r\n\r\n.e621x-ts-queue-link {\r\n  font-size: 11px;\r\n  color: #ffff00 !important;\r\n  font-weight: bold;\r\n  text-decoration: underline;\r\n  margin-left: auto;\r\n  margin-right: 8px;\r\n}\r\n.e621x-ts-queue-link:hover {\r\n  color: #ffffff !important;\r\n}\r\n\r\n/* ===== /posts/timeouted page ===== */\r\n.e621x-queue-page {\r\n  width: 100%;\r\n  max-width: 1600px;\r\n  margin: 20px auto;\r\n  padding: 0 20px 60px;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.e621x-queue-header {\r\n  background: var(--color-section, #152f56);\r\n  border: 1px solid var(--color-section-border, #2a4c7e);\r\n  border-radius: 4px;\r\n  padding: 16px 20px;\r\n  margin-bottom: 24px;\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  gap: 16px;\r\n}\r\n\r\n.e621x-queue-title-area h2 {\r\n  margin: 0 0 4px 0;\r\n  font-size: 18px;\r\n  color: #ffffff;\r\n  display: flex;\r\n  align-items: center;\r\n  gap: 10px;\r\n}\r\n\r\n.e621x-queue-badge {\r\n  background: #ffff00;\r\n  color: #000000;\r\n  font-size: 11px;\r\n  font-weight: 700;\r\n  padding: 2px 8px;\r\n  border-radius: 10px;\r\n}\r\n\r\n.e621x-queue-subtitle {\r\n  font-size: 12px;\r\n  color: #b4c7d9;\r\n}\r\n\r\n.e621x-queue-actions {\r\n  display: flex;\r\n  gap: 10px;\r\n  align-items: center;\r\n}\r\n\r\n.e621x-queue-status-pill {\r\n  font-size: 12px;\r\n  padding: 6px 12px;\r\n  background: #0a1b32;\r\n  border: 1px solid #24416b;\r\n  border-radius: 4px;\r\n  color: #e2e8f0;\r\n}\r\n.e621x-queue-status-pill.is-timeout {\r\n  border-color: #eab308;\r\n  color: #fef08a;\r\n}\r\n.e621x-queue-status-pill.is-ready {\r\n  border-color: #22c55e;\r\n  color: #86efac;\r\n}\r\n\r\n.e621x-queue-card-wrap {\r\n  position: relative;\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  gap: 6px;\r\n}\r\n\r\n.e621x-queue-card-meta {\r\n  width: var(--e621x-w, 160px);\r\n  background: #0a1b32;\r\n  border: 1px solid #24416b;\r\n  border-radius: 4px;\r\n  padding: 6px 8px;\r\n  box-sizing: border-box;\r\n  font-size: 11px;\r\n  color: #ffffff;\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 4px;\r\n}\r\n\r\n.e621x-queue-diff {\r\n  word-break: break-all;\r\n  font-family: monospace;\r\n  font-size: 10px;\r\n  color: #fef08a;\r\n  max-height: 48px;\r\n  overflow-y: auto;\r\n}\r\n\r\n.e621x-queue-card-actions {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  margin-top: 2px;\r\n}\r\n\r\n.e621x-queue-remove-btn {\r\n  background: transparent;\r\n  border: 1px solid #ef4444;\r\n  color: #ef4444;\r\n  font-size: 10px;\r\n  padding: 2px 6px;\r\n  border-radius: 2px;\r\n  cursor: pointer;\r\n  transition: all 0.15s;\r\n}\r\n.e621x-queue-remove-btn:hover {\r\n  background: #ef4444;\r\n  color: #ffffff;\r\n}\r\n\r\n.e621x-queue-empty {\r\n  text-align: center;\r\n  padding: 60px 20px;\r\n  background: var(--color-section, #152f56);\r\n  border: 1px solid var(--color-section-border, #2a4c7e);\r\n  border-radius: 4px;\r\n  color: #b4c7d9;\r\n}\r\n.e621x-queue-empty h3 {\r\n  color: #ffffff;\r\n  margin-bottom: 8px;\r\n}\r\n\r\n.hidden {\r\n  display: none !important;\r\n}\r\n\r\n/* ===== Tag Script Ctrl Preview Tooltip ===== */\r\n.e621x-ts-preview {\r\n  position: fixed;\r\n  z-index: 10000005;\r\n  pointer-events: none;\r\n  background: #000000;\r\n  border: 1px solid #ffffff;\r\n  border-radius: 2px;\r\n  padding: 8px 12px;\r\n  color: #ffffff;\r\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\r\n  font-size: 13px;\r\n  line-height: 1.5;\r\n  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.85);\r\n  white-space: nowrap;\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 2px;\r\n}\r\n\r\n.e621x-ts-preview.is-hidden {\r\n  display: none !important;\r\n}\r\n\r\n.e621x-ts-preview-line {\r\n  display: block;\r\n}\r\n\r\n.e621x-ts-preview-bullet {\r\n  color: #ffffff;\r\n  margin-right: 6px;\r\n  user-select: none;\r\n}\r\n\r\n/* Tag type colors matching e621 categories */\r\n.e621x-tag-type-0, .e621x-tag-type-general { color: #7eb3e6; }\r\n.e621x-tag-type-1, .e621x-tag-type-artist { color: #f2ac08; }\r\n.e621x-tag-type-2, .e621x-tag-type-contributor { color: #00aa00; }\r\n.e621x-tag-type-3, .e621x-tag-type-copyright { color: #dd00aa; }\r\n.e621x-tag-type-4, .e621x-tag-type-character { color: #00aa00; }\r\n.e621x-tag-type-5, .e621x-tag-type-species { color: #ed5d1f; }\r\n.e621x-tag-type-6, .e621x-tag-type-invalid { color: #ff3333; }\r\n.e621x-tag-type-7, .e621x-tag-type-meta { color: #ffffff; }\r\n.e621x-tag-type-8, .e621x-tag-type-lore { color: #2e8b57; }\r\n.e621x-tag-type-command { color: #fef08a; }\r\n.e621x-tag-type-prefix { color: #94a3b8; }\r\n\r\n/* ===== Specific Post Edits Filter ===== */\r\n.e621x-specific-edits-wrapper {\r\n  margin-top: 10px;\r\n  width: 100%;\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  text-align: center;\r\n}\r\n\r\n.e621x-specific-edits-trigger {\r\n  font-size: 11px;\r\n  color: #ffff00 !important;\r\n  font-weight: bold;\r\n  text-decoration: underline;\r\n  cursor: pointer;\r\n  display: inline-block;\r\n  text-align: center;\r\n  margin: 6px auto;\r\n  user-select: none;\r\n  transition: color 0.15s ease;\r\n}\r\n\r\n.e621x-specific-edits-trigger:hover {\r\n  color: #ffffff !important;\r\n}\r\n\r\n.e621x-specific-edits-panel {\r\n  background: var(--color-section, #152f56);\r\n  border: 1px solid var(--color-section-border, #2a4c7e);\r\n  border-radius: 4px;\r\n  padding: 12px 14px;\r\n  margin: 8px auto 0 auto;\r\n  width: 100%;\r\n  max-width: 480px;\r\n  box-sizing: border-box;\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 10px;\r\n  font-size: 12px;\r\n  text-align: left;\r\n}\r\n\r\n.e621x-specific-edits-panel.is-hidden {\r\n  display: none !important;\r\n}\r\n\r\n.e621x-specific-edits-header {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  border-bottom: 1px solid rgba(255, 255, 255, 0.12);\r\n  padding-bottom: 6px;\r\n}\r\n\r\n.e621x-specific-edits-title {\r\n  font-weight: bold;\r\n  color: #ffffff;\r\n  font-size: 12px;\r\n  letter-spacing: 0.3px;\r\n}\r\n\r\n.e621x-specific-edits-close {\r\n  background: transparent;\r\n  border: none;\r\n  color: #8a9ba8;\r\n  font-size: 16px;\r\n  line-height: 1;\r\n  cursor: pointer;\r\n  padding: 0 4px;\r\n}\r\n\r\n.e621x-specific-edits-close:hover {\r\n  color: #ffffff;\r\n}\r\n\r\n.e621x-specific-edits-fields {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 8px;\r\n}\r\n\r\n.e621x-specific-edits-field {\r\n  display: flex;\r\n  flex-direction: column;\r\n  gap: 3px;\r\n}\r\n\r\n.e621x-specific-edits-field label {\r\n  font-size: 11px;\r\n  color: #b4c7d9;\r\n  font-weight: 600;\r\n}\r\n\r\n.e621x-specific-edits-input {\r\n  width: 100%;\r\n  box-sizing: border-box;\r\n  font-size: 12px;\r\n  padding: 6px 8px;\r\n  background-color: #ffffff !important;\r\n  color: #000000 !important;\r\n  border: 1px solid #cccccc;\r\n  border-radius: 2px;\r\n  outline: none;\r\n  transition: background-color 0.15s ease, border-color 0.15s ease;\r\n}\r\n\r\n.e621x-specific-edits-input::placeholder {\r\n  color: #777777 !important;\r\n}\r\n\r\n.e621x-specific-edits-input:focus {\r\n  background-color: #ffffcc !important;\r\n  border-color: #bbbb33 !important;\r\n  color: #000000 !important;\r\n}\r\n\r\n.e621x-specific-edits-actions {\r\n  display: flex;\r\n  gap: 6px;\r\n  align-items: center;\r\n  flex-wrap: wrap;\r\n  margin-top: 4px;\r\n}\r\n\r\n.e621x-specific-edits-btn {\r\n  padding: 4px 12px;\r\n  font-size: 11px;\r\n  cursor: pointer;\r\n  white-space: nowrap;\r\n}\r\n\r\n.e621x-specific-edits-status {\r\n  font-size: 11px;\r\n  color: #fef08a;\r\n  background: #0a1b32;\r\n  border: 1px solid #24416b;\r\n  border-radius: 3px;\r\n  padding: 6px 10px;\r\n  line-height: 1.4;\r\n  word-break: break-word;\r\n}\r\n\r\n.e621x-specific-edits-status.is-error {\r\n  color: #ff6b6b;\r\n  border-color: #7f1d1d;\r\n}\r\n\r\n.e621x-specific-edits-status a {\r\n  color: #ffff00;\r\n  text-decoration: underline;\r\n  margin-left: 4px;\r\n  font-weight: 600;\r\n}\r\n\r\n/* ===== Tag Update Statistics Modal & Table ===== */\r\n.e621x-specific-edits-triggers-row {\r\n  display: flex;\r\n  gap: 10px;\r\n  align-items: center;\r\n  justify-content: center;\r\n  margin: 6px auto;\r\n}\r\n\r\n.e621x-trigger-sep {\r\n  color: #64748b;\r\n  font-size: 13px;\r\n  user-select: none;\r\n}\r\n\r\n.e621x-stats-overlay {\r\n  position: fixed;\r\n  inset: 0;\r\n  background: rgba(0, 0, 0, 0.8);\r\n  z-index: 1000000;\r\n  backdrop-filter: blur(3px);\r\n  display: flex;\r\n  align-items: center;\r\n  justify-content: center;\r\n}\r\n\r\n.e621x-stats-dialog {\r\n  width: 92%;\r\n  max-width: 1050px;\r\n  height: 85vh;\r\n  background: var(--color-section, #152f56);\r\n  border: 1px solid var(--color-section-border, #2a4c7e);\r\n  border-radius: 6px;\r\n  box-shadow: 0 14px 45px rgba(0, 0, 0, 0.85);\r\n  display: flex;\r\n  flex-direction: column;\r\n  overflow: hidden;\r\n  box-sizing: border-box;\r\n}\r\n\r\n.e621x-stats-header {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  padding: 12px 18px;\r\n  background: #0a1b32;\r\n  border-bottom: 1px solid var(--color-section-border, #2a4c7e);\r\n}\r\n\r\n.e621x-stats-title {\r\n  font-size: 15px;\r\n  font-weight: 700;\r\n  color: #ffffff;\r\n  letter-spacing: 0.4px;\r\n}\r\n\r\n.e621x-stats-close-btn {\r\n  background: transparent;\r\n  border: none;\r\n  color: #94a3b8;\r\n  font-size: 24px;\r\n  line-height: 1;\r\n  cursor: pointer;\r\n  padding: 0 4px;\r\n  transition: color 0.15s;\r\n}\r\n\r\n.e621x-stats-close-btn:hover {\r\n  color: #ffffff;\r\n}\r\n\r\n/* Progress Box */\r\n.e621x-stats-progress-box {\r\n  padding: 30px 24px;\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  gap: 16px;\r\n  background: rgba(10, 27, 50, 0.5);\r\n}\r\n\r\n.e621x-stats-progress-text {\r\n  font-size: 14px;\r\n  color: #fef08a;\r\n  font-weight: 600;\r\n  text-align: center;\r\n}\r\n\r\n.e621x-stats-progress-track {\r\n  width: 100%;\r\n  max-width: 500px;\r\n  height: 8px;\r\n  background: #0a1b32;\r\n  border: 1px solid #24416b;\r\n  border-radius: 4px;\r\n  overflow: hidden;\r\n}\r\n\r\n.e621x-stats-progress-fill {\r\n  height: 100%;\r\n  width: 0%;\r\n  background: #3b82f6;\r\n  transition: width 0.25s ease;\r\n}\r\n\r\n.e621x-stats-progress-actions {\r\n  display: flex;\r\n  gap: 12px;\r\n}\r\n\r\n/* Summary Bar */\r\n.e621x-stats-summary-bar {\r\n  display: grid;\r\n  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\r\n  gap: 10px;\r\n  padding: 12px 18px;\r\n  background: #0e223d;\r\n  border-bottom: 1px solid var(--color-section-border, #2a4c7e);\r\n}\r\n\r\n.e621x-stats-summary-card {\r\n  display: flex;\r\n  flex-direction: column;\r\n  align-items: center;\r\n  justify-content: center;\r\n  background: #0a1b32;\r\n  border: 1px solid #24416b;\r\n  border-radius: 4px;\r\n  padding: 8px 10px;\r\n  text-align: center;\r\n}\r\n\r\n.e621x-stats-summary-num {\r\n  font-size: 18px;\r\n  font-weight: 700;\r\n  color: #ffffff;\r\n}\r\n\r\n.e621x-stats-summary-lbl {\r\n  font-size: 10px;\r\n  text-transform: uppercase;\r\n  color: #94a3b8;\r\n  letter-spacing: 0.8px;\r\n  margin-top: 2px;\r\n}\r\n\r\n.e621x-color-add {\r\n  color: #22c55e !important;\r\n}\r\n\r\n.e621x-color-rem {\r\n  color: #ef4444 !important;\r\n}\r\n\r\n/* Toolbar */\r\n.e621x-stats-toolbar {\r\n  display: flex;\r\n  flex-wrap: wrap;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  gap: 10px;\r\n  padding: 10px 18px;\r\n  background: #0c1c30;\r\n  border-bottom: 1px solid #24416b;\r\n}\r\n\r\n.e621x-stats-toolbar-group {\r\n  display: flex;\r\n  align-items: center;\r\n  gap: 8px;\r\n  flex-wrap: wrap;\r\n}\r\n\r\n.e621x-stats-search {\r\n  width: 190px;\r\n  background: #ffffff !important;\r\n  color: #000000 !important;\r\n  padding: 5px 8px;\r\n  font-size: 12px;\r\n  border-radius: 2px;\r\n}\r\n\r\n.e621x-stats-select {\r\n  padding: 5px 8px;\r\n  font-size: 12px;\r\n  background: #0a1b32;\r\n  color: #ffffff;\r\n  border: 1px solid #24416b;\r\n  border-radius: 2px;\r\n  outline: none;\r\n  cursor: pointer;\r\n}\r\n\r\n.e621x-stats-btn {\r\n  padding: 5px 12px;\r\n  font-size: 11px;\r\n  cursor: pointer;\r\n  white-space: nowrap;\r\n}\r\n\r\n/* Table Area */\r\n.e621x-stats-table-wrap {\r\n  flex: 1;\r\n  overflow-y: auto;\r\n  padding: 0;\r\n  background: #0a1728;\r\n}\r\n\r\n.e621x-stats-table {\r\n  width: 100%;\r\n  border-collapse: collapse;\r\n  font-size: 12px;\r\n  text-align: left;\r\n}\r\n\r\n.e621x-stats-table th {\r\n  position: sticky;\r\n  top: 0;\r\n  background: #0a1b32;\r\n  color: #94a3b8;\r\n  font-size: 11px;\r\n  text-transform: uppercase;\r\n  letter-spacing: 0.6px;\r\n  padding: 9px 14px;\r\n  border-bottom: 1px solid #24416b;\r\n  z-index: 2;\r\n}\r\n\r\n.e621x-stats-table td {\r\n  padding: 7px 14px;\r\n  border-bottom: 1px solid rgba(255, 255, 255, 0.05);\r\n  color: #e2e8f0;\r\n}\r\n\r\n.e621x-stats-row:hover {\r\n  background: rgba(255, 255, 255, 0.04);\r\n}\r\n\r\n.e621x-stats-tag-link {\r\n  font-weight: 600;\r\n  text-decoration: none;\r\n  display: inline-block;\r\n  transition: opacity 0.15s;\r\n}\r\n\r\n.e621x-stats-tag-link:hover {\r\n  text-decoration: underline;\r\n  opacity: 0.85;\r\n}\r\n\r\n.e621x-cat-badge {\r\n  display: inline-block;\r\n  font-size: 9px;\r\n  font-weight: 700;\r\n  text-transform: uppercase;\r\n  letter-spacing: 0.6px;\r\n  padding: 1px 6px;\r\n  border-radius: 10px;\r\n  border: 1px solid currentColor;\r\n  background: rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n.e621x-stats-count-link {\r\n  font-weight: 700;\r\n  text-decoration: none;\r\n  cursor: pointer;\r\n}\r\n\r\n.e621x-stats-count-link:hover {\r\n  text-decoration: underline;\r\n}\r\n\r\n.e621x-stats-count-zero {\r\n  color: #64748b;\r\n}\r\n\r\n.e621x-stats-date {\r\n  color: #94a3b8;\r\n  font-family: monospace;\r\n  font-size: 11px;\r\n}\r\n\r\n.e621x-stats-empty {\r\n  text-align: center;\r\n  padding: 40px 10px;\r\n  color: #94a3b8;\r\n  font-size: 13px;\r\n}\r\n\r\n/* Pagination Bar */\r\n.e621x-stats-pagination {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  padding: 8px 18px;\r\n  background: #0a1b32;\r\n  border-top: 1px solid #24416b;\r\n}\r\n\r\n.e621x-stats-page-info {\r\n  font-size: 11px;\r\n  color: #94a3b8;\r\n}\r\n\r\n.e621x-stats-pagination-buttons {\r\n  display: flex;\r\n  gap: 8px;\r\n}\r\n\r\n.hidden,\r\n.e621x-stats-progress-box.hidden {\r\n  display: none !important;\r\n}\r\n"};
	
	const LOCALE_KEYS = {};
	const USED_LOCALE = "en";
	const CURRENT_LOCATION = window.location.href;
	
	const convertMatchPatternToRegExp = function convertMatchPatternToRegExp(pattern) {
	  if (pattern === "<all_urls>") {
	    return new RegExp(".*");
	  }
	  try {
	    const singleEscapedPattern = convertMatchPatternToRegExpString(
	      pattern
	    ).replace(/\\\\/g, "\\");
	    return new RegExp(singleEscapedPattern);
	  } catch (error) {
	    debug(
	      "Error converting match pattern to RegExp: %s, Error: %s",
	      pattern,
	      error.message
	    );
	    return new RegExp("$."); // Matches nothing on error
	  }
	};
	const convertMatchPatternToRegExpString = function convertMatchPatternToRegExpString(pattern) {
	  function escapeRegex(s) {
	    return s.replace(/[.*+?^${}()|[\]\\]/g, "\\\\$&");
	  }
	
	  if (typeof pattern !== "string" || !pattern) {
	    return "$."; // Matches nothing
	  }
	
	  const schemeMatch = pattern.match(/^(\*|https?|file|ftp):\/\//);
	  if (!schemeMatch) return "$."; // Invalid pattern
	  const scheme = schemeMatch[1];
	  pattern = pattern.substring(schemeMatch[0].length);
	  const schemeRegex = scheme === "*" ? "https?|file|ftp" : scheme;
	
	  const hostMatch = pattern.match(/^([^\/]+)/);
	  if (!hostMatch) return "$."; // Invalid pattern
	  const host = hostMatch[1];
	  pattern = pattern.substring(host.length); // Remainder is path
	
	  let hostRegex;
	  if (host === "*") {
	    hostRegex = "[^/]+"; // Matches any sequence of non-slash characters
	  } else if (host.startsWith("*.")) {
	    // Match any subdomain or the main domain
	    hostRegex = "(?:[^\\/]+\\.)?" + escapeRegex(host.substring(2));
	  } else {
	    hostRegex = escapeRegex(host); // Exact host match
	  }
	
	  let pathRegex = pattern;
	  if (!pathRegex.startsWith("/")) {
	    pathRegex = "/" + pathRegex; // Ensure path starts with /
	  }
	  // Convert glob (*) to regex (.*) and escape other special chars
	  pathRegex = pathRegex.split("*").map(escapeRegex).join(".*");
	
	  // Ensure the pattern covers the entire path segment correctly
	  if (pathRegex === "/.*") {
	    // Equivalent to /* in manifest, matches the root and anything after
	    pathRegex = "(?:/.*)?";
	  } else {
	    // Match the specific path and optionally query/hash or end of string
	    pathRegex = pathRegex + "(?:[?#]|$)";
	  }
	
	  // Combine and return the pattern string
	  // Needs double escaping for direct embedding in generated JS strings
	  const finalRegexString = `^${schemeRegex}:\\/\\/${hostRegex}${pathRegex}`;
	  return finalRegexString;
	};
	const ALL_PERMISSIONS = [
	  ...(INJECTED_MANIFEST.permissions || []),
	  ...(INJECTED_MANIFEST.optional_permissions || []),
	  ...(INJECTED_MANIFEST.host_permissions || []),
	  ...(INJECTED_MANIFEST.content_scripts
	    ?.map((cs) => cs.matches || [])
	    ?.flat() || []),
	];
	
	const isOrigin = (perm) => {
	  if (
	    perm.startsWith("*://") ||
	    perm.startsWith("http://") ||
	    perm.startsWith("https://")
	  ) {
	    return true;
	  }
	  return false;
	};
	const ORIGIN_PERMISSIONS = ALL_PERMISSIONS.filter(isOrigin);
	const EXTENSION_PERMISSIONS = ALL_PERMISSIONS.filter((perm) => !isOrigin(perm));
	
	function _testBlobCSP() {
	  try {
	    const code = `console.log("Blob CSP test");`;
	    const blob = new Blob([code], { type: "application/javascript" });
	    const blobUrl = URL.createObjectURL(blob);
	
	    const script = document.createElement("script");
	    script.src = blobUrl;
	
	    let blocked = false;
	    script.onerror = () => {
	      blocked = true;
	    };
	
	    document.head.appendChild(script);
	
	    return new Promise((resolve) => {
	      setTimeout(() => {
	        resolve(!blocked);
	        document.head.removeChild(script);
	        URL.revokeObjectURL(blobUrl);
	      }, 100);
	    });
	  } catch (e) {
	    return Promise.resolve(false);
	  }
	}
	
	let CAN_USE_BLOB_CSP = false;
	
	const waitForDOMEnd = () => {
	  if (document.readyState === "loading") {
	    return new Promise((resolve) =>
	      document.addEventListener("DOMContentLoaded", resolve, { once: true })
	    );
	  }
	  return Promise.resolve();
	};
	
	waitForDOMEnd().then(() => {
	  _testBlobCSP().then((result) => {
	    CAN_USE_BLOB_CSP = result;
	  });
	});
	
	function _base64ToBlob(base64, mimeType = "application/octet-stream") {
	  const binary = atob(base64);
	  const len = binary.length;
	  const bytes = new Uint8Array(len);
	  for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
	  return new Blob([bytes], { type: mimeType });
	}
	
	function _getMimeTypeFromPath(p) {
	  const ext = (p.split(".").pop() || "").toLowerCase();
	  const map = {
	    html: "text/html",
	    htm: "text/html",
	    js: "text/javascript",
	    css: "text/css",
	    json: "application/json",
	    png: "image/png",
	    jpg: "image/jpeg",
	    jpeg: "image/jpeg",
	    gif: "image/gif",
	    svg: "image/svg+xml",
	    webp: "image/webp",
	    ico: "image/x-icon",
	    woff: "font/woff",
	    woff2: "font/woff2",
	    ttf: "font/ttf",
	    otf: "font/otf",
	    eot: "application/vnd.ms-fontobject",
	  };
	  return map[ext] || "application/octet-stream";
	}
	
	function _isTextAsset(ext) {
	  return ["html", "htm", "js", "css", "json", "svg", "txt", "xml"].includes(
	    ext
	  );
	}
	
	function _createAssetUrl(path = "") {
	  if (path.startsWith("/")) path = path.slice(1);
	  const assetData = EXTENSION_ASSETS_MAP[path];
	  if (typeof assetData === "undefined") {
	    _warn("[runtime.getURL] Asset not found for", path);
	    return path;
	  }
	
	  const mime = _getMimeTypeFromPath(path);
	  const ext = (path.split(".").pop() || "").toLowerCase();
	
	  if (CAN_USE_BLOB_CSP) {
	    let blob;
	    if (_isTextAsset(ext)) {
	      blob = new Blob([assetData], { type: mime });
	    } else {
	      blob = _base64ToBlob(assetData, mime);
	    }
	
	    return URL.createObjectURL(blob);
	  } else {
	    if (_isTextAsset(ext)) {
	      return `data:${mime};base64,${btoa(assetData)}`;
	    } else {
	      return `data:${mime};base64,${assetData}`;
	    }
	  }
	}
	
	function _matchGlobPattern(pattern, path) {
	  if (!pattern || !path) return false;
	
	  pattern = pattern.replace(/\\/g, "/");
	  path = path.replace(/\\/g, "/");
	
	  if (pattern === path) return true;
	
	  let regexPattern = pattern
	    .replace(/[.+?^${}()|[\]\\]/g, "\\$&") // Escape regex chars
	    .replace(/\*\*/g, "__DOUBLESTAR__") // Temporarily replace **
	    .replace(/\*/g, "[^/]*") // * matches any chars except /
	    .replace(/__DOUBLESTAR__/g, ".*"); // ** matches any chars including /
	
	  regexPattern = "^" + regexPattern + "$";
	
	  try {
	    const regex = new RegExp(regexPattern);
	    return regex.test(path);
	  } catch (e) {
	    _error(`Invalid glob pattern: ${pattern}`, e);
	    return false;
	  }
	}
	
	function _isWebAccessibleResource(resourcePath, webAccessibleResources) {
	  if (
	    !Array.isArray(webAccessibleResources) ||
	    webAccessibleResources.length === 0
	  ) {
	    return false;
	  }
	
	  // Normalize the resource path
	  const normalizedPath = resourcePath.replace(/\\/g, "/").replace(/^\/+/, "");
	
	  for (const webAccessibleResource of webAccessibleResources) {
	    let patterns = [];
	
	    // Handle both manifest v2 and v3 formats
	    if (typeof webAccessibleResource === "string") {
	      // Manifest v2 format: array of strings
	      patterns = [webAccessibleResource];
	    } else if (
	      webAccessibleResource &&
	      Array.isArray(webAccessibleResource.resources)
	    ) {
	      // Manifest v3 format: objects with resources array
	      patterns = webAccessibleResource.resources;
	    }
	
	    // Check if the path matches any pattern
	    for (const pattern of patterns) {
	      if (_matchGlobPattern(pattern, normalizedPath)) {
	        return true;
	      }
	    }
	  }
	
	  return false;
	}
	
	window._matchGlobPattern = _matchGlobPattern;
	window._isWebAccessibleResource = _isWebAccessibleResource;
	
	// This function contains all the CSS injection and JS execution,
	// ordered by run_at timing internally using await.
	
  // #region Script Execution Logic
		async function executeAllScripts(globalThis, extensionCssData) {
		  const {chrome, browser, global, window, self} = globalThis;
		  const scriptName = "e621 extras";
		  _log(`Starting execution phases...`);
		
  // #region Document Start
			  if (typeof document !== 'undefined') {
			    _log(`Executing document-start phase...`);
			    
			    const scriptPaths = [];
			   _log(`  Executing JS (start): ${scriptPaths}`);
			
			   try {
			       // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically)
			      with (globalThis){;
			
			;}
			   } catch(e) { _error(`  Error executing scripts ${scriptPaths}`, e); }
			  
			  } else {
			      _log(`Skipping document-start phase (no document).`);
			  }
			
			  
  // #endregion
  // #region Wait for Document End DOMContentLoaded ---
			  if (typeof document !== 'undefined' && document.readyState === 'loading') {
			    _log(`Waiting for DOMContentLoaded...`);
			    await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, { once: true }));
			    _log(`DOMContentLoaded fired.`);
			  } else if (typeof document !== 'undefined') {
			    _log(`DOMContentLoaded already passed or not applicable.`);
			  }
			  
			
  // #endregion
  // #region Document End
			   if (typeof document !== 'undefined') {
			    _log(`Executing document-end phase...`);
			    
			        const cssKey_0 = "styles/extras.css";
			    try {
			      if (extensionCssData[cssKey_0]) {
			        _log(`  Injecting CSS (end): ${cssKey_0}`);
			        const style = document.createElement('style');
			        style.textContent = extensionCssData[cssKey_0];
			        (document.head || document.documentElement).appendChild(style);
			      } else { console.warn(`  CSS not found (end): ${cssKey_0}`); }
			    } catch(e) { _error(`  Failed injecting CSS (${cssKey_0}) in phase end`, e, extensionCssData); }
			  
			    const scriptPaths = ["scripts/constants.js","scripts/core.js","scripts/selectors.js","scripts/utils.js","scripts/net.js","scripts/ui/dom.js","scripts/ui/layout.js","scripts/ui/thumbnail.js","scripts/ui/dtext.js","scripts/ui/tooltips.js","scripts/blacklist.js","scripts/uploader.js","scripts/user_edits.js","scripts/tree/data.js","scripts/tree/export.js","scripts/tree/modal.js","scripts/hover_zoom/data.js","scripts/hover_zoom/index.js","scripts/tag_script/engine.js","scripts/tag_script/queue.js","scripts/tag_script/timeouted_page.js","scripts/tag_script/logger.js","scripts/tag_script/storage.js","scripts/tag_script/runner.js","scripts/tag_script/preview.js","scripts/tag_script/index.js","scripts/content.js"];
			   _log(`  Executing JS (end): ${scriptPaths}`);
			
			   try {
			       // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically)
			      with (globalThis){;
			// START: scripts/constants.js
			/* Shared by content scripts, popup and background — single source of truth for keys and message names. */
			globalThis.E621_CONSTANTS = Object.freeze({
			  SCHEMA_VERSION: 1,
			
			  STORAGE_KEYS: Object.freeze({
			    SCHEMA_VERSION: 'e621_schema_version',
			    DOWNVOTES: 'e621_downvoted_posts',
			    BLACKLIST_BACKUP: 'e621_clean_blacklist_backup',
			    BLACKLIST_LOCKED: 'e621_load_blacklist_disabled',
			    AUTO_UPDATE: 'e621_auto_update_enabled',
			    SHOW_UPLOADER: 'e621_extra_show_uploader',
			    ACTIVE_POPUP_TAB: 'e621_active_popup_tab',
			
			    /* Hover Zoom keys */
			    HZ_ENABLED: 'e621_hz_enabled',
			    HZ_LOAD_SAMPLE: 'e621_hz_load_sample',
			    HZ_TAGS: 'e621_hz_tags',
			    HZ_TIME: 'e621_hz_time',
			    HZ_AUDIO: 'e621_hz_audio',
			    HZ_DELAY: 'e621_hz_delay',
			    HZ_STICKY: 'e621_hz_sticky',
			
			    /* Queued / Timeouted Tag Scripts keys */
			    QUEUE_TAG_SCRIPTS_ENABLED: 'e621_queue_tag_scripts_enabled',
			    TAG_SCRIPT_QUEUE: 'e621_tag_script_queue',
			    QUEUE_TIMEOUT_UNTIL: 'e621_queue_timeout_until',
			    QUEUE_SLOT_TIMESTAMPS: 'e621_queue_slot_timestamps',
			
			    /* Preview & Tooltip suppression & Performance */
			    CTRL_PREVIEW_ENABLED: 'e621_ctrl_preview_enabled',
			    CTRL_PREVIEW_STICKY: 'e621_ctrl_preview_sticky',
			    CTRL_STICKY_ACTIVE: 'e621_ctrl_sticky_active',
			    HIDE_THUMB_TOOLTIPS: 'e621_hide_thumb_tooltips',
			    TRACKING_FPS: 'e621_tracking_fps'
			  }),
			
			  ACTIONS: Object.freeze({
			    BACKUP_BLACKLIST: 'backupBlacklist',
			    LOAD_DOWNVOTES: 'loadDownvotes',
			    UPLOAD_DOWNVOTES: 'uploadDownvotes',
			    UNLOAD_DOWNVOTES: 'unloadDownvotes',
			    SYNC_API: 'syncApi',
			    TOGGLE_AUTO_UPDATE: 'toggleAutoUpdate',
			    SHOW_POST_TREE: 'showPostTree',
			    FETCH_IMAGE: 'fetchImageAsDataUrl',
			    PROCESS_TAG_QUEUE: 'processTagQueue',
			    CLEAR_TAG_QUEUE: 'clearTagQueue',
			    ENQUEUE_TAG_SCRIPT: 'enqueueTagScript',
			    DEQUEUE_TAG_SCRIPT: 'dequeueTagScript',
			    SAVE_IMAGE: 'saveImage',
			    GET_IMAGE: 'getImage',
			    DELETE_IMAGE: 'deleteImage',
			    CLEAR_IMAGES: 'clearImages',
			    SET_CSRF_TOKEN: 'setCsrfToken'
			  })
			});
			// END: scripts/constants.js
			
			// START: scripts/core.js
			/* Minimal module registry with explicit dependencies.
			 * A wrong script order in manifest.json fails with a readable message instead of "undefined is not iterable". */
			globalThis.E621X = (function () {
			  'use strict';
			  const modules = new Map();
			
			  function get(name) {
			    if (!modules.has(name)) {
			      throw new Error(`[e621 extras] module "${name}" is not loaded — check script order in manifest.json`);
			    }
			    return modules.get(name);
			  }
			
			  function define(name, deps, factory) {
			    if (modules.has(name)) throw new Error(`[e621 extras] module "${name}" is defined twice`);
			    const resolved = deps.map(dep => {
			      if (!modules.has(dep)) {
			        throw new Error(`[e621 extras] module "${name}" requires "${dep}" — check script order in manifest.json`);
			      }
			      return modules.get(dep);
			    });
			    modules.set(name, Object.freeze(factory(...resolved)));
			  }
			
			  return { define, get };
			})();
			// END: scripts/core.js
			
			// START: scripts/selectors.js
			/* Every selector that depends on site markup (or on other scripts) lives here.
			 * After a site redesign, this is the only file to touch. */
			E621X.define('selectors', [], () => ({
			  // --- e621 markup ---
			  csrfMeta: 'meta[name="csrf-token"]',
			  postIdMeta: 'meta[name="post-id"]',
			  blacklistMeta: 'meta[name="blacklisted-tags"]',
			  siteUser: '#site-user',
			  imageContainer: '#image-container',
			  postInfoItems: '#post-information li',
			  postInfoLists: '#post-information ul',
			  thumbnail: 'article.thumbnail[data-id]',
			  postContainerForId: '[data-id], [data-post-id], article.thumbnail, article.post',
			  modeBox: '#mode-box',
			  modeSelect: '#mode-box-mode',
			  nativeTagScriptField: '#tag-script-field',
			  parentChildren: '.parent-children',
			  relationNotices: '.notice.notice-child, .notice.notice-parent',
			  voteButton: 'button[data-action="1"], button[data-action="-1"], .ptbr-vote-button',
			  commentVoteClasses: ['comment-vote-down-link', 'comment-vote-up-link'],
			
			  /* --- OPTIONAL integration with third-party scripts/extensions ---
			   * These are not part of e621. Everything works without them (defaults are used),
			   * but when present they give accurate vote state / thumbnail layout settings. */
			  thirdParty: {
			    voteContainer: '.ptbr-vote',
			    imageContainInput: '#ssc-image-contain',
			    cardSizeRadioName: 'ssc-card-size',
			    cornerRibbonsInput: '#ssc-corner-ribbons',
			    bodyAttrs: ['data-st-contain', 'data-st-size', 'data-st-cornerribbons'],
			    localStorageKeys: {
			      contain: 'e6.posts.contain',
			      size: 'e6.posts.size',
			      cornerRibbons: 'e6.posts.cribbons'
			    }
			  }
			}));
			// END: scripts/selectors.js
			
			// START: scripts/utils.js
			E621X.define('utils', ['selectors'], (SEL) => {
			  'use strict';
			  const { STORAGE_KEYS, SCHEMA_VERSION } = E621_CONSTANTS;
			  const TOAST_ID = 'e621-extras-toast';
			
			  const logWarn = (scope, err) => console.warn(`[e621 extras] ${scope}:`, err);
			  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
			  const extensionAlive = () => Boolean(globalThis.chrome?.runtime?.id);
			
			  // ---------- results ----------
			  const ok = (message, extra = {}) => ({ ok: true, message, ...extra });
			  const fail = (message, code = 'ERROR') => ({ ok: false, message, code });
			
			  // ---------- storage ----------
			  /* The extension context is invalidated after an update/reload; then storage calls must not throw. */
			  const storageGet = (key, fallback) => new Promise(resolve => {
			    if (!extensionAlive()) return resolve(fallback);
			    try {
			      chrome.storage.local.get([key], result => {
			        if (chrome.runtime.lastError) {
			          logWarn('storage.get', chrome.runtime.lastError.message);
			          return resolve(fallback);
			        }
			        resolve(result && result[key] !== undefined ? result[key] : fallback);
			      });
			    } catch (err) {
			      logWarn('storage.get', err);
			      resolve(fallback);
			    }
			  });
			
			  const storageSet = (key, value) => new Promise(resolve => {
			    if (!extensionAlive()) return resolve();
			    try {
			      chrome.storage.local.set({ [key]: value }, () => {
			        if (chrome.runtime.lastError) logWarn('storage.set', chrome.runtime.lastError.message);
			        resolve();
			      });
			    } catch (err) {
			      logWarn('storage.set', err);
			      resolve();
			    }
			  });
			
			  /* Idempotent migration. Uses the raw API on purpose: on a read failure we must NOT write fallbacks back. */
			  async function ensureStorageSchema() {
			    try {
			      const K = STORAGE_KEYS;
			      const data = await chrome.storage.local.get([K.SCHEMA_VERSION, K.DOWNVOTES]);
			      if ((data[K.SCHEMA_VERSION] || 0) >= SCHEMA_VERSION) return;
			      // v1: downvotes are stored as a de-duplicated array of numbers
			      const ids = Array.isArray(data[K.DOWNVOTES]) ? data[K.DOWNVOTES] : [];
			      await chrome.storage.local.set({
			        [K.DOWNVOTES]: [...new Set(ids.map(Number).filter(Number.isFinite))],
			        [K.SCHEMA_VERSION]: SCHEMA_VERSION
			      });
			    } catch (err) {
			      logWarn('schema migration', err);
			    }
			  }
			
			  // ---------- UI helpers ----------
			  function showToast(message, isError = false) {
			    document.getElementById(TOAST_ID)?.remove();
			    const toast = document.createElement('div');
			    toast.id = TOAST_ID;
			    toast.className = `e621x-toast${isError ? ' e621x-toast--error' : ''}`;
			    toast.textContent = message;
			    document.body.appendChild(toast);
			    setTimeout(() => {
			      toast.classList.add('is-hiding');
			      setTimeout(() => toast.remove(), 300);
			    }, 3500);
			  }
			
			  function downloadBlob(blob, filename) {
			    const url = URL.createObjectURL(blob);
			    const a = document.createElement('a');
			    a.href = url;
			    a.download = filename;
			    a.click();
			    URL.revokeObjectURL(url);
			  }
			
			  // ---------- site data ----------
			  function getCurrentUser() {
			    let userId = document.body?.dataset?.userId;
			    let userName = document.body?.dataset?.userName;
			    const isAnonymous = document.body?.dataset?.userIsAnonymous === 'true';
			
			    if (!userId || !userName || isAnonymous) {
			      const siteUserElem = document.querySelector(SEL.siteUser);
			      if (siteUserElem) {
			        try {
			          const data = JSON.parse(atob(siteUserElem.textContent.trim()));
			          if (data && data.name && !data.is?.anonymous) {
			            userId = String(data.id);
			            userName = data.name;
			          }
			        } catch (err) {
			          logWarn('parse #site-user', err);
			        }
			      }
			    }
			    if (!userId || !userName || isAnonymous) return null;
			    return { userId, userName };
			  }
			
			  const getCsrfToken = () => document.querySelector(SEL.csrfMeta)?.content || '';
			  const siteUrl = (path) => new URL(path, location.origin).href;
			
			  function getPostIdFromPage(el = null) {
			    if (el) {
			      const container = el.closest(SEL.postContainerForId);
			      if (container) {
			        const id = container.dataset.id || container.dataset.postId;
			        if (id && /^\d+$/.test(id)) return Number(id);
			      }
			    }
			    const meta = document.querySelector(SEL.postIdMeta);
			    if (meta?.content && /^\d+$/.test(meta.content)) return Number(meta.content);
			    const imgContainer = document.querySelector(SEL.imageContainer);
			    if (imgContainer?.dataset.id) return Number(imgContainer.dataset.id);
			    const match = location.pathname.match(/\/posts\/(\d+)/);
			    return match ? Number(match[1]) : null;
			  }
			
			  /* API posts expose tags either as `tag_string`, string `tags`, array, or category object. */
			  function getPostTagString(post) {
			    if (!post) return '';
			    if (typeof post.tag_string === 'string') return post.tag_string;
			    if (typeof post.tags === 'string') return post.tags;
			    if (Array.isArray(post.tags)) return post.tags.join(' ');
			    if (post.tags && typeof post.tags === 'object') return Object.values(post.tags).flat().join(' ');
			    return '';
			  }
			
			  // ---------- concurrency ----------
			  /* Runs tasks strictly one after another; a failed task does not block the next ones. */
			  function createQueue() {
			    let tail = Promise.resolve();
			    return {
			      run(task) {
			        const result = tail.then(task);
			        tail = result.catch(() => {});
			        return result;
			      }
			    };
			  }
			
			  async function mapLimit(items, limit, fn) {
			    const results = new Array(items.length);
			    let next = 0;
			    const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
			      while (next < items.length) {
			        const i = next++;
			        results[i] = await fn(items[i], i);
			      }
			    });
			    await Promise.all(workers);
			    return results;
			  }
			
			  /* Collects cleanup callbacks; run() executes them once, in reverse order. */
			  function createDisposer() {
			    const fns = [];
			    let done = false;
			    return {
			      add(fn) { if (done) fn(); else fns.push(fn); },
			      isDisposed: () => done,
			      run() {
			        if (done) return;
			        done = true;
			        while (fns.length) {
			          try { fns.pop()(); } catch (err) { logWarn('disposer', err); }
			        }
			      }
			    };
			  }
			
			  return {
			    logWarn, sleep, ok, fail,
			    storageGet, storageSet, ensureStorageSchema,
			    showToast, downloadBlob,
			    getCurrentUser, getCsrfToken, siteUrl, getPostIdFromPage, getPostTagString,
			    createQueue, mapLimit, createDisposer
			  };
			});
			// END: scripts/utils.js
			
			// START: scripts/net.js
			/* The only place that talks to the site API: one global throttle + bounded retries with backoff. */
			E621X.define('net', ['utils'], (U) => {
			  'use strict';
			
			  const MIN_INTERVAL_MS = 550;      // spacing between ANY two requests from this tab
			  const MAX_RETRIES = 4;
			  const BASE_BACKOFF_MS = 2000;
			  const MAX_BACKOFF_MS = 30000;
			  const RETRY_STATUSES = new Set([429, 502, 503, 504]);
			
			  class HttpError extends Error {
			    constructor(status, url) {
			      super(status ? `HTTP ${status}` : 'Network error');
			      this.name = 'HttpError';
			      this.status = status;
			      this.url = url;
			    }
			  }
			
			  let nextSlot = 0;
			  function waitForSlot() {
			    const now = Date.now();
			    const slot = Math.max(now, nextSlot);
			    nextSlot = slot + MIN_INTERVAL_MS;
			    return U.sleep(slot - now);
			  }
			
			  function backoffDelay(attempt, res) {
			    const retryAfter = Number(res?.headers.get('Retry-After'));
			    if (Number.isFinite(retryAfter) && retryAfter > 0) return Math.min(retryAfter * 1000, MAX_BACKOFF_MS);
			    return Math.min(BASE_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS);
			  }
			
			  /* Resolves with any non-retryable response (including 4xx); throws HttpError when retries are exhausted. */
			  async function request(url, init = {}, { retries = MAX_RETRIES } = {}) {
			    for (let attempt = 0; ; attempt++) {
			      await waitForSlot();
			      let res = null;
			      try {
			        res = await fetch(url, init);
			      } catch (err) {
			        if (attempt >= retries) throw new HttpError(0, url);
			      }
			      if (res && !RETRY_STATUSES.has(res.status)) return res;
			      if (attempt >= retries) throw new HttpError(res.status, url);
			      await U.sleep(backoffDelay(attempt, res));
			    }
			  }
			
			  async function fetchJson(url, init, opts) {
			    const res = await request(url, init, opts);
			    if (!res.ok) throw new HttpError(res.status, url);
			    return res.json();
			  }
			
			  function sendForm(url, method, params, opts) {
			    return request(url, {
			      method,
			      headers: {
			        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
			        'X-CSRF-Token': U.getCsrfToken()
			      },
			      body: new URLSearchParams(params).toString()
			    }, opts);
			  }
			
			  return { request, fetchJson, sendForm, HttpError };
			});
			// END: scripts/net.js
			
			// START: scripts/ui/dom.js
			E621X.define('dom', [], () => {
			  'use strict';
			
			  const ICONS = {
			    download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
			    star: '<path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"></path>',
			    messages_square: '<path d="M16 10a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 14.286V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path><path d="M20 9a2 2 0 0 1 2 2v10.286a.71.71 0 0 1-1.212.502l-2.202-2.202A2 2 0 0 0 17.172 19H10a2 2 0 0 1-2-2v-1"></path>',
			    arrow_up_dash: '<path d="M9 13a1 1 0 0 0-1-1H5.061a1 1 0 0 1-.75-1.811l6.836-6.835a1.207 1.207 0 0 1 1.707 0l6.835 6.835a1.207 1.207 0 0 1-.75 1.811H16a1 1 0 0 0-1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1z"></path><path d="M9 20h6"></path>',
			    arrow_down_dash: '<path d="M15 11a1 1 0 0 0 1 1h2.939a1 1 0 0 1 .75 1.811l-6.835 6.836a1.207 1.207 0 0 1-1.707 0L4.31 13.81a1 1 0 0 1 .75-1.811H8a1 1 0 0 0 1-1V9a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1z"></path><path d="M9 4h6"></path>'
			  };
			
			  /* Safe element factory: values go through setAttribute/textContent, never through innerHTML. */
			  function el(tag, attrs = {}, children = []) {
			    const node = document.createElement(tag);
			    for (const [key, value] of Object.entries(attrs)) {
			      if (value === null || value === undefined || value === false) continue;
			      if (key === 'class') node.className = value;
			      else if (key === 'text') node.textContent = String(value);
			      else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2).toLowerCase(), value);
			      else node.setAttribute(key, value === true ? '' : String(value));
			    }
			    for (const child of [].concat(children)) {
			      if (child !== null && child !== undefined && child !== false) node.append(child);
			    }
			    return node;
			  }
			
			  function icon(name, size = 16) {
			    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
			    svg.setAttribute('width', size);
			    svg.setAttribute('height', size);
			    svg.setAttribute('viewBox', '0 0 24 24');
			    svg.setAttribute('fill', 'none');
			    svg.setAttribute('stroke', 'currentColor');
			    svg.setAttribute('stroke-width', '2');
			    svg.setAttribute('stroke-linecap', 'round');
			    svg.setAttribute('stroke-linejoin', 'round');
			    svg.setAttribute('name', name);
			    svg.innerHTML = ICONS[name] || ''; // ICONS is a static trusted table
			    return svg;
			  }
			
			  function button({ text = '', icon: iconName = null, kinetic = true, primary = false, onClick = null, title = '', className = '' } = {}) {
			    const btn = el('button', {
			      type: 'button',
			      class: `${primary ? 'btn' : 'st-button'} ${kinetic ? 'kinetic' : ''} ${className}`.trim(),
			      title
			    });
			    if (iconName && ICONS[iconName]) btn.appendChild(icon(iconName, 16));
			    if (text) btn.appendChild(el('span', { text }));
			    if (typeof onClick === 'function') btn.addEventListener('click', onClick);
			    return btn;
			  }
			
			  /* Escapes text for HTML and XML (text nodes and quoted attributes). */
			  function escapeHtml(value) {
			    if (value === null || value === undefined) return '';
			    return String(value)
			      .replace(/&/g, '&amp;')
			      .replace(/"/g, '&quot;')
			      .replace(/'/g, '&#39;')
			      .replace(/</g, '&lt;')
			      .replace(/>/g, '&gt;');
			  }
			
			  function formatCount(num) {
			    if (num === null || num === undefined) return '0';
			    const n = Number(num);
			    if (Math.abs(n) >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'm';
			    if (Math.abs(n) >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
			    return String(n);
			  }
			
			  function formatDate(isoStr) {
			    if (!isoStr) return '';
			    const d = new Date(isoStr);
			    if (isNaN(d.getTime())) return String(isoStr);
			    const pad = n => String(n).padStart(2, '0');
			    const offsetMin = -d.getTimezoneOffset();
			    const sign = offsetMin >= 0 ? '+' : '-';
			    const offH = pad(Math.floor(Math.abs(offsetMin) / 60));
			    const offM = pad(Math.abs(offsetMin) % 60);
			    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
			           `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} ${sign}${offH}${offM}`;
			  }
			
			  return { el, icon, button, escapeHtml, formatCount, formatDate };
			});
			// END: scripts/ui/dom.js
			
			// START: scripts/ui/layout.js
			/* Reads thumbnail layout preferences (card size, contain/cover, ribbons).
			 * Uses third-party settings when present (see selectors.thirdParty), otherwise defaults. */
			E621X.define('layout', ['selectors'], (SEL) => {
			  'use strict';
			  const TP = SEL.thirdParty;
			
			  const CARD_SIZES = {
			    s: { width: 120, height: 165 },
			    m: { width: 160, height: 210 },
			    l: { width: 220, height: 285 }
			  };
			  const getCardDimensions = (size = 'm') => CARD_SIZES[size] || CARD_SIZES.m;
			
			  function getLayoutSettings() {
			    const body = document.body;
			    const ls = TP.localStorageKeys;
			
			    const containInput = document.querySelector(TP.imageContainInput);
			    const isContain = containInput
			      ? containInput.checked
			      : (body.getAttribute('data-st-contain') === 'true' || localStorage.getItem(ls.contain) === 'true');
			
			    const checkedRadio = document.querySelector(`input[name="${TP.cardSizeRadioName}"]:checked`);
			    const size = checkedRadio
			      ? checkedRadio.value
			      : (body.getAttribute('data-st-size') || localStorage.getItem(ls.size) || 'm');
			
			    const ribbonsInput = document.querySelector(TP.cornerRibbonsInput);
			    const showRibbons = ribbonsInput
			      ? ribbonsInput.checked
			      : (body.getAttribute('data-st-cornerribbons') !== 'false' && localStorage.getItem(ls.cornerRibbons) !== 'false');
			
			    return { isContain, size, showRibbons };
			  }
			
			  /* Card size / fit are CSS variables consumed by .e621x-thumb (see styles/extras.css). */
			  function applyLayoutVars(element, layout) {
			    const dims = getCardDimensions(layout.size);
			    element.style.setProperty('--e621x-w', `${dims.width}px`);
			    element.style.setProperty('--e621x-h', `${dims.height}px`);
			    element.style.setProperty('--e621x-fit', layout.isContain ? 'contain' : 'cover');
			  }
			
			  // One shared DOM listener + observer for all subscribers; fully detached when nobody listens.
			  const listeners = new Set();
			  let observer = null;
			
			  const notify = () => {
			    const settings = getLayoutSettings();
			    listeners.forEach(cb => cb(settings));
			  };
			
			  const onDocumentChange = (e) => {
			    const t = e.target;
			    if (t && (t.matches?.(TP.imageContainInput) || t.name === TP.cardSizeRadioName || t.matches?.(TP.cornerRibbonsInput))) {
			      notify();
			    }
			  };
			
			  /* Returns an unsubscribe function. */
			  function onLayoutSettingsChange(callback) {
			    listeners.add(callback);
			    if (listeners.size === 1) {
			      document.addEventListener('change', onDocumentChange);
			      observer = new MutationObserver(notify);
			      observer.observe(document.body, { attributes: true, attributeFilter: TP.bodyAttrs });
			    }
			    return () => {
			      listeners.delete(callback);
			      if (listeners.size === 0) {
			        document.removeEventListener('change', onDocumentChange);
			        observer?.disconnect();
			        observer = null;
			      }
			    };
			  }
			
			  return { getLayoutSettings, applyLayoutVars, onLayoutSettingsChange, getCardDimensions };
			});
			// END: scripts/ui/layout.js
			
			// START: scripts/ui/thumbnail.js
			E621X.define('thumbnail', ['utils', 'dom', 'layout'], (U, D, Layout) => {
			  'use strict';
			  const { el, icon, formatCount, formatDate } = D;
			
			  function createPin(position, gen) {
			    return el('div', {
			      class: `e621x-pin e621x-pin--${position}`,
			      title: `Generation ${gen}`,
			      text: gen
			    });
			  }
			
			  function createStat(extraClass, iconName, value) {
			    return el('span', { class: `thm-desc-m ${extraClass}` }, [icon(iconName, 16), ` ${value}`]);
			  }
			
			  /* Builds a card in the site's own thumbnail markup (so site CSS/JS treat it as native).
			   * Card size and object-fit come from CSS variables set by the container (Layout.applyLayoutVars). */
			  function createThumbnail(post, options = {}) {
			    const {
			      layout = Layout.getLayoutSettings(),
			      gen = 1,
			      hasParent = false,
			      hasChildren = false,
			      enablePins = true
			    } = options;
			
			    const rating = (post.rating || 's').toLowerCase();
			    const ratingClass = rating === 's' ? 'rating-safe' : (rating === 'q' ? 'rating-questionable' : 'rating-explicit');
			    const allTags = U.getPostTagString(post);
			    let flagsStr = '';
			    if (Array.isArray(post.flags)) {
			      flagsStr = post.flags.join(' ');
			    } else if (typeof post.flags === 'string') {
			      flagsStr = post.flags;
			    } else if (post.flags && typeof post.flags === 'object') {
			      flagsStr = Object.entries(post.flags).filter(([, v]) => Boolean(v)).map(([k]) => k).join(' ');
			    }
			
			    const ext = post.file?.ext || post.file_ext || post.fileExt || 'jpg';
			    const fallbackUrl = U.siteUrl('/images/download-preview.png');
			    const rawPreviewUrl = post.preview?.url || post.preview_url || post.previewUrl || '';
			    const hasPreview = Boolean(rawPreviewUrl && ext !== 'swf');
			    const previewUrl = hasPreview ? rawPreviewUrl : fallbackUrl;
			    const isDataUrl = previewUrl.startsWith('data:');
			    const previewWebp = (hasPreview && !isDataUrl) ? previewUrl.replace(/\.[a-zA-Z0-9]+$/, '.webp') : null;
			
			    const score = (typeof post.score === 'object' && post.score !== null)
			      ? (post.score.total ?? 0)
			      : (Number(post.score) || 0);
			    const favCount = post.fav_count ?? post.favCount ?? 0;
			    const commentCount = post.comment_count ?? post.commentCount ?? 0;
			
			    const isDeleted = flagsStr.includes('deleted');
			    const isFlagged = flagsStr.includes('flagged');
			    const isPending = flagsStr.includes('pending');
			    const statusStr = isDeleted ? 'deleted' : (isFlagged ? 'flagged' : (isPending ? 'pending' : 'active'));
			    const vote = post.own_vote ?? post.vote ?? 0;
			
			    const hasParentPost = hasParent || Boolean(post.relationships?.parent_id || post.has_parent || post.hasParent);
			    const hasChildrenPost = hasChildren || Boolean(post.relationships?.has_children || post.has_children || post.hasChildren);
			
			    const hoverText = `Rating: ${rating}\nID: ${post.id}\nDate: ${formatDate(post.created_at)}\nStatus: ${statusStr}\nScore: ${score}\n\n${allTags}`;
			
			    const img = el('img', {
			      class: 'thm-img-thumb',
			      src: previewUrl,
			      alt: `post #${post.id}`,
			      fetchpriority: 'high'
			    });
			    img.addEventListener('error', () => {
			      if (!img.src.startsWith('data:')) img.src = fallbackUrl;
			    }, { once: true });
			
			    const pictureChildren = [];
			    if (previewWebp) {
			      pictureChildren.push(el('source', { type: 'image/webp', srcset: previewWebp }));
			    }
			    if (!isDataUrl) {
			      pictureChildren.push(el('source', { type: `image/${ext === 'png' ? 'png' : 'jpeg'}`, srcset: previewUrl }));
			    }
			    pictureChildren.push(img);
			
			    const picture = el('picture', {}, pictureChildren);
			
			    const link = el('a', {
			      class: 'thm-link',
			      href: `/posts/${post.id}`,
			      target: '_blank',
			      rel: 'noopener',
			      title: hoverText,
			      'data-hover-text': hoverText,
			      'data-file-ext': ext
			    }, [
			      picture,
			      layout.showRibbons && hasParentPost && el('div', { class: 'ribbon left', title: 'Has a parent post.' }),
			      layout.showRibbons && hasChildrenPost && el('div', { class: 'ribbon right', title: 'Has children.' })
			    ]);
			
			    const isNegative = score < 0;
			    const scoreClass = isNegative ? 'thm-score-negative' : (score > 0 ? 'thm-score-positive' : '');
			
			    const desc = el('div', { class: `thm-desc thm-rating-${rating}` }, [
			      el('span', { class: 'thm-desc-a' }, [
			        createStat(`thm-score ${scoreClass}`, isNegative ? 'arrow_down_dash' : 'arrow_up_dash', formatCount(Math.abs(score))),
			        createStat('thm-favorites', 'star', formatCount(favCount)),
			        createStat('thm-comments', 'messages_square', formatCount(commentCount))
			      ]),
			      el('span', { class: 'thm-desc-b thm-rating', text: rating.toUpperCase() })
			    ]);
			
			    return el('article', {
			      class: `thumbnail e621x-thumb ${ratingClass} filter-matches`,
			      'data-id': post.id,
			      'data-flags': flagsStr,
			      'data-tags': allTags,
			      'data-rating': rating,
			      'data-file-ext': ext,
			      'data-width': post.file?.width ?? post.width ?? 0,
			      'data-height': post.file?.height ?? post.height ?? 0,
			      'data-size': post.file?.size ?? post.size ?? 0,
			      'data-created-at': JSON.stringify(post.created_at || ''),
			      'data-uploader': post.uploader || '',
			      'data-uploader-id': post.uploader_id ?? post.uploaderId ?? '',
			      'data-score': score,
			      'data-fav-count': favCount,
			      'data-is-favorited': String(Boolean(post.is_favorited)),
			      'data-vote': String(vote),
			      'data-comment-count': commentCount,
			      'data-pools': Array.isArray(post.pools) ? post.pools.join(' ') : (post.pools || ''),
			      'data-md5': post.file?.md5 || post.md5 || '',
			      'data-preview-url': previewUrl,
			      'data-preview-webp': previewWebp || '',
			      'data-sample-url': post.sample?.url || post.sample_url || '',
			      'data-file-url': post.file?.url || post.file_url || '',
			      'data-preview-width': post.preview?.width ?? 0,
			      'data-preview-height': post.preview?.height ?? 0
			    }, [
			      enablePins && hasParentPost && createPin('top', gen),
			      enablePins && hasChildrenPost && createPin('bottom', gen),
			      link,
			      desc
			    ]);
			  }
			
			  return { createThumbnail };
			});
			// END: scripts/ui/thumbnail.js
			
			// START: scripts/ui/dtext.js
			E621X.define('dtext', ['dom'], (D) => {
			  'use strict';
			  const { escapeHtml } = D;
			
			  /* Safe client-side DText parser (escapes before formatting to prevent XSS). */
			  function parseDText(rawText) {
			    if (!rawText) return '';
			
			    let text = String(rawText).replace(/\r\n?/g, '\n');
			    const blocks = [];
			    const inlineCodes = [];
			
			    const stashBlock = (html) => {
			      blocks.push(html);
			      return `\n\n\u0000B${blocks.length - 1}\u0000\n\n`;
			    };
			
			    // 1. [code] blocks
			    text = text.replace(/\[code\]([\s\S]*?)\[\/code\]/gi, (_, code) =>
			      stashBlock(`<pre class="dtext-code"><code>${escapeHtml(code.trim())}</code></pre>`));
			
			    // 2. Inline `code`
			    text = text.replace(/`([^`\n]+)`/g, (_, code) => {
			      inlineCodes.push(`<code class="inline-code">${escapeHtml(code)}</code>`);
			      return `\u0000C${inlineCodes.length - 1}\u0000`;
			    });
			
			    // 3. Escape everything remaining
			    text = escapeHtml(text);
			
			    // 4. [quote] tags -> structural markers
			    text = text
			      .replace(/\[quote(?:=[^\]]+)?\]/gi, '\n\u0000Q_OPEN\u0000\n')
			      .replace(/\[\/quote\]/gi, '\n\u0000Q_CLOSE\u0000\n');
			
			    // 5. Inline styles
			    text = text
			      .replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '<strong>$1</strong>')
			      .replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '<em>$1</em>')
			      .replace(/\[s\]([\s\S]*?)\[\/s\]/gi, '<s>$1</s>')
			      .replace(/\[u\]([\s\S]*?)\[\/u\]/gi, '<u>$1</u>');
			
			    // 6. Links (handles optional [brackets] for both full and relative URLs)
			    text = text
			      .replace(/&quot;((?:(?!&quot;).)+)&quot;:\[?(https?:\/\/[^\s\]<]+)\]?/g,
			        (_, label, url) => `<a href="${url}" rel="nofollow noreferrer" target="_blank">${label}</a>`)
			      .replace(/&quot;((?:(?!&quot;).)+)&quot;:\[?(\/(?!\/)[^\s\]<]*)\]?/g,
			        (_, label, path) => `<a href="${path}">${label}</a>`)
			      .replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g,
			        (_, title, label) => `<a href="/wiki_pages/show_or_new?title=${encodeURIComponent(title)}">${label}</a>`)
			      .replace(/\[\[([^\]]+)\]\]/g,
			        (_, title) => `<a href="/wiki_pages/show_or_new?title=${encodeURIComponent(title)}">${title}</a>`)
			      .replace(/\bpost #(\d+)/gi, '<a href="/posts/$1">post #$1</a>')
			      .replace(/\buser #(\d+)/gi, '<a href="/users/$1">user #$1</a>')
			      .replace(/\btopic #(\d+)/gi, '<a href="/forum_topics/$1">topic #$1</a>');
			
			    // 7. Structural line parsing: headers, lists, quotes, paragraphs
			    const result = [];
			    let listLevel = 0;
			    let paragraph = [];
			
			    const flushParagraph = () => {
			      const content = paragraph.join('<br>').trim();
			      if (content) result.push(`<p>${content}</p>`);
			      paragraph = [];
			    };
			    const closeList = () => {
			      while (listLevel > 0) { result.push('</ul>'); listLevel--; }
			    };
			
			    for (const line of text.split('\n')) {
			      const trimmed = line.trim();
			
			      if (!trimmed) {
			        flushParagraph();
			        continue;
			      }
			
			      if (trimmed === '\u0000Q_OPEN\u0000') {
			        flushParagraph();
			        closeList();
			        result.push('<blockquote class="dtext-quote">');
			        continue;
			      }
			
			      if (trimmed === '\u0000Q_CLOSE\u0000') {
			        flushParagraph();
			        closeList();
			        result.push('</blockquote>');
			        continue;
			      }
			
			      if (/^\u0000B\d+\u0000$/.test(trimmed)) {
			        flushParagraph();
			        closeList();
			        result.push(trimmed);
			        continue;
			      }
			
			      const header = trimmed.match(/^h([1-6])(?:#([a-zA-Z0-9_-]+))?\.\s+(.*)$/);
			      if (header) {
			        flushParagraph();
			        closeList();
			        result.push(`<h${header[1]}${header[2] ? ` id="${header[2]}"` : ''}>${header[3]}</h${header[1]}>`);
			        continue;
			      }
			
			      const item = trimmed.match(/^(\*+)\s+(.*)$/);
			      if (item) {
			        flushParagraph();
			        while (listLevel < item[1].length) { result.push('<ul>'); listLevel++; }
			        while (listLevel > item[1].length) { result.push('</ul>'); listLevel--; }
			        result.push(`<li>${item[2]}</li>`);
			        continue;
			      }
			
			      closeList();
			      paragraph.push(trimmed);
			    }
			    flushParagraph();
			    closeList();
			
			    // 8. Restore stashed blocks and inline codes
			    return result.join('\n')
			      .replace(/\u0000B(\d+)\u0000/g, (_, i) => blocks[Number(i)])
			      .replace(/\u0000C(\d+)\u0000/g, (_, i) => inlineCodes[Number(i)]);
			  }
			
			  return { parseDText, renderDText: async (text) => parseDText(text) };
			});
			// END: scripts/ui/dtext.js
			
			// START: scripts/ui/tooltips.js
			/* Tooltip suppressor: permanently disables native browser hover tooltips on thumbnails when enabled. */
			E621X.define('tooltipsManager', ['utils', 'selectors'], (U, SEL) => {
			  'use strict';
			  const { STORAGE_KEYS: KEYS } = E621_CONSTANTS;
			
			  let enabled = false;
			
			  function suppressTitle(el) {
			    if (!el) return;
			    const targets = el.hasAttribute('title') ? [el] : [];
			    targets.push(...el.querySelectorAll('[title]'));
			    for (const t of targets) {
			      const val = t.getAttribute('title');
			      if (val) {
			        t.setAttribute('data-stashed-title', val);
			        t.removeAttribute('title');
			      }
			    }
			  }
			
			  function restoreTitle(el) {
			    if (!el) return;
			    const targets = el.hasAttribute('data-stashed-title') ? [el] : [];
			    targets.push(...el.querySelectorAll('[data-stashed-title]'));
			    for (const t of targets) {
			      const val = t.getAttribute('data-stashed-title');
			      if (val) {
			        t.setAttribute('title', val);
			        t.removeAttribute('data-stashed-title');
			      }
			    }
			  }
			
			  function setupListeners() {
			    document.addEventListener('mouseover', (e) => {
			      if (!enabled) return;
			      const thumb = e.target.closest(SEL.thumbnail) || e.target.closest('.post-preview, div.post-thumbnail, .e621x-thumb, [data-id]');
			      if (thumb) {
			        suppressTitle(thumb);
			      }
			    }, true);
			  }
			
			  async function init() {
			    enabled = Boolean(await U.storageGet(KEYS.HIDE_THUMB_TOOLTIPS, false));
			    setupListeners();
			
			    chrome.storage.onChanged.addListener((changes, area) => {
			      if (area === 'local' && changes[KEYS.HIDE_THUMB_TOOLTIPS]) {
			        enabled = Boolean(changes[KEYS.HIDE_THUMB_TOOLTIPS].newValue);
			        if (!enabled) {
			          document.querySelectorAll('[data-stashed-title]').forEach(restoreTitle);
			        }
			      }
			    });
			  }
			
			  return { init };
			});
			// END: scripts/ui/tooltips.js
			
			// START: scripts/blacklist.js
			/* Blacklist / downvotes manager.
			 *
			 * Glossary (popup labels map to these):
			 *   downvotes        – IDs of posts the user downvoted on the site.
			 *   loadDownvotes    – site API  -> extension storage.
			 *   uploadDownvotes  – storage   -> site blacklist (adds `id:N` rules).
			 *   unloadDownvotes  – removes the `id:N` rules of stored downvotes from the blacklist.
			 *   backupBlacklist  – live blacklist -> extension storage (disabled while LOCKED).
			 *   LOCKED           – set while `id:N` rules are in the blacklist, so a backup would be polluted.
			 *
			 * Invariants:
			 *  - every public operation runs through ONE queue (no lost read-modify-write updates);
			 *  - the blacklist is persisted via the API and checked (res.ok) BEFORE local flags change;
			 *  - operations return Result objects ({ ok, message, code }), never magic strings.
			 */
			E621X.define('blacklist', ['utils', 'selectors', 'net'], (U, SEL, Net) => {
			  'use strict';
			  const { STORAGE_KEYS: KEYS } = E621_CONSTANTS;
			  const { ok, fail } = U;
			
			  const DOWNVOTES_PAGE_LIMIT = 320;
			  const RELOAD_DELAY_MS = 1500;
			
			  const queue = U.createQueue();
			  let reloadTimer = null;
			
			  // ---------- helpers ----------
			  const parseBlacklistText = (text) => String(text || '').split('\n').map(t => t.trim()).filter(Boolean);
			
			  function idFromRule(rule) {
			    const m = String(rule).trim().match(/^id:(\d+)$/i);
			    return m ? Number(m[1]) : null;
			  }
			
			  const readDownvotes = async () => (await U.storageGet(KEYS.DOWNVOTES, [])).map(Number);
			
			  /* Debounced: several operations in a row cause a single reload, after all writes and toasts. */
			  function scheduleReload() {
			    clearTimeout(reloadTimer);
			    reloadTimer = setTimeout(() => location.reload(), RELOAD_DELAY_MS);
			  }
			
			  const notLoggedIn = () => {
			    U.showToast('Sign in to e621 first', true);
			    return fail('Not logged in', 'NOT_LOGGED_IN');
			  };
			
			  const failFromError = (err) => {
			    U.logWarn('blacklist', err);
			    U.showToast(`Error: ${err.message}`, true);
			    return fail(err.message);
			  };
			
			  // ---------- blacklist I/O ----------
			  /* Reads directly from the page DOM (meta tag or #site-user) to avoid unnecessary HTTP requests. */
			  function readDomBlacklist() {
			    const meta = document.querySelector(SEL.blacklistMeta);
			    if (meta?.content) {
			      try {
			        const parsed = JSON.parse(meta.content);
			        if (Array.isArray(parsed)) return parsed.map(t => String(t).trim()).filter(Boolean);
			      } catch (err) {
			        U.logWarn('parsing blacklist meta tag', err);
			      }
			    }
			
			    const siteUser = document.querySelector(SEL.siteUser);
			    if (siteUser) {
			      try {
			        const data = JSON.parse(atob(siteUser.textContent.trim()));
			        if (Array.isArray(data?.blacklist)) return data.blacklist.map(t => String(t).trim()).filter(Boolean);
			      } catch (err) {
			        U.logWarn('parsing site-user blacklist', err);
			      }
			    }
			
			    return null;
			  }
			
			  /* Reads DOM first to avoid spamming /users/me.json on single downvotes.
			   * Falls back to API or forces API read for backups. */
			  async function fetchLiveBlacklist(forceApi = false) {
			    if (!forceApi) {
			      const domRules = readDomBlacklist();
			      if (domRules) return domRules;
			    }
			
			    try {
			      const data = await Net.fetchJson('/users/me.json');
			      if (typeof data.blacklisted_tags === 'string') return parseBlacklistText(data.blacklisted_tags);
			    } catch (err) {
			      U.logWarn('reading blacklist via API failed, checking DOM fallback', err);
			    }
			
			    const fallbackDom = readDomBlacklist();
			    if (fallbackDom) return fallbackDom;
			
			    throw new Error('Cannot read the current blacklist');
			  }
			
			  async function saveBlacklist(userId, rules) {
			    const res = await Net.sendForm(`/users/${userId}.json`, 'PATCH', {
			      'user[blacklisted_tags]': rules.join('\n')
			    });
			    if (!res.ok) throw new Net.HttpError(res.status, res.url);
			
			    // Keep the in-DOM meta tag updated so subsequent operations read without network round-trips
			    const meta = document.querySelector(SEL.blacklistMeta);
			    if (meta) meta.content = JSON.stringify(rules);
			  }
			
			  // ---------- operations (internal, NOT queued; composed by the public wrappers) ----------
			  async function backupBlacklistImpl() {
			    if (await U.storageGet(KEYS.BLACKLIST_LOCKED, false)) {
			      const msg = 'Disabled: downvotes are in blacklist.';
			      U.showToast(msg, true);
			      return fail(msg, 'LOCKED');
			    }
			    const user = U.getCurrentUser();
			    if (!user) return notLoggedIn();
			    try {
			      U.showToast('Backing up blacklist...');
			      const rules = await fetchLiveBlacklist(true); // force fresh API read for backups
			      await U.storageSet(KEYS.BLACKLIST_BACKUP, rules);
			      const msg = `Backup saved (${rules.length} rules)`;
			      U.showToast(msg);
			      return ok(msg);
			    } catch (err) {
			      return failFromError(err);
			    }
			  }
			
			  async function loadDownvotesImpl() {
			    const user = U.getCurrentUser();
			    if (!user) return notLoggedIn();
			    try {
			      U.showToast('Fetching downvoted posts...');
			      const ids = new Set();
			      let lowestId = null;
			
			      for (;;) {
			        let url = `/posts.json?tags=downvoted:${encodeURIComponent(user.userName)}&limit=${DOWNVOTES_PAGE_LIMIT}`;
			        if (lowestId) url += `&page=b${lowestId}`;
			        const data = await Net.fetchJson(url);
			        const posts = Array.isArray(data) ? data : (data.posts || []);
			        if (!posts.length) break;
			
			        posts.forEach(p => ids.add(p.id));
			        lowestId = Math.min(...posts.map(p => p.id));
			        if (posts.length < DOWNVOTES_PAGE_LIMIT) break;
			      }
			
			      await U.storageSet(KEYS.DOWNVOTES, [...ids]);
			      const msg = `Loaded ${ids.size} downvotes`;
			      U.showToast(msg);
			      return ok(msg);
			    } catch (err) {
			      return failFromError(err);
			    }
			  }
			
			  async function uploadDownvotesImpl() {
			    const user = U.getCurrentUser();
			    if (!user) return notLoggedIn();
			
			    const downvoted = await readDownvotes();
			    if (!downvoted.length) {
			      U.showToast('No saved downvotes found', true);
			      return fail('No downvotes in storage', 'NO_DOWNVOTES');
			    }
			
			    try {
			      U.showToast('Processing blacklist update...');
			      const current = await fetchLiveBlacklist();
			      const existing = new Set(current.map(idFromRule).filter(id => id !== null));
			      const toAdd = downvoted.filter(id => !existing.has(id));
			
			      if (toAdd.length) {
			        await saveBlacklist(user.userId, [...current, ...toAdd.map(id => `id:${id}`)]);
			      }
			      await U.storageSet(KEYS.BLACKLIST_LOCKED, true); // only after a successful save
			
			      if (!toAdd.length) {
			        U.showToast('All downvotes already blacklisted');
			        return ok('Already up to date');
			      }
			      const msg = `Added ${toAdd.length} posts to blacklist`;
			      U.showToast(msg);
			      scheduleReload();
			      return ok(msg);
			    } catch (err) {
			      return failFromError(err);
			    }
			  }
			
			  async function unloadDownvotesImpl() {
			    const user = U.getCurrentUser();
			    if (!user) return notLoggedIn();
			
			    const downvoted = new Set(await readDownvotes());
			    if (!downvoted.size) {
			      U.showToast('No downvotes stored to match', true);
			      return fail('No downvotes in storage', 'NO_DOWNVOTES');
			    }
			
			    try {
			      U.showToast('Removing downvotes...');
			      const current = await fetchLiveBlacklist();
			      const updated = current.filter(rule => !downvoted.has(idFromRule(rule)));
			      const removed = current.length - updated.length;
			
			      if (removed) await saveBlacklist(user.userId, updated);
			      await U.storageSet(KEYS.BLACKLIST_LOCKED, false); // only after a successful save
			
			      if (!removed) {
			        U.showToast('No downvotes found in blacklist');
			        return ok('None found to remove');
			      }
			      const msg = `Removed ${removed} posts`;
			      U.showToast(msg);
			      scheduleReload();
			      return ok(msg);
			    } catch (err) {
			      return failFromError(err);
			    }
			  }
			
			  async function syncApiImpl() {
			    const loaded = await loadDownvotesImpl();
			    if (!loaded.ok) return loaded;
			    return uploadDownvotesImpl();
			  }
			
			  async function toggleAutoUpdateImpl() {
			    if (await U.storageGet(KEYS.AUTO_UPDATE, false)) {
			      await U.storageSet(KEYS.AUTO_UPDATE, false);
			      U.showToast('Auto Update disabled');
			      return ok('Auto Update OFF');
			    }
			
			    U.showToast('Auto Update: syncing downvotes via API...');
			    const result = await syncApiImpl();
			    // "no downvotes yet" is fine; any other failure must NOT enable auto mode
			    if (!result.ok && result.code !== 'NO_DOWNVOTES') {
			      return fail(`Auto Update not enabled: ${result.message}`, result.code);
			    }
			    await U.storageSet(KEYS.AUTO_UPDATE, true);
			    U.showToast('Auto Update enabled & synced');
			    return ok(`Auto Update ON (${result.message})`);
			  }
			
			  // ---------- auto mode: single votes (throw on failure, caller reports) ----------
			  async function addPost(postId) {
			    const user = U.getCurrentUser();
			    if (!user) throw new Error('Not logged in');
			
			    const current = await fetchLiveBlacklist();
			    const rule = `id:${postId}`;
			    const present = current.some(r => r.trim().toLowerCase() === rule);
			    if (!present) await saveBlacklist(user.userId, [...current, rule]);
			
			    const ids = new Set(await readDownvotes());
			    ids.add(postId);
			    await U.storageSet(KEYS.DOWNVOTES, [...ids]);
			    await U.storageSet(KEYS.BLACKLIST_LOCKED, true);
			    if (!present) U.showToast(`Auto: #${postId} blacklisted (applies after reload)`);
			  }
			
			  async function removePost(postId) {
			    const user = U.getCurrentUser();
			    if (!user) throw new Error('Not logged in');
			
			    const current = await fetchLiveBlacklist();
			    const rule = `id:${postId}`;
			    const updated = current.filter(r => r.trim().toLowerCase() !== rule);
			    if (updated.length !== current.length) await saveBlacklist(user.userId, updated);
			
			    const ids = (await readDownvotes()).filter(id => id !== postId);
			    await U.storageSet(KEYS.DOWNVOTES, ids);
			    if (updated.length !== current.length) U.showToast(`Auto: #${postId} un-blacklisted`);
			  }
			
			  /* Vote state of the button's own post. A global querySelector fallback is only valid on a post page
			   * (button outside any thumbnail); on listings it would return another post's state. */
			  function readVoteState(voteBtn) {
			    let container = voteBtn.closest(SEL.thirdParty.voteContainer);
			    if (!container && !voteBtn.closest('article.thumbnail')) {
			      container = document.querySelector(SEL.thirdParty.voteContainer);
			    }
			    return container ? String(container.dataset.vote || '0') : null; // null = unknown
			  }
			
			  async function handleVote(action, postId, domState) {
			    if (!(await U.storageGet(KEYS.AUTO_UPDATE, false))) return;
			    try {
			      await queue.run(async () => {
			        const wasDownvoted = domState !== null
			          ? domState === '-1'
			          : (await readDownvotes()).includes(postId); // unknown DOM state: trust our own record
			        if (action === '-1') {
			          return wasDownvoted ? removePost(postId) : addPost(postId);
			        }
			        if (action === '1' && wasDownvoted) return removePost(postId);
			      });
			    } catch (err) {
			      U.logWarn('vote sync', err);
			      U.showToast(`Blacklist sync failed for #${postId}: ${err.message}`, true);
			    }
			  }
			
			  function setupVoteListener() {
			    document.addEventListener('click', (e) => {
			      const voteBtn = e.target.closest(SEL.voteButton);
			      if (!voteBtn) return;
			      if (SEL.commentVoteClasses.some(c => voteBtn.classList.contains(c))) return;
			      if (!chrome.runtime?.id) return;
			
			      const action = voteBtn.dataset.action;
			      if (action !== '1' && action !== '-1') return;
			      const postId = U.getPostIdFromPage(voteBtn);
			      if (!postId) return;
			
			      // Must be read synchronously: the site's own handler mutates the state right after this capture listener.
			      const domState = readVoteState(voteBtn);
			      handleVote(action, postId, domState);
			    }, true);
			  }
			
			  // ---------- public API (names match E621_CONSTANTS.ACTIONS) ----------
			  return {
			    init: setupVoteListener,
			    backupBlacklist: () => queue.run(backupBlacklistImpl),
			    loadDownvotes: () => queue.run(loadDownvotesImpl),
			    uploadDownvotes: () => queue.run(uploadDownvotesImpl),
			    unloadDownvotes: () => queue.run(unloadDownvotesImpl),
			    syncApi: () => queue.run(syncApiImpl),
			    toggleAutoUpdate: () => queue.run(toggleAutoUpdateImpl)
			  };
			});
			// END: scripts/blacklist.js
			
			// START: scripts/uploader.js
			E621X.define('uploader', ['utils', 'selectors', 'net', 'dom'], (U, SEL, Net, D) => {
			  'use strict';
			  const { STORAGE_KEYS } = E621_CONSTANTS;
			  const ITEM_ID = 'post-info-extra-uploader';
			  const rankCache = new Map();
			
			  /* Link to a user, styled with the site's rank classes (taken from the page, the cache, or /users/:id.json). */
			  function createUserLink(id, name = '') {
			    if (!/^\d+$/.test(String(id))) return D.el('span', { text: name || 'unknown user' });
			
			    const link = D.el('a', { href: `/users/${id}`, text: name || `User #${id}` });
			    const existing = document.querySelector(`a[href="/users/${id}"]`);
			    if (existing?.className) {
			      link.className = existing.className;
			      return link;
			    }
			
			    const cached = rankCache.get(String(id));
			    if (cached) {
			      link.textContent = cached.name;
			      link.className = cached.cssClass;
			      return link;
			    }
			
			    Net.fetchJson(`/users/${id}.json`)
			      .then(user => {
			        const level = String(user.level_string || 'member').toLowerCase().replace(/[^a-z0-9_-]/g, '');
			        const cssClass = user.is_banned ? 'user-blocked' : `user-${level}`;
			        rankCache.set(String(id), { name: user.name, cssClass });
			        link.textContent = user.name;
			        link.className = cssClass;
			      })
			      .catch(err => U.logWarn('uploader rank lookup', err));
			
			    return link;
			  }
			
			  const findInfoItem = (prefix, excludeId = null) =>
			    Array.from(document.querySelectorAll(SEL.postInfoItems)).find(li =>
			      li.id !== excludeId && li.textContent.trim().toLowerCase().startsWith(prefix));
			
			  function updateVisibility(enabled) {
			    const existing = document.getElementById(ITEM_ID);
			    if (!enabled) { existing?.remove(); return; }
			
			    // The site may already render a native "Uploader:" row
			    if (findInfoItem('uploader:', ITEM_ID)) { existing?.remove(); return; }
			    if (existing) return;
			
			    const container = document.querySelector(SEL.imageContainer);
			    const uploaderId = container?.dataset.uploaderId;
			    if (!uploaderId) return;
			
			    const li = D.el('li', { id: ITEM_ID }, [
			      'Uploader: ',
			      createUserLink(uploaderId, container.dataset.uploader || '')
			    ]);
			
			    const postedLi = findInfoItem('posted:');
			    if (postedLi) {
			      postedLi.insertAdjacentElement('afterend', li);
			    } else {
			      const lists = document.querySelectorAll(SEL.postInfoLists);
			      lists[lists.length - 1]?.appendChild(li);
			    }
			  }
			
			  async function init() {
			    updateVisibility(await U.storageGet(STORAGE_KEYS.SHOW_UPLOADER, true));
			    chrome.storage.onChanged.addListener((changes, area) => {
			      if (area === 'local' && changes[STORAGE_KEYS.SHOW_UPLOADER]) {
			        updateVisibility(changes[STORAGE_KEYS.SHOW_UPLOADER].newValue !== false);
			      }
			    });
			  }
			
			  return { init, updateVisibility };
			});
			// END: scripts/uploader.js
			
			// START: scripts/user_edits.js
			/* User Profile: Specific Post Edits filter & Tag Update Statistics dashboard.
			 * - Allows filtering post versions by specific tags.
			 * - Collects and visualizes comprehensive tag update statistics for any user:
			 *   most added tags, most removed tags, most common tags in edited posts, timeline dates,
			 *   with full category coloring matching e621 conventions. */
			E621X.define('userEdits', ['utils', 'selectors', 'net', 'dom'], (U, SEL, Net, D) => {
			  'use strict';
			
			  const STORAGE_KEY_PREFIX = 'e621x_specific_edits_';
			  const FORM_CONTAINER_ID = 'e621x-specific-edits-container';
			  const STATS_MODAL_ID = 'e621x-tag-stats-modal';
			
			  const CATEGORY_NAMES = {
			    0: 'General',
			    1: 'Artist',
			    2: 'Contributor',
			    3: 'Copyright',
			    4: 'Character',
			    5: 'Species',
			    6: 'Invalid',
			    7: 'Meta',
			    8: 'Lore'
			  };
			
			  const tagCategoryCache = new Map();
			  const inFlightCategories = new Set();
			  const userStatsCache = new Map(); // userId -> { statsMap, totalVersions, uniquePosts }
			
			  let isCounting = false;
			
			  function isUserPage() {
			    return /^\/users\/\d+/i.test(location.pathname) ||
			      (document.body?.classList?.contains('c-users') && document.body?.classList?.contains('a-show'));
			  }
			
			  function getProfileUserId() {
			    const match = location.pathname.match(/^\/users\/(\d+)/i);
			    if (match) return match[1];
			
			    const cardLink = document.querySelector('.user-card-name a[href*="/users/"]');
			    if (cardLink) {
			      const m = cardLink.href.match(/\/users\/(\d+)/);
			      if (m) return m[1];
			    }
			
			    const meta = document.querySelector('meta[name="user-id"]');
			    if (meta?.content && /^\d+$/.test(meta.content)) return meta.content;
			
			    const link = document.querySelector('a[href*="/post_versions?"]');
			    if (link) {
			      const m = link.href.match(/search(?:\[|%5B)updater_id(?:\]|%5D)=(\d+)/);
			      if (m) return m[1];
			    }
			
			    return null;
			  }
			
			  /* Takes the username directly from .user-card-name of the profile being viewed */
			  function getProfileUserName() {
			    const cardLink = document.querySelector('.user-card-name a, .user-card-name');
			    if (cardLink) {
			      const name = cardLink.textContent.trim();
			      if (name) return name;
			    }
			
			    const ogTitle = document.querySelector('meta[property="og:title"]');
			    if (ogTitle?.content) {
			      const clean = ogTitle.content.replace(/\s*-\s*e621.*$/i, '').trim();
			      if (clean) return clean;
			    }
			
			    const titleMatch = document.title.match(/User\s*-\s*(.+?)\s*-\s*e621/i);
			    if (titleMatch?.[1]) {
			      return titleMatch[1].trim();
			    }
			
			    const userId = getProfileUserId();
			    return userId ? `User #${userId}` : 'User';
			  }
			
			  /* Reads the exact number of post edits from the profile Changes block (e.g. <h5>4652</h5>) */
			  function getProfilePostEditsCount() {
			    const postVersionLink = document.querySelector('a[href*="/post_versions"][href*="updater_id"], a[href*="/post_versions"]');
			    if (postVersionLink) {
			      const h5 = postVersionLink.querySelector('h5');
			      if (h5) {
			        const num = parseInt(h5.textContent.replace(/[^\d]/g, ''), 10);
			        if (Number.isFinite(num) && num > 0) return num;
			      }
			      const rawNum = parseInt(postVersionLink.textContent.replace(/[^\d]/g, ''), 10);
			      if (Number.isFinite(rawNum) && rawNum > 0) return rawNum;
			    }
			
			    const changesLines = document.querySelectorAll('.profile-line');
			    for (const line of changesLines) {
			      const text = line.textContent || '';
			      if (text.includes('Changes') || text.includes('Post')) {
			        const link = line.querySelector('a[href*="/post_versions"]');
			        if (link) {
			          const num = parseInt(link.textContent.replace(/[^\d]/g, ''), 10);
			          if (Number.isFinite(num) && num > 0) return num;
			        }
			      }
			    }
			
			    return 0;
			  }
			
			  function seedCategoryCache() {
			    document.querySelectorAll('#tag-list li[class*="tag-type-"]').forEach(li => {
			      const match = li.className.match(/tag-type-(\d+)/);
			      const tagLink = li.querySelector('a.search-tag, a');
			      if (match && tagLink) {
			        const cat = Number(match[1]);
			        const name = tagLink.textContent.trim().toLowerCase().replace(/\s+/g, '_');
			        if (name) tagCategoryCache.set(name, cat);
			      }
			    });
			  }
			
			  async function fetchMissingCategories(tagNames) {
			    const missing = tagNames.filter(t => !tagCategoryCache.has(t) && !inFlightCategories.has(t));
			    if (!missing.length) return;
			
			    missing.forEach(t => inFlightCategories.add(t));
			    for (let i = 0; i < missing.length; i += 100) {
			      const chunk = missing.slice(i, i + 100);
			      try {
			        const url = `/tags.json?search[name]=${encodeURIComponent(chunk.join(','))}&limit=100`;
			        const data = await Net.fetchJson(url);
			        const list = Array.isArray(data) ? data : (data?.tags || []);
			        list.forEach(t => {
			          if (t?.name && t.category !== undefined) {
			            tagCategoryCache.set(String(t.name).toLowerCase(), Number(t.category));
			          }
			        });
			        chunk.forEach(t => {
			          if (!tagCategoryCache.has(t)) tagCategoryCache.set(t, 0);
			        });
			      } catch (err) {
			        U.logWarn('fetchMissingCategories', err);
			        chunk.forEach(t => {
			          if (!tagCategoryCache.has(t)) tagCategoryCache.set(t, 0);
			        });
			      } finally {
			        chunk.forEach(t => inFlightCategories.delete(t));
			      }
			    }
			  }
			
			  function extractTags(val) {
			    if (!val) return [];
			    if (Array.isArray(val)) return val.map(t => String(t).trim().toLowerCase()).filter(Boolean);
			    if (typeof val === 'string') return val.trim().toLowerCase().split(/\s+/).filter(Boolean);
			    return [];
			  }
			
			  function formatShortDate(timestamp) {
			    if (!timestamp) return '—';
			    const d = new Date(timestamp);
			    if (isNaN(d.getTime())) return '—';
			    const pad = n => String(n).padStart(2, '0');
			    return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
			  }
			
			  // ==========================================
			  // TAG UPDATE STATISTICS MODAL
			  // ==========================================
			  function openTagStatsModal(userId) {
			    const existing = document.getElementById(STATS_MODAL_ID);
			    if (existing) existing.remove();
			
			    const userName = getProfileUserName();
			    const expectedTotal = getProfilePostEditsCount();
			    let abortFetch = false;
			
			    // Active filters & sorting
			    let sortKey = 'added_desc';
			    let filterCategory = 'all';
			    let filterAction = 'all';
			    let filterUploadType = 'all';
			    let filterQuery = '';
			    let currentPage = 1;
			    const PAGE_SIZE = 50;
			
			    // Elements
			    const overlay = D.el('div', { id: STATS_MODAL_ID, class: 'e621x-stats-overlay' });
			    const dialog = D.el('div', { class: 'e621x-stats-dialog', role: 'dialog', 'aria-modal': 'true' });
			
			    // 1. Header
			    const titleSpan = D.el('span', { class: 'e621x-stats-title', text: `Tag Update Statistics — ${userName} (#${userId})` });
			    function closeModal() {
			      abortFetch = true;
			      document.removeEventListener('keydown', handleKeydown);
			      overlay.remove();
			    }
			
			    const closeBtn = D.el('button', {
			      type: 'button',
			      class: 'e621x-stats-close-btn',
			      text: '×',
			      'aria-label': 'Close',
			      onClick: closeModal
			    });
			
			    const header = D.el('div', { class: 'e621x-stats-header' }, [
			      titleSpan,
			      closeBtn
			    ]);
			
			    // 2. Fetch Progress Area
			    const progressText = D.el('div', { class: 'e621x-stats-progress-text', text: 'Preparing to fetch updates...' });
			    const progressBar = D.el('div', { class: 'e621x-stats-progress-fill' });
			    const progressTrack = D.el('div', { class: 'e621x-stats-progress-track' }, [progressBar]);
			
			    const btnStopView = D.button({
			      text: 'Stop & View',
			      title: 'Stop loading more edits and view collected stats',
			      className: 'e621x-stats-btn',
			      onClick: () => {
			        abortFetch = true;
			        progressText.textContent = 'Stopping and displaying collected stats...';
			      }
			    });
			
			    const btnCancel = D.button({
			      text: 'Cancel',
			      className: 'e621x-stats-btn',
			      onClick: closeModal
			    });
			
			    const progressBox = D.el('div', { class: 'e621x-stats-progress-box' }, [
			      progressText,
			      progressTrack,
			      D.el('div', { class: 'e621x-stats-progress-actions' }, [btnStopView, btnCancel])
			    ]);
			
			    // 3. Summary Cards
			    const summaryCardVersions = D.el('div', { class: 'e621x-stats-summary-card' }, [
			      D.el('span', { class: 'e621x-stats-summary-num', text: '0' }),
			      D.el('span', { class: 'e621x-stats-summary-lbl', text: 'Total Versions' })
			    ]);
			    const summaryCardPosts = D.el('div', { class: 'e621x-stats-summary-card' }, [
			      D.el('span', { class: 'e621x-stats-summary-num', text: '0' }),
			      D.el('span', { class: 'e621x-stats-summary-lbl', text: 'Unique Posts' })
			    ]);
			    const summaryCardAdded = D.el('div', { class: 'e621x-stats-summary-card' }, [
			      D.el('span', { class: 'e621x-stats-summary-num e621x-color-add', text: '0' }),
			      D.el('span', { class: 'e621x-stats-summary-lbl', text: 'Tags Added' })
			    ]);
			    const summaryCardRemoved = D.el('div', { class: 'e621x-stats-summary-card' }, [
			      D.el('span', { class: 'e621x-stats-summary-num e621x-color-rem', text: '0' }),
			      D.el('span', { class: 'e621x-stats-summary-lbl', text: 'Tags Removed' })
			    ]);
			    const summaryCardPool = D.el('div', { class: 'e621x-stats-summary-card' }, [
			      D.el('span', { class: 'e621x-stats-summary-num', text: '0' }),
			      D.el('span', { class: 'e621x-stats-summary-lbl', text: 'Tags in Posts' })
			    ]);
			
			    const summaryBar = D.el('div', { class: 'e621x-stats-summary-bar hidden', style: 'display: none;' }, [
			      summaryCardVersions,
			      summaryCardPosts,
			      summaryCardAdded,
			      summaryCardRemoved,
			      summaryCardPool
			    ]);
			
			    // 4. Controls Toolbar
			    const inputSearch = D.el('input', {
			      type: 'text',
			      class: 'st-input e621x-stats-search',
			      placeholder: 'Filter tags by name...',
			      autocomplete: 'off'
			    });
			
			    const selectSort = D.el('select', { class: 'st-select e621x-stats-select' }, [
			      D.el('option', { value: 'added_desc', text: 'Sort by: Most Added (↓)' }),
			      D.el('option', { value: 'added_asc', text: 'Sort by: Least Added (↑)' }),
			      D.el('option', { value: 'removed_desc', text: 'Sort by: Most Removed (↓)' }),
			      D.el('option', { value: 'removed_asc', text: 'Sort by: Least Removed (↑)' }),
			      D.el('option', { value: 'posts_desc', text: 'Sort by: Most in Posts (↓)' }),
			      D.el('option', { value: 'date_desc', text: 'Sort by: Date (Most Recent)' }),
			      D.el('option', { value: 'date_asc', text: 'Sort by: Date (Oldest First)' }),
			      D.el('option', { value: 'name_asc', text: 'Sort by: Name (A-Z)' })
			    ]);
			
			    const selectAction = D.el('select', { class: 'st-select e621x-stats-select' }, [
			      D.el('option', { value: 'all', text: 'All Tag Changes' }),
			      D.el('option', { value: 'added_only', text: 'Added Only (>0)' }),
			      D.el('option', { value: 'added_or_removed', text: 'Added or Removed (>0)' }),
			      D.el('option', { value: 'removed_only', text: 'Removed Only (>0)' }),
			      D.el('option', { value: 'both', text: 'Both Added & Removed' })
			    ]);
			
			    const selectUploadType = D.el('select', { class: 'st-select e621x-stats-select' }, [
			      D.el('option', { value: 'all', text: 'All (Uploads & Edits)' }),
			      D.el('option', { value: 'uploads_only', text: 'Uploads Only' }),
			      D.el('option', { value: 'non_uploads', text: 'Non-Uploads Only' })
			    ]);
			
			    const selectCat = D.el('select', { class: 'st-select e621x-stats-select' }, [
			      D.el('option', { value: 'all', text: 'All Categories' }),
			      ...Object.entries(CATEGORY_NAMES).map(([val, label]) =>
			        D.el('option', { value: val, text: label }))
			    ]);
			
			    const btnExportCsv = D.button({
			      text: 'Export CSV',
			      className: 'e621x-stats-btn',
			      title: 'Download tag statistics as CSV'
			    });
			
			    const btnRefresh = D.button({
			      text: 'Re-fetch',
			      className: 'e621x-stats-btn',
			      title: 'Clear cache and re-fetch all versions from API'
			    });
			
			    const toolbar = D.el('div', { class: 'e621x-stats-toolbar hidden', style: 'display: none;' }, [
			      D.el('div', { class: 'e621x-stats-toolbar-group' }, [
			        inputSearch,
			        selectSort,
			        selectAction,
			        selectUploadType,
			        selectCat
			      ]),
			      D.el('div', { class: 'e621x-stats-toolbar-group' }, [
			        btnExportCsv,
			        btnRefresh
			      ])
			    ]);
			
			    // 5. Table Container
			    const tableBody = D.el('tbody');
			    const table = D.el('table', { class: 'e621x-stats-table' }, [
			      D.el('thead', {}, [
			        D.el('tr', {}, [
			          D.el('th', { style: 'width: 32%', text: 'Tag' }),
			          D.el('th', { style: 'width: 14%', text: 'Category' }),
			          D.el('th', { style: 'width: 13%', text: 'Times Added' }),
			          D.el('th', { style: 'width: 13%', text: 'Times Removed' }),
			          D.el('th', { style: 'width: 13%', text: 'In Edited Posts' }),
			          D.el('th', { style: 'width: 15%', text: 'Last Activity' })
			        ])
			      ]),
			      tableBody
			    ]);
			
			    const tableWrap = D.el('div', { class: 'e621x-stats-table-wrap hidden', style: 'display: none;' }, [table]);
			
			    // 6. Pagination Footer
			    const pageInfo = D.el('span', { class: 'e621x-stats-page-info', text: 'Showing 0 tags' });
			    const btnPrev = D.button({ text: '« Prev', className: 'e621x-stats-btn', disabled: true });
			    const btnNext = D.button({ text: 'Next »', className: 'e621x-stats-btn', disabled: true });
			
			    const paginationBar = D.el('div', { class: 'e621x-stats-pagination hidden', style: 'display: none;' }, [
			      pageInfo,
			      D.el('div', { class: 'e621x-stats-pagination-buttons' }, [btnPrev, btnNext])
			    ]);
			
			    dialog.append(header, progressBox, summaryBar, toolbar, tableWrap, paginationBar);
			    overlay.appendChild(dialog);
			    document.body.appendChild(overlay);
			
			    // Keydown listener to close modal on Escape
			    function handleKeydown(e) {
			      if (e.key === 'Escape') {
			        closeModal();
			      }
			    }
			
			    document.addEventListener('keydown', handleKeydown);
			    let activeDataset = null;
			    let renderId = 0;
			    let isFetchingMissing = false;
			
			    async function renderActiveTable() {
			      if (!activeDataset) return;
			      const currentRenderId = ++renderId;
			
			      const {
			        statsMap,
			        totalVersions = 0,
			        uploadVersions = 0,
			        editVersions = 0,
			        uniquePosts = 0,
			        uniquePostsAll = uniquePosts,
			        uniquePostsUpload = 0,
			        uniquePostsEdit = 0
			      } = activeDataset;
			
			      // Map metrics according to the selected upload scope
			      let tags = Array.from(statsMap.values()).map(s => {
			        if (filterUploadType === 'uploads_only') {
			          return {
			            name: s.name,
			            added: s.uploadAdded || 0,
			            removed: s.uploadRemoved || 0,
			            inPosts: s.uploadInPosts || 0,
			            firstDate: s.uploadFirstDate || 0,
			            lastDate: s.uploadLastDate || 0
			          };
			        }
			        if (filterUploadType === 'non_uploads') {
			          return {
			            name: s.name,
			            added: s.editAdded || 0,
			            removed: s.editRemoved || 0,
			            inPosts: s.editInPosts || 0,
			            firstDate: s.editFirstDate || 0,
			            lastDate: s.editLastDate || 0
			          };
			        }
			        return {
			          name: s.name,
			          added: s.added || 0,
			          removed: s.removed || 0,
			          inPosts: s.inPosts || 0,
			          firstDate: s.firstDate || 0,
			          lastDate: s.lastDate || 0
			        };
			      });
			
			      // Synchronize Summary Cards with the active upload scope
			      let totalAddedCount = 0;
			      let totalRemovedCount = 0;
			      let totalInPostsCount = 0;
			
			      for (const s of tags) {
			        if (s.added > 0) totalAddedCount++;
			        if (s.removed > 0) totalRemovedCount++;
			        if (s.inPosts > 0) totalInPostsCount++;
			      }
			
			      let displayedVersions = totalVersions;
			      let displayedPosts = uniquePostsAll;
			      if (filterUploadType === 'uploads_only') {
			        displayedVersions = uploadVersions;
			        displayedPosts = uniquePostsUpload;
			      } else if (filterUploadType === 'non_uploads') {
			        displayedVersions = editVersions;
			        displayedPosts = uniquePostsEdit;
			      }
			
			      const numVersionsEl = summaryCardVersions.querySelector('.e621x-stats-summary-num');
			      if (numVersionsEl) numVersionsEl.textContent = (displayedVersions || 0).toLocaleString();
			
			      const numPostsEl = summaryCardPosts.querySelector('.e621x-stats-summary-num');
			      if (numPostsEl) numPostsEl.textContent = (displayedPosts || 0).toLocaleString();
			
			      const numAddedEl = summaryCardAdded.querySelector('.e621x-stats-summary-num');
			      if (numAddedEl) numAddedEl.textContent = totalAddedCount.toLocaleString();
			
			      const numRemovedEl = summaryCardRemoved.querySelector('.e621x-stats-summary-num');
			      if (numRemovedEl) numRemovedEl.textContent = totalRemovedCount.toLocaleString();
			
			      const numPoolEl = summaryCardPool.querySelector('.e621x-stats-summary-num');
			      if (numPoolEl) numPoolEl.textContent = totalInPostsCount.toLocaleString();
			
			      // Text Search
			      if (filterQuery) {
			        tags = tags.filter(t => t.name.includes(filterQuery));
			      }
			
			      // Action Filter
			      if (filterAction === 'added_only') {
			        tags = tags.filter(t => t.added > 0);
			      } else if (filterAction === 'added_or_removed') {
			        tags = tags.filter(t => t.added > 0 || t.removed > 0);
			      } else if (filterAction === 'removed_only') {
			        tags = tags.filter(t => t.removed > 0);
			      } else if (filterAction === 'both') {
			        tags = tags.filter(t => t.added > 0 && t.removed > 0);
			      }
			
			      // Category Filter
			      if (filterCategory !== 'all') {
			        const catNum = Number(filterCategory);
			        tags = tags.filter(t => (tagCategoryCache.get(t.name) ?? 0) === catNum);
			      }
			
			      // Sorting
			      tags.sort((a, b) => {
			        switch (sortKey) {
			          case 'added_desc':
			            return (b.added - a.added) || (b.inPosts - a.inPosts) || a.name.localeCompare(b.name);
			          case 'added_asc':
			            return (a.added - b.added) || (a.inPosts - b.inPosts) || a.name.localeCompare(b.name);
			          case 'removed_desc':
			            return (b.removed - a.removed) || (b.inPosts - a.inPosts) || a.name.localeCompare(b.name);
			          case 'removed_asc':
			            return (a.removed - b.removed) || (a.inPosts - b.inPosts) || a.name.localeCompare(b.name);
			          case 'posts_desc':
			            return (b.inPosts - a.inPosts) || (b.added - a.added) || a.name.localeCompare(b.name);
			          case 'date_desc':
			            return (b.lastDate - a.lastDate) || (b.added - a.added);
			          case 'date_asc':
			            return (a.firstDate - b.firstDate) || (a.lastDate - b.lastDate);
			          case 'name_asc':
			            return a.name.localeCompare(b.name);
			          default:
			            return 0;
			        }
			      });
			
			      const totalMatching = tags.length;
			      const totalPages = Math.max(1, Math.ceil(totalMatching / PAGE_SIZE));
			      if (currentPage > totalPages) currentPage = totalPages;
			      if (currentPage < 1) currentPage = 1;
			
			      const startIdx = (currentPage - 1) * PAGE_SIZE;
			      const pageItems = tags.slice(startIdx, startIdx + PAGE_SIZE);
			
			      // Lock pagination controls while categories are being fetched
			      btnPrev.disabled = true;
			      btnNext.disabled = true;
			
			      const visibleNames = pageItems.map(p => p.name);
			      const missing = visibleNames.filter(t => !tagCategoryCache.has(t));
			      if (missing.length > 0) {
			        isFetchingMissing = true;
			        try {
			          await fetchMissingCategories(visibleNames);
			        } finally {
			          isFetchingMissing = false;
			        }
			        // Discard stale renders triggered by fast filter or page changes
			        if (currentRenderId !== renderId) return;
			      }
			
			      tableBody.replaceChildren();
			
			      if (!pageItems.length) {
			        const emptyRow = D.el('tr', {}, [
			          D.el('td', { colspan: '6', class: 'e621x-stats-empty', text: 'No tags match the selected filters.' })
			        ]);
			        tableBody.appendChild(emptyRow);
			      } else {
			        pageItems.forEach(item => {
			          const cat = tagCategoryCache.get(item.name) ?? 0;
			          const catClass = `e621x-tag-type-${cat}`;
			          const catName = CATEGORY_NAMES[cat] || 'General';
			
			          const tagLink = D.el('a', {
			            href: `/posts?tags=${encodeURIComponent(item.name)}`,
			            target: '_blank',
			            rel: 'noopener',
			            class: `e621x-stats-tag-link ${catClass}`,
			            text: item.name
			          });
			
			          const catBadge = D.el('span', {
			            class: `e621x-cat-badge ${catClass}`,
			            text: catName
			          });
			
			          const versionParam = filterUploadType === 'uploads_only' ? '&search[version]=1' : '';
			          const addedLink = item.added > 0 ? D.el('a', {
			            href: `/post_versions?search[updater_id]=${userId}&search[tags_added]=${encodeURIComponent(item.name)}${versionParam}`,
			            target: '_blank',
			            rel: 'noopener',
			            class: 'e621x-stats-count-link e621x-color-add',
			            title: `View edits where ${userName} added ${item.name}`,
			            text: `+${item.added}`
			          }) : D.el('span', { class: 'e621x-stats-count-zero', text: '0' });
			
			          const removedLink = item.removed > 0 ? D.el('a', {
			            href: `/post_versions?search[updater_id]=${userId}&search[tags_removed]=${encodeURIComponent(item.name)}`,
			            target: '_blank',
			            rel: 'noopener',
			            class: 'e621x-stats-count-link e621x-color-rem',
			            title: `View edits where ${userName} removed ${item.name}`,
			            text: `-${item.removed}`
			          }) : D.el('span', { class: 'e621x-stats-count-zero', text: '0' });
			
			          const inPostsLink = item.inPosts > 0 ? D.el('a', {
			            href: `/post_versions?search[updater_id]=${userId}&search[tags]=${encodeURIComponent(item.name)}${versionParam}`,
			            target: '_blank',
			            rel: 'noopener',
			            class: 'e621x-stats-count-link',
			            title: `View edits on posts containing ${item.name}`,
			            text: String(item.inPosts)
			          }) : D.el('span', { class: 'e621x-stats-count-zero', text: '0' });
			
			          const row = D.el('tr', { class: 'e621x-stats-row' }, [
			            D.el('td', {}, [tagLink]),
			            D.el('td', {}, [catBadge]),
			            D.el('td', {}, [addedLink]),
			            D.el('td', {}, [removedLink]),
			            D.el('td', {}, [inPostsLink]),
			            D.el('td', { class: 'e621x-stats-date', text: formatShortDate(item.lastDate) })
			          ]);
			
			          tableBody.appendChild(row);
			        });
			      }
			
			      // Pagination status and boundaries
			      pageInfo.textContent = `Showing ${Math.min(totalMatching, startIdx + 1)}–${Math.min(totalMatching, startIdx + PAGE_SIZE)} of ${totalMatching} tags`;
			      btnPrev.disabled = currentPage <= 1;
			      btnNext.disabled = currentPage >= totalPages;
			    }
			
			    // Attach control listeners
			    inputSearch.addEventListener('input', () => {
			      filterQuery = inputSearch.value.trim().toLowerCase();
			      currentPage = 1;
			      renderActiveTable();
			    });
			
			    selectSort.addEventListener('change', () => {
			      sortKey = selectSort.value;
			      currentPage = 1;
			      renderActiveTable();
			    });
			
			    selectAction.addEventListener('change', () => {
			      filterAction = selectAction.value;
			      currentPage = 1;
			      renderActiveTable();
			    });
			
			    selectUploadType.addEventListener('change', () => {
			      filterUploadType = selectUploadType.value;
			      currentPage = 1;
			      renderActiveTable();
			    });
			
			    selectCat.addEventListener('change', () => {
			      filterCategory = selectCat.value;
			      currentPage = 1;
			      renderActiveTable();
			    });
			
			    btnPrev.addEventListener('click', () => {
			      if (isFetchingMissing) return;
			      if (currentPage > 1) {
			        currentPage--;
			        renderActiveTable();
			        tableWrap.scrollTop = 0;
			      }
			    });
			
			    btnNext.addEventListener('click', () => {
			      if (isFetchingMissing) return;
			      currentPage++;
			      renderActiveTable();
			      tableWrap.scrollTop = 0;
			    });
			
			    btnExportCsv.addEventListener('click', () => {
			      if (!activeDataset) return;
			      const rows = [
			        ['Tag', 'Category', 'Added Count', 'Removed Count', 'Posts Count', 'Last Activity Date']
			      ];
			      Array.from(activeDataset.statsMap.values()).forEach(s => {
			        let added = s.added;
			        let removed = s.removed;
			        let inPosts = s.inPosts;
			        let lastDate = s.lastDate;
			
			        if (filterUploadType === 'uploads_only') {
			          added = s.uploadAdded;
			          removed = s.uploadRemoved;
			          inPosts = s.uploadInPosts;
			          lastDate = s.uploadLastDate;
			        } else if (filterUploadType === 'non_uploads') {
			          added = s.editAdded;
			          removed = s.editRemoved;
			          inPosts = s.editInPosts;
			          lastDate = s.editLastDate;
			        }
			
			        const cat = CATEGORY_NAMES[tagCategoryCache.get(s.name) ?? 0] || 'General';
			        rows.push([
			          `"${s.name.replace(/"/g, '""')}"`,
			          `"${cat}"`,
			          added,
			          removed,
			          inPosts,
			          `"${formatShortDate(lastDate)}"`
			        ]);
			      });
			      const csvContent = rows.map(r => r.join(',')).join('\n');
			      const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
			      U.downloadBlob(blob, `tag_stats_user_${userId}_${filterUploadType}_${Date.now()}.csv`);
			    });
			
			    btnRefresh.addEventListener('click', () => {
			      userStatsCache.delete(userId);
			      startFetch();
			    });
			
			    // 7. Data Fetching and Processing Engine
			    async function startFetch() {
			      if (userStatsCache.has(userId)) {
			        activeDataset = userStatsCache.get(userId);
			        finishAndDisplay();
			        return;
			      }
			
			      abortFetch = false;
			      progressBox.classList.remove('hidden');
			      progressBox.style.display = '';
			
			      summaryBar.classList.add('hidden');
			      summaryBar.style.display = 'none';
			      toolbar.classList.add('hidden');
			      toolbar.style.display = 'none';
			      tableWrap.classList.add('hidden');
			      tableWrap.style.display = 'none';
			      paginationBar.classList.add('hidden');
			      paginationBar.style.display = 'none';
			
			      const statsMap = new Map();
			      const uniquePostsAll = new Set();
			      const uniquePostsUpload = new Set();
			      const uniquePostsEdit = new Set();
			      let totalVersions = 0;
			      let uploadVersions = 0;
			      let editVersions = 0;
			      let lowestId = null;
			      let pageNum = 1;
			      const PAGE_LIMIT = 320;
			
			      const getStat = (name) => {
			        let stat = statsMap.get(name);
			        if (!stat) {
			          stat = {
			            name,
			            // Combined totals
			            added: 0,
			            removed: 0,
			            inPosts: 0,
			            postIds: new Set(),
			            firstDate: 0,
			            lastDate: 0,
			            // Uploads only (version === 1)
			            uploadAdded: 0,
			            uploadRemoved: 0,
			            uploadInPosts: 0,
			            uploadPostIds: new Set(),
			            uploadFirstDate: 0,
			            uploadLastDate: 0,
			            // Non-uploads only (version > 1)
			            editAdded: 0,
			            editRemoved: 0,
			            editInPosts: 0,
			            editPostIds: new Set(),
			            editFirstDate: 0,
			            editLastDate: 0
			          };
			          statsMap.set(name, stat);
			        }
			        return stat;
			      };
			
			      const updateProgressUi = (currentCount, currentPg) => {
			        if (expectedTotal > 0) {
			          const denominator = Math.max(expectedTotal, currentCount);
			          const pct = Math.min(100, Math.round((currentCount / denominator) * 100));
			          progressBar.style.width = `${pct}%`;
			          progressText.textContent = `Fetching post updates: ${currentCount.toLocaleString()} / ${expectedTotal.toLocaleString()} (${pct}%) — page ${currentPg}...`;
			        } else {
			          const pct = Math.min(95, currentPg * 8);
			          progressBar.style.width = `${pct}%`;
			          progressText.textContent = `Fetching post updates: ${currentCount.toLocaleString()} loaded — page ${currentPg}...`;
			        }
			      };
			
			      // Set initial progress
			      updateProgressUi(0, 1);
			
			      try {
			        while (!abortFetch) {
			          let url = `/post_versions.json?search[updater_id]=${userId}&limit=${PAGE_LIMIT}`;
			          if (lowestId !== null) {
			            url += `&page=b${lowestId}`;
			          }
			
			          const data = await Net.fetchJson(url);
			          const list = Array.isArray(data) ? data : (data?.post_versions || []);
			          if (!list.length) break;
			
			          for (const v of list) {
			            totalVersions++;
			            const postId = Number(v.post_id || v.id);
			            const isUpload = Number(v.version) === 1;
			
			            if (isUpload) {
			              uploadVersions++;
			              if (postId) uniquePostsUpload.add(postId);
			            } else {
			              editVersions++;
			              if (postId) uniquePostsEdit.add(postId);
			            }
			            if (postId) uniquePostsAll.add(postId);
			
			            const vId = Number(v.id);
			            if (vId && (lowestId === null || vId < lowestId)) {
			              lowestId = vId;
			            }
			
			            const dateStr = v.updated_at || v.created_at;
			            const time = dateStr ? new Date(dateStr).getTime() : 0;
			
			            const added = extractTags(v.added_tags || v.tags_added);
			            const removed = extractTags(v.removed_tags || v.tags_removed);
			            const inPost = extractTags(v.tags);
			
			            added.forEach(tag => {
			              const s = getStat(tag);
			              s.added++;
			              if (time > s.lastDate) s.lastDate = time;
			              if (!s.firstDate || time < s.firstDate) s.firstDate = time;
			
			              if (isUpload) {
			                s.uploadAdded++;
			                if (time > s.uploadLastDate) s.uploadLastDate = time;
			                if (!s.uploadFirstDate || time < s.uploadFirstDate) s.uploadFirstDate = time;
			              } else {
			                s.editAdded++;
			                if (time > s.editLastDate) s.editLastDate = time;
			                if (!s.editFirstDate || time < s.editFirstDate) s.editFirstDate = time;
			              }
			            });
			
			            removed.forEach(tag => {
			              const s = getStat(tag);
			              s.removed++;
			              if (time > s.lastDate) s.lastDate = time;
			              if (!s.firstDate || time < s.firstDate) s.firstDate = time;
			
			              if (isUpload) {
			                s.uploadRemoved++;
			                if (time > s.uploadLastDate) s.uploadLastDate = time;
			                if (!s.uploadFirstDate || time < s.uploadFirstDate) s.uploadFirstDate = time;
			              } else {
			                s.editRemoved++;
			                if (time > s.editLastDate) s.editLastDate = time;
			                if (!s.editFirstDate || time < s.editFirstDate) s.editFirstDate = time;
			              }
			            });
			
			            inPost.forEach(tag => {
			              const s = getStat(tag);
			              if (postId) {
			                s.postIds.add(postId);
			                if (isUpload) s.uploadPostIds.add(postId);
			                else s.editPostIds.add(postId);
			              }
			              if (time > s.lastDate) s.lastDate = time;
			              if (!s.firstDate || time < s.firstDate) s.firstDate = time;
			
			              if (isUpload) {
			                if (time > s.uploadLastDate) s.uploadLastDate = time;
			                if (!s.uploadFirstDate || time < s.uploadFirstDate) s.uploadFirstDate = time;
			              } else {
			                if (time > s.editLastDate) s.editLastDate = time;
			                if (!s.editFirstDate || time < s.editFirstDate) s.editFirstDate = time;
			              }
			            });
			          }
			
			          // Immediately update progress bar with the real loaded versions count
			          updateProgressUi(totalVersions, pageNum);
			
			          if (list.length < PAGE_LIMIT) break;
			          pageNum++;
			        }
			      } catch (err) {
			        if (!abortFetch) {
			          U.logWarn(`fetch user edits #${userId}`, err);
			          progressText.textContent = `Warning: Fetch stopped early (${err.message}). Showing loaded results.`;
			          await U.sleep(1000);
			        }
			      }
			
			      // Compute inPosts counts
			      for (const stat of statsMap.values()) {
			        stat.inPosts = stat.postIds.size;
			        stat.uploadInPosts = stat.uploadPostIds.size;
			        stat.editInPosts = stat.editPostIds.size;
			      }
			
			      activeDataset = {
			        statsMap,
			        totalVersions,
			        uploadVersions,
			        editVersions,
			        uniquePosts: uniquePostsAll.size,
			        uniquePostsAll: uniquePostsAll.size,
			        uniquePostsUpload: uniquePostsUpload.size,
			        uniquePostsEdit: uniquePostsEdit.size
			      };
			
			      userStatsCache.set(userId, activeDataset);
			      finishAndDisplay();
			    }
			
			    function finishAndDisplay() {
			      if (!activeDataset) return;
			
			      // Hide progress box and reveal dashboard
			      progressBox.classList.add('hidden');
			      progressBox.style.display = 'none';
			
			      summaryBar.classList.remove('hidden');
			      summaryBar.style.display = '';
			      toolbar.classList.remove('hidden');
			      toolbar.style.display = '';
			      tableWrap.classList.remove('hidden');
			      tableWrap.style.display = '';
			      paginationBar.classList.remove('hidden');
			      paginationBar.style.display = '';
			
			      renderActiveTable();
			    }
			
			    startFetch();
			  }
			
			  // ==========================================
			  // SPECIFIC POST EDITS FILTER (EXISTING)
			  // ==========================================
			  function loadSavedQuery(userId) {
			    try {
			      const raw = localStorage.getItem(`${STORAGE_KEY_PREFIX}${userId}`) ||
			                  localStorage.getItem(`${STORAGE_KEY_PREFIX}last`);
			      return raw ? JSON.parse(raw) : null;
			    } catch (_) {
			      return null;
			    }
			  }
			
			  function saveQuery(userId, query) {
			    try {
			      const json = JSON.stringify(query);
			      localStorage.setItem(`${STORAGE_KEY_PREFIX}${userId}`, json);
			      localStorage.setItem(`${STORAGE_KEY_PREFIX}last`, json);
			    } catch (_) {
			      // localStorage disabled or full
			    }
			  }
			
			  function clearSavedQuery(userId) {
			    try {
			      localStorage.removeItem(`${STORAGE_KEY_PREFIX}${userId}`);
			    } catch (_) {
			      // Ignored
			    }
			  }
			
			  function buildUrl(userId, { tags, added, removed }, isJson = false) {
			    const endpoint = isJson ? '/post_versions.json' : '/post_versions';
			    const params = new URLSearchParams();
			
			    if (userId) {
			      params.set('search[updater_id]', userId);
			    }
			    if (tags?.trim()) {
			      params.set('search[tags]', tags.trim());
			    }
			    if (added?.trim()) {
			      params.set('search[tags_added]', added.trim());
			    }
			    if (removed?.trim()) {
			      params.set('search[tags_removed]', removed.trim());
			    }
			
			    return `${endpoint}?${params.toString()}`;
			  }
			
			  function createForm(userId) {
			    const container = D.el('div', {
			      id: FORM_CONTAINER_ID,
			      class: 'e621x-specific-edits-panel is-hidden'
			    });
			
			    const saved = loadSavedQuery(userId) || {};
			
			    const inputTags = D.el('input', {
			      type: 'text',
			      id: 'e621x-edits-tags',
			      class: 'st-input e621x-specific-edits-input',
			      placeholder: '(use space for AND matches)',
			      value: saved.tags || '',
			      autocomplete: 'off'
			    });
			
			    const inputAdded = D.el('input', {
			      type: 'text',
			      id: 'e621x-edits-added',
			      class: 'st-input e621x-specific-edits-input',
			      placeholder: '(use ~ for OR matches)',
			      value: saved.added || '',
			      autocomplete: 'off'
			    });
			
			    const inputRemoved = D.el('input', {
			      type: 'text',
			      id: 'e621x-edits-removed',
			      class: 'st-input e621x-specific-edits-input',
			      placeholder: '(use - to exclude tags)',
			      value: saved.removed || '',
			      autocomplete: 'off'
			    });
			
			    function removeStatus() {
			      const existing = container.querySelector('.e621x-specific-edits-status');
			      if (existing) existing.remove();
			    }
			
			    function showStatus(children, isError = false) {
			      removeStatus();
			      const status = D.el('div', {
			        class: `e621x-specific-edits-status${isError ? ' is-error' : ''}`
			      }, children);
			      container.appendChild(status);
			      return status;
			    }
			
			    function getValues() {
			      return {
			        tags: inputTags.value.trim(),
			        added: inputAdded.value.trim(),
			        removed: inputRemoved.value.trim()
			      };
			    }
			
			    function handleNavigate() {
			      const q = getValues();
			      if (!q.tags && !q.added && !q.removed) {
			        showStatus('Enter at least one tag filter (in post, added, or removed).', true);
			        return;
			      }
			      saveQuery(userId, q);
			      const url = buildUrl(userId, q, false);
			      window.open(url, '_blank', 'noopener');
			    }
			
			    async function handleCount() {
			      if (isCounting) return;
			
			      const q = getValues();
			      if (!q.tags && !q.added && !q.removed) {
			        showStatus('Enter at least one tag filter to count edits.', true);
			        return;
			      }
			
			      saveQuery(userId, q);
			      isCounting = true;
			      btnCount.style.cursor = 'wait';
			      showStatus('Counting edits...');
			
			      try {
			        let total = 0;
			        let page = 1;
			        const PAGE_LIMIT = 320;
			        const MAX_PAGES = 100;
			
			        while (page <= MAX_PAGES && isCounting) {
			          const statusEl = container.querySelector('.e621x-specific-edits-status');
			          if (statusEl) {
			            statusEl.textContent = total > 0
			              ? `Counting edits: ${total}... (page ${page})`
			              : `Counting edits... (page ${page})`;
			          }
			
			          const url = `${buildUrl(userId, q, true)}&limit=${PAGE_LIMIT}&page=${page}`;
			          const data = await Net.fetchJson(url);
			          const list = Array.isArray(data) ? data : (data?.post_versions || []);
			          total += list.length;
			
			          if (list.length < PAGE_LIMIT) break;
			          page++;
			        }
			
			        if (!isCounting) return;
			
			        const viewUrl = buildUrl(userId, q, false);
			        const limitText = page > MAX_PAGES ? '+' : '';
			
			        showStatus([
			          document.createTextNode(`Found ${total}${limitText} edit(s) by User #${userId}. `),
			          D.el('a', {
			            href: viewUrl,
			            target: '_blank',
			            rel: 'noopener',
			            text: '[View Edits]'
			          })
			        ]);
			      } catch (err) {
			        if (isCounting) {
			          showStatus(`Error counting edits: ${err.message}`, true);
			        }
			      } finally {
			        isCounting = false;
			        btnCount.style.cursor = '';
			      }
			    }
			
			    function handleClear() {
			      isCounting = false;
			      btnCount.style.cursor = '';
			      inputTags.value = '';
			      inputAdded.value = '';
			      inputRemoved.value = '';
			      clearSavedQuery(userId);
			      removeStatus();
			    }
			
			    [inputTags, inputAdded, inputRemoved].forEach(input => {
			      input.addEventListener('keydown', (e) => {
			        if (e.key === 'Enter') {
			          e.preventDefault();
			          handleNavigate();
			        }
			      });
			    });
			
			    const btnView = D.button({
			      text: 'View',
			      title: 'Open post versions in a new tab',
			      className: 'e621x-specific-edits-btn',
			      onClick: handleNavigate
			    });
			
			    const btnCount = D.button({
			      text: 'Count',
			      title: 'Count edits matching these tags via API',
			      className: 'e621x-specific-edits-btn',
			      onClick: handleCount
			    });
			
			    const btnClear = D.button({
			      text: 'Clear',
			      title: 'Clear all fields and remove results',
			      className: 'e621x-specific-edits-btn',
			      onClick: handleClear
			    });
			
			    const closeBtn = D.el('button', {
			      type: 'button',
			      class: 'e621x-specific-edits-close',
			      text: '×',
			      title: 'Close',
			      onClick: () => {
			        isCounting = false;
			        btnCount.style.cursor = '';
			        container.classList.add('is-hidden');
			      }
			    });
			
			    const header = D.el('div', { class: 'e621x-specific-edits-header' }, [
			      D.el('span', { class: 'e621x-specific-edits-title', text: `Specific Post Edits (User #${userId})` }),
			      closeBtn
			    ]);
			
			    const fields = D.el('div', { class: 'e621x-specific-edits-fields' }, [
			      D.el('div', { class: 'e621x-specific-edits-field' }, [
			        D.el('label', { for: 'e621x-edits-tags', text: 'Tags in post' }),
			        inputTags
			      ]),
			      D.el('div', { class: 'e621x-specific-edits-field' }, [
			        D.el('label', { for: 'e621x-edits-added', text: 'Tags added' }),
			        inputAdded
			      ]),
			      D.el('div', { class: 'e621x-specific-edits-field' }, [
			        D.el('label', { for: 'e621x-edits-removed', text: 'Tags removed' }),
			        inputRemoved
			      ])
			    ]);
			
			    const actions = D.el('div', { class: 'e621x-specific-edits-actions' }, [
			      btnView,
			      btnCount,
			      btnClear
			    ]);
			
			    container.append(header, fields, actions);
			    return container;
			  }
			
			  function toggleForm(form) {
			    const isHidden = form.classList.toggle('is-hidden');
			    if (!isHidden) {
			      const statsTabBtn = document.querySelector('tabs-menu button[name="stats"]');
			      if (statsTabBtn && !statsTabBtn.classList.contains('active')) {
			        statsTabBtn.click();
			      }
			      form.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
			      form.querySelector('input')?.focus();
			    }
			  }
			
			  function inject() {
			    if (!isUserPage()) return;
			    seedCategoryCache();
			
			    const userId = getProfileUserId();
			    if (!userId) return;
			
			    if (document.getElementById(FORM_CONTAINER_ID)) return;
			
			    const form = createForm(userId);
			    const postVersionLink = document.querySelector('a[href*="/post_versions"]');
			    const changesLine = postVersionLink?.closest('.profile-line');
			
			    if (changesLine) {
			      const triggerForm = D.el('a', {
			        role: 'button',
			        class: 'e621x-specific-edits-trigger',
			        text: 'view specific post edits',
			        onClick: (e) => {
			          e.preventDefault();
			          toggleForm(form);
			        }
			      });
			
			      const triggerStats = D.el('a', {
			        role: 'button',
			        class: 'e621x-specific-edits-trigger',
			        text: 'view tag update statistics',
			        onClick: (e) => {
			          e.preventDefault();
			          openTagStatsModal(userId);
			        }
			      });
			
			      const sep = D.el('span', { class: 'e621x-trigger-sep', text: '·' });
			
			      const triggersRow = D.el('div', { class: 'e621x-specific-edits-triggers-row' }, [
			        triggerForm,
			        sep,
			        triggerStats
			      ]);
			
			      const wrapper = D.el('div', { class: 'e621x-specific-edits-wrapper' }, [
			        triggersRow,
			        form
			      ]);
			      changesLine.appendChild(wrapper);
			    }
			  }
			
			  return { init: inject, openTagStatsModal };
			});
			// END: scripts/user_edits.js
			
			// START: scripts/tree/data.js
			E621X.define('treeData', ['net'], (Net) => {
			  'use strict';
			  const IDS_PER_REQUEST = 100;
			  const PARENT_SEARCH_LIMIT = 320;
			
			  const unwrapPost = (data) => data.post || data;
			  const unwrapList = (data) => (Array.isArray(data) ? data : (data.posts || []));
			
			  const fetchPost = async (id) => unwrapPost(await Net.fetchJson(`/posts/${id}.json`));
			
			  async function fetchByIds(ids) {
			    const found = [];
			    for (let i = 0; i < ids.length; i += IDS_PER_REQUEST) {
			      const chunk = ids.slice(i, i + IDS_PER_REQUEST);
			      found.push(...unwrapList(await Net.fetchJson(`/posts.json?tags=id:${chunk.join(',')}&limit=${IDS_PER_REQUEST}`)));
			    }
			    return found;
			  }
			
			  const fetchByParent = async (parentId) =>
			    unwrapList(await Net.fetchJson(`/posts.json?tags=parent:${parentId}&limit=${PARENT_SEARCH_LIMIT}`));
			
			  /* Walks up to the root, then down breadth-first.
			   * Partial failures do not abort the tree; they are reported in `warnings`. */
			  async function fetchFullTree(startPostId) {
			    const warnings = [];
			
			    let root;
			    try {
			      root = await fetchPost(startPostId);
			    } catch (err) {
			      throw new Error(`Post #${startPostId} not found (${err.message})`);
			    }
			
			    const visitedUp = new Set([root.id]);
			    while (root.relationships?.parent_id) {
			      const parentId = root.relationships.parent_id;
			      if (visitedUp.has(parentId)) break;
			      visitedUp.add(parentId);
			      try {
			        root = await fetchPost(parentId);
			      } catch (err) {
			        warnings.push(`parent #${parentId}: ${err.message}`);
			        break;
			      }
			    }
			
			    const postsMap = new Map([[root.id, root]]);
			    const seen = new Set([root.id]);
			    const queue = [root];
			
			    while (queue.length) {
			      const current = queue.shift();
			      if (!current.relationships?.has_children) continue;
			
			      const declared = current.relationships.children || [];
			      const idsToFetch = declared.filter(id => !seen.has(id));
			      idsToFetch.forEach(id => seen.add(id));
			
			      let fetched = [];
			      try {
			        if (declared.length) {
			          fetched = idsToFetch.length ? await fetchByIds(idsToFetch) : [];
			        } else {
			          fetched = await fetchByParent(current.id);
			        }
			      } catch (err) {
			        warnings.push(`children of #${current.id}: ${err.message}`);
			      }
			
			      for (const post of fetched) {
			        if (postsMap.has(post.id)) continue;
			        seen.add(post.id);
			        postsMap.set(post.id, post);
			        queue.push(post);
			      }
			    }
			
			    return { rootId: root.id, postsMap, warnings };
			  }
			
			  function buildHierarchy(rootId, postsMap) {
			    const nodes = new Map();
			    for (const [id, post] of postsMap) nodes.set(id, { post, children: [] });
			
			    const root = nodes.get(rootId);
			    if (!root) return null;
			
			    for (const [id, node] of nodes) {
			      if (id === rootId) continue;
			      const parentId = node.post.relationships?.parent_id;
			      (parentId && nodes.has(parentId) ? nodes.get(parentId) : root).children.push(node);
			    }
			
			    const sortSubtree = (n) => {
			      n.children.sort((a, b) => a.post.id - b.post.id);
			      n.children.forEach(sortSubtree);
			    };
			    sortSubtree(root);
			    return root;
			  }
			
			  return { fetchFullTree, buildHierarchy, fetchByIds };
			
			});
			// END: scripts/tree/data.js
			
			// START: scripts/tree/export.js
			E621X.define('treeExport', ['utils', 'dom'], (U, D) => {
			  'use strict';
			  const { ACTIONS } = E621_CONSTANTS;
			  const esc = D.escapeHtml;
			
			  const IMAGE_CONCURRENCY = 6;
			  const PADDING = 40;
			  const PIN_OFFSET = 16;
			  const DESC_HEIGHT = 28;
			  const RATING_COLORS = { E: '#e45f5f', Q: '#e4a15f', S: '#5fe476' };
			
			  function exportTreeToJson(rootNode, postsMap, rootId) {
			    const serialize = (n) => ({
			      id: n.post.id,
			      rating: n.post.rating,
			      score: n.post.score?.total || 0,
			      fav_count: n.post.fav_count || 0,
			      comment_count: n.post.comment_count || 0,
			      tags: n.post.tags,
			      file: n.post.file,
			      preview: n.post.preview,
			      relationships: n.post.relationships,
			      children: n.children.map(serialize)
			    });
			    const data = {
			      rootId,
			      exportedAt: new Date().toISOString(),
			      totalPosts: postsMap.size,
			      tree: serialize(rootNode)
			    };
			    U.downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `e621_post_tree_${rootId}.json`);
			    U.showToast(`Exported JSON for Tree #${rootId}`);
			  }
			
			  /* Image bytes are fetched by the background worker: a content-script fetch is bound by the page's CORS. */
			  async function fetchImageAsDataUrl(url) {
			    try {
			      const res = await chrome.runtime.sendMessage({ action: ACTIONS.FETCH_IMAGE, url });
			      if (res?.ok) return res.dataUrl;
			      U.logWarn('image fetch', res?.error);
			    } catch (err) {
			      U.logWarn('image fetch', err);
			    }
			    return null;
			  }
			
			  async function embedImages(renderedCards) {
			    const images = new Map();
			    await U.mapLimit(renderedCards, IMAGE_CONCURRENCY, async ({ post, card }) => {
			      const url = card.dataset.previewUrl;
			      if (url) images.set(post.id, await fetchImageAsDataUrl(url));
			    });
			    return images;
			  }
			
			  const relativeRect = (element, containerRect) => {
			    const r = element.getBoundingClientRect();
			    return { x: r.left - containerRect.left, y: r.top - containerRect.top, w: r.width, h: r.height };
			  };
			
			  function renderConnection(from, to) {
			    const x1 = from.x + from.w / 2;
			    const y1 = from.y + from.h + PIN_OFFSET;
			    const x2 = to.x + to.w / 2;
			    const y2 = to.y - PIN_OFFSET;
			    const midY = (y1 + y2) / 2;
			    return `  <path class="tree-line" d="M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}" />\n`;
			  }
			
			  function renderCard({ post, gen, hasParent, hasChildren }, rect, imageHref, preserveAspect) {
			    const { x, y, w, h } = rect;
			    const imgH = h - DESC_HEIGHT;
			    const score = post.score?.total || 0;
			    const rating = (post.rating || 's').toUpperCase();
			    const ratingColor = RATING_COLORS[rating] || RATING_COLORS.S;
			    const postUrl = U.siteUrl(`/posts/${post.id}`);
			
			    let svg = `  <g id="card-${post.id}">\n`;
			    svg += `    <a href="${esc(postUrl)}" target="_blank">\n`;
			    svg += `      <rect class="tree-card-bg" x="${x}" y="${y}" width="${w}" height="${h}" rx="4" />\n`;
			    if (imageHref) {
			      svg += `      <image href="${esc(imageHref)}" x="${x}" y="${y}" width="${w}" height="${imgH}" preserveAspectRatio="${preserveAspect}" />\n`;
			    }
			    svg += `      <rect class="tree-desc-bg" x="${x}" y="${y + imgH}" width="${w}" height="${DESC_HEIGHT}" />\n`;
			    svg += `      <text class="tree-text" x="${x + 8}" y="${y + imgH + 18}" font-size="11">▲ ${score}  ★ ${post.fav_count || 0}</text>\n`;
			    svg += `      <rect x="${x + w - 22}" y="${y + imgH + 6}" width="16" height="16" rx="2" fill="${ratingColor}" />\n`;
			    svg += `      <text x="${x + w - 14}" y="${y + imgH + 18}" font-size="10" font-weight="bold" fill="#000" text-anchor="middle">${esc(rating)}</text>\n`;
			    svg += `    </a>\n`;
			    if (hasParent) {
			      svg += `    <circle class="tree-pin-bg" cx="${x + w / 2}" cy="${y - 7}" r="9" />\n`;
			      svg += `    <text class="tree-pin-text" x="${x + w / 2}" y="${y - 7}">${gen}</text>\n`;
			    }
			    if (hasChildren) {
			      svg += `    <circle class="tree-pin-bg" cx="${x + w / 2}" cy="${y + h + 7}" r="9" />\n`;
			      svg += `    <text class="tree-pin-text" x="${x + w / 2}" y="${y + h + 7}">${gen}</text>\n`;
			    }
			    return svg + `  </g>\n`;
			  }
			
			  const SVG_STYLE = `<style>
			      .tree-card-bg { fill: #152f56; stroke: #2a4c7e; stroke-width: 1; }
			      .tree-desc-bg { fill: #0a1b32; }
			      .tree-text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; fill: #ffffff; }
			      .tree-line { stroke: rgba(255, 255, 255, 0.4); stroke-width: 1.5; fill: none; }
			      .tree-pin-bg { fill: #152f56; stroke: #3b6aa0; stroke-width: 2; }
			      .tree-pin-text { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 10px; font-weight: bold; fill: #ffffff; text-anchor: middle; dominant-baseline: central; }
			    </style>\n`;
			
			  async function exportTreeToSvg({ treeContainer, renderedCards, lineConnections, rootId, isContain }) {
			    U.showToast('Embedding images into SVG...');
			    const images = await embedImages(renderedCards);
			
			    const containerRect = treeContainer.getBoundingClientRect();
			    const rects = new Map(renderedCards.map(c => [c.card, relativeRect(c.card, containerRect)]));
			
			    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
			    for (const r of rects.values()) {
			      minX = Math.min(minX, r.x);
			      minY = Math.min(minY, r.y - 18);
			      maxX = Math.max(maxX, r.x + r.w);
			      maxY = Math.max(maxY, r.y + r.h + 18);
			    }
			    const vbX = Math.floor(minX - PADDING);
			    const vbY = Math.floor(minY - PADDING);
			    const vbW = Math.ceil(maxX - minX + PADDING * 2);
			    const vbH = Math.ceil(maxY - minY + PADDING * 2);
			
			    let svg = `<?xml version="1.0" encoding="UTF-8"?>\n`;
			    svg += `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vbX} ${vbY} ${vbW} ${vbH}" width="${vbW}" height="${vbH}">\n`;
			    svg += SVG_STYLE;
			    svg += `<rect x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}" fill="#0a1322" />\n`;
			
			    svg += `<g id="connections">\n`;
			    for (const { fromEl, toEl } of lineConnections) {
			      svg += renderConnection(rects.get(fromEl) || relativeRect(fromEl, containerRect), rects.get(toEl) || relativeRect(toEl, containerRect));
			    }
			    svg += `</g>\n<g id="cards">\n`;
			
			    const preserveAspect = isContain ? 'xMidYMid meet' : 'xMidYMid slice';
			    for (const entry of renderedCards) {
			      const href = images.get(entry.post.id) || entry.card.dataset.previewUrl || '';
			      svg += renderCard(entry, rects.get(entry.card), href, preserveAspect);
			    }
			    svg += `</g>\n</svg>`;
			
			    U.downloadBlob(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }), `e621_post_tree_${rootId}.svg`);
			    U.showToast(`Exported SVG for Tree #${rootId}`);
			  }
			
			  return { exportTreeToJson, exportTreeToSvg };
			});
			// END: scripts/tree/export.js
			
			// START: scripts/tree/modal.js
			E621X.define('tree',
			  ['utils', 'selectors', 'dom', 'layout', 'thumbnail', 'treeData', 'treeExport'],
			  (U, SEL, D, Layout, Thumb, TreeData, TreeExport) => {
			  'use strict';
			
			  const SVG_NS = 'http://www.w3.org/2000/svg';
			  const FIRST_DRAW_DELAY_MS = 60;
			  let closeActiveModal = null;
			
			  // ---------- building blocks ----------
			  function createModalShell(targetPostId) {
			    const titleSpan = D.el('span', { class: 'e621x-tree-title', text: `Post Tree (#${targetPostId})` });
			    const btnExportJson = D.button({ text: 'Export JSON', icon: 'download' });
			    const btnExportSvg = D.button({ text: 'Export SVG', icon: 'download' });
			    const closeBtn = D.el('button', {
			      type: 'button', class: 'e621x-tree-close st-button kinetic', text: '×', 'aria-label': 'Close'
			    });
			    const contentArea = D.el('div', { class: 'e621x-tree-content' }, [
			      D.el('div', { class: 'e621x-tree-message', text: 'Loading full dependency tree...' })
			    ]);
			    const dialog = D.el('div', { class: 'e621x-tree-dialog', role: 'dialog', 'aria-modal': 'true' }, [
			      D.el('div', { class: 'e621x-tree-titlebar' }, [
			        titleSpan,
			        D.el('div', { class: 'e621x-tree-toolbar' }, [btnExportJson, btnExportSvg, closeBtn])
			      ]),
			      contentArea
			    ]);
			    const overlay = D.el('div', { class: 'e621x-tree-overlay' });
			    return { overlay, dialog, titleSpan, btnExportJson, btnExportSvg, closeBtn, contentArea };
			  }
			
			  /* Breadth-first: tiers[i] holds generation i+1 as { node, parentNode }. */
			  function buildTiers(rootNode) {
			    const tiers = [];
			    const queue = [{ node: rootNode, parentNode: null, gen: 1 }];
			    while (queue.length) {
			      const { node, parentNode, gen } = queue.shift();
			      (tiers[gen - 1] ||= []).push({ node, parentNode });
			      node.children.forEach(child => queue.push({ node: child, parentNode: node, gen: gen + 1 }));
			    }
			    return tiers;
			  }
			
			  const createDivider = (gen, count) => D.el('div', { class: 'e621x-tree-divider' }, [
			    D.el('div', { class: 'e621x-tree-divider-line' }),
			    D.el('span', { class: 'e621x-tree-divider-label', text: `Generation ${gen} (${count})` }),
			    D.el('div', { class: 'e621x-tree-divider-line' })
			  ]);
			
			  function bindLineHighlight(card, postId, svgCanvas) {
			    const id = String(postId);
			    card.addEventListener('mouseenter', () => {
			      svgCanvas.querySelectorAll('path').forEach(p => {
			        const related = p.dataset.from === id || p.dataset.to === id;
			        p.classList.toggle('is-active', related);
			        p.classList.toggle('is-dim', !related);
			      });
			    });
			    card.addEventListener('mouseleave', () => {
			      svgCanvas.querySelectorAll('path').forEach(p => p.classList.remove('is-active', 'is-dim'));
			    });
			  }
			
			  function createLineDrawer(treeContainer, svgCanvas, lineConnections) {
			    return function drawLines() {
			      svgCanvas.replaceChildren();
			      const containerRect = treeContainer.getBoundingClientRect();
			      const width = Math.max(treeContainer.scrollWidth, treeContainer.offsetWidth);
			      const height = Math.max(treeContainer.scrollHeight, treeContainer.offsetHeight);
			      svgCanvas.setAttribute('width', width);
			      svgCanvas.setAttribute('height', height);
			
			      for (const { fromEl, toEl, fromId, toId } of lineConnections) {
			        const f = fromEl.getBoundingClientRect();
			        const t = toEl.getBoundingClientRect();
			        const x1 = f.left + f.width / 2 - containerRect.left;
			        const y1 = f.bottom - containerRect.top + 16;
			        const x2 = t.left + t.width / 2 - containerRect.left;
			        const y2 = t.top - containerRect.top - 16;
			        const midY = (y1 + y2) / 2;
			
			        const path = document.createElementNS(SVG_NS, 'path');
			        path.setAttribute('class', 'e621x-tree-line');
			        path.setAttribute('d', `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`);
			        path.dataset.from = String(fromId);
			        path.dataset.to = String(toId);
			        svgCanvas.appendChild(path);
			      }
			    };
			  }
			
			  function renderTree(shell, tree, disposer, getLayout) {
			    const { rootId, postsMap, warnings } = tree;
			    const rootNode = TreeData.buildHierarchy(rootId, postsMap);
			    if (!rootNode) throw new Error('Root post is missing');
			
			    const incomplete = warnings.length ? `, incomplete: ${warnings.length} request(s) failed` : '';
			    shell.titleSpan.textContent = `Post Tree: Root #${rootId} (${postsMap.size} posts in tree${incomplete})`;
			    if (warnings.length) U.logWarn('tree', warnings.join('; '));
			
			    const treeContainer = D.el('div', { class: 'e621x-tree' });
			    const svgCanvas = document.createElementNS(SVG_NS, 'svg');
			    svgCanvas.setAttribute('class', 'e621x-tree-lines');
			    treeContainer.appendChild(svgCanvas);
			
			    const cardMap = new Map();
			    const renderedCards = [];
			    const lineConnections = [];
			    const tiers = buildTiers(rootNode);
			
			    tiers.forEach((tierItems, index) => {
			      const gen = index + 1;
			      const cardsRow = D.el('div', { class: 'e621x-tree-cards' });
			
			      for (const { node, parentNode } of tierItems) {
			        const hasParent = Boolean(parentNode);
			        const hasChildren = node.children.length > 0;
			        const card = Thumb.createThumbnail(node.post, { layout: getLayout(), gen, hasParent, hasChildren, enablePins: true });
			        bindLineHighlight(card, node.post.id, svgCanvas);
			        cardsRow.appendChild(card);
			        cardMap.set(node.post.id, card);
			        renderedCards.push({ card, post: node.post, gen, hasParent, hasChildren });
			      }
			
			      treeContainer.appendChild(
			        D.el('div', { class: 'e621x-tree-tier', 'data-gen': gen }, [createDivider(gen, tierItems.length), cardsRow])
			      );
			    });
			
			    for (const tierItems of tiers) {
			      for (const { node, parentNode } of tierItems) {
			        if (!parentNode) continue;
			        const fromEl = cardMap.get(parentNode.post.id);
			        const toEl = cardMap.get(node.post.id);
			        if (fromEl && toEl) lineConnections.push({ fromEl, toEl, fromId: parentNode.post.id, toId: node.post.id });
			      }
			    }
			
			    treeContainer.appendChild(D.el('div', { class: 'e621x-tree-spacer' }));
			    shell.contentArea.replaceChildren(treeContainer);
			
			    const drawLines = createLineDrawer(treeContainer, svgCanvas, lineConnections);
			
			    shell.btnExportJson.onclick = () => TreeExport.exportTreeToJson(rootNode, postsMap, rootId);
			    shell.btnExportSvg.onclick = async () => {
			      shell.btnExportSvg.disabled = true;
			      try {
			        await TreeExport.exportTreeToSvg({
			          treeContainer, renderedCards, lineConnections, rootId, isContain: getLayout().isContain
			        });
			      } catch (err) {
			        U.showToast(`SVG export failed: ${err.message}`, true);
			      } finally {
			        shell.btnExportSvg.disabled = false;
			      }
			    };
			
			    // everything below is released by the disposer when the modal closes
			    const resizeObserver = new ResizeObserver(drawLines);
			    resizeObserver.observe(treeContainer);
			    disposer.add(() => resizeObserver.disconnect());
			
			    window.addEventListener('resize', drawLines);
			    disposer.add(() => window.removeEventListener('resize', drawLines));
			
			    const timer = setTimeout(drawLines, FIRST_DRAW_DELAY_MS);
			    disposer.add(() => clearTimeout(timer));
			
			    return drawLines;
			  }
			
			  // ---------- public: open modal ----------
			  async function openPostTreeModal(targetPostId) {
			    closeActiveModal?.();
			
			    const disposer = U.createDisposer();
			    const shell = createModalShell(targetPostId);
			    let layout = Layout.getLayoutSettings();
			    Layout.applyLayoutVars(shell.dialog, layout);
			    document.body.append(shell.overlay, shell.dialog);
			
			    const close = () => disposer.run();
			    closeActiveModal = close;
			    disposer.add(() => { if (closeActiveModal === close) closeActiveModal = null; });
			    disposer.add(() => { shell.overlay.remove(); shell.dialog.remove(); });
			
			    shell.closeBtn.addEventListener('click', close);
			    shell.overlay.addEventListener('click', close);
			
			    const onKeydown = (e) => { if (e.key === 'Escape') close(); };
			    document.addEventListener('keydown', onKeydown);
			    disposer.add(() => document.removeEventListener('keydown', onKeydown));
			
			    try {
			      const tree = await TreeData.fetchFullTree(targetPostId);
			      if (disposer.isDisposed()) return; // closed while loading
			      const drawLines = renderTree(shell, tree, disposer, () => layout);
			
			      disposer.add(Layout.onLayoutSettingsChange((next) => {
			        layout = next;
			        Layout.applyLayoutVars(shell.dialog, next);
			        drawLines();
			      }));
			    } catch (err) {
			      if (disposer.isDisposed()) return;
			      U.logWarn('tree', err);
			      shell.contentArea.replaceChildren(
			        D.el('div', { class: 'e621x-tree-message e621x-tree-message--error', text: `Error loading tree: ${err.message}` })
			      );
			    }
			  }
			
			  // ---------- "show post tree" button next to parent/child notices ----------
			  function setupPageNoticeTreeButton() {
			    document.querySelector(SEL.parentChildren)?.classList.add('e621x-notice-row');
			
			    const notices = document.querySelectorAll(SEL.relationNotices);
			    if (!notices.length) return;
			    notices.forEach(n => n.classList.add('e621x-notice-inline'));
			
			    if (document.getElementById('btn-show-post-tree')) return;
			    const currentPostId = U.getPostIdFromPage();
			    if (!currentPostId) return;
			
			    const treeBtn = D.button({
			      text: 'show post tree',
			      className: 'e621x-tree-btn',
			      onClick: (e) => { e.preventDefault(); openPostTreeModal(currentPostId); }
			    });
			    treeBtn.id = 'btn-show-post-tree';
			    notices[notices.length - 1].appendChild(treeBtn);
			  }
			
			  return { init: setupPageNoticeTreeButton, open: openPostTreeModal };
			});
			// END: scripts/tree/modal.js
			
			// START: scripts/hover_zoom/data.js
			/* Data extraction and formatting helpers for Hover Zoom. */
			E621X.define('hoverZoomData', [], () => {
			  'use strict';
			
			  const PLACEHOLDER_IMG = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
			
			  const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
			
			  function parsePostFromElement(el) {
			    if (!el) return null;
			
			    // Skip main post image/video on individual post pages
			    if (el.classList?.contains('fit-window') || el.closest('.fit-window, #image-container')) {
			      return null;
			    }
			
			    const thumb = el.closest('article.thumbnail, .post-preview, div.post-thumbnail, [data-id]');
			    if (!thumb || thumb.id === 'image-container' || thumb.closest('#image-container, .fit-window')) {
			      return null;
			    }
			
			    const d = thumb.dataset || {};
			    const id = Number(d.id || thumb.getAttribute('data-id'));
			    if (!id) return null;
			
			    const fileUrl = d.fileUrl || '';
			    const sampleUrl = d.sampleUrl || d.largeFileUrl || fileUrl;
			    const previewUrl = d.previewUrl || d.previewFileUrl || thumb.querySelector('img')?.src || '';
			
			    // Extract file extension from dataset or URL
			    let ext = (d.fileExt || '').toLowerCase();
			    if (!ext && sampleUrl) {
			      const match = sampleUrl.match(/\.([a-z0-9]+)(?:\?|$)/i);
			      if (match) ext = match[1].toLowerCase();
			    }
			
			    return {
			      id,
			      ext,
			      isVideo: ext === 'webm' || ext === 'mp4',
			      isDeleted: d.flags?.includes('deleted') || thumb.classList.contains('post-status-deleted'),
			      sampleUrl,
			      previewUrl,
			      fileUrl,
			      element: thumb
			    };
			  }
			
			  return {
			    PLACEHOLDER_IMG,
			    clamp,
			    parsePostFromElement
			  };
			});
			// END: scripts/hover_zoom/data.js
			
			// START: scripts/hover_zoom/index.js
			E621X.define('hoverZoom', ['utils', 'selectors', 'dom', 'hoverZoomData'], (U, SEL, D, HZData) => {
			  'use strict';
			  const { STORAGE_KEYS: KEYS } = E621_CONSTANTS;
			  const { clamp, parsePostFromElement, PLACEHOLDER_IMG } = HZData;
			
			  const VIEWPORT_MARGIN = 12;
			  const CURSOR_OFFSET_X = 24;
			
			  let enabled = true;
			  let loadSample = true; // default true: load /data/sample/*
			  let targetFps = 0; // 0 = native monitor refresh rate via requestAnimationFrame
			  let dom = null;
			  let activePost = null;
			  let shiftPressed = false;
			  let lastClientX = 0;
			  let lastClientY = 0;
			  let scheduled = false;
			  let lastTime = 0;
			  let stashedTitles = [];
			
			  // ---------- Exact Dimension Calculation ----------
			  function updateMediaDimensions(mediaEl) {
			    if (!activePost || !dom) return;
			
			    const naturalW = mediaEl.naturalWidth || mediaEl.videoWidth || 0;
			    const naturalH = mediaEl.naturalHeight || mediaEl.videoHeight || 0;
			    if (!naturalW || !naturalH) return;
			
			    const maxW = Math.floor(window.innerWidth * 0.55);
			    const maxH = Math.floor(window.innerHeight * 0.88);
			
			    let w = naturalW;
			    let h = naturalH;
			
			    // Scale down to fit inside max bounds while preserving exact aspect ratio
			    if (w > maxW) {
			      h = Math.round(h * (maxW / w));
			      w = maxW;
			    }
			    if (h > maxH) {
			      w = Math.round(w * (maxH / h));
			      h = maxH;
			    }
			
			    mediaEl.style.width = `${w}px`;
			    mediaEl.style.height = `${h}px`;
			    dom.container.style.width = `${w}px`;
			    dom.container.style.height = `${h}px`;
			
			    dom.container.classList.remove('is-hidden');
			    alignWindow(lastClientX, lastClientY);
			
			    window.dispatchEvent(new CustomEvent('e621x-zoom-change', { detail: { active: true } }));
			  }
			
			  // ---------- DOM Setup ----------
			  function createZoomContainer() {
			    if (document.getElementById('e621x-zoom-container')) return;
			
			    const container = D.el('div', {
			      id: 'e621x-zoom-container',
			      class: 'e621x-zoom is-hidden'
			    });
			
			    const img = D.el('img', {
			      id: 'e621x-zoom-img',
			      class: 'e621x-zoom-media is-hidden',
			      src: PLACEHOLDER_IMG,
			      alt: ''
			    });
			
			    const video = D.el('video', {
			      id: 'e621x-zoom-video',
			      class: 'e621x-zoom-media is-hidden',
			      loop: true,
			      playsinline: true,
			      controls: false,
			      autoplay: true,
			      muted: true
			    });
			
			    img.addEventListener('load', () => {
			      if (img.src !== PLACEHOLDER_IMG) updateMediaDimensions(img);
			    });
			
			    // Fallback: if sample 404s, seamlessly load full fileUrl
			    img.addEventListener('error', () => {
			      if (img.src && !img.src.startsWith('data:') && activePost?.fileUrl && img.src !== activePost.fileUrl) {
			        img.src = activePost.fileUrl;
			      }
			    });
			
			    video.addEventListener('loadedmetadata', () => {
			      updateMediaDimensions(video);
			    });
			
			    video.addEventListener('error', () => {
			      if (video.src && activePost?.fileUrl && video.src !== activePost.fileUrl) {
			        video.src = activePost.fileUrl;
			        video.play().catch(() => {});
			      }
			    });
			
			    container.append(img, video);
			    document.body.appendChild(container);
			
			    dom = { container, img, video };
			  }
			
			  // ---------- Tooltip Suppressor ----------
			  function suppressTooltips(element) {
			    restoreTooltips();
			    if (!element) return;
			
			    const targets = element.hasAttribute('title') ? [element] : [];
			    targets.push(...element.querySelectorAll('[title]'));
			
			    for (const el of targets) {
			      const title = el.getAttribute('title');
			      if (title) {
			        stashedTitles.push({ el, title });
			        el.removeAttribute('title');
			      }
			    }
			  }
			
			  function restoreTooltips() {
			    while (stashedTitles.length) {
			      const { el, title } = stashedTitles.pop();
			      try { el.setAttribute('title', title); } catch (_) {
			        // Ignored
			      }
			    }
			  }
			
			  // ---------- Positioning ----------
			  function alignWindow(clientX, clientY) {
			    if (!dom || dom.container.classList.contains('is-hidden')) return;
			
			    const vw = window.innerWidth;
			    const vh = window.innerHeight;
			    const rect = dom.container.getBoundingClientRect();
			    const boxW = rect.width;
			    const boxH = rect.height;
			    if (!boxW || !boxH) return;
			
			    let left;
			    if (clientX < vw / 2) {
			      left = clientX + CURSOR_OFFSET_X;
			      if (left + boxW > vw - VIEWPORT_MARGIN) left = vw - boxW - VIEWPORT_MARGIN;
			    } else {
			      left = clientX - boxW - CURSOR_OFFSET_X;
			      if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN;
			    }
			
			    const top = clamp(clientY - (boxH / 2), VIEWPORT_MARGIN, vh - boxH - VIEWPORT_MARGIN);
			
			    dom.container.style.left = `${Math.round(left)}px`;
			    dom.container.style.top = `${Math.round(top)}px`;
			  }
			
			  function requestAlignWindow() {
			    if (scheduled) return;
			    scheduled = true;
			
			    const now = performance.now();
			    const minInterval = targetFps > 0 ? (1000 / targetFps) : 0;
			    const wait = Math.max(0, minInterval - (now - lastTime));
			
			    const execute = () => {
			      requestAnimationFrame(() => {
			        scheduled = false;
			        lastTime = performance.now();
			        alignWindow(lastClientX, lastClientY);
			      });
			    };
			
			    if (wait > 0) {
			      setTimeout(execute, wait);
			    } else {
			      execute();
			    }
			  }
			
			  // ---------- Zoom Lifecycle ----------
			  function stopZoom() {
			    activePost = null;
			    restoreTooltips();
			
			    if (!dom) return;
			    const wasVisible = !dom.container.classList.contains('is-hidden');
			    dom.container.classList.add('is-hidden');
			    dom.container.style.width = '';
			    dom.container.style.height = '';
			
			    dom.img.classList.add('is-hidden');
			    dom.img.style.width = '';
			    dom.img.style.height = '';
			    dom.img.src = PLACEHOLDER_IMG;
			
			    dom.video.classList.add('is-hidden');
			    dom.video.style.width = '';
			    dom.video.style.height = '';
			    dom.video.pause();
			    dom.video.removeAttribute('src');
			    dom.video.load();
			
			    if (wasVisible) {
			      window.dispatchEvent(new CustomEvent('e621x-zoom-change', { detail: { active: false } }));
			    }
			  }
			
			  function startZoom(post) {
			    if (!enabled || !shiftPressed || !post || post.ext === 'swf' || post.isDeleted) return;
			
			    activePost = post;
			    suppressTooltips(post.element);
			
			    const targetMediaUrl = loadSample
			      ? (post.sampleUrl || post.fileUrl)
			      : (post.fileUrl || post.sampleUrl);
			
			    if (post.isVideo) {
			      dom.img.classList.add('is-hidden');
			      dom.video.classList.remove('is-hidden');
			      dom.video.src = targetMediaUrl;
			      dom.video.play().catch(() => {});
			    } else {
			      dom.video.classList.add('is-hidden');
			      dom.img.classList.remove('is-hidden');
			
			      const targetUrl = targetMediaUrl || post.previewUrl;
			      dom.img.src = targetUrl;
			
			      if (dom.img.complete && dom.img.naturalWidth > 0) {
			        updateMediaDimensions(dom.img);
			      }
			    }
			  }
			
			  // ---------- Event Listeners ----------
			  function setupEvents() {
			    document.addEventListener('mousemove', (e) => {
			      lastClientX = e.clientX;
			      lastClientY = e.clientY;
			
			      requestAlignWindow();
			    }, { passive: true });
			
			    document.addEventListener('mouseover', (e) => {
			      if (!enabled || !shiftPressed) return;
			      const thumb = e.target.closest(SEL.thumbnail) || e.target.closest('.post-preview, div.post-thumbnail');
			      if (!thumb) return;
			
			      const post = parsePostFromElement(thumb);
			      if (post && activePost?.id !== post.id) {
			        startZoom(post);
			      }
			    });
			
			    document.addEventListener('mouseout', (e) => {
			      if (!activePost) return;
			      const thumb = e.target.closest(SEL.thumbnail) || e.target.closest('.post-preview, div.post-thumbnail');
			      if (!thumb) return;
			
			      const related = e.relatedTarget ? e.relatedTarget.closest(SEL.thumbnail) || e.relatedTarget.closest('.post-preview') : null;
			      if (related === thumb) return;
			
			      stopZoom();
			    });
			
			    window.addEventListener('keydown', (e) => {
			      if (e.key !== 'Shift' || shiftPressed) return;
			      shiftPressed = true;
			
			      if (enabled && !activePost) {
			        const hovered = document.elementFromPoint(lastClientX, lastClientY);
			        const post = parsePostFromElement(hovered);
			        if (post) startZoom(post);
			      }
			    });
			
			    window.addEventListener('keyup', (e) => {
			      if (e.key !== 'Shift') return;
			      shiftPressed = false;
			      stopZoom();
			    });
			
			    window.addEventListener('blur', () => {
			      shiftPressed = false;
			      stopZoom();
			    });
			
			    window.addEventListener('scroll', () => {
			      if (activePost) stopZoom();
			    }, { passive: true });
			  }
			
			  async function init() {
			    createZoomContainer();
			    const data = await chrome.storage.local.get([KEYS.HZ_ENABLED, KEYS.HZ_LOAD_SAMPLE, KEYS.TRACKING_FPS]);
			    enabled = data[KEYS.HZ_ENABLED] !== false;
			    loadSample = data[KEYS.HZ_LOAD_SAMPLE] !== false;
			    targetFps = Number(data[KEYS.TRACKING_FPS]) || 0;
			    setupEvents();
			
			    chrome.storage.onChanged.addListener((changes, area) => {
			      if (area === 'local') {
			        if (changes[KEYS.HZ_ENABLED]) {
			          enabled = changes[KEYS.HZ_ENABLED].newValue !== false;
			          if (!enabled) stopZoom();
			        }
			        if (changes[KEYS.HZ_LOAD_SAMPLE]) {
			          loadSample = changes[KEYS.HZ_LOAD_SAMPLE].newValue !== false;
			        }
			        if (changes[KEYS.TRACKING_FPS] !== undefined) {
			          targetFps = Number(changes[KEYS.TRACKING_FPS].newValue) || 0;
			        }
			      }
			    });
			  }
			
			  return { init };
			});
			// END: scripts/hover_zoom/index.js
			
			// START: scripts/tag_script/engine.js
			/* Tag script engine: pure functions, no DOM. Loaded as a content script and by Node tests. */
			(function (root, factory) {
			  const api = factory();
			  if (typeof module !== 'undefined' && module.exports) module.exports = api;
			  else root.E621X.define('tagScriptEngine', [], () => api);
			})(globalThis, function () {
			  'use strict';
			
			  // ---------- parsing ----------
			  function parseIfCommand(cmdStr) {
			    if (!cmdStr.startsWith('[') || !cmdStr.endsWith(']')) return null;
			    const inner = cmdStr.slice(1, -1).trim();
			    if (!inner.startsWith('if')) return null;
			
			    const afterIf = inner.slice(2).trim();
			    const commaIdx = afterIf.indexOf(',');
			    if (commaIdx === -1) return null;
			
			    return {
			      predicate: afterIf.slice(0, commaIdx).trim(),
			      action: afterIf.slice(commaIdx + 1).trim()
			    };
			  }
			
			  // ---------- predicates ----------
			  function evaluateSimplePredicate(predicate, post) {
			    return (predicate.match(/\S+/g) || []).every(x =>
			      x.startsWith('-') ? !post.tagsSet.has(x.slice(1)) : post.tagsSet.has(x));
			  }
			
			  // ---------- execution ----------
			  /* Returns the tag diff string ('' if nothing changes). */
			  function runTagScript(post, scriptContent) {
			    const tokens = scriptContent.match(/\[.+?\]|\S+/g) || [];
			    let isReset = false;
			    const diffs = [];
			
			    for (const cmd of tokens) {
			      if (cmd === '[reset]') {
			        isReset = true;
			        diffs.length = 0;
			        continue;
			      }
			
			      if (cmd.startsWith('[if')) {
			        const parsed = parseIfCommand(cmd);
			        if (parsed && evaluateSimplePredicate(parsed.predicate, post)) {
			          diffs.push(parsed.action);
			        }
			        continue;
			      }
			
			      diffs.push(cmd);
			    }
			
			    if (isReset) {
			      return [...Array.from(post.tagsSet, t => `-${t}`), ...diffs].join(' ');
			    }
			    return diffs.join(' ');
			  }
			
			  /* Normalises an API post (/posts/:id.json) into the shape the predicates expect. */
			  function postDataFromApi(post) {
			    const tagString = typeof post.tag_string === 'string'
			      ? post.tag_string
			      : Object.values(post.tags || {}).flat().join(' ');
			
			    return {
			      id: Number(post.id),
			      tagsSet: new Set(tagString.split(/\s+/).filter(Boolean))
			    };
			  }
			
			  return { runTagScript, postDataFromApi };
			});
			// END: scripts/tag_script/engine.js
			
			// START: scripts/tag_script/queue.js
			/* Tag script queue client: delegates mutations and processing to background.js
			 * and uses IndexedDB for image storage to avoid storage quota issues. */
			E621X.define('tagScriptQueue', ['utils', 'net'], (U, Net) => {
			  'use strict';
			  const { STORAGE_KEYS: KEYS, ACTIONS } = E621_CONSTANTS;
			  const HOURLY_EDIT_LIMIT = 150;
			  const ONE_HOUR_MS = 60 * 60 * 1000;
			
			  async function isEnabled() {
			    return Boolean(await U.storageGet(KEYS.QUEUE_TAG_SCRIPTS_ENABLED, false));
			  }
			
			  async function setEnabled(val) {
			    await U.storageSet(KEYS.QUEUE_TAG_SCRIPTS_ENABLED, Boolean(val));
			  }
			
			  async function getTimeoutUntil() {
			    return Number(await U.storageGet(KEYS.QUEUE_TIMEOUT_UNTIL, 0)) || 0;
			  }
			
			  async function isTimeoutActive() {
			    if (!(await isEnabled())) return false;
			    const until = await getTimeoutUntil();
			    return Date.now() < until;
			  }
			
			  async function getRemainingTimeoutMs() {
			    const until = await getTimeoutUntil();
			    return Math.max(0, until - Date.now());
			  }
			
			  async function getQueue() {
			    return (await U.storageGet(KEYS.TAG_SCRIPT_QUEUE, {})) || {};
			  }
			
			  async function getQueueCount() {
			    const q = await getQueue();
			    return Object.keys(q).length;
			  }
			
			  async function getSlotSchedule() {
			    return (await U.storageGet(KEYS.QUEUE_SLOT_TIMESTAMPS, [])) || [];
			  }
			
			  // ---------- IndexedDB image operations via background ----------
			  async function saveImage(postId, dataUrl) {
			    if (!dataUrl) return;
			    try {
			      await chrome.runtime.sendMessage({ action: ACTIONS.SAVE_IMAGE, postId, dataUrl });
			    } catch (err) {
			      U.logWarn('saveImage to IndexedDB', err);
			    }
			  }
			
			  async function getImage(postId) {
			    try {
			      const res = await chrome.runtime.sendMessage({ action: ACTIONS.GET_IMAGE, postId });
			      return res?.dataUrl || null;
			    } catch (err) {
			      U.logWarn('getImage from IndexedDB', err);
			      return null;
			    }
			  }
			
			  async function deleteImage(postId) {
			    try {
			      await chrome.runtime.sendMessage({ action: ACTIONS.DELETE_IMAGE, postId });
			    } catch (err) {
			      U.logWarn('deleteImage from IndexedDB', err);
			    }
			  }
			
			  async function clearImages() {
			    try {
			      await chrome.runtime.sendMessage({ action: ACTIONS.CLEAR_IMAGES });
			    } catch (err) {
			      U.logWarn('clearImages from IndexedDB', err);
			    }
			  }
			
			  /* Fetches image as Base64 via service worker (avoids CORS) */
			  async function fetchImageAsBase64(url) {
			    if (!url || typeof url !== 'string' || url.startsWith('data:')) return url || null;
			    try {
			      const res = await chrome.runtime.sendMessage({ action: ACTIONS.FETCH_IMAGE, url });
			      if (res?.ok && res.dataUrl) return res.dataUrl;
			    } catch (_) {}
			    return null;
			  }
			
			  /* Fast extraction of Base64 from loaded DOM img tag, falls back to fetch */
			  async function getBase64FromThumb(thumb, previewUrl) {
			    const img = thumb?.querySelector?.('img');
			    if (img && img.complete && img.naturalWidth > 0) {
			      try {
			        const canvas = document.createElement('canvas');
			        canvas.width = img.naturalWidth;
			        canvas.height = img.naturalHeight;
			        const ctx = canvas.getContext('2d');
			        ctx.drawImage(img, 0, 0);
			        const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
			        if (dataUrl && dataUrl.startsWith('data:image/')) return dataUrl;
			      } catch (_) {
			        // Canvas tainted by CORS, fallback to background service worker
			      }
			    }
			
			    const targetUrl = previewUrl || thumb?.dataset?.previewUrl || img?.src;
			    if (targetUrl) {
			      const b64 = await fetchImageAsBase64(targetUrl);
			      if (b64) return b64;
			    }
			    return null;
			  }
			
			  function extractThumbData(thumb) {
			    if (!thumb) return {};
			    const d = thumb.dataset || {};
			    const img = thumb.querySelector('img');
			    const previewUrl = d.previewUrl || img?.src || '';
			
			    return {
			      previewUrl,
			      score: Number(d.score || 0),
			      favCount: Number(d.favCount || 0),
			      commentCount: Number(d.commentCount || 0),
			      rating: String(d.rating || 's').toLowerCase(),
			      fileExt: String(d.fileExt || 'jpg').toLowerCase(),
			      tags: d.tags || '',
			      flags: d.flags || '',
			      hasParent: thumb.querySelector('.ribbon.left') !== null || d.hasParent === 'true',
			      hasChildren: thumb.querySelector('.ribbon.right') !== null || d.hasChildren === 'true'
			    };
			  }
			
			  async function enqueue(postId, editData) {
			    const id = Number(postId);
			    const thumbnailDataUrl = editData.thumbnailDataUrl;
			    if (thumbnailDataUrl) {
			      await saveImage(id, thumbnailDataUrl);
			    }
			
			    const sanitized = { ...editData };
			    delete sanitized.thumbnailDataUrl;
			
			    const res = await chrome.runtime.sendMessage({
			      action: ACTIONS.ENQUEUE_TAG_SCRIPT,
			      postId: id,
			      editData: sanitized
			    });
			
			    return res?.queue || (await getQueue());
			  }
			
			  async function enqueueFromThumb(thumb, editData) {
			    const postId = Number(thumb.dataset.id);
			    const meta = extractThumbData(thumb);
			    const b64 = await getBase64FromThumb(thumb, meta.previewUrl);
			
			    return enqueue(postId, {
			      ...meta,
			      ...editData,
			      thumbnailDataUrl: b64 || meta.previewUrl
			    });
			  }
			
			  async function dequeue(postId) {
			    const id = Number(postId);
			    await deleteImage(id);
			    const res = await chrome.runtime.sendMessage({
			      action: ACTIONS.DEQUEUE_TAG_SCRIPT,
			      postId: id
			    });
			    return res?.queue || (await getQueue());
			  }
			
			  async function clearQueue() {
			    await clearImages();
			    await chrome.runtime.sendMessage({ action: ACTIONS.CLEAR_TAG_QUEUE });
			  }
			
			  async function processQueue() {
			    try {
			      const res = await chrome.runtime.sendMessage({ action: ACTIONS.PROCESS_TAG_QUEUE });
			      if (res?.ok) {
			        return res;
			      }
			      return { total: 0, updated: 0, errors: 1, error: res?.error };
			    } catch (err) {
			      U.logWarn('processQueue request failed', err);
			      return { total: 0, updated: 0, errors: 1, error: err.message };
			    }
			  }
			
			  async function checkPostLockStatus(postId, changes = '') {
			    const thumb = document.querySelector(`article.thumbnail[data-id="${postId}"]`);
			    if (thumb) {
			      const flags = (thumb.dataset.flags || '').split(/\s+/);
			      if (flags.includes('deleted') || thumb.classList.contains('post-status-deleted')) {
			        return { isLocked: true, reason: 'Post is deleted' };
			      }
			      if (flags.includes('status_locked') || flags.includes('statuslocked')) {
			        return { isLocked: true, reason: 'Post is status locked' };
			      }
			      if (flags.includes('rating_locked') && /(?:^|\s)[-+]?rating:[sqe]/i.test(changes)) {
			        return { isLocked: true, reason: `Rating is locked on post #${postId}` };
			      }
			    }
			
			    try {
			      const data = await Net.fetchJson(`/posts/${postId}.json`);
			      const post = data?.post || data;
			      if (!post) return { isLocked: false, reason: '' };
			
			      const flags = post.flags || {};
			      if (flags.status_locked || post.is_status_locked) {
			        return { isLocked: true, reason: 'Post is status locked' };
			      }
			      if (flags.deleted || post.is_deleted) {
			        return { isLocked: true, reason: 'Post is deleted' };
			      }
			
			      let lockedTags = [];
			      if (Array.isArray(post.locked_tags)) {
			        lockedTags = post.locked_tags;
			      } else if (typeof post.locked_tags === 'string') {
			        lockedTags = post.locked_tags.split(/\s+/).filter(Boolean);
			      }
			
			      if (lockedTags.length > 0 && changes) {
			        const lockedSet = new Set(lockedTags.map(t => String(t).trim().toLowerCase()));
			        const tokens = String(changes).split(/\s+/).filter(Boolean);
			        for (const token of tokens) {
			          if (token.includes(':') && !token.startsWith('-') && !token.startsWith('+')) continue;
			          const cleanTag = token.replace(/^[-+]/, '').toLowerCase();
			          if (lockedSet.has(cleanTag)) {
			            return { isLocked: true, reason: `Tag "${cleanTag}" is locked on post #${postId}` };
			          }
			        }
			      }
			
			      if ((flags.rating_locked || post.is_rating_locked) && /(?:^|\s)[-+]?rating:[sqe]/i.test(changes)) {
			        return { isLocked: true, reason: `Rating is locked on post #${postId}` };
			      }
			
			      return { isLocked: false, reason: '' };
			    } catch (err) {
			      U.logWarn(`checkPostLockStatus #${postId}`, err);
			      return { isLocked: false, reason: '' };
			    }
			  }
			
			  function isHourlyLimitResponse(status, errorText) {
			    if (status === 429) return true;
			    if (status === 403) {
			      const msg = String(errorText || '').toLowerCase();
			      return msg.includes('hourly limit') || msg.includes('limit exceeded') || msg.includes('throttl');
			    }
			    return false;
			  }
			
			  async function parseErrorBody(res) {
			    try {
			      const data = await res.json();
			      if (data.reason) return data.reason;
			      if (data.errors) {
			        if (typeof data.errors === 'object') return Object.values(data.errors).flat().join('; ');
			        if (Array.isArray(data.errors)) return data.errors.join('; ');
			      }
			      if (data.message) return data.message;
			    } catch (_) {}
			    return `HTTP ${res.status}`;
			  }
			
			  function formatDuration(ms) {
			    const totalSec = Math.ceil(ms / 1000);
			    const hours = Math.floor(totalSec / 3600);
			    const mins = Math.floor((totalSec % 3600) / 60);
			    const secs = totalSec % 60;
			    if (hours > 0) return `${hours}h ${mins}m`;
			    if (mins > 0) return `${mins}m ${secs}s`;
			    return `${secs}s`;
			  }
			
			  async function getQueueTimingInfo() {
			    const queue = await getQueue();
			    const count = Object.keys(queue).length;
			    if (count === 0) {
			      return { count: 0, nextMs: 0, allMs: 0, isTimeout: false, nextSlot: 0, allSlot: 0 };
			    }
			
			    let slots = await getSlotSchedule();
			    const now = Date.now();
			
			    if (!slots.length) {
			      const until = await getTimeoutUntil();
			      const nextMs = Math.max(0, until - now);
			      return { count, nextMs, allMs: nextMs, isTimeout: nextMs > 0, nextSlot: until, allSlot: until };
			    }
			
			    while (slots.length < count) {
			      const baseIdx = Math.max(0, slots.length - HOURLY_EDIT_LIMIT);
			      slots.push((slots[baseIdx] || now) + ONE_HOUR_MS);
			    }
			
			    const nextSlot = slots[0] || now;
			    const nextMs = Math.max(0, nextSlot - now);
			    const allSlotIdx = Math.min(count - 1, slots.length - 1);
			    const allSlot = slots[allSlotIdx] || nextSlot;
			    const allMs = Math.max(0, allSlot - now);
			
			    return { count, nextMs, allMs, isTimeout: nextMs > 0, nextSlot, allSlot };
			  }
			
			  async function init() {}
			
			  return {
			    init,
			    isEnabled,
			    setEnabled,
			    getTimeoutUntil,
			    isTimeoutActive,
			    getRemainingTimeoutMs,
			    getQueueTimingInfo,
			    getSlotSchedule,
			    checkPostLockStatus,
			    isHourlyLimitResponse,
			    getQueue,
			    getQueueCount,
			    enqueue,
			    enqueueFromThumb,
			    extractThumbData,
			    fetchImageAsBase64,
			    dequeue,
			    clearQueue,
			    formatDuration,
			    processQueue,
			    parseErrorBody,
			    saveImage,
			    getImage,
			    deleteImage,
			    clearImages,
			    HOURLY_EDIT_LIMIT
			  };
			});
			// END: scripts/tag_script/queue.js
			
			// START: scripts/tag_script/timeouted_page.js
			E621X.define('timeoutedPage',
			  ['utils', 'dom', 'layout', 'thumbnail', 'tagScriptQueue', 'treeData'],
			  (U, D, Layout, Thumb, Queue, TreeData) => {
			  'use strict';
			
			  const isTimeoutedPage = () => /^\/posts\/timeouted\/?$/i.test(location.pathname);
			  let countdownTimer = null;
			
			  function createHeader(queueCount, timing, onProcess, onClear) {
			    const isTimeout = timing.isTimeout;
			    const statusText = isTimeout
			      ? (queueCount > 1
			          ? `Next in ${Queue.formatDuration(timing.nextMs)} · All finish in ${Queue.formatDuration(timing.allMs)}`
			          : `Next in ${Queue.formatDuration(timing.nextMs)}`)
			      : 'Status: Ready to submit';
			
			    const statusPill = D.el('div', {
			      id: 'e621x-queue-status-pill',
			      class: `e621x-queue-status-pill ${isTimeout ? 'is-timeout' : 'is-ready'}`,
			      text: statusText
			    });
			
			    const btnProcess = D.button({
			      id: 'e621x-btn-process-queue',
			      text: 'Process Queue Now',
			      primary: true,
			      onClick: onProcess
			    });
			    btnProcess.disabled = queueCount === 0;
			
			    const btnClear = D.button({
			      text: 'Clear Queue',
			      onClick: onClear
			    });
			    btnClear.disabled = queueCount === 0;
			
			    return D.el('header', { class: 'e621x-queue-header' }, [
			      D.el('div', { class: 'e621x-queue-title-area' }, [
			        D.el('h2', {}, [
			          'Queued Tag Scripts',
			          D.el('span', { class: 'e621x-queue-badge', text: `${queueCount} posts` })
			        ]),
			        D.el('span', { class: 'e621x-queue-subtitle', text: 'Delayed post edits waiting to be sent to e621' })
			      ]),
			      D.el('div', { class: 'e621x-queue-actions' }, [
			        statusPill,
			        btnProcess,
			        btnClear
			      ])
			    ]);
			  }
			
			  function startLiveCountdown(untilNext, untilAll, count, onTimeoutElapsed) {
			    if (countdownTimer) clearInterval(countdownTimer);
			    if (!untilNext || untilNext <= Date.now()) return;
			
			    countdownTimer = setInterval(() => {
			      const now = Date.now();
			      const nextRemaining = untilNext - now;
			      const allRemaining = untilAll - now;
			      const pill = document.getElementById('e621x-queue-status-pill');
			
			      if (nextRemaining <= 0) {
			        clearInterval(countdownTimer);
			        countdownTimer = null;
			        if (pill) {
			          pill.className = 'e621x-queue-status-pill is-ready';
			          pill.textContent = 'Status: Ready to submit';
			        }
			        if (typeof onTimeoutElapsed === 'function') onTimeoutElapsed();
			      } else if (pill) {
			        pill.textContent = count > 1
			          ? `Next in ${Queue.formatDuration(nextRemaining)} · All finish in ${Queue.formatDuration(allRemaining)}`
			          : `Next in ${Queue.formatDuration(nextRemaining)}`;
			      }
			    }, 1000);
			  }
			
			  function createQueueCard(post, queueItem, onRemove) {
			    const layout = Layout.getLayoutSettings();
			    const card = Thumb.createThumbnail(post, { layout, enablePins: false });
			    card.classList.add('e621x-thumb-queued');
			
			    const meta = D.el('div', { class: 'e621x-queue-card-meta' }, [
			      D.el('div', { class: 'e621x-queue-diff', title: queueItem.changes, text: queueItem.changes }),
			      D.el('div', { class: 'e621x-queue-card-actions' }, [
			        D.el('span', { text: queueItem.reason ? `Reason: ${queueItem.reason}` : '' }),
			        D.el('button', {
			          type: 'button',
			          class: 'e621x-queue-remove-btn',
			          text: 'Remove',
			          onClick: (e) => {
			            e.preventDefault();
			            e.stopPropagation();
			            onRemove(post.id);
			          }
			        })
			      ])
			    ]);
			
			    return D.el('div', { class: 'e621x-queue-card-wrap', 'data-post-id': post.id }, [
			      card,
			      meta
			    ]);
			  }
			
			  async function renderPage() {
			    document.title = 'Queued Tag Scripts - e621';
			
			    let mainContainer = document.querySelector('#c-posts #content, #content, #page, main');
			    if (!mainContainer) {
			      mainContainer = document.createElement('div');
			      document.body.appendChild(mainContainer);
			    }
			
			    const pageRoot = D.el('div', { class: 'e621x-queue-page' }, [
			      D.el('div', { class: 'e621x-tree-message', text: 'Loading queued posts...' })
			    ]);
			
			    mainContainer.replaceChildren(pageRoot);
			
			    const layout = Layout.getLayoutSettings();
			    Layout.applyLayoutVars(pageRoot, layout);
			
			    async function refresh() {
			      if (countdownTimer) {
			        clearInterval(countdownTimer);
			        countdownTimer = null;
			      }
			
			      const queue = await Queue.getQueue();
			      const ids = Object.keys(queue).map(Number).filter(n => Number.isInteger(n) && n > 0);
			      const timing = await Queue.getQueueTimingInfo();
			
			      const handleProcess = async () => {
			        await Queue.processQueue();
			        await refresh();
			      };
			
			      const handleClear = async () => {
			        if (!confirm('Clear all queued tag scripts?')) return;
			        await Queue.clearQueue();
			        U.showToast('Queue cleared');
			        await refresh();
			      };
			
			      const handleRemove = async (postId) => {
			        await Queue.dequeue(postId);
			        U.showToast(`Post #${postId} removed from queue`);
			        await refresh();
			      };
			
			      const header = createHeader(ids.length, timing, handleProcess, handleClear);
			
			      if (!ids.length) {
			        pageRoot.replaceChildren(
			          header,
			          D.el('div', { class: 'e621x-queue-empty' }, [
			            D.el('h3', { text: 'No tag scripts in queue' }),
			            D.el('p', { text: 'When hourly edit limits are active, clicked posts will queue here with a yellow outline.' })
			          ])
			        );
			        return;
			      }
			
			      // Check which posts lack metadata
			      const missingIds = ids.filter(id => {
			        const it = queue[id];
			        return !it || it.score === undefined;
			      });
			
			      const fetchedMap = new Map();
			      if (missingIds.length > 0) {
			        pageRoot.replaceChildren(header, D.el('div', { class: 'e621x-tree-message', text: `Caching data for ${missingIds.length} post(s)...` }));
			        try {
			          const posts = await TreeData.fetchByIds(missingIds);
			          for (const p of posts) {
			            fetchedMap.set(p.id, p);
			            const previewUrl = p.preview?.url || '';
			            if (previewUrl) {
			              const b64 = await Queue.fetchImageAsBase64(previewUrl);
			              if (b64) await Queue.saveImage(p.id, b64);
			            }
			          }
			        } catch (err) {
			          U.logWarn('timeouted page fetch missing', err);
			        }
			      }
			
			      const grid = D.el('div', { class: 'e621x-tree-cards' });
			
			      for (const id of ids) {
			        const item = queue[id] || {};
			        const fetched = fetchedMap.get(id);
			        const storedImage = await Queue.getImage(id);
			
			        const post = fetched || {
			          id,
			          rating: item.rating || 's',
			          score: item.score ?? 0,
			          fav_count: item.favCount ?? 0,
			          comment_count: item.commentCount ?? 0,
			          file: { ext: item.fileExt || 'jpg' },
			          tags: item.tags || '',
			          flags: item.flags || '',
			          has_parent: item.hasParent ?? false,
			          has_children: item.hasChildren ?? false,
			          preview: { url: storedImage || item.previewUrl || '' }
			        };
			
			        grid.appendChild(createQueueCard(post, item, handleRemove));
			      }
			
			      pageRoot.replaceChildren(header, grid);
			
			      if (timing.isTimeout) {
			        startLiveCountdown(timing.nextSlot, timing.allSlot, ids.length, () => {
			          U.showToast('Slot available! Queue ready.');
			        });
			      }
			    }
			
			    await refresh();
			
			    chrome.storage.onChanged.addListener((changes, area) => {
			      if (area === 'local' && (changes[E621_CONSTANTS.STORAGE_KEYS.TAG_SCRIPT_QUEUE] || changes[E621_CONSTANTS.STORAGE_KEYS.QUEUE_SLOT_TIMESTAMPS])) {
			        refresh();
			      }
			    });
			
			    Layout.onLayoutSettingsChange((next) => {
			      Layout.applyLayoutVars(pageRoot, next);
			    });
			  }
			
			  async function init() {
			    if (isTimeoutedPage()) {
			      await renderPage();
			    }
			  }
			
			  return { init, renderPage };
			});
			// END: scripts/tag_script/timeouted_page.js
			
			// START: scripts/tag_script/logger.js
			if (false) {
			  E621X.define('tagScriptLogger', ['utils'], (U) => {
			    'use strict';
			
			    const LOG_LIMIT = 1000;
			    const logs = [];
			
			    function formatLogTime(d = new Date()) {
			      const pad = (n, len = 2) => String(n).padStart(len, '0');
			      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
			             `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
			    }
			
			    function add(type, event, reaction, details = {}) {
			      const entry = {
			        time: formatLogTime(),
			        type,       // 'KEYBOARD' | 'CLICK' | 'STATE_CHANGE' | 'ACTION'
			        event,
			        reaction,
			        ...details
			      };
			      logs.push(entry);
			      if (logs.length > LOG_LIMIT) logs.shift();
			      return entry;
			    }
			
			    function key(e, reaction, extra = {}) {
			      const targetDesc = e.target ? `${e.target.tagName.toLowerCase()}${e.target.id ? '#' + e.target.id : ''}${e.target.className ? '.' + String(e.target.className).trim().split(/\s+/).join('.') : ''}` : 'none';
			      const mods = [];
			      if (e.ctrlKey) mods.push('Ctrl');
			      if (e.altKey) mods.push('Alt');
			      if (e.shiftKey) mods.push('Shift');
			      if (e.metaKey) mods.push('Meta');
			      const modStr = mods.length ? ` [${mods.join('+')}]` : '';
			
			      return add('KEYBOARD', `Key '${e.key}'${modStr} on <${targetDesc}>`, reaction, {
			        key: e.key,
			        code: e.code,
			        target: targetDesc,
			        activeElement: document.activeElement ? document.activeElement.tagName : 'none',
			        ...extra
			      });
			    }
			
			    function click(e, reaction, extra = {}) {
			      const target = e.target;
			      const desc = target ? `${target.tagName.toLowerCase()}${target.id ? '#' + target.id : ''}${target.className ? '.' + String(target.className).trim().split(/\s+/).join('.') : ''}` : 'unknown';
			      const text = target?.textContent ? target.textContent.trim().slice(0, 30) : '';
			
			      return add('CLICK', `Click on <${desc}>${text ? ` ("${text}")` : ''}`, reaction, {
			        target: desc,
			        text,
			        ...extra
			      });
			    }
			
			    function valueChange(prop, oldValue, newValue, source, extra = {}) {
			      return add('STATE_CHANGE', `${prop} changed`, `[${source}] ${JSON.stringify(oldValue)} ➔ ${JSON.stringify(newValue)}`, {
			        property: prop,
			        oldValue,
			        newValue,
			        source,
			        ...extra
			      });
			    }
			
			    function action(name, status, details = {}) {
			      return add('ACTION', `fn: ${name}`, status, details);
			    }
			
			    function getFormattedLogText() {
			      if (!logs.length) return '=== Tag Script Diagnostic Log is empty ===';
			      const lines = ['=== Tag Script Diagnostic Log ==='];
			      logs.forEach((l) => {
			        lines.push(`[${l.time}] [${l.type}] ${l.event} => ${l.reaction}`);
			        if (l.source) lines.push(`    Source: ${l.source}`);
			        if (l.property) lines.push(`    Change: ${l.property} = ${JSON.stringify(l.oldValue)} -> ${JSON.stringify(l.newValue)}`);
			      });
			      return lines.join('\n');
			    }
			
			    function tagscriptLog() {
			      if (!logs.length) {
			        console.log('%c[e621 extras] Tag script log is empty.', 'color: #888');
			        return [];
			      }
			      console.group(`%c[e621 extras] Tag Script Diagnostic Log (${logs.length} entries)`, 'color: #3b82f6; font-weight: bold;');
			      console.table(logs.map(l => ({
			        Time: l.time,
			        Type: l.type,
			        Event: l.event,
			        Reaction: l.reaction,
			        Source: l.source || l.property || ''
			      })));
			      console.groupEnd();
			      return logs;
			    }
			
			    tagscriptLog.raw = () => logs;
			    tagscriptLog.text = getFormattedLogText;
			    tagscriptLog.clear = () => {
			      logs.length = 0;
			      console.log('[e621 extras] Tag script log cleared.');
			    };
			    tagscriptLog.export = () => {
			      const blob = new Blob([JSON.stringify(logs, null, 2)], { type: 'application/json' });
			      U.downloadBlob(blob, `tagscript_log_${Date.now()}.json`);
			    };
			
			    globalThis.tagscriptLog = tagscriptLog;
			    if (typeof window !== 'undefined') window.tagscriptLog = tagscriptLog;
			
			    setInterval(() => {tagscriptLog()},5000)
			
			    return { add, key, click, valueChange, action, tagscriptLog };
			  });
			} else {
			  E621X.define('tagScriptLogger', [], () => {
			    'use strict';
			    const noop = () => {};
			    const disabledFn = () => {
			      console.log('[e621 extras] Tag script logging is disabled.');
			      return [];
			    };
			    disabledFn.raw = () => [];
			    disabledFn.text = () => 'Logging is disabled.';
			    disabledFn.clear = noop;
			    disabledFn.export = noop;
			
			    globalThis.tagscriptLog = disabledFn;
			    if (typeof window !== 'undefined') window.tagscriptLog = disabledFn;
			
			    return {
			      add: noop,
			      key: noop,
			      click: noop,
			      valueChange: noop,
			      action: noop,
			      tagscriptLog: disabledFn
			    };
			  });
			}
			// END: scripts/tag_script/logger.js
			
			// START: scripts/tag_script/storage.js
			/* Storage & cookie persistence for tag script slots (1-9), presets, and sticky keys.
			 * Tab-specific state (active slots 1-9, active preset, sticky state) is isolated per-tab
			 * via sessionStorage with automatic seeding, preventing cross-tab preset overwrites. */
			E621X.define('tagScriptStorage', ['utils', 'tagScriptLogger'], (U, Log) => {
			  'use strict';
			
			  const LS = {
			    CURRENT_ID: 'current_tag_script_id',
			    scriptKey: (id) => `tag-script-${id}`,
			    STICKY: 'e621_tag_script_sticky',
			    ACTIVE_STICKY_SLOTS: 'e621_tag_script_active_sticky_slots',
			    SLOT_BEFORE_STICKY: 'e621_tag_script_slot_before_sticky',
			    PRESETS: 'e621_tag_script_presets',
			    ACTIVE_PRESET: 'e621_tag_script_active_preset'
			  };
			
			  const TAB_SEEDED_KEY = 'e621_tag_script_tab_seeded';
			
			  function getCookie(name) {
			    const match = document.cookie.match(
			      new RegExp('(?:^|;\\s*)' + name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '=([^;]*)')
			    );
			    return match ? decodeURIComponent(match[1]) : null;
			  }
			
			  function getStoredPresets() {
			    try {
			      const raw = localStorage.getItem(LS.PRESETS);
			      if (!raw) return [];
			      const parsed = JSON.parse(raw);
			      return Array.isArray(parsed) ? parsed : [];
			    } catch (err) {
			      U.logWarn('getStoredPresets', err);
			      return [];
			    }
			  }
			
			  function saveStoredPresets(presets, source = 'unknown') {
			    try {
			      const oldPresets = getStoredPresets();
			      localStorage.setItem(LS.PRESETS, JSON.stringify(presets));
			      Log.valueChange('presets_list', oldPresets.map(p => p.name), presets.map(p => p.name), source);
			    } catch (err) {
			      U.logWarn('saveStoredPresets', err);
			    }
			  }
			
			  /* Ensures this browser tab has its own isolated copy of active scripts & state in sessionStorage. */
			  function ensureTabStorageSeeded() {
			    try {
			      if (sessionStorage.getItem(TAB_SEEDED_KEY)) return;
			
			      const activePreset = localStorage.getItem(LS.ACTIVE_PRESET) || '';
			      sessionStorage.setItem(LS.ACTIVE_PRESET, activePreset);
			
			      const presets = getStoredPresets();
			      const matchedPreset = activePreset
			        ? presets.find(p => p.name.toLowerCase() === activePreset.toLowerCase())
			        : null;
			
			      for (let i = 1; i <= 9; i++) {
			        const key = LS.scriptKey(i);
			        let script = '';
			        if (matchedPreset && matchedPreset.scripts) {
			          script = matchedPreset.scripts[i] || '';
			        } else {
			          script = localStorage.getItem(key) || getCookie(key) || '';
			        }
			        sessionStorage.setItem(key, script);
			      }
			
			      const sticky = (matchedPreset && typeof matchedPreset.sticky === 'boolean')
			        ? String(matchedPreset.sticky)
			        : (localStorage.getItem(LS.STICKY) || 'false');
			      sessionStorage.setItem(LS.STICKY, sticky);
			
			      const activeStickySlots = localStorage.getItem(LS.ACTIVE_STICKY_SLOTS) || '[]';
			      sessionStorage.setItem(LS.ACTIVE_STICKY_SLOTS, activeStickySlots);
			
			      const curId = localStorage.getItem(LS.CURRENT_ID) || '1';
			      sessionStorage.setItem(LS.CURRENT_ID, curId);
			
			      const slotBeforeSticky = localStorage.getItem(LS.SLOT_BEFORE_STICKY) || '1';
			      sessionStorage.setItem(LS.SLOT_BEFORE_STICKY, slotBeforeSticky);
			
			      sessionStorage.setItem(TAB_SEEDED_KEY, 'true');
			    } catch (err) {
			      U.logWarn('ensureTabStorageSeeded', err);
			    }
			  }
			
			  function tabGet(key, fallback = null) {
			    ensureTabStorageSeeded();
			    try {
			      const sVal = sessionStorage.getItem(key);
			      if (sVal !== null && sVal !== undefined) return sVal;
			    } catch (_) {
			      // sessionStorage unavailable
			    }
			
			    try {
			      const lVal = localStorage.getItem(key);
			      if (lVal !== null && lVal !== undefined) return lVal;
			    } catch (_) {
			      // localStorage unavailable
			    }
			
			    return fallback;
			  }
			
			  function tabSet(key, value, syncToLocal = false) {
			    ensureTabStorageSeeded();
			    const str = String(value ?? '');
			    try {
			      sessionStorage.setItem(key, str);
			    } catch (_) {
			      // sessionStorage unavailable
			    }
			
			    if (syncToLocal) {
			      try {
			        localStorage.setItem(key, str);
			      } catch (_) {
			        // localStorage unavailable
			      }
			    }
			  }
			
			  function tabRemove(key, removeFromLocal = false) {
			    ensureTabStorageSeeded();
			    try {
			      sessionStorage.removeItem(key);
			    } catch (_) {
			      // sessionStorage unavailable
			    }
			    if (removeFromLocal) {
			      try {
			        localStorage.removeItem(key);
			      } catch (_) {
			        // localStorage unavailable
			      }
			    }
			  }
			
			  function getCurrentScriptId() {
			    let id = parseInt(tabGet(LS.CURRENT_ID, '1'), 10);
			    return (id >= 1 && id <= 10) ? id : 1;
			  }
			
			  function setCurrentScriptId(id, source = 'unknown') {
			    const num = (id >= 1 && id <= 10) ? id : 1;
			    const oldId = getCurrentScriptId();
			    const str = String(num);
			    tabSet(LS.CURRENT_ID, str, true);
			    if (oldId !== num) {
			      Log.valueChange('current_tag_script_id', oldId, num, source);
			    }
			  }
			
			  function getStoredScript(id) {
			    const num = (id >= 1 && id <= 9) ? id : null;
			    if (!num) return ''; // Slot 10 has no script
			    const key = LS.scriptKey(num);
			    const val = tabGet(key);
			    return val !== null && val !== undefined ? val : '';
			  }
			
			  function setStoredScript(id, content, source = 'unknown', shouldSyncPreset = false) {
			    if (id < 1 || id > 9) return; // Slot 10 is virtual, never persist
			    const key = LS.scriptKey(id);
			    const oldVal = getStoredScript(id);
			    const text = String(content ?? '');
			    if (oldVal !== text) {
			      const hasActivePreset = Boolean(getActivePresetName());
			      tabSet(key, text, !hasActivePreset);
			      Log.valueChange(`slot_${id} (${key})`, oldVal, text, source);
			    }
			    if (shouldSyncPreset) {
			      syncActivePresetScript(id, text, source);
			    }
			  }
			
			  function clearAllStoredScripts(source = 'unknown') {
			    setActivePresetName('', source);
			    for (let i = 1; i <= 9; i++) {
			      setStoredScript(i, '', `${source} -> slot #${i}`, false);
			    }
			  }
			
			  const getActivePresetName = () => tabGet(LS.ACTIVE_PRESET, '') || '';
			
			  const setActivePresetName = (name, source = 'unknown') => {
			    const oldName = getActivePresetName();
			    const nextName = String(name || '');
			    if (oldName !== nextName) {
			      if (!nextName) tabRemove(LS.ACTIVE_PRESET, true);
			      else tabSet(LS.ACTIVE_PRESET, nextName, true);
			      Log.valueChange('active_preset', oldName, nextName, source);
			    }
			  };
			
			  function syncActivePresetScript(id, content, source) {
			    const activeName = getActivePresetName();
			    if (!activeName) return;
			
			    const presets = getStoredPresets();
			    const preset = presets.find(p => p.name.toLowerCase() === activeName.toLowerCase());
			    if (!preset) return;
			
			    if (!preset.scripts) preset.scripts = {};
			    const text = String(content || '').trim();
			    if (text) {
			      preset.scripts[id] = text;
			    } else {
			      delete preset.scripts[id];
			    }
			
			    saveStoredPresets(presets, `syncActivePresetScript("${activeName}" -> slot #${id} via ${source})`);
			  }
			
			  /* Sticky keys mode helpers */
			  const isStickyMode = () => tabGet(LS.STICKY, 'false') === 'true';
			
			  const setStickyMode = (value, source = 'unknown') => {
			    const oldVal = isStickyMode();
			    const nextVal = Boolean(value);
			    if (oldVal !== nextVal) {
			      tabSet(LS.STICKY, String(nextVal), true);
			      Log.valueChange('sticky_mode', oldVal, nextVal, source);
			    }
			  };
			
			  function getActiveStickySlots() {
			    try {
			      const raw = tabGet(LS.ACTIVE_STICKY_SLOTS);
			      if (!raw) return [];
			      const parsed = JSON.parse(raw);
			      return Array.isArray(parsed) ? parsed.map(Number).filter(n => n >= 1 && n <= 9) : [];
			    } catch (_) {
			      return [];
			    }
			  }
			
			  function setActiveStickySlots(slots, source = 'unknown') {
			    const valid = Array.from(new Set(slots.map(Number).filter(n => n >= 1 && n <= 9))).sort((a, b) => a - b);
			    tabSet(LS.ACTIVE_STICKY_SLOTS, JSON.stringify(valid), false);
			    Log.valueChange('active_sticky_slots', [], valid, source);
			  }
			
			  function getSlotBeforeSticky() {
			    const val = parseInt(tabGet(LS.SLOT_BEFORE_STICKY, '1'), 10);
			    return (val >= 1 && val <= 9) ? val : 1;
			  }
			
			  function setSlotBeforeSticky(slot) {
			    const val = (slot >= 1 && slot <= 9) ? slot : 1;
			    tabSet(LS.SLOT_BEFORE_STICKY, String(val), false);
			  }
			
			  function hasAnyScripts(activeFieldVal = '') {
			    if (activeFieldVal && activeFieldVal.trim()) return true;
			    for (let i = 1; i <= 9; i++) {
			      if (getStoredScript(i).trim()) return true;
			    }
			    return false;
			  }
			
			  return {
			    getCurrentScriptId,
			    setCurrentScriptId,
			    getStoredScript,
			    setStoredScript,
			    clearAllStoredScripts,
			    getStoredPresets,
			    saveStoredPresets,
			    getActivePresetName,
			    setActivePresetName,
			    syncActivePresetScript,
			    isStickyMode,
			    setStickyMode,
			    getActiveStickySlots,
			    setActiveStickySlots,
			    getSlotBeforeSticky,
			    setSlotBeforeSticky,
			    hasAnyScripts
			  };
			});
			// END: scripts/tag_script/storage.js
			
			// START: scripts/tag_script/runner.js
			/* Execution engine: applies tag scripts to single posts or bulk selections.
			 * In Sticky Keys mode, multiple active tag scripts are combined and executed in a single request. */
			E621X.define('tagScriptRunner',
			  ['utils', 'selectors', 'net', 'tagScriptEngine', 'tagScriptQueue', 'tagScriptStorage', 'tagScriptLogger'],
			  (U, SEL, Net, Engine, Queue, Storage, Log) => {
			  'use strict';
			
			  const PLAN_PREVIEW_COUNT = 5;
			  const QUEUE_LINK_ID = 'e621x-tag-script-queue-link';
			
			  let bulkRunning = false;
			  const inFlight = new Set();
			
			  const getScriptField = () =>
			    document.getElementById('e621x-tag-script-field') || document.querySelector(SEL.nativeTagScriptField);
			  const getReason = () => document.getElementById('e621x-tag-script-reason')?.value || '';
			
			  function readScript() {
			    if (Storage.isStickyMode()) {
			      const activeSlots = Storage.getActiveStickySlots();
			      if (activeSlots.length > 1) {
			        return activeSlots
			          .slice()
			          .sort((a, b) => a - b)
			          .map(id => Storage.getStoredScript(id).trim())
			          .filter(Boolean)
			          .join(' ');
			      }
			      if (activeSlots.length === 1) {
			        return Storage.getStoredScript(activeSlots[0]).trim();
			      }
			      return '';
			    }
			
			    const curId = Storage.getCurrentScriptId();
			    if (curId === 10) return '';
			
			    return (getScriptField()?.value ?? Storage.getStoredScript(curId)).trim();
			  }
			
			  function postDataFromThumb(thumb) {
			    const d = thumb.dataset || {};
			    const tags = (d.tags || '').split(/\s+/).filter(Boolean);
			    const flags = (d.flags || '').split(/\s+/).filter(Boolean);
			
			    const isDeleted = flags.includes('deleted') || thumb.classList.contains('post-status-deleted');
			    const isPending = flags.includes('pending') || thumb.classList.contains('post-status-pending');
			    const isFlagged = flags.includes('flagged') || thumb.classList.contains('post-status-flagged');
			
			    return {
			      id: Number(d.id || 0),
			      tagsSet: new Set(tags),
			      rating: String(d.rating || 's').toLowerCase(),
			      flags,
			      status: isDeleted ? 'deleted' : (isFlagged ? 'flagged' : (isPending ? 'pending' : 'active')),
			      file_ext: String(d.fileExt || '').toLowerCase(),
			      width: Number(d.width) || 0,
			      height: Number(d.height) || 0,
			      size: Number(d.size) || 0,
			      score: Number(d.score) || 0,
			      fav_count: Number(d.favCount) || 0,
			      comment_count: Number(d.commentCount) || 0,
			      uploader: String(d.uploader || '').toLowerCase(),
			      uploader_id: Number(d.uploaderId) || 0,
			      pools: (d.pools || '').split(/\s+/).map(Number).filter(n => Number.isInteger(n) && n > 0),
			      has_parent: thumb.querySelector('.ribbon.left') !== null || d.hasParent === 'true',
			      has_children: thumb.querySelector('.ribbon.right') !== null || d.hasChildren === 'true'
			    };
			  }
			
			  async function sendTagScript(postId, changes, reason) {
			    const res = await Net.sendForm(`/posts/${postId}.json`, 'PATCH', {
			      'post[tag_string_diff]': changes,
			      'post[edit_reason]': reason
			    }, { retries: 1 });
			    if (!res.ok) {
			      const msg = await Queue.parseErrorBody(res);
			      const err = new Error(msg);
			      err.status = res.status;
			      err.response = res;
			      throw err;
			    }
			    return res.json();
			  }
			
			  function applyLocalTagDiff(currentTagString, diffString) {
			    const tags = new Set(String(currentTagString || '').split(/\s+/).filter(Boolean));
			    const tokens = diffString.split(/\s+/).filter(Boolean);
			    for (const token of tokens) {
			      if (token.startsWith('-')) {
			        tags.delete(token.slice(1).toLowerCase());
			      } else if (!token.includes(':')) {
			        tags.add(token.toLowerCase());
			      }
			    }
			    return Array.from(tags).join(' ');
			  }
			
			  function refreshThumbnail(thumb, postId, responseData, changes) {
			    const post = responseData?.post || responseData;
			    const tags = U.getPostTagString(post) || (changes ? applyLocalTagDiff(thumb.dataset.tags, changes) : '');
			
			    if (!tags) return false;
			    thumb.dataset.tags = tags;
			
			    const ratingMatch = changes?.match(/(?:^|\s)[-+]?rating:([sqe])/i);
			    if (ratingMatch) thumb.dataset.rating = ratingMatch[1].toLowerCase();
			    else if (post?.rating) thumb.dataset.rating = post.rating;
			
			    return true;
			  }
			
			  async function syncQueuedOutlines() {
			    const queue = await Queue.getQueue();
			    const queuedIds = new Set(Object.keys(queue).map(String));
			
			    document.querySelectorAll(SEL.thumbnail).forEach(thumb => {
			      const id = String(thumb.dataset.id);
			      const isQueued = queuedIds.has(id);
			      thumb.classList.toggle('e621x-thumb-queued', isQueued);
			      if (isQueued) {
			        thumb.classList.remove('e621x-thumb-updated', 'e621x-thumb-locked');
			      }
			    });
			
			    const link = document.getElementById(QUEUE_LINK_ID);
			    if (link) {
			      const count = queuedIds.size;
			      link.textContent = `Queue (${count})`;
			      link.classList.toggle('hidden', count === 0 && !(await Queue.isEnabled()));
			    }
			  }
			
			  async function handleThumbClick(thumb) {
			    const postId = thumb.dataset.id;
			    Log.click({ target: thumb }, `Thumbnail click for post #${postId}`);
			    if (bulkRunning) return U.showToast('A bulk tag script is running — please wait', true);
			    if (inFlight.has(postId)) return;
			
			    const script = readScript();
			    if (!script) return U.showToast('Tag script is empty', true);
			
			    const postData = postDataFromThumb(thumb);
			
			    if (postData.status === 'deleted' || postData.flags.includes('status_locked')) {
			      thumb.classList.remove('e621x-thumb-updated', 'e621x-thumb-queued');
			      thumb.classList.add('e621x-thumb-locked');
			      const reason = postData.status === 'deleted' ? 'Post is deleted' : 'Post is status locked';
			      return U.showToast(`Post #${postId} cannot be edited: ${reason}`, true);
			    }
			
			    const changes = Engine.runTagScript(postData, script);
			    if (!changes.trim()) return U.showToast(`Post #${postId}: No changes needed`);
			
			    const queueEnabled = await Queue.isEnabled();
			    const queueCount = await Queue.getQueueCount();
			    const isTimeout = await Queue.isTimeoutActive();
			
			    // If queue is non-empty, do not attempt immediate edit or slot check: append directly to queue
			    if (queueEnabled && (queueCount > 0 || isTimeout)) {
			      await Queue.enqueueFromThumb(thumb, {
			        changes,
			        reason: getReason()
			      });
			      thumb.classList.remove('e621x-thumb-updated', 'e621x-thumb-locked');
			      thumb.classList.add('e621x-thumb-queued');
			      thumb.dataset.tags = applyLocalTagDiff(thumb.dataset.tags, changes);
			      const remMs = await Queue.getRemainingTimeoutMs();
			      U.showToast(queueCount > 0
			        ? `Added to queue (#${postId}). Position: ${queueCount + 1}`
			        : `Hourly limit active. Post #${postId} queued (${Queue.formatDuration(remMs)} left).`);
			      await syncQueuedOutlines();
			      return;
			    }
			
			    inFlight.add(postId);
			    try {
			      U.showToast(`Updating post #${postId}...`);
			      try {
			        const data = await sendTagScript(postId, changes, getReason());
			        refreshThumbnail(thumb, postId, data, changes);
			        thumb.classList.remove('e621x-thumb-queued', 'e621x-thumb-locked');
			        thumb.classList.add('e621x-thumb-updated');
			        await Queue.dequeue(postId);
			        U.showToast(`Post #${postId} updated`);
			      } catch (err) {
			        if (queueEnabled && Queue.isHourlyLimitResponse(err.status, err.message)) {
			          const check = await Queue.verifyHourlyLimitExceeded();
			          if (check.isLimit || err.status === 429) {
			            const duration = check.isLimit ? check.nextAvailableMs : (60 * 1000);
			            await Queue.setTimeoutUntil(Date.now() + duration);
			            await Queue.enqueueFromThumb(thumb, {
			              changes,
			              reason: getReason()
			            });
			            thumb.classList.remove('e621x-thumb-updated', 'e621x-thumb-locked');
			            thumb.classList.add('e621x-thumb-queued');
			            thumb.dataset.tags = applyLocalTagDiff(thumb.dataset.tags, changes);
			            U.showToast(`Hourly limit reached. Post #${postId} queued for ${Queue.formatDuration(duration)}.`, true);
			            await syncQueuedOutlines();
			            return;
			          }
			        }
			
			        const isLockedMsg = /locked|is deleted|cannot be edited/i.test(err.message);
			        let lockReason = isLockedMsg ? err.message : '';
			        if (!isLockedMsg) {
			          const lockCheck = await Queue.checkPostLockStatus(postId, changes);
			          if (lockCheck.isLocked) lockReason = lockCheck.reason;
			        }
			
			        if (lockReason) {
			          thumb.classList.remove('e621x-thumb-queued', 'e621x-thumb-updated');
			          thumb.classList.add('e621x-thumb-locked');
			          U.showToast(`Post #${postId} cannot be edited: ${lockReason}`, true);
			          return;
			        }
			
			        throw err;
			      }
			    } catch (err) {
			      U.showToast(`Error #${postId}: ${err.message}`, true);
			    } finally {
			      inFlight.delete(postId);
			    }
			  }
			
			  function buildPlan(thumbs, script) {
			    const items = [];
			    let skipped = 0;
			
			    for (let i = 0; i < thumbs.length; i++) {
			      const thumb = thumbs[i];
			      try {
			        const postData = postDataFromThumb(thumb);
			        if (postData.status === 'deleted' || postData.flags.includes('status_locked')) {
			          skipped++;
			          continue;
			        }
			        const changes = Engine.runTagScript(postData, script).trim();
			        if (changes) items.push({ thumb, id: thumb.dataset.id, changes });
			      } catch (err) {
			        skipped++;
			        U.logWarn(`checking post #${thumb.dataset.id}`, err);
			      }
			    }
			    return { items, skipped };
			  }
			
			  function formatPlanSummary(plan, total) {
			    const truncate = (s, n) => (s.length > n ? `${s.slice(0, n)}…` : s);
			    const lines = [`${plan.items.length} of ${total} posts will be edited.`];
			    if (plan.skipped) lines.push(`${plan.skipped} posts could not be checked and will be skipped.`);
			    lines.push('');
			    plan.items.slice(0, PLAN_PREVIEW_COUNT).forEach(({ id, changes }) => lines.push(`#${id}: ${truncate(changes, 80)}`));
			    if (plan.items.length > PLAN_PREVIEW_COUNT) lines.push(`…and ${plan.items.length - PLAN_PREVIEW_COUNT} more`);
			    lines.push('', 'Apply the tag script?');
			    return lines.join('\n');
			  }
			
			  async function applyPlan(items, reason) {
			    const queueEnabled = await Queue.isEnabled();
			    let updated = 0;
			    let errors = 0;
			
			    for (let i = 0; i < items.length; i++) {
			      const { thumb, id, changes } = items[i];
			      U.showToast(`Applying ${i + 1}/${items.length}...`);
			      try {
			        const data = await sendTagScript(id, changes, reason);
			        refreshThumbnail(thumb, id, data);
			        thumb.classList.remove('e621x-thumb-queued', 'e621x-thumb-locked');
			        thumb.classList.add('e621x-thumb-updated');
			        await Queue.dequeue(id);
			        updated++;
			      } catch (err) {
			        if (queueEnabled && Queue.isHourlyLimitResponse(err.status, err.message)) {
			          const check = await Queue.verifyHourlyLimitExceeded();
			          if (check.isLimit || err.status === 429) {
			            const duration = check.isLimit ? check.nextAvailableMs : (60 * 1000);
			
			            for (let j = i; j < items.length; j++) {
			              const rem = items[j];
			              await Queue.enqueueFromThumb(rem.thumb, {
			                changes: rem.changes,
			                reason
			              });
			              rem.thumb.classList.remove('e621x-thumb-updated', 'e621x-thumb-locked');
			              rem.thumb.classList.add('e621x-thumb-queued');
			              rem.thumb.dataset.tags = applyLocalTagDiff(rem.thumb.dataset.tags, rem.changes);
			            }
			
			            await Queue.setTimeoutUntil(Date.now() + duration);
			            U.showToast(`Limit reached at post #${id}. Queued remaining ${items.length - i} posts (${Queue.formatDuration(duration)} left).`, true);
			            await syncQueuedOutlines();
			            return;
			          }
			        }
			
			        const isLockedMsg = /locked|is deleted|cannot be edited/i.test(err.message);
			        let lockReason = isLockedMsg ? err.message : '';
			        if (!isLockedMsg) {
			          const lockCheck = await Queue.checkPostLockStatus(id, changes);
			          if (lockCheck.isLocked) lockReason = lockCheck.reason;
			        }
			
			        if (lockReason) {
			          thumb.classList.remove('e621x-thumb-queued', 'e621x-thumb-updated');
			          thumb.classList.add('e621x-thumb-locked');
			          U.showToast(`Post #${id} skipped: ${lockReason}`, true);
			          errors++;
			          continue;
			        }
			
			        errors++;
			        U.logWarn(`updating post #${id}`, err);
			      }
			    }
			    U.showToast(`Tag Script applied: ${updated} updated${errors ? `, ${errors} errors` : ''}`, errors > 0);
			  }
			
			  async function handleApplyAll() {
			    Log.action('handleApplyAll', 'Apply all button triggered');
			    if (bulkRunning) return;
			    const script = readScript();
			    if (!script) return U.showToast('Tag script is empty', true);
			
			    const thumbs = Array.from(document.querySelectorAll(SEL.thumbnail));
			    if (!thumbs.length) return U.showToast('No posts found on this page', true);
			
			    bulkRunning = true;
			    try {
			      const plan = await buildPlan(thumbs, script);
			      if (!plan) return;
			      if (!plan.items.length) return U.showToast('No changes needed');
			
			      const queueEnabled = await Queue.isEnabled();
			      const queueCount = await Queue.getQueueCount();
			      const isTimeout = await Queue.isTimeoutActive();
			
			      // If queue already has items or timeout is active, enqueue all without checking slots
			      if (queueEnabled && (queueCount > 0 || isTimeout)) {
			        for (const item of plan.items) {
			          await Queue.enqueueFromThumb(item.thumb, {
			            changes: item.changes,
			            reason: getReason()
			          });
			          item.thumb.classList.add('e621x-thumb-queued');
			          item.thumb.dataset.tags = applyLocalTagDiff(item.thumb.dataset.tags, item.changes);
			        }
			        U.showToast(`Queued ${plan.items.length} posts`);
			        await syncQueuedOutlines();
			        return;
			      }
			
			      if (!confirm(formatPlanSummary(plan, thumbs.length))) return;
			      await applyPlan(plan.items, getReason());
			    } catch (err) {
			      U.showToast(`Error: ${err.message}`, true);
			    } finally {
			      bulkRunning = false;
			      await syncQueuedOutlines();
			    }
			  }
			
			  return {
			    postDataFromThumb,
			    sendTagScript,
			    applyLocalTagDiff,
			    refreshThumbnail,
			    syncQueuedOutlines,
			    handleThumbClick,
			    handleApplyAll,
			    readScript,
			    getScriptField,
			    getReason
			  };
			});
			// END: scripts/tag_script/runner.js
			
			// START: scripts/tag_script/preview.js
			/* Rendered preview tooltip of active tag scripts on holding/toggling CTRL.
			 * Smoothly follows cursor matching display refresh rate or custom FPS limit.
			 * Automatically mirrors to opposite side when Hover Zoom is active to avoid collisions. */
			E621X.define('tagScriptPreview', ['utils', 'selectors', 'net', 'dom', 'tagScriptStorage', 'tagScriptRunner'], (U, SEL, Net, D, Storage, Runner) => {
			  'use strict';
			  const { STORAGE_KEYS: KEYS } = E621_CONSTANTS;
			
			  const TOOLTIP_ID = 'e621x-ts-preview';
			  const VIEWPORT_MARGIN = 12;
			  const CURSOR_OFFSET_X = 24;
			
			  const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
			
			  const tagCategoryCache = new Map();
			  const inFlightTags = new Set();
			
			  let tooltipEl = null;
			  let isCtrlDown = false;
			  let isStickyCtrl = false;
			  let targetFps = 0; // 0 = native monitor refresh rate via requestAnimationFrame
			  let scheduled = false;
			  let lastTime = 0;
			  let lastClientX = 0;
			  let lastClientY = 0;
			
			  function isTagScriptMode() {
			    const select = document.querySelector(SEL.modeSelect);
			    return select?.value === 'tag-script-ext';
			  }
			
			  function collectPageTags() {
			    document.querySelectorAll('#tag-list li[class*="tag-type-"]').forEach(li => {
			      const match = li.className.match(/tag-type-(\d+)/);
			      const tagLink = li.querySelector('a.search-tag, a');
			      if (match && tagLink) {
			        const cat = Number(match[1]);
			        const name = tagLink.textContent.trim().toLowerCase().replace(/\s+/g, '_');
			        if (name) tagCategoryCache.set(name, cat);
			      }
			    });
			  }
			
			  async function fetchMissingTags(tagNames) {
			    const missing = tagNames.filter(t => !tagCategoryCache.has(t) && !inFlightTags.has(t));
			    if (!missing.length) return;
			
			    missing.forEach(t => inFlightTags.add(t));
			    try {
			      const url = `/tags.json?search[name]=${encodeURIComponent(missing.join(','))}&limit=100`;
			      const data = await Net.fetchJson(url);
			      const list = Array.isArray(data) ? data : (data?.tags || []);
			      list.forEach(t => {
			        if (t?.name && t.category !== undefined) {
			          tagCategoryCache.set(String(t.name).toLowerCase(), Number(t.category));
			        }
			      });
			      missing.forEach(t => {
			        if (!tagCategoryCache.has(t)) tagCategoryCache.set(t, 0);
			      });
			      if (isCtrlDown && isTagScriptMode() && tooltipEl && !tooltipEl.classList.contains('is-hidden')) {
			        renderPreview();
			      }
			    } catch (_) {
			      missing.forEach(t => tagCategoryCache.set(t, 0));
			    } finally {
			      missing.forEach(t => inFlightTags.delete(t));
			    }
			  }
			
			  function getActiveScripts() {
			    if (Storage.isStickyMode()) {
			      const slots = Storage.getActiveStickySlots();
			      if (!slots.length) return [];
			      return slots
			        .slice()
			        .sort((a, b) => a - b)
			        .map(id => ({ id, text: Storage.getStoredScript(id).trim() }))
			        .filter(s => Boolean(s.text));
			    }
			    const currentId = Storage.getCurrentScriptId();
			    if (currentId === 10) return [];
			    const field = Runner.getScriptField();
			    const text = (field ? field.value : Storage.getStoredScript(currentId)).trim();
			    return text ? [{ id: currentId, text }] : [];
			  }
			
			  function formatScriptLine(scriptText) {
			    const tokens = String(scriptText || '').trim().split(/\s+/).filter(Boolean);
			    if (!tokens.length) return '';
			
			    let line = '';
			    let currentLineLen = 0;
			    const missing = [];
			
			    for (let i = 0; i < tokens.length; i++) {
			      const token = tokens[i];
			      let prefix = '';
			      let rawTag = token;
			
			      if (token.startsWith('-') || token.startsWith('+') || token.startsWith('~')) {
			        prefix = token[0];
			        rawTag = token.slice(1);
			      }
			
			      const cleanTag = rawTag.toLowerCase();
			      let catClass = 'e621x-tag-type-general';
			
			      if (token.startsWith('[') && token.endsWith(']')) {
			        catClass = 'e621x-tag-type-command';
			      } else if (rawTag.includes(':')) {
			        catClass = 'e621x-tag-type-meta';
			      } else if (tagCategoryCache.has(cleanTag)) {
			        const cat = tagCategoryCache.get(cleanTag);
			        catClass = `e621x-tag-type-${cat}`;
			      } else {
			        missing.push(cleanTag);
			        catClass = 'e621x-tag-type-0';
			      }
			
			      const tokenHtml = `${prefix ? `<span class="e621x-tag-type-prefix">${prefix}</span>` : ''}<span class="${catClass}">${D.escapeHtml(rawTag)}</span>`;
			      const tokenLen = token.length + (i > 0 ? 1 : 0);
			
			      if (currentLineLen + tokenLen > 80 && currentLineLen > 0) {
			        line += '<br>&nbsp;&nbsp;';
			        currentLineLen = 2;
			      } else if (i > 0) {
			        line += ' ';
			        currentLineLen += 1;
			      }
			
			      line += tokenHtml;
			      currentLineLen += token.length;
			    }
			
			    if (missing.length > 0) {
			      fetchMissingTags(missing);
			    }
			
			    return line;
			  }
			
			  function alignTooltip(clientX, clientY) {
			    if (!tooltipEl || tooltipEl.classList.contains('is-hidden')) return;
			
			    const vw = window.innerWidth;
			    const vh = window.innerHeight;
			    const rect = tooltipEl.getBoundingClientRect();
			    const boxW = rect.width;
			    const boxH = rect.height;
			    if (!boxW || !boxH) return;
			
			    const zoomContainer = document.getElementById('e621x-zoom-container');
			    const isZoomActive = zoomContainer && !zoomContainer.classList.contains('is-hidden');
			
			    const placeOnRight = isZoomActive ? (clientX >= vw / 2) : (clientX < vw / 2);
			
			    let left;
			    if (placeOnRight) {
			      left = clientX + CURSOR_OFFSET_X;
			      if (left + boxW > vw - VIEWPORT_MARGIN) left = vw - boxW - VIEWPORT_MARGIN;
			    } else {
			      left = clientX - boxW - CURSOR_OFFSET_X;
			      if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN;
			    }
			
			    let top = clamp(clientY - (boxH / 2), VIEWPORT_MARGIN, vh - boxH - VIEWPORT_MARGIN);
			
			    if (left <= clientX + 8 && left + boxW >= clientX - 8) {
			      if (clientY < vh / 2) {
			        top = clamp(clientY + CURSOR_OFFSET_X + 12, VIEWPORT_MARGIN, vh - boxH - VIEWPORT_MARGIN);
			      } else {
			        top = clamp(clientY - boxH - CURSOR_OFFSET_X - 12, VIEWPORT_MARGIN, vh - boxH - VIEWPORT_MARGIN);
			      }
			    }
			
			    tooltipEl.style.left = `${Math.round(left)}px`;
			    tooltipEl.style.top = `${Math.round(top)}px`;
			  }
			
			  function requestAlignTooltip() {
			    if (scheduled) return;
			    scheduled = true;
			
			    const now = performance.now();
			    const minInterval = targetFps > 0 ? (1000 / targetFps) : 0;
			    const wait = Math.max(0, minInterval - (now - lastTime));
			
			    const execute = () => {
			      requestAnimationFrame(() => {
			        scheduled = false;
			        lastTime = performance.now();
			        if (isCtrlDown && isTagScriptMode()) {
			          alignTooltip(lastClientX, lastClientY);
			        }
			      });
			    };
			
			    if (wait > 0) {
			      setTimeout(execute, wait);
			    } else {
			      execute();
			    }
			  }
			
			  function renderPreview() {
			    if (!isTagScriptMode()) {
			      hideTooltip();
			      return;
			    }
			
			    const scripts = getActiveScripts();
			    if (!scripts.length) {
			      hideTooltip();
			      return;
			    }
			
			    if (lastClientX === 0 && lastClientY === 0) {
			      // Coordinates not known yet (e.g. right after reload); wait for cursor movement
			      return;
			    }
			
			    tooltipEl.replaceChildren();
			    for (const script of scripts) {
			      const row = document.createElement('div');
			      row.className = 'e621x-ts-preview-line';
			      row.innerHTML = `<span class="e621x-ts-preview-bullet">•</span>${formatScriptLine(script.text)}`;
			      tooltipEl.appendChild(row);
			    }
			
			    tooltipEl.classList.remove('is-hidden');
			    alignTooltip(lastClientX, lastClientY);
			  }
			
			  function showTooltip() {
			    if (!tooltipEl || !isTagScriptMode()) return;
			    renderPreview();
			  }
			
			  function hideTooltip() {
			    if (!tooltipEl) return;
			    tooltipEl.classList.add('is-hidden');
			    tooltipEl.replaceChildren();
			  }
			
			  function syncPreview() {
			    if (!isTagScriptMode()) {
			      hideTooltip();
			      return;
			    }
			    if (isCtrlDown || (tooltipEl && !tooltipEl.classList.contains('is-hidden'))) {
			      const scripts = getActiveScripts();
			      if (!scripts.length) {
			        hideTooltip();
			        return;
			      }
			      renderPreview();
			    }
			  }
			
			  async function isFeatureEnabled() {
			    return (await U.storageGet(KEYS.CTRL_PREVIEW_ENABLED, true)) !== false;
			  }
			
			  function setupEvents() {
			    const handlePointerActivity = (e) => {
			      lastClientX = e.clientX;
			      lastClientY = e.clientY;
			
			      if (!isCtrlDown || !isTagScriptMode() || !tooltipEl) return;
			
			      if (tooltipEl.classList.contains('is-hidden')) {
			        renderPreview();
			        return;
			      }
			
			      requestAlignTooltip();
			    };
			
			    document.addEventListener('mousemove', handlePointerActivity, { passive: true });
			    document.addEventListener('mouseover', (e) => {
			      if (!isCtrlDown || !isTagScriptMode() || !tooltipEl || !tooltipEl.classList.contains('is-hidden')) return;
			      lastClientX = e.clientX;
			      lastClientY = e.clientY;
			      renderPreview();
			    }, { passive: true });
			
			    document.addEventListener('click', (e) => {
			      if (!isStickyCtrl && !e.ctrlKey && isCtrlDown) {
			        isCtrlDown = false;
			        hideTooltip();
			      }
			    });
			
			    window.addEventListener('e621x-zoom-change', () => {
			      if (isCtrlDown && isTagScriptMode()) {
			        alignTooltip(lastClientX, lastClientY);
			      }
			    });
			
			    window.addEventListener('keydown', async (e) => {
			      if (e.key === 'Escape') {
			        // Sticky Ctrl preview MUST NOT be dismissed or deactivated by Escape
			        if (isStickyCtrl) return;
			
			        if (isCtrlDown || (tooltipEl && !tooltipEl.classList.contains('is-hidden'))) {
			          isCtrlDown = false;
			          hideTooltip();
			          if (document.activeElement?.tagName === 'SELECT') {
			            document.activeElement.blur();
			          }
			        }
			        return;
			      }
			
			      if (['TEXTAREA', 'INPUT'].includes(document.activeElement?.tagName)) return;
			
			      if (e.key !== 'Control' || e.repeat) return;
			      if (!isTagScriptMode()) return;
			      if (!(await isFeatureEnabled())) return;
			
			      if (isStickyCtrl) {
			        isCtrlDown = !isCtrlDown;
			        await U.storageSet(KEYS.CTRL_STICKY_ACTIVE, isCtrlDown);
			        if (isCtrlDown) {
			          showTooltip();
			        } else {
			          hideTooltip();
			        }
			      } else {
			        if (!isCtrlDown) {
			          isCtrlDown = true;
			          showTooltip();
			        }
			      }
			    });
			
			    window.addEventListener('keyup', (e) => {
			      if (e.key !== 'Control') return;
			      if (isStickyCtrl) return;
			
			      isCtrlDown = false;
			      hideTooltip();
			    });
			
			    window.addEventListener('blur', () => {
			      if (isStickyCtrl) return;
			      isCtrlDown = false;
			      hideTooltip();
			    });
			
			    window.addEventListener('focus', () => {
			      if (isStickyCtrl && isCtrlDown && isTagScriptMode()) {
			        alignTooltip(lastClientX, lastClientY);
			      }
			    });
			  }
			
			  async function init() {
			    if (document.getElementById(TOOLTIP_ID)) return;
			
			    tooltipEl = document.createElement('div');
			    tooltipEl.id = TOOLTIP_ID;
			    tooltipEl.className = 'e621x-ts-preview is-hidden';
			    document.body.appendChild(tooltipEl);
			
			    const isEnabled = (await U.storageGet(KEYS.CTRL_PREVIEW_ENABLED, true)) !== false;
			    isStickyCtrl = Boolean(await U.storageGet(KEYS.CTRL_PREVIEW_STICKY, false));
			    if (isEnabled && isStickyCtrl) {
			      isCtrlDown = Boolean(await U.storageGet(KEYS.CTRL_STICKY_ACTIVE, false));
			    } else {
			      isCtrlDown = false;
			    }
			    targetFps = Number(await U.storageGet(KEYS.TRACKING_FPS, 0)) || 0;
			
			    chrome.storage.onChanged.addListener((changes, area) => {
			      if (area === 'local') {
			        if (changes[KEYS.CTRL_PREVIEW_STICKY] !== undefined) {
			          isStickyCtrl = Boolean(changes[KEYS.CTRL_PREVIEW_STICKY].newValue);
			          if (!isStickyCtrl && isCtrlDown) {
			            isCtrlDown = false;
			            hideTooltip();
			            U.storageSet(KEYS.CTRL_STICKY_ACTIVE, false);
			          }
			        }
			        if (changes[KEYS.CTRL_PREVIEW_ENABLED] !== undefined && !changes[KEYS.CTRL_PREVIEW_ENABLED].newValue) {
			          isCtrlDown = false;
			          hideTooltip();
			          U.storageSet(KEYS.CTRL_STICKY_ACTIVE, false);
			        }
			        if (changes[KEYS.CTRL_STICKY_ACTIVE] !== undefined && isStickyCtrl) {
			          const nextActive = Boolean(changes[KEYS.CTRL_STICKY_ACTIVE].newValue);
			          if (isCtrlDown !== nextActive) {
			            isCtrlDown = nextActive;
			            if (isCtrlDown) {
			              syncPreview();
			            } else {
			              hideTooltip();
			            }
			          }
			        }
			        if (changes[KEYS.TRACKING_FPS] !== undefined) {
			          targetFps = Number(changes[KEYS.TRACKING_FPS].newValue) || 0;
			        }
			      }
			    });
			
			    collectPageTags();
			    setupEvents();
			  }
			
			  return { init, renderPreview, syncPreview, hideTooltip };
			});
			// END: scripts/tag_script/preview.js
			
			// START: scripts/tag_script/index.js
			/* UI panel, presets management, sticky keys, hotkeys and lifecycle orchestration. */
			E621X.define('tagScript',
			  ['utils', 'selectors', 'tagScriptStorage', 'tagScriptRunner', 'tagScriptPreview', 'tagScriptQueue', 'tagScriptLogger'],
			  (U, SEL, Storage, Runner, Preview, Queue, Log) => {
			  'use strict';
			
			  const IDS = {
			    ui: 'e621x-tag-script-ui',
			    ownField: 'e621x-tag-script-field',
			    reason: 'e621x-tag-script-reason',
			    all: 'e621x-tag-script-all',
			    sticky: 'e621x-tag-script-sticky',
			    presetSelect: 'e621x-tag-script-preset-select',
			    presetName: 'e621x-tag-script-preset-name',
			    presetSave: 'e621x-tag-script-preset-save',
			    presetDelete: 'e621x-tag-script-preset-delete',
			    queueLink: 'e621x-tag-script-queue-link'
			  };
			
			  const MODE_VALUE = 'tag-script-ext';
			
			  let usesNative = false;
			  let isProgrammaticInput = false;
			
			  const getModeSelect = () => document.querySelector(SEL.modeSelect);
			  const isTagScriptMode = () => getModeSelect()?.value === MODE_VALUE;
			
			  function setFieldDisplayValue(val, source) {
			    const field = Runner.getScriptField();
			    if (!field) return;
			    const oldVal = field.value;
			    if (oldVal === val) return;
			    isProgrammaticInput = true;
			    try {
			      field.value = val;
			      Log.valueChange('field.value', oldVal, val, source);
			      field.dispatchEvent(new Event('input', { bubbles: true }));
			      field.dispatchEvent(new Event('change', { bubbles: true }));
			    } finally {
			      isProgrammaticInput = false;
			    }
			  }
			
			  function syncStickyUi() {
			    const field = Runner.getScriptField();
			    if (!field) return;
			
			    if (!Storage.isStickyMode()) {
			      field.disabled = false;
			      const currentId = Storage.getCurrentScriptId();
			      const validId = (currentId >= 1 && currentId <= 9) ? currentId : (Storage.getSlotBeforeSticky() || 1);
			      Storage.setCurrentScriptId(validId, 'syncStickyUi restore');
			      setFieldDisplayValue(Storage.getStoredScript(validId), 'syncStickyUi restore');
			      return;
			    }
			
			    const activeSlots = Storage.getActiveStickySlots();
			    if (activeSlots.length > 0) {
			      field.disabled = true;
			      field.value = `[${activeSlots.sort((a, b) => a - b).join(', ')}] hold ctrl to view tags`;
			      Storage.setCurrentScriptId(10, 'sticky active slots');
			    } else {
			      Storage.setCurrentScriptId(10, 'sticky empty slots');
			      field.disabled = true;
			      field.value = 'Use numbers on keyboard';
			    }
			  }
			
			  function handleClearScripts(source = 'handleClearScripts') {
			    Log.action('handleClearScripts', `Clearing all 9 slots (source: ${source})`);
			    Storage.clearAllStoredScripts(source);
			    Storage.setActiveStickySlots([]);
			
			    const nameInput = document.getElementById(IDS.presetName);
			    if (nameInput) nameInput.value = '';
			
			    if (Storage.isStickyMode()) {
			      Storage.setCurrentScriptId(10, 'cleared in sticky');
			    } else {
			      const restoreSlot = Storage.getSlotBeforeSticky() || 1;
			      Storage.setCurrentScriptId(restoreSlot, 'cleared');
			    }
			
			    syncStickyUi();
			    updatePresetDropdown('');
			    Preview.syncPreview();
			    U.showToast('Tag scripts cleared');
			    Log.add('ACTION', 'handleClearScripts', 'Cleared all 9 tag script slots and active preset');
			  }
			
			  function updatePresetDropdown(selectedName = '') {
			    const select = document.getElementById(IDS.presetSelect);
			    if (!select) return;
			
			    const presets = Storage.getStoredPresets();
			    select.replaceChildren();
			
			    const defaultOpt = document.createElement('option');
			    defaultOpt.value = '';
			    defaultOpt.textContent = 'Select preset...';
			    select.appendChild(defaultOpt);
			
			    const customOpt = document.createElement('option');
			    customOpt.value = '__custom__';
			    customOpt.textContent = 'Select preset...';
			    customOpt.hidden = true;
			    select.appendChild(customOpt);
			
			    let matched = false;
			    presets.forEach(p => {
			      const opt = document.createElement('option');
			      opt.value = p.name;
			      opt.textContent = p.name;
			      if (selectedName && p.name.toLowerCase() === selectedName.toLowerCase()) {
			        opt.selected = true;
			        matched = true;
			      }
			      select.appendChild(opt);
			    });
			
			    if (!matched) {
			      if (selectedName === '__custom__' || (Storage.hasAnyScripts(Runner.getScriptField()?.value) && !selectedName)) {
			        customOpt.selected = true;
			        select.value = '__custom__';
			      } else {
			        defaultOpt.selected = true;
			        select.value = '';
			      }
			    }
			  }
			
			  function handleLoadPreset(name) {
			    Log.action('handleLoadPreset', `Loading preset "${name}"`);
			    if (!name || name === '__clear__') {
			      handleClearScripts('preset dropdown: Select preset...');
			      return;
			    }
			
			    const presets = Storage.getStoredPresets();
			    const preset = presets.find(p => p.name.toLowerCase() === name.toLowerCase());
			    if (!preset) {
			      Log.add('ACTION', 'handleLoadPreset', `Preset "${name}" not found in storage, clearing scripts`);
			      handleClearScripts('preset not found');
			      return;
			    }
			
			    Storage.setActivePresetName(preset.name, `handleLoadPreset("${preset.name}")`);
			    Storage.setActiveStickySlots([]);
			    for (let i = 1; i <= 9; i++) {
			      const scriptContent = preset.scripts?.[i] || '';
			      Storage.setStoredScript(i, scriptContent, `handleLoadPreset("${preset.name}") -> slot #${i}`, false);
			    }
			
			    let currentId = Storage.getCurrentScriptId();
			    const hasCurrent = Boolean(preset.scripts?.[currentId]?.trim());
			    if (!hasCurrent) {
			      if (preset.scripts?.[1]?.trim()) {
			        currentId = 1;
			      } else {
			        const firstSlot = [1, 2, 3, 4, 5, 6, 7, 8, 9].find(slot => Boolean(preset.scripts?.[slot]?.trim()));
			        if (firstSlot) currentId = firstSlot;
			      }
			      Storage.setCurrentScriptId(currentId, `handleLoadPreset("${preset.name}"): auto-select populated slot`);
			    }
			
			    if (Storage.isStickyMode()) {
			      Storage.setCurrentScriptId(10, 'preset loaded in sticky');
			      syncStickyUi();
			    } else {
			      const scriptVal = Storage.getStoredScript(currentId);
			      setFieldDisplayValue(scriptVal, `handleLoadPreset("${preset.name}") for slot #${currentId}`);
			    }
			
			    updatePresetDropdown(preset.name);
			    Preview.syncPreview();
			    U.showToast(`Preset "${preset.name}" loaded`);
			    Log.add('ACTION', 'handleLoadPreset', `Successfully loaded preset "${preset.name}"`);
			  }
			
			  function handleSavePreset() {
			    const nameInput = document.getElementById(IDS.presetName);
			    const name = nameInput?.value.trim();
			
			    if (!name) {
			      U.showToast('Enter a preset name', true);
			      nameInput?.focus();
			      return;
			    }
			
			    const scripts = {};
			    let hasAny = false;
			    for (let i = 1; i <= 9; i++) {
			      const val = Storage.getStoredScript(i).trim();
			      if (val) {
			        scripts[i] = val;
			        hasAny = true;
			      }
			    }
			
			    if (!hasAny) {
			      U.showToast('No tag scripts to save in preset', true);
			      return;
			    }
			
			    const presets = Storage.getStoredPresets();
			    const existingIdx = presets.findIndex(p => p.name.toLowerCase() === name.toLowerCase());
			    const presetData = {
			      name,
			      scripts
			    };
			
			    if (existingIdx >= 0) {
			      presets[existingIdx] = presetData;
			    } else {
			      presets.push(presetData);
			    }
			
			    Storage.saveStoredPresets(presets, `handleSavePreset("${name}")`);
			    Storage.setActivePresetName(name, `handleSavePreset("${name}")`);
			    if (nameInput) nameInput.value = '';
			    updatePresetDropdown(name);
			    U.showToast(`Preset "${name}" saved`);
			  }
			
			  function handleDeletePreset() {
			    Log.action('handleDeletePreset', 'Delete preset button clicked');
			    const select = document.getElementById(IDS.presetSelect);
			    const selectedVal = select?.value;
			    const activeName = Storage.getActivePresetName();
			
			    let targetName = '';
			    if (selectedVal && selectedVal !== '__custom__' && selectedVal !== '__clear__') {
			      targetName = selectedVal;
			    } else if (activeName) {
			      targetName = activeName;
			    }
			
			    if (!targetName) {
			      U.showToast('Select a preset from dropdown to delete', true);
			      Log.add('ACTION', 'handleDeletePreset', 'Failed: No preset selected in dropdown');
			      return;
			    }
			
			    const presets = Storage.getStoredPresets();
			    const idx = presets.findIndex(p => p.name.toLowerCase() === targetName.toLowerCase());
			    if (idx === -1) {
			      U.showToast(`Preset "${targetName}" not found`, true);
			      Log.add('ACTION', 'handleDeletePreset', `Failed: Preset "${targetName}" not found in storage`);
			      return;
			    }
			
			    const deletedName = presets[idx].name;
			    presets.splice(idx, 1);
			    Storage.saveStoredPresets(presets, `handleDeletePreset("${deletedName}")`);
			
			    if (Storage.getActivePresetName().toLowerCase() === deletedName.toLowerCase()) {
			      handleClearScripts(`deleted active preset "${deletedName}"`);
			    } else {
			      updatePresetDropdown(Storage.getActivePresetName());
			      U.showToast(`Preset "${deletedName}" deleted`);
			    }
			
			    const nameInput = document.getElementById(IDS.presetName);
			    if (nameInput && nameInput.value.trim().toLowerCase() === deletedName.toLowerCase()) {
			      nameInput.value = '';
			    }
			    Log.add('ACTION', 'handleDeletePreset', `Successfully deleted preset "${deletedName}"`);
			  }
			
			  function buildUi(withOwnField) {
			    const ui = document.createElement('div');
			    ui.id = IDS.ui;
			    ui.innerHTML = `
			      ${withOwnField ? `<input id="${IDS.ownField}" data-autocomplete="tag-edit" placeholder="Enter tag script" class="st-input e621x-ts-input" />` : ''}
			      <div class="e621x-ts-row">
			        <button id="${IDS.all}" type="button" class="st-button e621x-ts-all">All</button>
			        <input id="${IDS.reason}" placeholder="Reason" maxlength="250" class="st-input e621x-ts-input e621x-ts-grow" />
			      </div>
			      <div class="e621x-ts-row">
			        <select id="${IDS.presetSelect}" class="st-select e621x-ts-input e621x-ts-grow">
			          <option value="">Select preset...</option>
			        </select>
			      </div>
			      <div class="e621x-ts-row">
			        <input id="${IDS.presetName}" placeholder="Preset name" class="st-input e621x-ts-input e621x-ts-grow" />
			        <button id="${IDS.presetSave}" type="button" class="st-button e621x-ts-btn">Save</button>
			        <button id="${IDS.presetDelete}" type="button" class="st-button e621x-ts-btn">Delete</button>
			      </div>
			      <div class="e621x-ts-row e621x-ts-row--spread">
			        <label class="e621x-ts-label" title="Combine multiple active tag scripts in a single request">
			          <input type="checkbox" id="${IDS.sticky}" />
			          <span>Sticky Keys</span>
			        </label>
			        <a id="${IDS.queueLink}" href="/posts/timeouted" target="_blank" class="e621x-ts-queue-link hidden">Queue (0)</a>
			      </div>`;
			    return ui;
			  }
			
			  function injectUI() {
			    const modeBox = document.querySelector(SEL.modeBox);
			    const select = getModeSelect();
			    if (!modeBox || !select) return;
			
			    usesNative = Boolean(document.querySelector(SEL.nativeTagScriptField));
			
			    if (!Array.from(select.options).some(o => o.value === MODE_VALUE)) {
			      select.append(new Option('Tag Script (extension)', MODE_VALUE));
			    }
			    if (document.getElementById(IDS.ui)) return;
			
			    // Always create our dedicated field to keep extension mode isolated from native tag scripts
			    const ui = buildUi(true);
			    modeBox.appendChild(ui);
			
			    const ownField = document.getElementById(IDS.ownField);
			    const stickyCheckbox = document.getElementById(IDS.sticky);
			    const presetSelect = document.getElementById(IDS.presetSelect);
			    const presetNameInput = document.getElementById(IDS.presetName);
			    const presetSaveBtn = document.getElementById(IDS.presetSave);
			    const presetDeleteBtn = document.getElementById(IDS.presetDelete);
			    const allBtn = document.getElementById(IDS.all);
			
			    stickyCheckbox.checked = Storage.isStickyMode();
			    stickyCheckbox.addEventListener('change', (e) => {
			      const isSticky = stickyCheckbox.checked;
			      Storage.setStickyMode(isSticky, 'checkbox change');
			      if (isSticky) {
			        const cur = Storage.getCurrentScriptId();
			        const validCur = cur >= 1 && cur <= 9 ? cur : 1;
			        Storage.setSlotBeforeSticky(validCur);
			
			        const hasScript = Boolean(Storage.getStoredScript(validCur).trim());
			        Storage.setActiveStickySlots(hasScript ? [validCur] : []);
			      } else {
			        Storage.setActiveStickySlots([]);
			        const restoreSlot = Storage.getSlotBeforeSticky();
			        Storage.setCurrentScriptId(restoreSlot, 'sticky disabled');
			      }
			      syncStickyUi();
			      Preview.syncPreview();
			      Log.click(e, `Sticky Keys toggled to ${isSticky ? 'ON' : 'OFF'}`);
			      U.showToast(`Tag Scripts: Sticky Keys ${isSticky ? 'ON' : 'OFF'}`);
			    });
			
			    const activePreset = Storage.getActivePresetName();
			    updatePresetDropdown(activePreset);
			    if (activePreset) {
			      const presets = Storage.getStoredPresets();
			      const matched = presets.find(p => p.name.toLowerCase() === activePreset.toLowerCase());
			      if (!matched) {
			        Storage.setActivePresetName('', 'init unmatched');
			      }
			    }
			
			    presetSelect.addEventListener('change', (e) => {
			      const val = presetSelect.value;
			      Log.click(e, `Preset dropdown selected: "${val}"`);
			      if (!val || val === '__clear__') {
			        handleClearScripts('preset dropdown: Select preset...');
			      } else if (val !== '__custom__') {
			        handleLoadPreset(val);
			      }
			      presetSelect.blur();
			    });
			
			    presetSaveBtn.addEventListener('click', (e) => {
			      e.preventDefault();
			      Log.click(e, 'Save preset button clicked');
			      handleSavePreset();
			    });
			
			    presetDeleteBtn.addEventListener('click', (e) => {
			      e.preventDefault();
			      Log.click(e, 'Delete preset button clicked');
			      handleDeletePreset();
			    });
			
			    presetNameInput.addEventListener('keydown', (e) => {
			      if (e.key === 'Enter') {
			        e.preventDefault();
			        Log.key(e, 'Enter pressed in preset name input -> triggering handleSavePreset');
			        handleSavePreset();
			      }
			    });
			
			    const handleFieldInput = (field, source) => {
			      if (isProgrammaticInput) return;
			      if (Storage.isStickyMode()) return;
			      const val = field.value;
			      if (val.includes('hold ctrl to view tags') || val.includes('Use numbers on')) return;
			
			      const currentId = Storage.getCurrentScriptId();
			      if (currentId >= 1 && currentId <= 9) {
			        Storage.setStoredScript(currentId, val, `user typing in ${source} (slot #${currentId})`, true);
			        Preview.syncPreview();
			      }
			    };
			
			    const nativeField = document.querySelector(SEL.nativeTagScriptField);
			    if (nativeField) {
			      nativeField.addEventListener('input', () => handleFieldInput(nativeField, 'nativeField'));
			    }
			
			    if (ownField) {
			      ownField.addEventListener('input', () => handleFieldInput(ownField, 'ownField'));
			    }
			
			    const syncMode = () => {
			      const active = select.value === MODE_VALUE;
			      ui.classList.toggle('is-visible', active);
			      if (active) {
			        const curPreset = Storage.getActivePresetName();
			        updatePresetDropdown(curPreset);
			        syncStickyUi();
			        if (!Storage.isStickyMode()) {
			          const id = Storage.getCurrentScriptId();
			          const validId = (id >= 1 && id <= 9) ? id : 1;
			          const scriptVal = Storage.getStoredScript(validId);
			          setFieldDisplayValue(scriptVal, 'mode-box switch to tag-script');
			          const field = Runner.getScriptField();
			          if (field) field.focus();
			          U.showToast(`Switched to tag script #${validId}. Use 1-9 keys to switch.`);
			          Log.add('ACTION', 'syncMode', `Activated tag-script mode on slot #${validId}`);
			        }
			        Preview.syncPreview();
			      } else {
			        Preview.hideTooltip();
			      }
			    };
			    select.addEventListener('change', (e) => {
			      Log.click(e, `Mode dropdown changed to "${select.value}"`);
			      syncMode();
			    });
			    if (select.value === MODE_VALUE) syncMode();
			
			    allBtn.addEventListener('click', (e) => {
			      e.preventDefault();
			      Log.click(e, 'All button clicked');
			      Runner.handleApplyAll();
			    });
			
			    Runner.syncQueuedOutlines();
			  }
			
			  function setupThumbnailClickListener() {
			    document.addEventListener('click', (e) => {
			      if (!isTagScriptMode()) return;
			
			      const thumb = e.target.closest(SEL.thumbnail);
			      if (!thumb) return;
			
			      e.preventDefault();
			      e.stopPropagation();
			      Runner.handleThumbClick(thumb);
			    }, true);
			  }
			
			  function setupHotkeys() {
			    window.addEventListener('keydown', (e) => {
			      if (!isTagScriptMode()) return;
			
			      if (['TEXTAREA', 'INPUT', 'SELECT'].includes(document.activeElement?.tagName)) {
			        return;
			      }
			
			      if (Storage.isStickyMode() && e.key === 'Escape' && !e.ctrlKey && !e.altKey && !e.metaKey) {
			        if (document.querySelector('.e621x-tree-dialog')) return;
			
			        const activeSlots = Storage.getActiveStickySlots();
			        if (activeSlots.length > 0) {
			          e.preventDefault();
			          e.stopPropagation();
			
			          Storage.setActiveStickySlots([], 'hotkey [Escape]');
			          syncStickyUi();
			          Preview.syncPreview();
			          U.showToast('All slots deselected');
			          Log.key(e, 'Escape pressed -> Deselected all active sticky slots');
			          return;
			        }
			      }
			
			      if (!/^[1-9]$/.test(e.key) || e.ctrlKey || e.altKey || e.metaKey) {
			        return;
			      }
			
			      const newId = Number(e.key);
			      e.preventDefault();
			      e.stopPropagation();
			
			      if (Storage.isStickyMode()) {
			        let activeSlots = Storage.getActiveStickySlots();
			        if (activeSlots.includes(newId)) {
			          activeSlots = activeSlots.filter(id => id !== newId);
			          U.showToast(`Slot #${newId} disabled`);
			        } else {
			          if (!Storage.getStoredScript(newId).trim()) return;
			          activeSlots.push(newId);
			          U.showToast(`Slot #${newId} enabled`);
			        }
			        Storage.setActiveStickySlots(activeSlots, `hotkey [${newId}] toggle`);
			        syncStickyUi();
			        Preview.syncPreview();
			      } else {
			        const prevId = Storage.getCurrentScriptId();
			        Log.key(e, `Switched slot #${prevId} ➔ #${newId}`, { prevId, newId });
			
			        Storage.setCurrentScriptId(newId, `hotkey [${newId}]`);
			        const scriptVal = Storage.getStoredScript(newId);
			        setFieldDisplayValue(scriptVal, `hotkey [${newId}]`);
			        Preview.syncPreview();
			
			        U.showToast(`Switched to tag script #${newId}. Use 1-9 keys to switch.`);
			      }
			    }, true);
			  }
			
			  function init() {
			    [injectUI, setupThumbnailClickListener, setupHotkeys, Preview.init, Runner.syncQueuedOutlines].forEach(feature => {
			      try {
			        Promise.resolve(feature()).catch(err => U.logWarn(`tagScript.${feature.name}`, err));
			      } catch (err) {
			        U.logWarn(`tagScript.${feature.name}`, err);
			      }
			    });
			
			    chrome.storage.onChanged.addListener((changes, area) => {
			      if (area === 'local' && (changes[E621_CONSTANTS.STORAGE_KEYS.TAG_SCRIPT_QUEUE] || changes[E621_CONSTANTS.STORAGE_KEYS.QUEUE_TAG_SCRIPTS_ENABLED])) {
			        Runner.syncQueuedOutlines();
			      }
			    });
			  }
			
			  return { init, syncQueuedOutlines: Runner.syncQueuedOutlines };
			});
			// END: scripts/tag_script/index.js
			
			// START: scripts/content.js
			(function () {
			  'use strict';
			  const { ACTIONS } = E621_CONSTANTS;
			  const U = E621X.get('utils');
			  const blacklist = E621X.get('blacklist');
			  const tree = E621X.get('tree');
			  const queue = E621X.get('tagScriptQueue');
			
			  const handlers = {
			    [ACTIONS.SHOW_POST_TREE]: async (req) => {
			      if (!req.postId) return U.fail('Invalid post ID');
			      tree.open(req.postId);
			      return U.ok('Opening post tree...');
			    },
			    [ACTIONS.BACKUP_BLACKLIST]: () => blacklist.backupBlacklist(),
			    [ACTIONS.LOAD_DOWNVOTES]: () => blacklist.loadDownvotes(),
			    [ACTIONS.UPLOAD_DOWNVOTES]: () => blacklist.uploadDownvotes(),
			    [ACTIONS.UNLOAD_DOWNVOTES]: () => blacklist.unloadDownvotes(),
			    [ACTIONS.SYNC_API]: () => blacklist.syncApi(),
			    [ACTIONS.TOGGLE_AUTO_UPDATE]: () => blacklist.toggleAutoUpdate(),
			    [ACTIONS.PROCESS_TAG_QUEUE]: async () => {
			      const res = await queue.processQueue();
			      return U.ok(`Queue processed: ${res.updated || 0} updated`);
			    }
			  };
			
			  chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
			    if (chrome.runtime?.id && sender?.id && sender.id !== chrome.runtime.id) return false;
			
			    const handler = handlers[request?.action];
			    if (!handler) {
			      sendResponse({ ok: false, status: 'Unknown action' });
			      return false;
			    }
			
			    Promise.resolve()
			      .then(() => handler(request))
			      .catch(err => {
			        U.logWarn(`action ${request.action}`, err);
			        return U.fail(err.message || String(err));
			      })
			      .then(result => sendResponse({ ok: result.ok, status: result.message }));
			    return true;
			  });
			
			  window.addEventListener('message', async (event) => {
			    if (!event.data || !event.data.fromE621Popup) return;
			    const { action } = event.data;
			    const handler = handlers[action];
			    const port = event.ports && event.ports[0];
			
			    if (!handler) {
			      if (port) port.postMessage({ ok: false, status: 'Unknown action' });
			      return;
			    }
			
			    try {
			      const result = await Promise.resolve(handler(event.data));
			      if (port) port.postMessage({ ok: result.ok, status: result.message });
			    } catch (err) {
			      U.logWarn(`postMessage action ${action}`, err);
			      if (port) port.postMessage({ ok: false, status: err.message || String(err) });
			    }
			  });
			
			  (async function start() {
			    await U.ensureStorageSchema();
			    const csrf = U.getCsrfToken();
			    if (csrf && chrome.runtime?.id) {
			      chrome.runtime.sendMessage({ action: ACTIONS.SET_CSRF_TOKEN, csrfToken: csrf }).catch(() => {});
			    }
			    for (const name of ['uploader', 'userEdits', 'tree', 'blacklist', 'hoverZoom', 'tagScriptQueue', 'timeoutedPage', 'tagScript', 'tooltipsManager']) {
			      try {
			        await E621X.get(name).init();
			      } catch (err) {
			        U.logWarn(`init ${name}`, err);
			      }
			    }
			  })();
			})();
			// END: scripts/content.js
			;}
			   } catch(e) { _error(`  Error executing scripts ${scriptPaths}`, e); }
			  
			  } else {
			      _log(`Skipping document-end phase (no document).`);
			  }
			
			  
  // #endregion
  // #region Wait for Document Idle
			  _log(`Waiting for document idle state...`);
			  if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') {
			      await new Promise(resolve => window.requestIdleCallback(resolve, { timeout: 2000 })); // 2-second timeout fallback
			      _log(`requestIdleCallback fired or timed out.`);
			  } else {
			      // Fallback: wait a short period after DOMContentLoaded/current execution if requestIdleCallback is unavailable
			      await new Promise(resolve => setTimeout(resolve, 50));
			      _log(`Idle fallback timer completed.`);
			  }
			  
			
  // #endregion
  // #region Document Idle
			   if (typeof document !== 'undefined') {
			    _log(`Executing document-idle phase...`);
			    
			    const scriptPaths = [];
			   _log(`  Executing JS (idle): ${scriptPaths}`);
			
			   try {
			       // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically)
			      with (globalThis){;
			
			;}
			   } catch(e) { _error(`  Error executing scripts ${scriptPaths}`, e); }
			  
			  } else {
			      _log(`Skipping document-idle phase (no document).`);
			  }
			
			  _log(`All execution phases complete, re-firing load events.`);
			  document.dispatchEvent(new Event("DOMContentLoaded", {
			    bubbles: true,
			    cancelable: true
			  }));
			}
			
  // #endregion
// #region Event Listener No changes needed here ---
			window.addEventListener("message", (event) => {
			  if (event.data.type === "openOptionsPage") {
			    openOptionsPage();
			  }
			  if (event.data.type === "openPopupPage") {
			    openPopupPage();
			  }
			  if (event.data.type === "closeOptionsPage") {
			    closeOptionsModal();
			  }
			  if (event.data.type === "closePopupPage") {
			    closePopupModal();
			  }
			});
			
// #endregion
// #region Refactored Modal Closing Functions Promise-based ---
			
			function closeOptionsModal() {
			  return new Promise((resolve) => {
			    const DURATION = 100;
			    const backdrop = document.getElementById("extension-options-backdrop");
			    const modal = document.getElementById("extension-options-modal");
			
			    if (!backdrop || !modal) {
			      return resolve();
			    }
			
			    modal.style.animation = `modalCloseAnimation ${DURATION / 1000}s ease-out forwards`;
			    backdrop.style.animation = `backdropFadeOut ${DURATION / 1000}s ease-out forwards`;
			
			    setTimeout(() => {
			      if (confirm("Close options and reload the page?")) {
			        window.location.reload(); // Note: This will stop further execution
			      } else {
			        backdrop.remove();
			      }
			      resolve();
			    }, DURATION);
			  });
			}
			
			function closePopupModal() {
			  return new Promise((resolve) => {
			    const DURATION = 100;
			    const backdrop = document.getElementById("extension-popup-backdrop");
			    const modal = document.getElementById("extension-popup-modal");
			
			    if (!backdrop || !modal) {
			      return resolve();
			    }
			
			    modal.style.animation = `modalCloseAnimation ${DURATION / 1000}s ease-out forwards`;
			    backdrop.style.animation = `backdropFadeOut ${DURATION / 1000}s ease-out forwards`;
			
			    setTimeout(() => {
			      backdrop.remove();
			      resolve();
			    }, DURATION);
			  });
			}
			
// #endregion
// #region Simplified Public API Functions ---
			
			async function openPopupPage() {
			  if (!POPUP_PAGE_PATH || typeof EXTENSION_ASSETS_MAP === "undefined") {
			    _warn("No popup page available.");
			    return;
			  }
			  await openModal({
			    type: "popup",
			    pagePath: POPUP_PAGE_PATH,
			    defaultTitle: "Extension Popup",
			    closeFn: closePopupModal,
			  });
			}
			
			async function openOptionsPage() {
			  if (!OPTIONS_PAGE_PATH || typeof EXTENSION_ASSETS_MAP === "undefined") {
			    _warn("No options page available.");
			    return;
			  }
			  await openModal({
			    type: "options",
			    pagePath: OPTIONS_PAGE_PATH,
			    defaultTitle: "Extension Options",
			    closeFn: closeOptionsModal,
			  });
			}
			
// #endregion
// #region Generic Modal Logic Style Injection ---
			
			let stylesInjected = false;
			function injectGlobalStyles() {
			  if (stylesInjected) return;
			  stylesInjected = true;
			
			  const styles = `
			        .extension-backdrop {
			            position: fixed;
			            top: 0; left: 0;
			            width: 100vw; height: 100vh;
			            background: rgba(0, 0, 0, 0.13);
			            backdrop-filter: blur(3px);
			            z-index: 2147483646;
			            display: flex;
			            align-items: center;
			            justify-content: center;
			            animation: backdropFadeIn 0.3s ease-out forwards;
			        }
			
			        .extension-modal {
			            z-index: 2147483647;
			            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
			            --background: #ffffff;
			            --rad: 10px;
			            --border: #666;
			            --border-thickness: 2px;
			            display: flex;
			            flex-direction: column;
			            overflow: hidden;
			            animation: modalOpenAnimation 0.3s ease-out forwards;
			        }
			
			        /* Size specific styles */
			        .extension-modal.popup-size {
			            width: 400px; height: 600px;
			            max-width: calc(100vw - 40px);
			            max-height: calc(100vh - 40px);
			        }
			        .extension-modal.options-size {
			            width: calc(100vw - 80px); height: calc(100vh - 80px);
			            max-width: 1200px;
			            max-height: 800px;
			        }
			
			        /* Common modal components */
			        .extension-modal .modal-header {
			            display: flex; justify-content: space-between; align-items: flex-end;
			            padding: 0 16px; position: relative; flex-shrink: 0;
			        }
			        .extension-modal .tab {
			            padding: 12px 16px; color: #606266;
			            display: flex; align-items: center; gap: 8px;
			            font-size: 14px; cursor: pointer;
			            border-radius: var(--rad) var(--rad) 0 0;
			            transition: background-color 0.2s ease; user-select: none;
			        }
			        .extension-modal .tab.active, .extension-modal .tab.close-button {
			            background-color: var(--background);
			            border: var(--border-thickness) solid var(--border);
			            border-bottom-color: var(--background);
			            margin-bottom: -1px; z-index: 1;
			            color: #303133; font-weight: 500;
			        }
			        .extension-modal .tab.close-button { padding: 8px; }
			        .extension-modal .tab.close-button:hover { background-color: #f5f7fa; }
			        .extension-modal .tab svg { stroke: currentColor; }
			        .extension-modal .tab.active img { width: 16px; height: 16px; }
			        .extension-modal .tab.close-button svg { width: 20px; height: 20px; }
			
			        .extension-modal .modal-content {
			            flex-grow: 1; position: relative;
			            border-radius: var(--rad); overflow: hidden;
			            bottom: calc(var(--border-thickness) - 1px);
			            border: var(--border-thickness) solid var(--border);
			        }
			        .extension-modal .modal-content iframe {
			            width: 100%; height: 100%; border: 0; background: white;
			        }
			
			        /* Animations */
			        @keyframes backdropFadeIn { from { opacity: 0; backdrop-filter: blur(0px); } to { opacity: 1; backdrop-filter: blur(3px); } }
			        @keyframes backdropFadeOut { from { opacity: 1; backdrop-filter: blur(3px); } to { opacity: 0; backdrop-filter: blur(0px); } }
			        @keyframes modalOpenAnimation { from { transform: scaleY(0.8); opacity: 0; } to { transform: scaleY(1); opacity: 1; } }
			        @keyframes modalCloseAnimation { from { transform: scaleY(1); opacity: 1; } to { transform: scaleY(0.8); opacity: 0; } }
			    `;
			  const styleSheet = document.createElement("style");
			  styleSheet.id = "extension-global-styles";
			  styleSheet.innerText = styles;
			  document.head.appendChild(styleSheet);
			}
			
			async function openModal(config) {
			  injectGlobalStyles();
			
			  const { type, pagePath, defaultTitle, closeFn } = config;
			  const html = EXTENSION_ASSETS_MAP[pagePath];
			  if (!html) {
			    _warn(`${defaultTitle} HTML not found in asset map`);
			    return;
			  }
			
			  const backdropId = `extension-${type}-backdrop`;
			  const modalId = `extension-${type}-modal`;
			  const sizeClass = `${type}-size`;
			
// #endregion
  // #region Smoothly close the other modal if it s open ---
			  const otherType = type === "popup" ? "options" : "popup";
			  const otherBackdrop = document.getElementById(
			    `extension-${otherType}-backdrop`
			  );
			  if (otherBackdrop) {
			    // Await the correct close function
			    await (otherType === "popup" ? closePopupModal() : closeOptionsModal());
			  }
			
			  let backdrop = document.getElementById(backdropId);
			  let modal, iframe;
			
			  if (!backdrop) {
			    backdrop = document.createElement("div");
			    backdrop.id = backdropId;
			    backdrop.className = "extension-backdrop";
			
			    modal = document.createElement("div");
			    modal.id = modalId;
			    modal.className = `extension-modal ${sizeClass}`;
			
			    const extensionName = INJECTED_MANIFEST.name || defaultTitle;
			    const iconSrc =
			      EXTENSION_ICON ||
			      "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik00IDdoM2ExIDEgMCAwIDAgMSAtMXYtMWEyIDIgMCAwIDEgNCAwdjFhMSAxIDAgMCAwIDEgMWgzYTEgMSAwIDAgMSAxIDF2M2ExIDEgMCAwIDAgMSAxaDFhMiAyIDAgMCAxIDAgNGgtMWExIDEgMCAwIDAgLTEgMXYzYTEgMSAwIDAgMSAtMSAxaC0zYTEgMSAwIDAgMSAtMSAtMXYtMWEyIDIgMCAwIDAgLTQgMHYxYTEgMSAwIDAgMSAtMSAxaC0zYTEgMSAwIDAgMSAtMSAtMXYtM2ExIDEgMCAwIDEgMSAtMWgxYTIgMiAwIDAgMCAwIC00aC0xYTEgMSAwIDAgMSAtMSAtMXYtM2ExIDEgMCAwIDEgMSAtMSIgLz48L3N2Zz4=";
			
			    modal.innerHTML = `
			            <div class="modal-header">
			                <div class="tab active">
			                    <img src="${iconSrc}" onerror="this.style.display='none'">
			                    <span>${extensionName}</span>
			                </div>
			                <div class="tab close-button">
			                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
			                       <line x1="18" y1="6" x2="6" y2="18"></line>
			                       <line x1="6" y1="6" x2="18" y2="18"></line>
			                    </svg>
			                </div>
			            </div>
			            <div class="modal-content">
			                <iframe></iframe>
			            </div>
			        `;
			
			    backdrop.appendChild(modal);
			
			    backdrop.addEventListener("click", (e) => {
			      if (e.target === backdrop) closeFn();
			    });
			    modal.querySelector(".close-button").addEventListener("click", closeFn);
			
			    document.body.appendChild(backdrop);
			    iframe = modal.querySelector("iframe");
			  } else {
			    // If it already exists, just make sure it's visible
			    backdrop.style.display = "flex";
			    modal = backdrop.querySelector(".extension-modal");
			    iframe = modal.querySelector("iframe");
			  }
			
			  // Load content into iframe
			  try {
			    const polyfillString = generateCompletePolyfillForIframe();
			    const doc = new DOMParser().parseFromString(html, "text/html");
			    const cspNonce = Array.from(document.scripts).map(s => s.nonce).find(Boolean)
        || document.querySelector('meta[name="csp-nonce"]')?.content
        || document.querySelector('[nonce]')?.nonce
        || '';

      const script = doc.createElement("script");
      script.textContent = polyfillString;
      doc.head.insertAdjacentElement("afterbegin", script);

      if (cspNonce) {
        doc.querySelectorAll('script').forEach(s => s.setAttribute('nonce', cspNonce));
      }

      iframe.srcdoc = doc.documentElement.outerHTML;
			  } catch (e) {
			    _error("Error generating complete polyfill for iframe", e);
			    iframe.srcdoc = html;
			  }
			}
			
			function generateCompletePolyfillForIframe() {
			  const polyfillString = "\n// -- Messaging implementation\n\nfunction createEventBus(\n  scopeId,\n  type = \"page\", // \"page\" or \"iframe\"\n  { allowedOrigin = \"*\", children = [], parentWindow = null } = {}\n) {\n  if (!scopeId) throw new Error(\"createEventBus requires a scopeId\");\n\n  const handlers = {};\n\n  function handleIncoming(ev) {\n    if (allowedOrigin !== \"*\" && ev.origin !== allowedOrigin) return;\n\n    const msg = ev.data;\n    if (!msg || msg.__eventBus !== true || msg.scopeId !== scopeId) return;\n\n    const { event, payload } = msg;\n\n    // PAGE: if it's an INIT from an iframe, adopt it\n    if (type === \"page\" && event === \"__INIT__\") {\n      const win = ev.source;\n      if (win && !children.includes(win)) {\n        children.push(win);\n      }\n      return;\n    }\n\n    (handlers[event] || []).forEach((fn) =>\n      fn(payload, { origin: ev.origin, source: ev.source })\n    );\n  }\n\n  window.addEventListener(\"message\", handleIncoming);\n\n  function emitTo(win, event, payload) {\n    const envelope = {\n      __eventBus: true,\n      scopeId,\n      event,\n      payload,\n    };\n    win.postMessage(envelope, allowedOrigin);\n  }\n\n  // IFRAME: announce to page on startup\n  if (type === \"iframe\") {\n    setTimeout(() => {\n      const pw = parentWindow || window.parent;\n      if (pw && pw.postMessage) {\n        emitTo(pw, \"__INIT__\", null);\n      }\n    }, 0);\n  }\n\n  return {\n    on(event, fn) {\n      handlers[event] = handlers[event] || [];\n      handlers[event].push(fn);\n    },\n    off(event, fn) {\n      if (!handlers[event]) return;\n      handlers[event] = handlers[event].filter((h) => h !== fn);\n    },\n    /**\n     * Emits an event.\n     * @param {string} event - The event name.\n     * @param {any} payload - The event payload.\n     * @param {object} [options] - Emission options.\n     * @param {Window} [options.to] - A specific window to target. If provided, message is ONLY sent to the target.\n     */\n    emit(event, payload, { to } = {}) {\n      // If a specific target window is provided, send only to it and DO NOT dispatch locally.\n      // This prevents a port from receiving its own messages.\n      if (to) {\n        if (to && typeof to.postMessage === \"function\") {\n          emitTo(to, event, payload);\n        }\n        return; // Exit after targeted send.\n      }\n\n      // For broadcast messages (no 'to' target), dispatch locally first.\n      (handlers[event] || []).forEach((fn) =>\n        fn(payload, { origin: location.origin, source: window })\n      );\n\n      // Then propagate the broadcast to other windows.\n      if (type === \"page\") {\n        children.forEach((win) => emitTo(win, event, payload));\n      } else {\n        const pw = parentWindow || window.parent;\n        if (pw && pw.postMessage) {\n          emitTo(pw, event, payload);\n        }\n      }\n    },\n  };\n}\n\nfunction createRuntime(type = \"background\", bus) {\n  let nextId = 1;\n  const pending = {};\n  const msgListeners = [];\n\n  let nextPortId = 1;\n  const ports = {};\n  const onConnectListeners = [];\n\n  function parseArgs(args) {\n    let target, message, options, callback;\n    const arr = [...args];\n    if (arr.length === 0) {\n      throw new Error(\"sendMessage requires at least one argument\");\n    }\n    if (arr.length === 1) {\n      return { message: arr[0] };\n    }\n    // last object could be options\n    if (\n      arr.length &&\n      typeof arr[arr.length - 1] === \"object\" &&\n      !Array.isArray(arr[arr.length - 1])\n    ) {\n      options = arr.pop();\n    }\n    // last function is callback\n    if (arr.length && typeof arr[arr.length - 1] === \"function\") {\n      callback = arr.pop();\n    }\n    if (\n      arr.length === 2 &&\n      (typeof arr[0] === \"string\" || typeof arr[0] === \"number\")\n    ) {\n      [target, message] = arr;\n    } else {\n      [message] = arr;\n    }\n    return { target, message, options, callback };\n  }\n\n  if (type === \"background\") {\n    bus.on(\"__REQUEST__\", ({ id, message }, { source }) => {\n      let responded = false,\n        isAsync = false;\n      function sendResponse(resp) {\n        if (responded) return;\n        responded = true;\n        // Target the response directly back to the window that sent the request.\n        bus.emit(\"__RESPONSE__\", { id, response: resp }, { to: source });\n      }\n      const results = msgListeners\n        .map((fn) => {\n          try {\n            // msg, sender, sendResponse\n            const ret = fn(message, { id, tab: { id: source } }, sendResponse);\n            if (ret === true || (ret && typeof ret.then === \"function\")) {\n              isAsync = true;\n              return ret;\n            }\n            return ret;\n          } catch (e) {\n            _error(e);\n          }\n        })\n        .filter((r) => r !== undefined);\n\n      const promises = results.filter((r) => r && typeof r.then === \"function\");\n      if (!isAsync && promises.length === 0) {\n        const out = results.length === 1 ? results[0] : results;\n        sendResponse(out);\n      } else if (promises.length) {\n        Promise.all(promises).then((vals) => {\n          if (!responded) {\n            const out = vals.length === 1 ? vals[0] : vals;\n            sendResponse(out);\n          }\n        });\n      }\n    });\n  }\n\n  if (type !== \"background\") {\n    bus.on(\"__RESPONSE__\", ({ id, response }) => {\n      const entry = pending[id];\n      if (!entry) return;\n      entry.resolve(response);\n      if (entry.callback) entry.callback(response);\n      delete pending[id];\n    });\n  }\n\n  function sendMessage(...args) {\n    // Background should be able to send message to itself\n    // if (type === \"background\") {\n    //   throw new Error(\"Background cannot sendMessage to itself\");\n    // }\n    const { target, message, callback } = parseArgs(args);\n    const id = nextId++;\n    const promise = new Promise((resolve) => {\n      pending[id] = { resolve, callback };\n      bus.emit(\"__REQUEST__\", { id, message });\n    });\n    return promise;\n  }\n\n  bus.on(\"__PORT_CONNECT__\", ({ portId, name }, { source }) => {\n    if (type !== \"background\") return;\n    const backgroundPort = makePort(\"background\", portId, name, source);\n    ports[portId] = backgroundPort;\n\n    onConnectListeners.forEach((fn) => fn(backgroundPort));\n\n    // send back a CONNECT_ACK so the client can\n    // start listening on its end:\n    bus.emit(\"__PORT_CONNECT_ACK__\", { portId, name }, { to: source });\n  });\n\n  // Clients handle the ACK and finalize their Port object by learning the remote window.\n  bus.on(\"__PORT_CONNECT_ACK__\", ({ portId, name }, { source }) => {\n    if (type === \"background\") return; // ignore\n    const p = ports[portId];\n    if (!p) return;\n    // Call the port's internal finalize method to complete the handshake\n    if (p._finalize) {\n      p._finalize(source);\n    }\n  });\n\n  // Any port message travels via \"__PORT_MESSAGE__\"\n  bus.on(\"__PORT_MESSAGE__\", (envelope, { source }) => {\n    const { portId } = envelope;\n    const p = ports[portId];\n    if (!p) return;\n    p._receive(envelope, source);\n  });\n\n  // Any port disconnect:\n  bus.on(\"__PORT_DISCONNECT__\", ({ portId }) => {\n    const p = ports[portId];\n    if (!p) return;\n    p._disconnect();\n    delete ports[portId];\n  });\n\n  // Refactored makePort to correctly manage internal state and the connection handshake.\n  function makePort(side, portId, name, remoteWindow) {\n    let onMessageHandlers = [];\n    let onDisconnectHandlers = [];\n    let buffer = [];\n    // Unique instance ID for this port instance\n    const instanceId = Math.random().toString(36).slice(2) + Date.now();\n    // These state variables are part of the closure and are updated by _finalize\n    let _ready = side === \"background\";\n\n    function _drainBuffer() {\n      buffer.forEach((m) => _post(m));\n      buffer = [];\n    }\n\n    function _post(msg) {\n      // Always use the 'to' parameter for port messages, making them directional.\n      // Include senderInstanceId\n      bus.emit(\n        \"__PORT_MESSAGE__\",\n        { portId, msg, senderInstanceId: instanceId },\n        { to: remoteWindow }\n      );\n    }\n\n    function postMessage(msg) {\n      if (!_ready) {\n        buffer.push(msg);\n      } else {\n        _post(msg);\n      }\n    }\n\n    function _receive(envelope, source) {\n      // envelope: { msg, senderInstanceId }\n      if (envelope.senderInstanceId === instanceId) return; // Don't dispatch to self\n      onMessageHandlers.forEach((fn) =>\n        fn(envelope.msg, { id: portId, tab: { id: source } })\n      );\n    }\n\n    function disconnect() {\n      // Also use the 'to' parameter for disconnect messages\n      bus.emit(\"__PORT_DISCONNECT__\", { portId }, { to: remoteWindow });\n      _disconnect();\n      delete ports[portId];\n    }\n\n    function _disconnect() {\n      onDisconnectHandlers.forEach((fn) => fn());\n      onMessageHandlers = [];\n      onDisconnectHandlers = [];\n    }\n\n    // This function is called on the client port when the ACK is received from background.\n    // It updates the port's state, completing the connection.\n    function _finalize(win) {\n      remoteWindow = win; // <-- This is the crucial part: learn the destination\n      _ready = true;\n      _drainBuffer();\n    }\n\n    return {\n      name,\n      sender: {\n        id: portId,\n      },\n      onMessage: {\n        addListener(fn) {\n          onMessageHandlers.push(fn);\n        },\n        removeListener(fn) {\n          onMessageHandlers = onMessageHandlers.filter((x) => x !== fn);\n        },\n      },\n      onDisconnect: {\n        addListener(fn) {\n          onDisconnectHandlers.push(fn);\n        },\n        removeListener(fn) {\n          onDisconnectHandlers = onDisconnectHandlers.filter((x) => x !== fn);\n        },\n      },\n      postMessage,\n      disconnect,\n      // Internal methods used by the runtime\n      _receive,\n      _disconnect,\n      _finalize, // Expose the finalizer for the ACK handler\n    };\n  }\n\n  function connect(connectInfo = {}) {\n    if (type === \"background\") {\n      throw new Error(\"Background must use onConnect, not connect()\");\n    }\n    const name = connectInfo.name || \"\";\n    const portId = nextPortId++;\n    // create the client side port\n    // remoteWindow is initially null; it will be set by _finalize upon ACK.\n    const clientPort = makePort(\"client\", portId, name, null);\n    ports[portId] = clientPort;\n\n    // fire the connect event across the bus\n    bus.emit(\"__PORT_CONNECT__\", { portId, name });\n    return clientPort;\n  }\n\n  function onConnect(fn) {\n    if (type !== \"background\") {\n      throw new Error(\"connect event only fires in background\");\n    }\n    onConnectListeners.push(fn);\n  }\n\n  return {\n    // rpc:\n    sendMessage,\n    onMessage: {\n      addListener(fn) {\n        msgListeners.push(fn);\n      },\n      removeListener(fn) {\n        const i = msgListeners.indexOf(fn);\n        if (i >= 0) msgListeners.splice(i, 1);\n      },\n    },\n\n    // port API:\n    connect,\n    onConnect: {\n      addListener(fn) {\n        onConnect(fn);\n      },\n      removeListener(fn) {\n        const i = onConnectListeners.indexOf(fn);\n        if (i >= 0) onConnectListeners.splice(i, 1);\n      },\n    },\n  };\n}\n\n\n// --- Abstraction Layer: PostMessage Target\n\nlet nextRequestId = 1;\nconst pendingRequests = new Map(); // requestId -> { resolve, reject, timeout }\n\nfunction sendAbstractionRequest(method, args = []) {\n  return new Promise((resolve, reject) => {\n    const requestId = nextRequestId++;\n\n    const timeout = setTimeout(() => {\n      pendingRequests.delete(requestId);\n      reject(new Error(`PostMessage request timeout for method: ${method}`));\n    }, 10000);\n\n    pendingRequests.set(requestId, { resolve, reject, timeout });\n\n    window.parent.postMessage({\n      type: \"abstraction-request\",\n      requestId,\n      method,\n      args,\n    });\n  });\n}\n\nwindow.addEventListener(\"message\", (event) => {\n  const { type, requestId, success, result, error } = event.data;\n\n  if (type === \"abstraction-response\") {\n    const pending = pendingRequests.get(requestId);\n    if (pending) {\n      clearTimeout(pending.timeout);\n      pendingRequests.delete(requestId);\n\n      if (success) {\n        pending.resolve(result);\n      } else {\n        const err = new Error(error.message);\n        err.stack = error.stack;\n        pending.reject(err);\n      }\n    }\n  }\n});\n\nasync function _storageSet(items) {\n  return sendAbstractionRequest(\"_storageSet\", [items]);\n}\n\nasync function _storageGet(keys) {\n  return sendAbstractionRequest(\"_storageGet\", [keys]);\n}\n\nasync function _storageRemove(keysToRemove) {\n  return sendAbstractionRequest(\"_storageRemove\", [keysToRemove]);\n}\n\nasync function _storageClear() {\n  return sendAbstractionRequest(\"_storageClear\");\n}\n\nasync function _cookieList(details) {\n  return sendAbstractionRequest(\"_cookieList\", [details]);\n}\n\nasync function _cookieSet(details) {\n  return sendAbstractionRequest(\"_cookieSet\", [details]);\n}\n\nasync function _cookieDelete(details) {\n  return sendAbstractionRequest(\"_cookieDelete\", [details]);\n}\n\nasync function _fetch(url, options) {\n  return sendAbstractionRequest(\"_fetch\", [url, options]);\n}\n\nfunction _registerMenuCommand(name, func) {\n  _warn(\"_registerMenuCommand called from iframe context:\", name);\n  return sendAbstractionRequest(\"_registerMenuCommand\", [\n    name,\n    func.toString(),\n  ]);\n}\n\nfunction _openTab(url, active) {\n  return sendAbstractionRequest(\"_openTab\", [url, active]);\n}\n\nasync function _initStorage() {\n  return sendAbstractionRequest(\"_initStorage\");\n}\n\n\nconst EXTENSION_ASSETS_MAP = {{EXTENSION_ASSETS_MAP}};\n\n// -- Polyfill Implementation\nfunction buildPolyfill({ isBackground = false, isOtherPage = false } = {}) {\n  // Generate a unique context ID for this polyfill instance\n  const contextType = isBackground\n    ? \"background\"\n    : isOtherPage\n      ? \"options\"\n      : \"content\";\n  const contextId = `${contextType}_${Math.random()\n    .toString(36)\n    .substring(2, 15)}`;\n\n  const IS_IFRAME = \"true\" === \"true\";\n  const BUS = (function () {\n    if (globalThis.__BUS) {\n      return globalThis.__BUS;\n    }\n    globalThis.__BUS = createEventBus(\n      \"e621-extras\",\n      IS_IFRAME ? \"iframe\" : \"page\",\n    );\n    return globalThis.__BUS;\n  })();\n  const RUNTIME = createRuntime(isBackground ? \"background\" : \"tab\", BUS);\n  const createNoopListeners = () => ({\n    addListener: (callback) => {\n      _log(\"addListener\", callback);\n    },\n    removeListener: (callback) => {\n      _log(\"removeListener\", callback);\n    },\n  });\n  // TODO: Stub\n  const storageChangeListeners = new Set();\n  function broadcastStorageChange(changes, areaName) {\n    storageChangeListeners.forEach((listener) => {\n      listener(changes, areaName);\n    });\n  }\n\n  let REQ_PERMS = [];\n\n  // --- Chrome polyfill\n  let chrome = {\n    extension: {\n      isAllowedIncognitoAccess: () => Promise.resolve(true),\n      sendMessage: (...args) => _messagingHandler.sendMessage(...args),\n    },\n    permissions: {\n      // TODO: Remove origin permission means exclude from origin in startup (when checking for content scripts)\n      request: (permissions, callback) => {\n        _log(\"permissions.request\", permissions, callback);\n        if (Array.isArray(permissions)) {\n          REQ_PERMS = [...REQ_PERMS, ...permissions];\n        }\n        if (typeof callback === \"function\") {\n          callback(permissions);\n        }\n        return Promise.resolve(permissions);\n      },\n      contains: (permissions, callback) => {\n        if (typeof callback === \"function\") {\n          callback(true);\n        }\n        return Promise.resolve(true);\n      },\n      getAll: () => {\n        return Promise.resolve({\n          permissions: EXTENSION_PERMISSIONS,\n          origins: ORIGIN_PERMISSIONS,\n        });\n      },\n      onAdded: createNoopListeners(),\n      onRemoved: createNoopListeners(),\n    },\n    i18n: {\n      getUILanguage: () => {\n        return USED_LOCALE || \"en\";\n      },\n      getMessage: (key, substitutions = []) => {\n        if (typeof substitutions === \"string\") {\n          substitutions = [substitutions];\n        }\n        if (typeof LOCALE_KEYS !== \"undefined\" && LOCALE_KEYS[key]) {\n          return LOCALE_KEYS[key].message?.replace(\n            /\\$(\\d+)/g,\n            (match, p1) => substitutions[p1 - 1] || match,\n          );\n        }\n        return key;\n      },\n    },\n    alarms: {\n      onAlarm: createNoopListeners(),\n      create: () => {\n        _log(\"alarms.create\", arguments);\n      },\n      get: () => {\n        _log(\"alarms.get\", arguments);\n      },\n    },\n    runtime: {\n      ...RUNTIME,\n      onInstalled: createNoopListeners(),\n      onStartup: createNoopListeners(),\n      // TODO: Postmessage to parent to open options page or call openOptionsPage\n      openOptionsPage: () => {\n        // const url = chrome.runtime.getURL(OPTIONS_PAGE_PATH);\n        // console.log(\"openOptionsPage\", _openTab, url, EXTENSION_ASSETS_MAP);\n        // _openTab(url);\n        if (typeof openOptionsPage === \"function\") {\n          openOptionsPage();\n        } else if (window.parent) {\n          window.parent.postMessage({ type: \"openOptionsPage\" }, \"*\");\n        } else {\n          _warn(\"openOptionsPage not available.\");\n        }\n      },\n      getManifest: () => {\n        // The manifest object will be injected into the scope where buildPolyfill is called\n        if (typeof INJECTED_MANIFEST !== \"undefined\") {\n          return JSON.parse(JSON.stringify(INJECTED_MANIFEST)); // Return deep copy\n        }\n        _warn(\"INJECTED_MANIFEST not found for chrome.runtime.getManifest\");\n        return { name: \"Unknown\", version: \"0.0\", manifest_version: 2 };\n      },\n      getURL: (path) => {\n        if (!path) return \"\";\n        if (path.startsWith(\"/\")) {\n          path = path.substring(1);\n        }\n\n        if (typeof _createAssetUrl === \"function\") {\n          return _createAssetUrl(path);\n        }\n\n        _warn(\n          `chrome.runtime.getURL fallback for '${path}'. Assets may not be available.`,\n        );\n        // Attempt a relative path resolution (highly context-dependent and likely wrong)\n        try {\n          if (window.location.protocol.startsWith(\"http\")) {\n            return new URL(path, window.location.href).toString();\n          }\n        } catch (e) {\n          /* ignore error, fallback */\n        }\n        return path;\n      },\n      id: \"polyfilled-extension-\" + Math.random().toString(36).substring(2, 15),\n      lastError: null,\n      setUninstallURL: () => {},\n      setUpdateURL: () => {},\n      getPlatformInfo: async () => {\n        const platform = {\n          os: \"unknown\",\n          arch: \"unknown\",\n          nacl_arch: \"unknown\",\n        };\n\n        if (typeof navigator !== \"undefined\") {\n          const userAgent = navigator.userAgent.toLowerCase();\n          if (userAgent.includes(\"mac\")) platform.os = \"mac\";\n          else if (userAgent.includes(\"win\")) platform.os = \"win\";\n          else if (userAgent.includes(\"linux\")) platform.os = \"linux\";\n          else if (userAgent.includes(\"android\")) platform.os = \"android\";\n          else if (userAgent.includes(\"ios\")) platform.os = \"ios\";\n\n          if (userAgent.includes(\"x86_64\") || userAgent.includes(\"amd64\")) {\n            platform.arch = \"x86-64\";\n          } else if (userAgent.includes(\"i386\") || userAgent.includes(\"i686\")) {\n            platform.arch = \"x86-32\";\n          } else if (userAgent.includes(\"arm\")) {\n            platform.arch = \"arm\";\n          }\n        }\n\n        return platform;\n      },\n      getBrowserInfo: async () => {\n        const info = {\n          name: \"unknown\",\n          version: \"unknown\",\n          buildID: \"unknown\",\n        };\n\n        if (typeof navigator !== \"undefined\") {\n          const userAgent = navigator.userAgent;\n          if (userAgent.includes(\"Chrome\")) {\n            info.name = \"Chrome\";\n            const match = userAgent.match(/Chrome\\/([0-9.]+)/);\n            if (match) info.version = match[1];\n          } else if (userAgent.includes(\"Firefox\")) {\n            info.name = \"Firefox\";\n            const match = userAgent.match(/Firefox\\/([0-9.]+)/);\n            if (match) info.version = match[1];\n          } else if (userAgent.includes(\"Safari\")) {\n            info.name = \"Safari\";\n            const match = userAgent.match(/Version\\/([0-9.]+)/);\n            if (match) info.version = match[1];\n          }\n        }\n\n        return info;\n      },\n    },\n    storage: {\n      local: {\n        get: function (keys, callback) {\n          if (typeof _storageGet !== \"function\")\n            throw new Error(\"_storageGet not defined\");\n\n          const promise = _storageGet(keys);\n\n          if (typeof callback === \"function\") {\n            promise\n              .then((result) => {\n                try {\n                  callback(result);\n                } catch (e) {\n                  _error(\"Error in storage.get callback:\", e);\n                }\n              })\n              .catch((error) => {\n                _error(\"Storage.get error:\", error);\n                callback({});\n              });\n            return;\n          }\n\n          return promise;\n        },\n        set: function (items, callback) {\n          if (typeof _storageSet !== \"function\")\n            throw new Error(\"_storageSet not defined\");\n\n          const promise = _storageSet(items).then((result) => {\n            broadcastStorageChange(items, \"local\");\n            return result;\n          });\n\n          if (typeof callback === \"function\") {\n            promise\n              .then((result) => {\n                try {\n                  callback(result);\n                } catch (e) {\n                  _error(\"Error in storage.set callback:\", e);\n                }\n              })\n              .catch((error) => {\n                _error(\"Storage.set error:\", error);\n                callback();\n              });\n            return;\n          }\n\n          return promise;\n        },\n        remove: function (keys, callback) {\n          if (typeof _storageRemove !== \"function\")\n            throw new Error(\"_storageRemove not defined\");\n\n          const promise = _storageRemove(keys).then((result) => {\n            const changes = {};\n            const keyList = Array.isArray(keys) ? keys : [keys];\n            keyList.forEach((key) => {\n              changes[key] = { oldValue: undefined, newValue: undefined };\n            });\n            broadcastStorageChange(changes, \"local\");\n            return result;\n          });\n\n          if (typeof callback === \"function\") {\n            promise\n              .then((result) => {\n                try {\n                  callback(result);\n                } catch (e) {\n                  _error(\"Error in storage.remove callback:\", e);\n                }\n              })\n              .catch((error) => {\n                _error(\"Storage.remove error:\", error);\n                callback();\n              });\n            return;\n          }\n\n          return promise;\n        },\n        clear: function (callback) {\n          if (typeof _storageClear !== \"function\")\n            throw new Error(\"_storageClear not defined\");\n\n          const promise = _storageClear().then((result) => {\n            broadcastStorageChange({}, \"local\");\n            return result;\n          });\n\n          if (typeof callback === \"function\") {\n            promise\n              .then((result) => {\n                try {\n                  callback(result);\n                } catch (e) {\n                  _error(\"Error in storage.clear callback:\", e);\n                }\n              })\n              .catch((error) => {\n                _error(\"Storage.clear error:\", error);\n                callback();\n              });\n            return;\n          }\n\n          return promise;\n        },\n        onChanged: {\n          addListener: (callback) => {\n            storageChangeListeners.add(callback);\n          },\n          removeListener: (callback) => {\n            storageChangeListeners.delete(callback);\n          },\n        },\n      },\n      sync: {\n        get: function (keys, callback) {\n          _warn(\"chrome.storage.sync polyfill maps to local\");\n          return chrome.storage.local.get(keys, callback);\n        },\n        set: function (items, callback) {\n          _warn(\"chrome.storage.sync polyfill maps to local\");\n\n          const promise = chrome.storage.local.set(items).then((result) => {\n            broadcastStorageChange(items, \"sync\");\n            return result;\n          });\n\n          if (typeof callback === \"function\") {\n            promise\n              .then((result) => {\n                try {\n                  callback(result);\n                } catch (e) {\n                  _error(\"Error in storage.sync.set callback:\", e);\n                }\n              })\n              .catch((error) => {\n                _error(\"Storage.sync.set error:\", error);\n                callback();\n              });\n            return;\n          }\n\n          return promise;\n        },\n        remove: function (keys, callback) {\n          _warn(\"chrome.storage.sync polyfill maps to local\");\n\n          const promise = chrome.storage.local.remove(keys).then((result) => {\n            const changes = {};\n            const keyList = Array.isArray(keys) ? keys : [keys];\n            keyList.forEach((key) => {\n              changes[key] = { oldValue: undefined, newValue: undefined };\n            });\n            broadcastStorageChange(changes, \"sync\");\n            return result;\n          });\n\n          if (typeof callback === \"function\") {\n            promise\n              .then((result) => {\n                try {\n                  callback(result);\n                } catch (e) {\n                  _error(\"Error in storage.sync.remove callback:\", e);\n                }\n              })\n              .catch((error) => {\n                _error(\"Storage.sync.remove error:\", error);\n                callback();\n              });\n            return;\n          }\n\n          return promise;\n        },\n        clear: function (callback) {\n          _warn(\"chrome.storage.sync polyfill maps to local\");\n\n          const promise = chrome.storage.local.clear().then((result) => {\n            broadcastStorageChange({}, \"sync\");\n            return result;\n          });\n\n          if (typeof callback === \"function\") {\n            promise\n              .then((result) => {\n                try {\n                  callback(result);\n                } catch (e) {\n                  _error(\"Error in storage.sync.clear callback:\", e);\n                }\n              })\n              .catch((error) => {\n                _error(\"Storage.sync.clear error:\", error);\n                callback();\n              });\n            return;\n          }\n\n          return promise;\n        },\n        onChanged: {\n          addListener: (callback) => {\n            storageChangeListeners.add(callback);\n          },\n          removeListener: (callback) => {\n            storageChangeListeners.delete(callback);\n          },\n        },\n      },\n      onChanged: {\n        addListener: (callback) => {\n          storageChangeListeners.add(callback);\n        },\n        removeListener: (callback) => {\n          storageChangeListeners.delete(callback);\n        },\n      },\n      managed: {\n        get: function (keys, callback) {\n          _warn(\"chrome.storage.managed polyfill is read-only empty.\");\n\n          const promise = Promise.resolve({});\n\n          if (typeof callback === \"function\") {\n            promise.then((result) => {\n              try {\n                callback(result);\n              } catch (e) {\n                _error(\"Error in storage.managed.get callback:\", e);\n              }\n            });\n            return;\n          }\n\n          return promise;\n        },\n      },\n    },\n    cookies: (function () {\n      const cookieChangeListeners = new Set();\n      function broadcastCookieChange(changeInfo) {\n        cookieChangeListeners.forEach((listener) => {\n          try {\n            listener(changeInfo);\n          } catch (e) {\n            _error(\"Error in cookies.onChanged listener:\", e);\n          }\n        });\n      }\n\n      function handlePromiseCallback(promise, callback) {\n        if (typeof callback === \"function\") {\n          promise\n            .then((result) => callback(result))\n            .catch((error) => {\n              // chrome.runtime.lastError = { message: error.message }; // TODO: Implement lastError\n              _error(error);\n              callback(); // Call with undefined on error\n            });\n          return;\n        }\n        return promise;\n      }\n\n      return {\n        get: function (details, callback) {\n          if (typeof _cookieList !== \"function\") {\n            return handlePromiseCallback(\n              Promise.reject(new Error(\"_cookieList not defined\")),\n              callback,\n            );\n          }\n          const promise = _cookieList({\n            url: details.url,\n            name: details.name,\n            storeId: details.storeId,\n            partitionKey: details.partitionKey,\n          }).then((cookies) => {\n            if (!cookies || cookies.length === 0) {\n              return null;\n            }\n            // Sort by path length (longest first), then creation time (earliest first, if available)\n            cookies.sort((a, b) => {\n              const pathLenDiff = (b.path || \"\").length - (a.path || \"\").length;\n              if (pathLenDiff !== 0) return pathLenDiff;\n              return (a.creationTime || 0) - (b.creationTime || 0);\n            });\n            return cookies[0];\n          });\n          return handlePromiseCallback(promise, callback);\n        },\n\n        getAll: function (details, callback) {\n          if (typeof _cookieList !== \"function\") {\n            return handlePromiseCallback(\n              Promise.reject(new Error(\"_cookieList not defined\")),\n              callback,\n            );\n          }\n          if (details.partitionKey) {\n            _warn(\n              \"cookies.getAll: partitionKey is not fully supported in this environment.\",\n            );\n          }\n          const promise = _cookieList(details);\n          return handlePromiseCallback(promise, callback);\n        },\n\n        set: function (details, callback) {\n          const promise = (async () => {\n            if (\n              typeof _cookieSet !== \"function\" ||\n              typeof _cookieList !== \"function\"\n            ) {\n              throw new Error(\"_cookieSet or _cookieList not defined\");\n            }\n            if (details.partitionKey) {\n              _warn(\n                \"cookies.set: partitionKey is not fully supported in this environment.\",\n              );\n            }\n\n            const getDetails = {\n              url: details.url,\n              name: details.name,\n              storeId: details.storeId,\n            };\n            const oldCookies = await _cookieList(getDetails);\n            const oldCookie = oldCookies && oldCookies[0];\n\n            if (oldCookie) {\n              broadcastCookieChange({\n                cause: \"overwrite\",\n                cookie: oldCookie,\n                removed: true,\n              });\n            }\n\n            await _cookieSet(details);\n            const newCookies = await _cookieList(getDetails);\n            const newCookie = newCookies && newCookies[0];\n\n            if (newCookie) {\n              broadcastCookieChange({\n                cause: \"explicit\",\n                cookie: newCookie,\n                removed: false,\n              });\n            }\n            return newCookie || null;\n          })();\n          return handlePromiseCallback(promise, callback);\n        },\n\n        remove: function (details, callback) {\n          const promise = (async () => {\n            if (\n              typeof _cookieDelete !== \"function\" ||\n              typeof _cookieList !== \"function\"\n            ) {\n              throw new Error(\"_cookieDelete or _cookieList not defined\");\n            }\n            const oldCookies = await _cookieList(details);\n            const oldCookie = oldCookies && oldCookies[0];\n\n            if (!oldCookie) return null; // Nothing to remove\n\n            await _cookieDelete(details);\n\n            broadcastCookieChange({\n              cause: \"explicit\",\n              cookie: oldCookie,\n              removed: true,\n            });\n\n            return {\n              url: details.url,\n              name: details.name,\n              storeId: details.storeId || \"0\",\n              partitionKey: details.partitionKey,\n            };\n          })();\n          return handlePromiseCallback(promise, callback);\n        },\n\n        getAllCookieStores: function (callback) {\n          const promise = Promise.resolve([\n            { id: \"0\", tabIds: [1] }, // Mock store for the current context\n          ]);\n          return handlePromiseCallback(promise, callback);\n        },\n\n        getPartitionKey: function (details, callback) {\n          _warn(\n            \"chrome.cookies.getPartitionKey is not supported in this environment.\",\n          );\n          const promise = Promise.resolve({ partitionKey: {} }); // Return empty partition key\n          return handlePromiseCallback(promise, callback);\n        },\n\n        onChanged: {\n          addListener: (callback) => {\n            if (typeof callback === \"function\") {\n              cookieChangeListeners.add(callback);\n            }\n          },\n          removeListener: (callback) => {\n            cookieChangeListeners.delete(callback);\n          },\n        },\n      };\n    })(),\n    tabs: {\n      query: async (queryInfo) => {\n        _warn(\"chrome.tabs.query polyfill only returns current tab info.\");\n        const dummyId = Math.floor(Math.random() * 1000) + 1;\n        return [\n          {\n            id: dummyId,\n            url: CURRENT_LOCATION,\n            active: true,\n            windowId: 1,\n            status: \"complete\",\n          },\n        ];\n      },\n      create: async ({ url, active = true }) => {\n        _log(`[Polyfill tabs.create] URL: ${url}`);\n        if (typeof _openTab !== \"function\")\n          throw new Error(\"_openTab not defined\");\n        _openTab(url, active);\n        const dummyId = Math.floor(Math.random() * 1000) + 1001;\n        return Promise.resolve({\n          id: dummyId,\n          url: url,\n          active,\n          windowId: 1,\n        });\n      },\n      sendMessage: async (tabId, message) => {\n        _warn(\n          `chrome.tabs.sendMessage polyfill (to tab ${tabId}) redirects to runtime.sendMessage (current context).`,\n        );\n        return chrome.runtime.sendMessage(message);\n      },\n      onActivated: createNoopListeners(),\n      onUpdated: createNoopListeners(),\n      onRemoved: createNoopListeners(),\n      onReplaced: createNoopListeners(),\n      onCreated: createNoopListeners(),\n      onMoved: createNoopListeners(),\n      onDetached: createNoopListeners(),\n      onAttached: createNoopListeners(),\n    },\n    windows: {\n      onFocusChanged: createNoopListeners(),\n      onCreated: createNoopListeners(),\n      onRemoved: createNoopListeners(),\n      onFocused: createNoopListeners(),\n      onFocus: createNoopListeners(),\n      onBlur: createNoopListeners(),\n      onFocused: createNoopListeners(),\n    },\n    notifications: {\n      create: async (notificationId, options) => {\n        try {\n          let id = notificationId;\n          let notificationOptions = options;\n\n          if (typeof notificationId === \"object\" && notificationId !== null) {\n            notificationOptions = notificationId;\n            id = \"notification_\" + Math.random().toString(36).substring(2, 15);\n          } else if (typeof notificationId === \"string\" && options) {\n            id = notificationId;\n            notificationOptions = options;\n          } else {\n            throw new Error(\"Invalid parameters for notifications.create\");\n          }\n\n          if (!notificationOptions || typeof notificationOptions !== \"object\") {\n            throw new Error(\"Notification options must be an object\");\n          }\n\n          const {\n            title,\n            message,\n            iconUrl,\n            type = \"basic\",\n          } = notificationOptions;\n\n          if (!title || !message) {\n            throw new Error(\"Notification must have title and message\");\n          }\n\n          if (\"Notification\" in window) {\n            if (Notification.permission === \"granted\") {\n              const notification = new Notification(title, {\n                body: message,\n                icon: iconUrl,\n                tag: id,\n              });\n\n              _log(`[Notifications] Created notification: ${id}`);\n              return id;\n            } else if (Notification.permission === \"default\") {\n              const permission = await Notification.requestPermission();\n              if (permission === \"granted\") {\n                const notification = new Notification(title, {\n                  body: message,\n                  icon: iconUrl,\n                  tag: id,\n                });\n                _log(\n                  `[Notifications] Created notification after permission: ${id}`,\n                );\n                return id;\n              } else {\n                _warn(\"[Notifications] Permission denied for notifications\");\n                return id;\n              }\n            } else {\n              _warn(\"[Notifications] Notifications are blocked\");\n              return id;\n            }\n          } else {\n            _warn(\n              \"[Notifications] Native notifications not supported, using console fallback\",\n            );\n            _log(`[Notification] ${title}: ${message}`);\n            return id;\n          }\n        } catch (error) {\n          _error(\"[Notifications] Error creating notification:\", error.message);\n          throw error;\n        }\n      },\n      clear: async (notificationId) => {\n        _log(`[Notifications] Clear notification: ${notificationId}`);\n        // For native notifications, there's no direct way to clear by ID\n        // This is a limitation of the Web Notifications API\n        return true;\n      },\n      getAll: async () => {\n        _warn(\"[Notifications] getAll not fully supported in polyfill\");\n        return {};\n      },\n      getPermissionLevel: async () => {\n        if (\"Notification\" in window) {\n          const permission = Notification.permission;\n          return { level: permission === \"granted\" ? \"granted\" : \"denied\" };\n        }\n        return { level: \"denied\" };\n      },\n    },\n    contextMenus: {\n      create: (createProperties, callback) => {\n        try {\n          if (!createProperties || typeof createProperties !== \"object\") {\n            throw new Error(\"Context menu create properties must be an object\");\n          }\n\n          const { id, title, contexts = [\"page\"], onclick } = createProperties;\n          const menuId =\n            id || `menu_${Math.random().toString(36).substring(2, 15)}`;\n\n          if (!title || typeof title !== \"string\") {\n            throw new Error(\"Context menu must have a title\");\n          }\n\n          // Store menu items for potential use\n          if (!window._polyfillContextMenus) {\n            window._polyfillContextMenus = new Map();\n          }\n\n          window._polyfillContextMenus.set(menuId, {\n            id: menuId,\n            title,\n            contexts,\n            onclick,\n            enabled: createProperties.enabled !== false,\n          });\n\n          _log(\n            `[ContextMenus] Created context menu item: ${title} (${menuId})`,\n          );\n\n          // Try to register a menu command as fallback\n          if (typeof _registerMenuCommand === \"function\") {\n            try {\n              _registerMenuCommand(\n                title,\n                onclick ||\n                  (() => {\n                    _log(`Context menu clicked: ${title}`);\n                  }),\n              );\n            } catch (e) {\n              _warn(\n                \"[ContextMenus] Failed to register as menu command:\",\n                e.message,\n              );\n            }\n          }\n\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n\n          return menuId;\n        } catch (error) {\n          _error(\"[ContextMenus] Error creating context menu:\", error.message);\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n          throw error;\n        }\n      },\n      update: (id, updateProperties, callback) => {\n        try {\n          if (\n            !window._polyfillContextMenus ||\n            !window._polyfillContextMenus.has(id)\n          ) {\n            throw new Error(`Context menu item not found: ${id}`);\n          }\n\n          const menuItem = window._polyfillContextMenus.get(id);\n          Object.assign(menuItem, updateProperties);\n\n          _log(`[ContextMenus] Updated context menu item: ${id}`);\n\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n        } catch (error) {\n          _error(\"[ContextMenus] Error updating context menu:\", error.message);\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n        }\n      },\n      remove: (menuItemId, callback) => {\n        try {\n          if (\n            window._polyfillContextMenus &&\n            window._polyfillContextMenus.has(menuItemId)\n          ) {\n            window._polyfillContextMenus.delete(menuItemId);\n            _log(`[ContextMenus] Removed context menu item: ${menuItemId}`);\n          } else {\n            _warn(\n              `[ContextMenus] Context menu item not found for removal: ${menuItemId}`,\n            );\n          }\n\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n        } catch (error) {\n          _error(\"[ContextMenus] Error removing context menu:\", error.message);\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n        }\n      },\n      removeAll: (callback) => {\n        try {\n          if (window._polyfillContextMenus) {\n            const count = window._polyfillContextMenus.size;\n            window._polyfillContextMenus.clear();\n            _log(`[ContextMenus] Removed all ${count} context menu items`);\n          }\n\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n        } catch (error) {\n          _error(\n            \"[ContextMenus] Error removing all context menus:\",\n            error.message,\n          );\n          if (callback && typeof callback === \"function\") {\n            setTimeout(() => callback(), 0);\n          }\n        }\n      },\n      onClicked: {\n        addListener: (callback) => {\n          if (!window._polyfillContextMenuListeners) {\n            window._polyfillContextMenuListeners = new Set();\n          }\n          window._polyfillContextMenuListeners.add(callback);\n          _log(\"[ContextMenus] Added click listener\");\n        },\n        removeListener: (callback) => {\n          if (window._polyfillContextMenuListeners) {\n            window._polyfillContextMenuListeners.delete(callback);\n            _log(\"[ContextMenus] Removed click listener\");\n          }\n        },\n      },\n    },\n  };\n\n  const tc = (fn) => {\n    try {\n      fn();\n    } catch (e) {}\n  };\n  const loggingProxyHandler = (_key) => ({\n    get(target, key, receiver) {\n      tc(() => _log(`[${contextType}] [CHROME - ${_key}] Getting ${key}`));\n      return Reflect.get(target, key, receiver);\n    },\n    set(target, key, value, receiver) {\n      tc(() =>\n        _log(`[${contextType}] [CHROME - ${_key}] Setting ${key} to ${value}`),\n      );\n      return Reflect.set(target, key, value, receiver);\n    },\n    has(target, key) {\n      tc(() =>\n        _log(`[${contextType}] [CHROME - ${_key}] Checking if ${key} exists`),\n      );\n      return Reflect.has(target, key);\n    },\n  });\n  chrome = Object.fromEntries(\n    Object.entries(chrome).map(([key, value]) => [\n      key,\n      new Proxy(value, loggingProxyHandler(key)),\n    ]),\n  );\n\n  // Alias browser to chrome for common Firefox pattern\n  const browser = new Proxy(chrome, loggingProxyHandler);\n\n  const oldGlobalThis = globalThis;\n  const oldWindow = window;\n  const oldSelf = self;\n  const oldGlobal = globalThis;\n  const __globalsStorage = {};\n\n  const TO_MODIFY = [oldGlobalThis, oldWindow, oldSelf, oldGlobal];\n  const set = (k, v) => {\n    __globalsStorage[k] = v;\n    TO_MODIFY.forEach((target) => {\n      target[k] = v;\n    });\n  };\n  const proxyHandler = {\n    get(target, key, receiver) {\n      const fns = [\n        () => __globalsStorage[key],\n        () => Reflect.get(target, key, target),\n        () => target[key],\n      ];\n      const out = fns\n        .map((f) => {\n          try {\n            let out = f();\n            return out;\n          } catch (e) {\n            return undefined;\n          }\n        })\n        .find((f) => f !== undefined);\n      if (typeof out === \"function\") {\n        return out.bind(target);\n      }\n      return out;\n    },\n    set(target, key, value, receiver) {\n      try {\n        tc(() => _log(`[${contextType}] Setting ${key} to ${value}`));\n        set(key, value);\n        return Reflect.set(target, key, value, receiver);\n      } catch (e) {\n        _error(\"Error setting\", key, value, e);\n        try {\n          target[key] = value;\n          return true;\n        } catch (e) {\n          _error(\"Error setting\", key, value, e);\n        }\n        return false;\n      }\n    },\n    has(target, key) {\n      try {\n        return key in __globalsStorage || key in target;\n      } catch (e) {\n        _error(\"Error has\", key, e);\n        try {\n          return key in __globalsStorage || key in target;\n        } catch (e) {\n          _error(\"Error has\", key, e);\n        }\n        return false;\n      }\n    },\n    getOwnPropertyDescriptor(target, key) {\n      try {\n        if (key in __globalsStorage) {\n          return {\n            configurable: true,\n            enumerable: true,\n            writable: true,\n            value: __globalsStorage[key],\n          };\n        }\n        // fall back to the real globalThis\n        const desc = Reflect.getOwnPropertyDescriptor(target, key);\n        // ensure it's configurable so the with‑scope binding logic can override it\n        if (desc && !desc.configurable) {\n          desc.configurable = true;\n        }\n        return desc;\n      } catch (e) {\n        _error(\"Error getOwnPropertyDescriptor\", key, e);\n        return {\n          configurable: true,\n          enumerable: true,\n          writable: true,\n          value: undefined,\n        };\n      }\n    },\n\n    defineProperty(target, key, descriptor) {\n      try {\n        // Normalize descriptor to avoid mixed accessor & data attributes\n        const hasAccessor = \"get\" in descriptor || \"set\" in descriptor;\n\n        if (hasAccessor) {\n          // Build a clean descriptor without value/writable when accessors present\n          const normalized = {\n            configurable:\n              \"configurable\" in descriptor ? descriptor.configurable : true,\n            enumerable:\n              \"enumerable\" in descriptor ? descriptor.enumerable : false,\n          };\n          if (\"get\" in descriptor) normalized.get = descriptor.get;\n          if (\"set\" in descriptor) normalized.set = descriptor.set;\n\n          // Store accessor references for inspection but avoid breaking invariants\n          set(key, {\n            get: descriptor.get,\n            set: descriptor.set,\n          });\n\n          return Reflect.defineProperty(target, key, normalized);\n        }\n\n        // Data descriptor path\n        set(key, descriptor.value);\n        return Reflect.defineProperty(target, key, descriptor);\n      } catch (e) {\n        _error(\"Error defineProperty\", key, descriptor, e);\n        return false;\n      }\n    },\n  };\n\n  // Create proxies once proxyHandler is defined\n  const proxyWindow = new Proxy(oldWindow, proxyHandler);\n  const proxyGlobalThis = new Proxy(oldGlobalThis, proxyHandler);\n  const proxyGlobal = new Proxy(oldGlobal, proxyHandler);\n  const proxySelf = new Proxy(oldSelf, proxyHandler);\n\n  // Seed storage with core globals so lookups succeed inside `with` blocks\n  Object.assign(__globalsStorage, {\n    chrome,\n    browser,\n    window: proxyWindow,\n    globalThis: proxyGlobalThis,\n    global: proxyGlobal,\n    self: proxySelf,\n    document: oldWindow.document,\n  });\n\n  const __globals = {\n    chrome,\n    browser,\n    window: proxyWindow,\n    globalThis: proxyGlobalThis,\n    global: proxyGlobal,\n    self: proxySelf,\n    __globals: __globalsStorage,\n  };\n\n  __globals.contextId = contextId;\n  __globals.contextType = contextType;\n  __globals.module = undefined;\n  __globals.amd = undefined;\n  __globals.define = undefined;\n  __globals.importScripts = (...args) => {\n    _log(\"importScripts\", args);\n  };\n\n  return __globals;\n}\n\n\nif (typeof window !== 'undefined') {\n    window.buildPolyfill = buildPolyfill;\n}\n"
			  let newMap = JSON.parse(JSON.stringify(EXTENSION_ASSETS_MAP));
			  delete newMap[OPTIONS_PAGE_PATH];
			  const PASS_ON = Object.fromEntries(
			    Object.entries({
			      LOCALE_KEYS,
			      INJECTED_MANIFEST,
			      USED_LOCALE,
			      EXTENSION_ICON,
			      CURRENT_LOCATION,
			      OPTIONS_PAGE_PATH,
			      CAN_USE_BLOB_CSP,
			      ALL_PERMISSIONS,
			      ORIGIN_PERMISSIONS,
			      EXTENSION_PERMISSIONS,
			      SCRIPT_NAME,
			      _base64ToBlob,
			      _getMimeTypeFromPath,
			      _isTextAsset,
			      _createAssetUrl,
			      _matchGlobPattern,
			      _isWebAccessibleResource,
			      _log,
			      _warn,
			      _error,
			    }).map((i) => {
			      let out = [...i];
			      if (typeof i[1] === "function") {
			        out[1] = i[1].toString();
			      } else {
			        out[1] = JSON.stringify(i[1]);
			      }
			      return out;
			    })
			  );
			  _log(PASS_ON);
			  return `
			    ${Object.entries(PASS_ON)
			      .map(
			        (i) =>
			          `const ${i[0]} = ${i[1]};\nwindow[${JSON.stringify(i[0])}] = ${i[0]}`
			      )
			      .join("\n")}
			
			        _log("Initialized polyfill", {${Object.keys(PASS_ON).join(", ")}})
			        ${polyfillString.replaceAll("{{EXTENSION_ASSETS_MAP}}", `JSON.parse(unescape(atob("${btoa(encodeURIComponent(JSON.stringify(EXTENSION_ASSETS_MAP)))}")))`)}
			
			        // Initialize the polyfill context for options page
			        const polyfillCtx = buildPolyfill({ isOtherPage: true });
			        const APPLY_TO = [window, self, globalThis];
			        for (const obj of APPLY_TO) {
			            obj.chrome = polyfillCtx.chrome;
			            obj.browser = polyfillCtx.browser;
			            obj.INJECTED_MANIFEST = ${JSON.stringify(INJECTED_MANIFEST)};
			        }
			    `;
			}
			
			async function main() {
			  _log(`Initializing...`, performance.now());
			
			  if (typeof _initStorage === "function") {
			    try {
			      _initStorage()
			        .then(() => {
			          _log(`Storage initialized.`);
			        })
			        .catch((e) => {
			          _error("Error during storage initialization:", e);
			        });
			    } catch (e) {
			      _error("Error during storage initialization:", e);
			    }
			  }
			
			  _log(`Starting content scripts...`);
			
			  const currentUrl = window.location.href;
			  let shouldRunAnyScript = false;
			  _log(`Checking URL: ${currentUrl}`);
			
			  if (
			    CONTENT_SCRIPT_CONFIGS_FOR_MATCHING &&
			    CONTENT_SCRIPT_CONFIGS_FOR_MATCHING.length > 0
			  ) {
			    for (const config of CONTENT_SCRIPT_CONFIGS_FOR_MATCHING) {
			      if (
			        config.matches &&
			        config.matches.some((pattern) => {
			          try {
			            const regex = convertMatchPatternToRegExp(pattern);
			            if (regex.test(currentUrl)) {
			              return true;
			            }
			            return false;
			          } catch (e) {
			            _error(`Error testing match pattern "${pattern}":`, e);
			            return false;
			          }
			        })
			      ) {
			        shouldRunAnyScript = true;
			        _log(`URL match found via config:`, config);
			        break;
			      }
			    }
			  } else {
			    _log(`No content script configurations found in manifest data.`);
			  }
			
			  if (shouldRunAnyScript) {
			    let polyfillContext;
			    try {
			      polyfillContext = buildPolyfill({ isBackground: false });
			    } catch (e) {
			      _error(`Failed to build polyfill:`, e);
			      return;
			    }
			
			    _log(`Polyfill built. Executing combined script logic...`);
			    // async function executeAllScripts({chrome, browser, global, window, globalThis, self, __globals}, extensionCssData) {
			    await executeAllScripts.call(
			      polyfillContext.globalThis,
			      polyfillContext,
			      extensionCssData
			    );
			  } else {
			    _log(
			      `No matching content script patterns for this URL. No scripts will be executed.`
			    );
			  }
			
			  if (OPTIONS_PAGE_PATH) {
			    if (typeof _registerMenuCommand === "function") {
			      try {
			        _registerMenuCommand("Open Options", openOptionsPage);
			        _log(`Options menu command registered.`);
			      } catch (e) {
			        _error("Failed to register menu command", e);
			      }
			    }
			  }
			
			  if (POPUP_PAGE_PATH) {
			    if (typeof _registerMenuCommand === "function") {
			      try {
			        _registerMenuCommand("Open Popup", openPopupPage);
			        _log(`Popup menu command registered.`);
			      } catch (e) {
			        _error("Failed to register popup menu command", e);
			      }
			    }
			  }
			
			  _log(`Initialization sequence complete.`);
			}
			
			main()//.catch((e) => _error(`Error during script initialization:`, e));
			
			try {
			  const fnKey = "OPEN_OPTIONS_PAGE_" + String(SCRIPT_NAME).replace(/\s+/g, "_");
			  window[fnKey] = openOptionsPage;
			} catch (e) {}
			
			try {
			  const fnKey = "OPEN_POPUP_PAGE_" + String(SCRIPT_NAME).replace(/\s+/g, "_");
			  window[fnKey] = openPopupPage;
			} catch (e) {}
			
			
			})();
  // #endregion
  // #endregion
    // #endregion