JanitorAI Character Card Scraper

Export JanitorAI character cards as SillyTavern-compatible PNGs.

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 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         JanitorAI Character Card Scraper
// @namespace    https://sleazyfork.org/en/scripts/537206-janitorai-character-card-scraper
// @version      0.5.0
// @description  Export JanitorAI character cards as SillyTavern-compatible PNGs.
// @match        https://janitorai.com/*
// @icon         https://ella.janitorai.com/hotlink-ok/favicon-32x32.png
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

var JanitorAICharacterCardScraper = (function(exports) {
  "use strict";
  function isObjectLike(value) {
    return value !== null && typeof value === "object";
  }
  function isPlainObject(value) {
    return isObjectLike(value) && !Array.isArray(value);
  }
  function asPlainObject(value) {
    return isPlainObject(value) ? value : null;
  }
  function asPlainObjectOrEmpty(value) {
    return asPlainObject(value) ?? {};
  }
  function cloneShallow(value) {
    if (Array.isArray(value)) return [...value];
    if (isPlainObject(value)) return { ...value };
    return value;
  }
  function asArray(value) {
    if (Array.isArray(value)) return value;
    if (value === null || value === void 0) return [];
    return [value];
  }
  function asArrayStrict(value) {
    return Array.isArray(value) ? value : [];
  }
  function uniqueStrings(values, { allowEmpty = false } = {}) {
    const seen = /* @__PURE__ */ new Set();
    const output = [];
    for (const item of asArray(values)) {
      const normalized = typeof item === "string" ? item.trim() : "";
      if (!allowEmpty && !normalized) continue;
      if (seen.has(normalized)) continue;
      seen.add(normalized);
      output.push(normalized);
    }
    return output;
  }
  function asString(value) {
    return typeof value === "string" ? value : "";
  }
  function asTrimmedString(value) {
    return typeof value === "string" ? value.trim() : "";
  }
  function normalizeOptionalString(value) {
    const normalized = asTrimmedString(value);
    return normalized || null;
  }
  function normalizeStringArray(values) {
    return uniqueStrings(values);
  }
  function toAbsoluteUrl(baseUrl, path) {
    if (path.startsWith("http://") || path.startsWith("https://")) {
      return path;
    }
    return `${baseUrl}${path}`;
  }
  function normalizeChatIdValue(value) {
    if (typeof value === "number" && Number.isFinite(value)) {
      return value;
    }
    const normalizedString = asTrimmedString(value);
    if (!normalizedString) return null;
    if (/^\d+$/.test(normalizedString)) {
      const numeric = Number(normalizedString);
      if (Number.isFinite(numeric)) return numeric;
    }
    return normalizedString;
  }
  function normalizeBoolean(value, fallback = false) {
    return typeof value === "boolean" ? value : fallback;
  }
  function resolveLatestMessageCandidate(messageResponse) {
    if (Array.isArray(messageResponse)) {
      for (let index = messageResponse.length - 1; index >= 0; index -= 1) {
        const candidate = messageResponse[index];
        if (isPlainObject(candidate)) return candidate;
      }
      return null;
    }
    return isPlainObject(messageResponse) ? messageResponse : null;
  }
  function mergeChatMessages(baseMessages, latestMessage) {
    const merged = Array.isArray(baseMessages) ? [...baseMessages] : [];
    if (!latestMessage) return merged;
    const knownIds = new Set(merged.map((message) => message.id));
    if (!knownIds.has(latestMessage.id)) {
      merged.push(latestMessage);
    }
    return merged;
  }
  function resolveChatSource(chatPayload = null, chat = null) {
    const directChat = asPlainObjectOrEmpty(chat);
    if (Object.keys(directChat).length > 0) return directChat;
    const nestedChat = asPlainObjectOrEmpty(chatPayload?.chat);
    if (Object.keys(nestedChat).length > 0) return nestedChat;
    return asPlainObjectOrEmpty(chatPayload);
  }
  function buildGenerateAlphaChat(chatPayload = null, chat = null) {
    const source = resolveChatSource(chatPayload, chat);
    const normalizedChat = {
      character_id: asString(source.character_id ?? source.characterId).trim(),
      id: normalizeChatIdValue(source.id ?? source.chat_id ?? source.chatId),
      summary: asString(source.summary),
      user_id: asString(source.user_id ?? source.userId).trim()
    };
    if (!normalizedChat.character_id) {
      throw new Error("GenerateAlpha payload requires a character_id.");
    }
    if (normalizedChat.id === null) {
      throw new Error("GenerateAlpha payload requires a chat ID.");
    }
    return normalizedChat;
  }
  function buildGenerateAlphaChatMessage(messageCandidate) {
    const source = asPlainObjectOrEmpty(messageCandidate);
    const message = typeof source.message === "string" ? source.message : "";
    if (!message.trim()) return null;
    const chatId = normalizeChatIdValue(source.chat_id ?? source.chatId);
    const messageId = normalizeChatIdValue(source.id);
    if (chatId === null || messageId === null) return null;
    return {
      chat_id: chatId,
      created_at: asString(source.created_at ?? source.createdAt),
      id: messageId,
      is_bot: normalizeBoolean(source.is_bot ?? source.isBot, false),
      is_main: normalizeBoolean(source.is_main ?? source.isMain, true),
      message
    };
  }
  function resolveChatMessagesSource({
    chatMessages = null,
    chatPayload = null
  } = {}) {
    if (Array.isArray(chatMessages)) return chatMessages;
    if (Array.isArray(chatPayload?.chatMessages)) return chatPayload.chatMessages;
    return [];
  }
  const Placeholder = (() => {
    const uuidSegment = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID().split("-")[0] : Math.random().toString(36).slice(2, 10);
    const prefix = `__VM_${uuidSegment}_`;
    let c = 0;
    return {
      generate: () => prefix + c++,
      prefix
    };
  })();
  function buildGenerateAlphaProfile(profile = null) {
    if (!isPlainObject(profile)) return null;
    const source = asPlainObjectOrEmpty(profile);
    const normalizedProfile = {};
    const id = normalizeOptionalString(source.id);
    const userAppearance = normalizeOptionalString(
      source.user_appearance ?? source.userAppearance
    );
    const userName = normalizeOptionalString(source.user_name ?? source.userName);
    if (id) normalizedProfile.id = id;
    normalizedProfile.name = Placeholder.generate();
    if (userAppearance) normalizedProfile.user_appearance = userAppearance;
    if (userName) normalizedProfile.user_name = userName;
    return Object.keys(normalizedProfile).length > 0 ? normalizedProfile : null;
  }
  function hasOwn(source, propertyName) {
    return Object.prototype.hasOwnProperty.call(source, propertyName);
  }
  function readOwnMappedValue(source, propertyNames) {
    for (const propertyName of propertyNames) {
      if (!hasOwn(source, propertyName)) continue;
      return { found: true, value: source[propertyName] };
    }
    return { found: false, value: void 0 };
  }
  const GENERATE_ALPHA_USER_CONFIG_FIELD_MAPPINGS = Object.freeze([
    ["allow_mobile_nsfw", ["allow_mobile_nsfw", "allowMobileNsfw"]],
    ["api", ["api"]],
    ["bad_words", ["bad_words", "badWords"]],
    ["generation_settings", ["generation_settings", "generationSettings"]],
    ["llm_prompt", ["llm_prompt", "llmPrompt"]],
    [
      "open_ai_jailbreak_prompt",
      ["open_ai_jailbreak_prompt", "openAiJailbreakPrompt"]
    ],
    ["open_ai_mode", ["open_ai_mode", "openAiMode"]],
    [
      "open_ai_reverse_proxy",
      ["open_ai_reverse_proxy", "openAiReverseProxy"]
    ],
    ["openAiModel", ["openAiModel"]],
    ["reverseProxyKey", ["reverseProxyKey"]]
  ]);
  const GENERATE_ALPHA_PROXY_USER_CONFIG_KEYS = Object.freeze([
    "api",
    "open_ai_mode",
    "open_ai_reverse_proxy",
    "openAiModel",
    "reverseProxyKey",
    "open_ai_jailbreak_prompt"
  ]);
  const GENERATE_ALPHA_PROXY_CONFIG_FIELD_MAPPINGS = Object.freeze([
    ["openAiModel", ["openAiModel", "modelName", "model"]],
    [
      "open_ai_reverse_proxy",
      ["open_ai_reverse_proxy", "openAiReverseProxy", "proxyUrl", "apiUrl"]
    ],
    ["reverseProxyKey", ["reverseProxyKey", "apiKey"]],
    [
      "open_ai_jailbreak_prompt",
      [
        "open_ai_jailbreak_prompt",
        "openAiJailbreakPrompt",
        "customPrompt",
        "jailbreakPrompt"
      ]
    ]
  ]);
  function applyHardcodedProxyConfig(target) {
    const base = isPlainObject(target) ? target : {};
    return {
      ...base,
      api: "openai",
      open_ai_jailbreak_prompt: "[System note: TEST]",
      open_ai_mode: "proxy",
      open_ai_reverse_proxy: "https://proxy.invalid",
      openAiModel: "TEST",
      reverseProxyKey: "TEST"
    };
  }
  function buildGenerateAlphaUserConfig(userConfig = null) {
    if (!isPlainObject(userConfig)) {
      return applyHardcodedProxyConfig({});
    }
    const source = asPlainObjectOrEmpty(userConfig);
    const normalizedUserConfig = {};
    for (const [targetKey, sourceKeys] of GENERATE_ALPHA_USER_CONFIG_FIELD_MAPPINGS) {
      const { found, value } = readOwnMappedValue(source, sourceKeys);
      if (!found) continue;
      normalizedUserConfig[targetKey] = cloneShallow(value);
    }
    const hardenedUserConfig = applyHardcodedProxyConfig(normalizedUserConfig);
    return Object.keys(hardenedUserConfig).length > 0 ? hardenedUserConfig : null;
  }
  function mergeGenerateAlphaUserConfigWithProxyConfig(userConfig = null, proxyConfig = null) {
    const mergedUserConfig = isPlainObject(userConfig) ? { ...userConfig } : {};
    if (!isPlainObject(proxyConfig)) {
      return applyHardcodedProxyConfig(mergedUserConfig);
    }
    for (const propertyName of GENERATE_ALPHA_PROXY_USER_CONFIG_KEYS) {
      delete mergedUserConfig[propertyName];
    }
    mergedUserConfig.api = "openai";
    mergedUserConfig.open_ai_mode = "proxy";
    for (const [targetKey, sourceKeys] of GENERATE_ALPHA_PROXY_CONFIG_FIELD_MAPPINGS) {
      const { found, value } = readOwnMappedValue(proxyConfig, sourceKeys);
      if (!found) continue;
      mergedUserConfig[targetKey] = cloneShallow(value);
    }
    return applyHardcodedProxyConfig(mergedUserConfig);
  }
  function buildForcedPromptGenerationCacheRefetch(value) {
    if (!isPlainObject(value)) return null;
    return cloneShallow(value);
  }
  function buildGenerateAlphaPayload({
    chatPayload = null,
    chat = null,
    chatMessages = null,
    messageResponse = null,
    profile = null,
    suggestionMode = void 0,
    suggestionPerspective = void 0,
    userConfig = null,
    clientPlatform = "web",
    generateMode = "NEW",
    generateType = "CHAT",
    forcedPromptGenerationCacheRefetch = null
  } = {}) {
    const normalizedChat = buildGenerateAlphaChat(chatPayload, chat);
    const baseMessages = resolveChatMessagesSource({
      chatMessages,
      chatPayload
    }).map(buildGenerateAlphaChatMessage).filter(Boolean);
    const latestMessageCandidate = resolveLatestMessageCandidate(messageResponse);
    const latestMessage = buildGenerateAlphaChatMessage(latestMessageCandidate);
    if (!latestMessage) {
      throw new Error(
        "GenerateAlpha payload requires latest user message context."
      );
    }
    let normalizedMessages = mergeChatMessages(baseMessages, latestMessage);
    if (normalizedMessages.length === 0) {
      throw new Error("GenerateAlpha payload requires at least one chat message.");
    }
    const normalizedProfile = buildGenerateAlphaProfile(profile);
    if (!normalizedProfile) {
      throw new Error("GenerateAlpha payload requires profile context.");
    }
    const normalizedUserConfig = buildGenerateAlphaUserConfig(userConfig);
    if (!normalizedUserConfig) {
      throw new Error("GenerateAlpha payload requires userConfig context.");
    }
    const normalizedForcedPromptGenerationCacheRefetch = buildForcedPromptGenerationCacheRefetch(forcedPromptGenerationCacheRefetch);
    if (!normalizedForcedPromptGenerationCacheRefetch) {
      throw new Error(
        "GenerateAlpha payload requires forcedPromptGenerationCacheRefetch context."
      );
    }
    const payload = {
      chat: normalizedChat,
      chatMessages: normalizedMessages,
      clientPlatform: asString(clientPlatform).trim() || "web",
      forcedPromptGenerationCacheRefetch: normalizedForcedPromptGenerationCacheRefetch,
      generateMode: asString(generateMode).trim() || "NEW",
      generateType: asString(generateType).trim() || "CHAT",
      profile: normalizedProfile,
      userConfig: normalizedUserConfig
    };
    if (suggestionMode !== void 0) {
      payload.suggestionMode = suggestionMode;
    }
    if (suggestionPerspective !== void 0) {
      payload.suggestionPerspective = suggestionPerspective;
    }
    return payload;
  }
  function buildSendMessageRequestBody(chatId, payload = {}) {
    const source = asPlainObjectOrEmpty(payload);
    const resolvedChatId = normalizeChatIdValue(
      source.chat_id ?? source.chatId ?? chatId
    );
    if (resolvedChatId === null) {
      throw new Error("Send-message payload requires a valid chat ID.");
    }
    const message = typeof source.message === "string" ? source.message : "";
    if (!message.trim()) {
      throw new Error("Send-message payload requires a non-empty message.");
    }
    const characterId = asString(
      source.character_id ?? source.characterId
    ).trim();
    const body = {
      is_bot: normalizeBoolean(source.is_bot ?? source.isBot, false),
      is_main: normalizeBoolean(source.is_main ?? source.isMain, true),
      message,
      chat_id: resolvedChatId
    };
    const scriptIds = normalizeStringArray(source.script_ids ?? source.scriptIds);
    if (scriptIds.length > 0) {
      body.script_ids = scriptIds;
    }
    if (characterId) {
      body.character_id = characterId;
    }
    const excludedKeys = /* @__PURE__ */ new Set([
      "characterId",
      "character_id",
      "chatId",
      "chat_id",
      "isBot",
      "is_bot",
      "isMain",
      "is_main",
      "message",
      "scriptIds",
      "script_ids"
    ]);
    for (const [key, value] of Object.entries(source)) {
      if (excludedKeys.has(key) || value === void 0) continue;
      body[key] = value;
    }
    return body;
  }
  const SUPABASE_AUTH_STORAGE_KEY_PATTERN = /(?:^|:)(sb-[a-z0-9-]+-auth-token)(?:\.(\d+))?$/i;
  const SUPABASE_LEGACY_AUTH_STORAGE_KEY = "supabase.auth.token";
  const BASE64_VALUE_PREFIX = "base64-";
  function normalizeAccessToken(value) {
    const normalized = asTrimmedString(value);
    if (!normalized) return null;
    return /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(normalized) ? normalized : null;
  }
  function readDocumentCookie(documentRef = globalThis.document) {
    try {
      return typeof documentRef?.cookie === "string" ? documentRef.cookie : "";
    } catch {
      return "";
    }
  }
  function parseCookieEntries(cookieString) {
    if (typeof cookieString !== "string" || !cookieString.trim()) {
      return [];
    }
    return cookieString.split(/;\s*/).map((cookiePart) => {
      const separatorIndex = cookiePart.indexOf("=");
      if (separatorIndex === -1) return null;
      const key = cookiePart.slice(0, separatorIndex).trim();
      const value = cookiePart.slice(separatorIndex + 1).trim();
      return key ? [key, value] : null;
    }).filter(Boolean);
  }
  function parseSupabaseAuthKey(key) {
    if (typeof key !== "string") return null;
    if (key === SUPABASE_LEGACY_AUTH_STORAGE_KEY) {
      return { baseKey: key, chunkIndex: null };
    }
    const match = key.match(SUPABASE_AUTH_STORAGE_KEY_PATTERN);
    if (!match?.[1]) return null;
    return {
      baseKey: match[1],
      chunkIndex: match[2] ? Number(match[2]) : null
    };
  }
  function decodeBase64Value(value) {
    const normalized = asTrimmedString(value);
    if (!normalized) return null;
    const encoded = normalized.startsWith(BASE64_VALUE_PREFIX) ? normalized.slice(BASE64_VALUE_PREFIX.length) : normalized;
    if (!encoded) return null;
    try {
      if (typeof globalThis.atob === "function") {
        const binary = globalThis.atob(encoded);
        const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
        if (typeof TextDecoder !== "undefined") {
          return new TextDecoder().decode(bytes);
        }
        return binary;
      }
    } catch {
    }
    try {
      if (typeof Buffer !== "undefined") {
        return Buffer.from(encoded, "base64").toString("utf8");
      }
    } catch {
    }
    return null;
  }
  function extractAccessTokenFromSessionCandidate(candidate, depth = 0) {
    if (depth > 6 || candidate == null) return null;
    const directToken = normalizeAccessToken(candidate);
    if (directToken) return directToken;
    if (typeof candidate === "string") {
      const trimmed = candidate.trim();
      if (!trimmed) return null;
      try {
        return extractAccessTokenFromSessionCandidate(JSON.parse(trimmed), depth + 1);
      } catch {
        if (trimmed.startsWith(BASE64_VALUE_PREFIX)) {
          const decoded = decodeBase64Value(trimmed);
          if (decoded) {
            return extractAccessTokenFromSessionCandidate(decoded, depth + 1);
          }
        }
        return null;
      }
    }
    if (Array.isArray(candidate)) {
      for (const item of candidate) {
        const nestedToken = extractAccessTokenFromSessionCandidate(item, depth + 1);
        if (nestedToken) return nestedToken;
      }
      return null;
    }
    if (typeof candidate === "object") {
      const directCandidates = [
        candidate.access_token,
        candidate.currentSession,
        candidate.session,
        candidate.data,
        candidate.user
      ];
      for (const value of directCandidates) {
        const nestedToken = extractAccessTokenFromSessionCandidate(value, depth + 1);
        if (nestedToken) return nestedToken;
      }
      for (const value of Object.values(candidate)) {
        const nestedToken = extractAccessTokenFromSessionCandidate(value, depth + 1);
        if (nestedToken) return nestedToken;
      }
    }
    return null;
  }
  function collectSupabaseSessionCandidates(entries = []) {
    const groupedCandidates = /* @__PURE__ */ new Map();
    const directCandidates = [];
    for (const [key, value] of entries) {
      const parsedKey = parseSupabaseAuthKey(key);
      if (!parsedKey) continue;
      if (parsedKey.baseKey === SUPABASE_LEGACY_AUTH_STORAGE_KEY) {
        directCandidates.push(value);
        continue;
      }
      const group = groupedCandidates.get(parsedKey.baseKey) ?? { directValues: [], chunks: /* @__PURE__ */ new Map() };
      if (parsedKey.chunkIndex === null) {
        group.directValues.push(value);
      } else {
        group.chunks.set(parsedKey.chunkIndex, value);
      }
      groupedCandidates.set(parsedKey.baseKey, group);
    }
    const candidates = [...directCandidates];
    for (const group of groupedCandidates.values()) {
      candidates.push(...group.directValues);
      if (group.chunks.size > 0) {
        candidates.push(
          Array.from(group.chunks.entries()).sort((left, right) => left[0] - right[0]).map(([, value]) => value).join("")
        );
      }
    }
    return candidates.filter((candidate) => candidate != null && candidate !== "");
  }
  function readAccessTokenFromEntries(entries = []) {
    for (const sessionCandidate of collectSupabaseSessionCandidates(entries)) {
      const token = extractAccessTokenFromSessionCandidate(sessionCandidate);
      if (token) return token;
    }
    return null;
  }
  function readAccessTokenFromDocumentCookies(documentRef = globalThis.document) {
    return readAccessTokenFromEntries(
      parseCookieEntries(readDocumentCookie(documentRef))
    );
  }
  function isSupabaseLikeClient$1(value) {
    return isObjectLike(value) && typeof value.auth?.getSession === "function";
  }
  async function readPageRuntimeAccessToken(runtimeBridge) {
    if (!isSupabaseLikeClient$1(runtimeBridge?.supabase)) return null;
    try {
      const sessionResult = await runtimeBridge.supabase.auth.getSession();
      return extractAccessTokenFromSessionCandidate(
        sessionResult?.data?.session ?? sessionResult?.data ?? sessionResult?.session ?? sessionResult
      );
    } catch {
      return null;
    }
  }
  function createAccessTokenResolver({
    documentRef,
    ensurePageRuntimeBridge
  }) {
    return async function resolveAccessToken({
      currentAuthorization = null,
      allowRuntimeFallback = true
    } = {}) {
      const heuristicAccessToken = readAccessTokenFromDocumentCookies(documentRef);
      if (heuristicAccessToken && (!currentAuthorization || `Bearer ${heuristicAccessToken}` !== currentAuthorization || !allowRuntimeFallback)) {
        return heuristicAccessToken;
      }
      if (!allowRuntimeFallback) {
        return heuristicAccessToken;
      }
      const runtimeAccessToken = await readPageRuntimeAccessToken(
        await ensurePageRuntimeBridge()
      );
      return runtimeAccessToken ?? heuristicAccessToken;
    };
  }
  const DEFAULT_TIMEOUT_MS = 15e3;
  const DEFAULT_ACCEPT_HEADER = "application/json, text/plain, */*";
  function parseMaybeJson(text) {
    if (typeof text !== "string" || !text.trim()) return null;
    try {
      return JSON.parse(text);
    } catch {
      return { rawText: text };
    }
  }
  function wait(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  function createFetchHeadersInit(headers) {
    const headerObject = Object.fromEntries(headers.entries());
    Object.defineProperties(headerObject, {
      get: {
        enumerable: false,
        value(name) {
          const normalizedName = asTrimmedString(name).toLowerCase();
          return normalizedName ? headerObject[normalizedName] ?? null : null;
        }
      },
      has: {
        enumerable: false,
        value(name) {
          const normalizedName = asTrimmedString(name).toLowerCase();
          return normalizedName ? Object.prototype.hasOwnProperty.call(headerObject, normalizedName) : false;
        }
      }
    });
    return headerObject;
  }
  function attachAbortSignal(externalSignal, controller) {
    if (!externalSignal) return;
    if (externalSignal.aborted) {
      controller.abort(externalSignal.reason);
      return;
    }
    externalSignal.addEventListener(
      "abort",
      () => {
        controller.abort(externalSignal.reason);
      },
      { once: true }
    );
  }
  function toJsonBody(headers, body) {
    if (body === null || body === void 0) return void 0;
    const hasFormData = typeof FormData !== "undefined";
    const hasBlob = typeof Blob !== "undefined";
    const hasArrayBuffer = typeof ArrayBuffer !== "undefined";
    if (typeof body === "string" || body instanceof URLSearchParams || hasFormData && body instanceof FormData || hasBlob && body instanceof Blob || hasArrayBuffer && body instanceof ArrayBuffer) {
      return body;
    }
    if (!headers.has("content-type")) {
      headers.set("content-type", "application/json");
    }
    return JSON.stringify(body);
  }
  function createJanitorRequestTransport({
    baseUrl,
    fetchImpl,
    defaultHeaders,
    defaultRetries,
    resolveAccessToken
  }) {
    return async function request(path, {
      method = "GET",
      headers = {},
      body = void 0,
      timeoutMs = DEFAULT_TIMEOUT_MS,
      retries = defaultRetries,
      signal = null
    } = {}) {
      const url = toAbsoluteUrl(baseUrl, path);
      let attempt = 0;
      let refreshedAuthorizationAfter401 = false;
      let accessTokenOverride = null;
      while (attempt <= retries) {
        const controller = new AbortController();
        attachAbortSignal(signal, controller);
        const timeoutId = setTimeout(() => {
          controller.abort(new Error("Request timed out."));
        }, timeoutMs);
        try {
          const mergedHeaders = new Headers(defaultHeaders);
          for (const [key, value] of Object.entries(headers)) {
            if (value !== void 0 && value !== null) {
              mergedHeaders.set(key, value);
            }
          }
          if (!mergedHeaders.has("accept")) {
            mergedHeaders.set("accept", DEFAULT_ACCEPT_HEADER);
          }
          const hadConfiguredAuthorization = mergedHeaders.has("authorization");
          if (!hadConfiguredAuthorization) {
            const accessToken = accessTokenOverride ?? await resolveAccessToken({
              allowRuntimeFallback: true
            });
            if (accessToken) {
              mergedHeaders.set("authorization", `Bearer ${accessToken}`);
            }
          }
          const requestAuthorization = mergedHeaders.get("authorization");
          const finalBody = toJsonBody(mergedHeaders, body);
          const response = await fetchImpl(url, {
            method,
            headers: createFetchHeadersInit(mergedHeaders),
            body: finalBody,
            credentials: "include",
            signal: controller.signal
          });
          const rawText = await response.text();
          const data = parseMaybeJson(rawText);
          if (!response.ok) {
            const error = new Error(
              `Janitor request failed: ${method} ${path} -> HTTP ${response.status}`
            );
            error.status = response.status;
            error.data = data;
            if (response.status === 401 && !hadConfiguredAuthorization && !refreshedAuthorizationAfter401) {
              refreshedAuthorizationAfter401 = true;
              const refreshedAccessToken = await resolveAccessToken({
                currentAuthorization: requestAuthorization,
                allowRuntimeFallback: true
              });
              if (refreshedAccessToken && `Bearer ${refreshedAccessToken}` !== requestAuthorization) {
                accessTokenOverride = refreshedAccessToken;
                continue;
              }
            }
            throw error;
          }
          return data;
        } catch (error) {
          if (typeof error?.status === "number") {
            throw error;
          }
          const isLastAttempt = attempt === retries;
          if (isLastAttempt) throw error;
          await wait(150 * (attempt + 1));
        } finally {
          clearTimeout(timeoutId);
        }
        attempt += 1;
      }
      throw new Error(`Unexpected retry loop exit for ${method} ${path}`);
    };
  }
  const GENERATE_ALPHA_USER_CONFIG_KEYS = Object.freeze([
    "allow_mobile_nsfw",
    "api",
    "bad_words",
    "generation_settings",
    "llm_prompt",
    "openAiModel",
    "open_ai_jailbreak_prompt",
    "open_ai_mode",
    "open_ai_reverse_proxy"
  ]);
  const GENERATE_ALPHA_RUNTIME_BRIDGE_STORE_KEYS = Object.freeze([
    "userStore",
    "userPersonaStore",
    "storageStore",
    "chatStore"
  ]);
  function readDocumentHtml(documentRef = globalThis.document) {
    try {
      if (typeof documentRef?.documentElement?.innerHTML === "string") {
        return documentRef.documentElement.innerHTML;
      }
      if (typeof documentRef?.documentElement?.outerHTML === "string") {
        return documentRef.documentElement.outerHTML;
      }
      if (typeof documentRef?.documentElement?.textContent === "string") {
        return documentRef.documentElement.textContent;
      }
    } catch {
    }
    return "";
  }
  function extractJsonObjectLiteral(source, startIndex) {
    let depth = 0;
    let inString = false;
    let escaped = false;
    for (let i2 = startIndex; i2 < source.length; i2 += 1) {
      const char = source[i2];
      if (inString) {
        if (escaped) {
          escaped = false;
          continue;
        }
        if (char === "\\") {
          escaped = true;
          continue;
        }
        if (char === '"') {
          inString = false;
        }
        continue;
      }
      if (char === '"') {
        inString = true;
        continue;
      }
      if (char === "{") {
        if (depth === 0) {
          startIndex = i2;
        }
        depth += 1;
        continue;
      }
      if (char === "}") {
        depth -= 1;
        if (depth === 0) {
          return source.slice(startIndex, i2 + 1);
        }
      }
    }
    return null;
  }
  function extractQuotedStringLiteral(source, startIndex) {
    const quote = source[startIndex];
    if (quote !== '"' && quote !== "'") return null;
    let escaped = false;
    for (let i2 = startIndex + 1; i2 < source.length; i2 += 1) {
      const char = source[i2];
      if (escaped) {
        escaped = false;
        continue;
      }
      if (char === "\\") {
        escaped = true;
        continue;
      }
      if (char === quote) {
        return source.slice(startIndex, i2 + 1);
      }
    }
    return null;
  }
  function parseStoreStateFromHtml(html) {
    const source = typeof html === "string" ? html : "";
    if (!source) return null;
    const assignPattern = /window\._storeState_\s*=\s*(?=\{|JSON\.parse\s*\()/;
    const assignMatch = assignPattern.exec(source);
    if (!assignMatch) return null;
    const cursor = assignMatch.index + assignMatch[0].length;
    const assignmentSlice = source.slice(cursor);
    if (assignmentSlice.startsWith("JSON.parse")) {
      const parenIndex = assignmentSlice.indexOf("(");
      if (parenIndex === -1) return null;
      let stringCursor = parenIndex + 1;
      while (stringCursor < assignmentSlice.length && /\s/.test(assignmentSlice[stringCursor])) {
        stringCursor += 1;
      }
      const rawLiteral = extractQuotedStringLiteral(assignmentSlice, stringCursor);
      if (!rawLiteral) return null;
      const decodedLiteral = JSON.parse(rawLiteral);
      return JSON.parse(decodedLiteral);
    }
    const objectStart = assignmentSlice.indexOf("{");
    if (objectStart === -1) return null;
    const rawObject = extractJsonObjectLiteral(assignmentSlice, objectStart);
    if (!rawObject) return null;
    return JSON.parse(rawObject);
  }
  function getDocumentWindow(documentRef = globalThis.document) {
    try {
      return documentRef?.defaultView ?? null;
    } catch {
      return null;
    }
  }
  function readStoreStateCandidate(store) {
    try {
      if (typeof store?.getState === "function") {
        const state2 = store.getState();
        if (isObjectLike(state2)) return state2;
      }
    } catch {
    }
    return isObjectLike(store) ? store : null;
  }
  function readBootstrapStoreState(documentRef = globalThis.document) {
    try {
      const windowRef = getDocumentWindow(documentRef) ?? globalThis;
      const liveState = windowRef?._storeState_;
      const resolvedLiveState = readStoreStateCandidate(liveState);
      if (resolvedLiveState) return resolvedLiveState;
    } catch {
    }
    const html = readDocumentHtml(documentRef);
    const storeState = parseStoreStateFromHtml(html);
    if (!storeState) {
      throw new Error("Unable to locate window._storeState_ in document HTML.");
    }
    return storeState;
  }
  function cloneRuntimeValue(value) {
    return cloneShallow(value);
  }
  function buildRuntimeProfile(userStoreState) {
    const source = asPlainObject(userStoreState);
    if (!source) return null;
    const profile = {};
    const id = asTrimmedString(source.id);
    const name = asTrimmedString(source.name);
    const userAppearance = asTrimmedString(source.profile);
    const userName = asTrimmedString(source.user_name ?? source.userName);
    if (id) profile.id = id;
    if (name) profile.name = name;
    if (userAppearance) profile.user_appearance = userAppearance;
    if (userName) profile.user_name = userName;
    return Object.keys(profile).length > 0 ? profile : null;
  }
  function buildRuntimeUserConfig(userStoreState) {
    const mergedSource = asPlainObject(userStoreState?.config ?? userStoreState);
    if (!mergedSource) return null;
    const userConfig = {};
    for (const propertyName of GENERATE_ALPHA_USER_CONFIG_KEYS) {
      if (!Object.prototype.hasOwnProperty.call(mergedSource, propertyName)) continue;
      userConfig[propertyName] = cloneRuntimeValue(mergedSource[propertyName]);
    }
    const selectedProxyConfigId = asTrimmedString(mergedSource.selectedProxyConfigId);
    const proxyConfigurations = Array.isArray(mergedSource.proxyConfigurations) ? mergedSource.proxyConfigurations : [];
    if (selectedProxyConfigId && proxyConfigurations.length > 0) {
      const matchedConfig = proxyConfigurations.find(
        (config) => asTrimmedString(config?.id) === selectedProxyConfigId
      );
      if (matchedConfig && Object.prototype.hasOwnProperty.call(matchedConfig, "apiKey")) {
        userConfig.reverseProxyKey = cloneRuntimeValue(matchedConfig.apiKey);
      }
    }
    return Object.keys(userConfig).length > 0 ? userConfig : null;
  }
  function buildRuntimeForcedPromptGenerationCacheRefetch(userStoreState) {
    const source = asPlainObject(
      userStoreState?.forcedPromptGenerationCacheRefetch ?? userStoreState
    );
    if (!source) return null;
    return { ...source };
  }
  function readGenerateAlphaRuntimeContext({ documentRef } = {}) {
    const storeState = readBootstrapStoreState(documentRef);
    const userState = asPlainObject(storeState?.user);
    if (!userState) {
      throw new Error("GenerateAlpha bootstrap is missing user state.");
    }
    const profile = buildRuntimeProfile(userState.profile);
    if (!profile) {
      throw new Error("GenerateAlpha requires user.profile bootstrap data.");
    }
    const userConfig = buildRuntimeUserConfig(userState.config);
    if (!userConfig) {
      throw new Error("GenerateAlpha requires user.config bootstrap data.");
    }
    const forcedPromptGenerationCacheRefetch = buildRuntimeForcedPromptGenerationCacheRefetch(
      userState.forcedPromptGenerationCacheRefetch
    );
    if (!forcedPromptGenerationCacheRefetch) {
      throw new Error(
        "GenerateAlpha requires forcedPromptGenerationCacheRefetch bootstrap data."
      );
    }
    const context = {
      profile,
      userConfig,
      forcedPromptGenerationCacheRefetch
    };
    return context;
  }
  function pushUniqueRef(refs, candidate) {
    if (!candidate || refs.includes(candidate)) return;
    refs.push(candidate);
  }
  function getDocumentScripts(documentRef) {
    if (!documentRef) return [];
    if (documentRef.scripts && typeof documentRef.scripts.length === "number") {
      return Array.from(documentRef.scripts);
    }
    if (typeof documentRef.querySelectorAll === "function") {
      return Array.from(documentRef.querySelectorAll("script"));
    }
    return [];
  }
  function getPerformanceResourceUrls(documentRef) {
    const documentWindow = getDocumentWindow(documentRef);
    const performanceRef = documentWindow?.performance ?? globalThis.performance;
    if (!performanceRef || typeof performanceRef.getEntriesByType !== "function") {
      return [];
    }
    try {
      return performanceRef.getEntriesByType("resource").filter((entry) => {
        const name = asTrimmedString(entry?.name);
        const initiatorType = asTrimmedString(entry?.initiatorType).toLowerCase();
        return Boolean(name) && (initiatorType === "script" || initiatorType === "link");
      }).map((entry) => entry.name);
    } catch {
      return [];
    }
  }
  function collectCandidateScriptUrls(documentRef, baseUrl) {
    let baseHostname = null;
    try {
      baseHostname = new URL(baseUrl).hostname;
    } catch {
      return [];
    }
    const uniqueUrls = /* @__PURE__ */ new Set();
    const rawUrls = [];
    for (const script of getDocumentScripts(documentRef)) {
      const rawUrl = typeof script?.src === "string" && script.src ? script.src : script?.getAttribute?.("src");
      if (rawUrl) rawUrls.push(rawUrl);
    }
    if (typeof documentRef?.querySelectorAll === "function") {
      for (const assetEl of documentRef.querySelectorAll(
        'link[rel="modulepreload"][href], link[rel="preload"][as="script"][href], link[as="script"][href]'
      )) {
        const rawUrl = typeof assetEl?.href === "string" && assetEl.href ? assetEl.href : assetEl?.getAttribute?.("href");
        if (rawUrl) rawUrls.push(rawUrl);
      }
    }
    rawUrls.push(...getPerformanceResourceUrls(documentRef));
    for (const rawUrl of rawUrls) {
      if (!rawUrl) continue;
      try {
        const resolvedUrl = new URL(rawUrl, baseUrl).toString();
        const { hostname } = new URL(resolvedUrl);
        if (hostname !== baseHostname && !hostname.endsWith(`.${baseHostname}`) && !baseHostname.endsWith(`.${hostname}`)) {
          continue;
        }
        uniqueUrls.add(resolvedUrl);
      } catch {
      }
    }
    return Array.from(uniqueUrls).sort((left, right) => {
      const score = (url) => {
        let value = 0;
        if (url.includes("assets.")) value += 4;
        if (url.includes("index-")) value += 3;
        if (url.includes("/_next/static/chunks/")) value += 2;
        if (url.includes("app-")) value += 1;
        if (url.includes("main")) value += 1;
        if (url.includes("webpack")) value -= 1;
        return value;
      };
      return score(right) - score(left);
    });
  }
  function isSupabaseLikeClient(value) {
    return isObjectLike(value) && typeof value.auth?.getSession === "function";
  }
  function isAxiosLikeClient(value) {
    return isObjectLike(value) && typeof value.request === "function" && isObjectLike(value.defaults);
  }
  function isStoreManagerInstance(value) {
    if (!isObjectLike(value)) return false;
    try {
      const stores = typeof value.getStores === "function" ? value.getStores() : value.stores;
      return stores instanceof Map;
    } catch {
      return false;
    }
  }
  function getStoreManagerRoots(manager) {
    const roots = [];
    pushUniqueRef(roots, manager?.initState);
    try {
      const stores = typeof manager?.getStores === "function" ? manager.getStores() : manager?.stores;
      pushUniqueRef(roots, stores);
    } catch {
    }
    return roots;
  }
  function extractPageRuntimeStoreContainerCandidate(value) {
    if (!isObjectLike(value)) return null;
    const candidate = { root: value };
    for (const propertyName of GENERATE_ALPHA_RUNTIME_BRIDGE_STORE_KEYS) {
      const store = value[propertyName];
      if (!readStoreStateCandidate(store)) continue;
      candidate[propertyName] = store;
    }
    return Object.keys(candidate).length > 1 ? candidate : null;
  }
  function composePageRuntimeBridge(connectionCandidate, storeCandidate) {
    if (!connectionCandidate) return null;
    return {
      ...connectionCandidate,
      storeRoot: storeCandidate?.root ?? connectionCandidate.root,
      userStore: storeCandidate?.userStore ?? connectionCandidate.root?.userStore ?? null,
      userPersonaStore: storeCandidate?.userPersonaStore ?? connectionCandidate.root?.userPersonaStore ?? null,
      storageStore: storeCandidate?.storageStore ?? connectionCandidate.root?.storageStore ?? null,
      chatStore: storeCandidate?.chatStore ?? connectionCandidate.root?.chatStore ?? null
    };
  }
  function extractPageRuntimeBridgeCandidate(value) {
    if (!isObjectLike(value)) return null;
    const supabase = isSupabaseLikeClient(value.supabase) ? value.supabase : isSupabaseLikeClient(value) ? value : null;
    const endpoints = isObjectLike(value.endpoints) ? value.endpoints : null;
    const apiClient = isAxiosLikeClient(value.apiClient) ? value.apiClient : isAxiosLikeClient(endpoints?.apiClient) ? endpoints.apiClient : isAxiosLikeClient(value) ? value : null;
    if (endpoints && supabase) {
      return { root: value, endpoints, supabase, apiClient };
    }
    if (apiClient && supabase) {
      return { root: value, endpoints: value, supabase, apiClient };
    }
    return null;
  }
  function enqueueRuntimeSearchValues(queue, value, depth) {
    if (!value) return;
    if (value instanceof Map) {
      for (const item of value.values()) {
        queue.push([item, depth + 1]);
      }
      return;
    }
    if (Array.isArray(value)) {
      for (const item of value) {
        queue.push([item, depth + 1]);
      }
      return;
    }
    if (!isObjectLike(value)) return;
    try {
      if (typeof value.getState === "function") {
        const state2 = value.getState();
        if (isObjectLike(state2)) {
          queue.push([state2, depth + 1]);
        }
      }
    } catch {
    }
    for (const child of Object.values(value)) {
      if (isObjectLike(child)) {
        queue.push([child, depth + 1]);
      }
    }
  }
  function findPageRuntimeBridgeCandidate(roots, maxDepth = 4) {
    const queue = roots.map((root) => [root, 0]);
    const seen = /* @__PURE__ */ new Set();
    let connectionCandidate = null;
    let storeCandidate = null;
    while (queue.length > 0) {
      const [value, depth] = queue.shift();
      if (!isObjectLike(value) || seen.has(value)) continue;
      seen.add(value);
      if (!connectionCandidate) {
        connectionCandidate = extractPageRuntimeBridgeCandidate(value);
      }
      if (!storeCandidate) {
        storeCandidate = extractPageRuntimeStoreContainerCandidate(value);
      }
      if (connectionCandidate && storeCandidate) break;
      if (depth >= maxDepth) continue;
      enqueueRuntimeSearchValues(queue, value, depth);
    }
    return composePageRuntimeBridge(connectionCandidate, storeCandidate);
  }
  function collectRuntimeModuleUrls(documentRef, baseUrl, { includeFallbackCandidates = false } = {}) {
    const uniqueUrls = /* @__PURE__ */ new Set();
    const addUrl = (rawUrl) => {
      if (!rawUrl) return;
      try {
        const resolvedUrl = new URL(rawUrl, baseUrl).toString();
        if (/\.js(?:$|\?)/i.test(resolvedUrl)) {
          uniqueUrls.add(resolvedUrl);
        }
      } catch {
      }
    };
    for (const script of getDocumentScripts(documentRef)) {
      const rawUrl = typeof script?.src === "string" && script.src ? script.src : script?.getAttribute?.("src");
      const type = asTrimmedString(
        script?.type ?? script?.getAttribute?.("type")
      ).toLowerCase();
      if (!rawUrl) continue;
      if (type === "module" || rawUrl.includes("index-")) {
        addUrl(rawUrl);
      }
    }
    if (includeFallbackCandidates) {
      for (const candidate of collectCandidateScriptUrls(documentRef, baseUrl)) {
        addUrl(candidate);
      }
    }
    return Array.from(uniqueUrls).sort((left, right) => {
      const score = (url) => {
        let value = 0;
        if (url.includes("index-")) value += 5;
        if (url.includes("assets.")) value += 3;
        if (url.includes("main")) value += 2;
        if (url.includes("polyfills")) value -= 4;
        if (url.includes("turnstile")) value -= 3;
        return value;
      };
      return score(right) - score(left);
    });
  }
  async function defaultPageRuntimeImporter(moduleUrl) {
    return import(
      /* @vite-ignore */
      moduleUrl
    );
  }
  function getStoreManagerFromModuleExport(exportedValue) {
    if (typeof exportedValue !== "function") return null;
    return isStoreManagerInstance(exportedValue.instance) ? exportedValue.instance : null;
  }
  async function discoverPageRuntimeBridge({
    documentRef,
    baseUrl,
    importModule = defaultPageRuntimeImporter
  }) {
    if (!getDocumentWindow(documentRef) || typeof importModule !== "function") {
      return null;
    }
    const attemptedModuleUrls = /* @__PURE__ */ new Set();
    for (const moduleUrls of [
      collectRuntimeModuleUrls(documentRef, baseUrl),
      collectRuntimeModuleUrls(documentRef, baseUrl, {
        includeFallbackCandidates: true
      })
    ]) {
      for (const moduleUrl of moduleUrls) {
        if (attemptedModuleUrls.has(moduleUrl)) continue;
        attemptedModuleUrls.add(moduleUrl);
        try {
          const moduleNamespace = await importModule(moduleUrl);
          if (!isObjectLike(moduleNamespace)) continue;
          for (const exportedValue of Object.values(moduleNamespace)) {
            const manager = getStoreManagerFromModuleExport(exportedValue);
            if (!manager) continue;
            const bridge = findPageRuntimeBridgeCandidate(getStoreManagerRoots(manager));
            if (bridge) {
              return { moduleUrl, manager, ...bridge };
            }
          }
        } catch {
        }
      }
    }
    return null;
  }
  function createPageRuntimeBridgeResolver({
    documentRef,
    baseUrl,
    importModule
  }) {
    let cachedRuntimeBridge = null;
    let runtimeBridgePromise = null;
    return async function ensurePageRuntimeBridge() {
      if (cachedRuntimeBridge) return cachedRuntimeBridge;
      if (runtimeBridgePromise) return runtimeBridgePromise;
      runtimeBridgePromise = discoverPageRuntimeBridge({
        documentRef,
        baseUrl,
        importModule
      });
      try {
        const runtimeBridge = await runtimeBridgePromise;
        if (runtimeBridge) {
          cachedRuntimeBridge = runtimeBridge;
        }
        return runtimeBridge;
      } finally {
        runtimeBridgePromise = null;
      }
    };
  }
  const UUID_LIKE_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
  function getDefaultFetch(documentRef = globalThis.document) {
    const documentWindow = getDocumentWindow(documentRef);
    try {
      if (typeof documentWindow?.fetch === "function") {
        return documentWindow.fetch.bind(documentWindow);
      }
    } catch {
    }
    try {
      return typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : null;
    } catch {
      return null;
    }
  }
  function resolveChatIdFromPayload(payload) {
    const direct = normalizeChatIdValue(payload);
    if (direct !== null) return direct;
    if (!payload || typeof payload !== "object") return null;
    const candidateValues = [
      payload?.chat?.id,
      payload?.id,
      payload?.chat_id,
      payload?.chatId,
      payload?.data?.chat?.id,
      payload?.data?.id,
      payload?.data?.chat_id,
      payload?.data?.chatId
    ];
    for (const candidate of candidateValues) {
      const normalized = normalizeChatIdValue(candidate);
      if (normalized !== null) return normalized;
    }
    return null;
  }
  function resolveChatBootstrapResultOrThrow(payload, errorMessage) {
    const chatId = resolveChatIdFromPayload(payload);
    if (chatId === null) {
      throw new Error(errorMessage);
    }
    return { chatId, payload };
  }
  function extractCharacterIdFromInput(value) {
    const source = asTrimmedString(value);
    if (!source) return null;
    const directMatch = source.match(UUID_LIKE_REGEX);
    if (directMatch && directMatch[0].length === source.length) {
      return directMatch[0];
    }
    let searchTarget = source;
    try {
      const maybeUrl = new URL(source, "https://janitorai.com");
      searchTarget = decodeURIComponent(maybeUrl.pathname);
    } catch {
    }
    const routeMatch = searchTarget.match(
      /\/characters\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[_/?#-]|$)/i
    );
    if (routeMatch?.[1]) return routeMatch[1];
    const fallbackMatch = searchTarget.match(UUID_LIKE_REGEX);
    if (fallbackMatch?.[0]) return fallbackMatch[0];
    return null;
  }
  function resolveCharacterIdOrThrow(value) {
    const characterId = extractCharacterIdFromInput(value);
    if (!characterId) {
      throw new Error("Unable to resolve character ID from input.");
    }
    return characterId;
  }
  function createJanitorClient({
    baseUrl = "https://janitorai.com",
    fetchImpl = null,
    defaultHeaders = {},
    defaultRetries = 1,
    documentRef = globalThis.document,
    pageRuntimeImporter = null
  } = {}) {
    const resolvedFetchImpl = fetchImpl ?? getDefaultFetch(documentRef);
    const resolvedPageRuntimeImporter = pageRuntimeImporter ?? defaultPageRuntimeImporter;
    let serverTimeOffsetMs = 0;
    if (typeof resolvedFetchImpl !== "function") {
      throw new Error("createJanitorClient requires a valid fetch implementation.");
    }
    const wrappedFetch = async (...args) => {
      const response = await resolvedFetchImpl(...args);
      try {
        const dateHeader = response?.headers?.get?.("date");
        const serverTime = Date.parse(dateHeader);
        if (Number.isFinite(serverTime)) {
          serverTimeOffsetMs = serverTime - Date.now();
        }
      } catch {
      }
      return response;
    };
    const ensurePageRuntimeBridge = createPageRuntimeBridgeResolver({
      documentRef,
      baseUrl,
      importModule: resolvedPageRuntimeImporter
    });
    const resolveAccessToken = createAccessTokenResolver({
      documentRef,
      ensurePageRuntimeBridge
    });
    const request = createJanitorRequestTransport({
      baseUrl,
      fetchImpl: wrappedFetch,
      defaultHeaders,
      defaultRetries,
      resolveAccessToken
    });
    async function createChat(characterId) {
      const resolvedCharacterId = resolveCharacterIdOrThrow(characterId);
      const body = { character_id: resolvedCharacterId };
      const payload = await request("/hampter/chats", {
        method: "POST",
        body
      });
      return resolveChatBootstrapResultOrThrow(
        payload,
        "Unable to resolve chat ID from create-chat response."
      );
    }
    async function startTemporaryChat(characterId) {
      return createChat(characterId);
    }
    async function sendMessage(chatId, messageOrPayload, options = {}) {
      const payload = isObjectLike(messageOrPayload) && !Array.isArray(messageOrPayload) ? { ...messageOrPayload, ...options } : { ...options, message: messageOrPayload };
      return request(`/hampter/chats/${encodeURIComponent(chatId)}/messages`, {
        method: "POST",
        body: buildSendMessageRequestBody(chatId, payload)
      });
    }
    return {
      request,
      getServerNowMs() {
        return Date.now() + serverTimeOffsetMs;
      },
      async getGenerateAlphaRuntimeContext() {
        return readGenerateAlphaRuntimeContext({ documentRef });
      },
      extractCharacterIdFromInput,
      async getCharacter(characterId) {
        const resolvedCharacterId = resolveCharacterIdOrThrow(characterId);
        return request(`/hampter/characters/${encodeURIComponent(resolvedCharacterId)}`, {
          method: "GET"
        });
      },
      async getChat(chatId) {
        return request(`/hampter/chats/${encodeURIComponent(chatId)}`, {
          method: "GET"
        });
      },
      async deleteChat(chatId) {
        return request(`/hampter/chats/${encodeURIComponent(chatId)}`, {
          method: "DELETE"
        });
      },
      createChat,
      startTemporaryChat,
      sendMessage,
      async generateAlpha(payload) {
        return request("/generateAlpha", {
          method: "POST",
          body: payload
        });
      }
    };
  }
  function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }
  function normalizeTagIdentifier(value) {
    return asString(value).toLowerCase().replace(/[^a-z0-9]+/g, "");
  }
  function extractTaggedBlocks(content) {
    const source = asString(content);
    const regex = /<([^>\n/][^>\n]*)>([\s\S]*?)<\/\1>/gi;
    const blocks = [];
    let match = regex.exec(source);
    while (match) {
      blocks.push({
        tagName: asString(match[1]).trim(),
        content: asString(match[2]).trim(),
        start: match.index,
        end: regex.lastIndex
      });
      match = regex.exec(source);
    }
    return blocks;
  }
  function removeTaggedBlocks(content, predicate) {
    const source = asString(content);
    const matchingBlocks = extractTaggedBlocks(source).filter(
      (block) => predicate(block.tagName)
    );
    if (matchingBlocks.length === 0) return source;
    let output = "";
    let cursor = 0;
    for (const block of matchingBlocks) {
      output += source.slice(cursor, block.start);
      cursor = block.end;
    }
    output += source.slice(cursor);
    return output;
  }
  function findTagBlock(content, tagName) {
    const expectedTagName = normalizeTagIdentifier(tagName);
    return extractTaggedBlocks(content).find(
      (block) => normalizeTagIdentifier(block.tagName) === expectedTagName
    ) ?? null;
  }
  function isPersonaTag(tagName) {
    return normalizeTagIdentifier(tagName).includes("persona");
  }
  function isUserPersonaTag(tagName) {
    const normalizedTagName = normalizeTagIdentifier(tagName);
    return normalizedTagName === "userpersona" || normalizedTagName === "userspersona";
  }
  function stripLeadingSystemNote(content) {
    const source = asString(content);
    return source.replace(/^\s*\[System note:[^\]]*]\s*(?:\r?\n)*/i, "");
  }
  function stripUserPersonaBlocks(content) {
    return removeTaggedBlocks(content, isUserPersonaTag);
  }
  function findFallbackDescriptionBoundary(content) {
    const source = asString(content);
    const boundaryIndexes = [
      source.search(/<\s*scenario\b/i),
      source.search(/<\s*example_dialogs\b/i)
    ].filter((index) => index >= 0);
    if (boundaryIndexes.length === 0) return -1;
    return Math.min(...boundaryIndexes);
  }
  function extractSystemContentFromMessages(messages = []) {
    for (const message of messages) {
      if (message?.role !== "system") continue;
      const content = asString(message?.content);
      if (content.trim()) return content;
    }
    return "";
  }
  function extractTagBlockContent(content, tagName) {
    const block = findTagBlock(stripLeadingSystemNote(content), tagName);
    return block?.content ?? "";
  }
  function extractPersonaBlock(content) {
    const blocks = extractTaggedBlocks(stripLeadingSystemNote(content));
    for (const block of blocks) {
      if (!isPersonaTag(block.tagName)) continue;
      if (isUserPersonaTag(block.tagName)) continue;
      return block.content;
    }
    return "";
  }
  function extractFallbackDescription(content) {
    const contentWithoutSystemNote = stripLeadingSystemNote(content);
    const contentWithoutUserPersona = stripUserPersonaBlocks(contentWithoutSystemNote);
    const boundaryIndex = findFallbackDescriptionBoundary(contentWithoutUserPersona);
    if (boundaryIndex < 0) return contentWithoutUserPersona.trim();
    return contentWithoutUserPersona.slice(0, boundaryIndex).trim();
  }
  function normalizePersonaMacro(text, personaLabel) {
    const source = asString(text);
    const anchor = asString(personaLabel).trim();
    if (!source || !anchor) return source;
    const escapedAnchor = escapeRegExp(anchor);
    const possessiveRegex = new RegExp(`\\b${escapedAnchor}(?:'|’)s\\b`, "g");
    const plainRegex = new RegExp(`\\b${escapedAnchor}\\b`, "g");
    return source.replace(possessiveRegex, "{{user}}'s").replace(plainRegex, "{{user}}");
  }
  function normalizePersonaMacroInFields(fields, personaLabel) {
    const normalized = {};
    for (const [key, value] of Object.entries(fields)) {
      if (typeof value === "string") {
        normalized[key] = normalizePersonaMacro(value, personaLabel);
        continue;
      }
      if (Array.isArray(value)) {
        normalized[key] = value.map(
          (item) => typeof item === "string" ? normalizePersonaMacro(item, personaLabel) : item
        );
        continue;
      }
      normalized[key] = value;
    }
    return normalized;
  }
  function parseGenerateAlphaFields(messages = []) {
    const systemContent = stripLeadingSystemNote(extractSystemContentFromMessages(messages));
    const description = extractPersonaBlock(systemContent) || extractFallbackDescription(systemContent);
    const scenario = extractTagBlockContent(systemContent, "Scenario");
    const mesExample = extractTagBlockContent(systemContent, "example_dialogs");
    return {
      personaAnchor: null,
      systemContent,
      description,
      scenario,
      mesExample
    };
  }
  function resolveCharacterTitle(normalizedCharacter, { useChatName = false } = {}) {
    const name = normalizedCharacter?.name ?? "";
    const chatName = normalizedCharacter?.chatName ?? "";
    return useChatName ? chatName || name : name || chatName;
  }
  const DEFAULT_FILENAME = "character-card";
  const DEFAULT_CREATOR = "unknown";
  const JANITORAI_BASE_URL = "https://janitorai.com";
  const JANITORAI_AVATAR_BASE_URL = "https://ella.janitorai.com";
  const DEFAULT_FUTURE_SKEW_MS = 12e4;
  const DEFAULT_PAST_SKEW_MS = 12e4;
  function buildCharacterUrl(characterId) {
    const id = asTrimmedString(characterId);
    return id ? `${JANITORAI_BASE_URL}/characters/${id}` : null;
  }
  function buildProfileUrl(profileId) {
    const id = asTrimmedString(profileId);
    return id ? `${JANITORAI_BASE_URL}/profiles/${id}` : null;
  }
  function buildAvatarUrl(filename, { width = null } = {}) {
    const name = asTrimmedString(filename);
    if (!name) return null;
    const baseUrl = `${JANITORAI_AVATAR_BASE_URL}/bot-avatars/${name}`;
    return width ? `${baseUrl}?width=${width}` : baseUrl;
  }
  function stripJanitorAttribution(text) {
    const source = asString(text);
    if (!source) return "";
    return source.replace(/\n{0,3}\s*created by[^\n]*janitorai\.com\s*$/i, "").trim();
  }
  function buildAvatar1200Url(avatarFilename) {
    return buildAvatarUrl(avatarFilename, { width: 1200 });
  }
  function findChatOpeningBotMessage(chatPayload) {
    for (const message of asArrayStrict(chatPayload?.chatMessages)) {
      if (message?.is_bot !== true) continue;
      if (message?.is_main === false) continue;
      const text = asString(message?.message).trim();
      if (text) return text;
    }
    return null;
  }
  function collectFirstMessages(character, chatPayload) {
    const candidates = [];
    const normalizeCandidate = (value) => stripJanitorAttribution(value).replace(/\r\n?/g, "\n").trim();
    const pushCandidate = (value) => {
      if (typeof value !== "string") return;
      candidates.push(normalizeCandidate(value));
    };
    pushCandidate(chatPayload?.character?.first_message);
    pushCandidate(findChatOpeningBotMessage(chatPayload));
    pushCandidate(character?.first_message);
    asArrayStrict(chatPayload?.character?.first_messages).forEach(pushCandidate);
    asArrayStrict(character?.first_messages).forEach(pushCandidate);
    return uniqueStrings(candidates, { allowEmpty: true });
  }
  const buildFirstMessages = collectFirstMessages;
  function normalizeTagNames(tags) {
    return uniqueStrings(
      asArrayStrict(tags).map((tag) => {
        if (typeof tag === "string") return tag;
        return asString(tag?.name);
      })
    );
  }
  function pickCardTitle(normalized, { useChatName = false } = {}) {
    return resolveCharacterTitle(normalized, { useChatName }) || DEFAULT_FILENAME;
  }
  function normalizeCharacterFields({
    characterPayload,
    chatPayload = null,
    generateAlphaFields = {}
  }) {
    const character = characterPayload ?? {};
    const chat = chatPayload?.chat ?? {};
    const personaAnchor = asString(generateAlphaFields?.personaAnchor).trim() || null;
    const showDefinition = Boolean(character?.showdefinition);
    const descriptionRaw = showDefinition ? asString(character?.personality) || asString(generateAlphaFields?.description) : asString(generateAlphaFields?.description) || asString(character?.personality);
    const scenarioRaw = showDefinition ? asString(character?.scenario) || asString(generateAlphaFields?.scenario) : asString(generateAlphaFields?.scenario) || asString(character?.scenario);
    const mesExampleRaw = showDefinition ? asString(character?.example_dialogs) || asString(generateAlphaFields?.mesExample) : asString(generateAlphaFields?.mesExample) || asString(character?.example_dialogs);
    const firstMessages = buildFirstMessages(character, chatPayload);
    const firstMesRaw = firstMessages[0] ?? "";
    const alternateGreetingsRaw = firstMessages.slice(1);
    const cleanedFields = {
      description: stripJanitorAttribution(descriptionRaw),
      scenario: stripJanitorAttribution(scenarioRaw),
      firstMes: stripJanitorAttribution(firstMesRaw),
      mesExample: stripJanitorAttribution(mesExampleRaw),
      alternateGreetings: alternateGreetingsRaw.map(stripJanitorAttribution)
    };
    const normalizedFields = normalizePersonaMacroInFields(cleanedFields, personaAnchor);
    const characterId = asString(character?.id) || asString(chat?.character_id) || null;
    const creatorId = asString(character?.creator_id) || null;
    return {
      characterId,
      characterUrl: buildCharacterUrl(characterId),
      name: asString(character?.name) || "",
      chatName: asString(character?.chat_name) || null,
      creator: {
        id: creatorId,
        name: asString(character?.creator_name) || null,
        verified: Boolean(character?.creator_verified),
        profileUrl: buildProfileUrl(creatorId)
      },
      avatar: {
        filename: asString(character?.avatar) || null,
        url1200: buildAvatar1200Url(character?.avatar)
      },
      tags: normalizeTagNames(character?.tags),
      customTags: normalizeTagNames(character?.custom_tags),
      tokenCounts: character?.token_counts ?? null,
      stats: character?.stats ?? null,
      description: normalizedFields.description,
      scenario: normalizedFields.scenario,
      firstMes: normalizedFields.firstMes,
      mesExample: normalizedFields.mesExample,
      alternateGreetings: uniqueStrings(normalizedFields.alternateGreetings, { allowEmpty: true }),
      creatorNotesHtml: asString(character?.description) || "",
      source: {
        chatId: chat?.id ?? null,
        chatCreatedAt: chat?.created_at ?? null,
        personaAnchor
      },
      raw: {
        characterPayload: character,
        chatPayload,
        generateAlphaFields
      }
    };
  }
  async function fetchAvatarBytes(imageUrl, fetchImpl, onProgress) {
    if (typeof fetchImpl !== "function") {
      throw new Error("PNG export requires a valid fetch implementation.");
    }
    const response = await fetchImpl(imageUrl);
    if (!response.ok) {
      throw new Error(`Avatar request failed with status ${response.status}.`);
    }
    const total = parseInt(response.headers.get("Content-Length") ?? "0", 10);
    if (typeof onProgress !== "function" || total <= 0 || !response.body) {
      return new Uint8Array(await response.arrayBuffer());
    }
    const reader = response.body.getReader();
    const chunks = [];
    let received = 0;
    for (; ; ) {
      const { done, value } = await reader.read();
      if (done) break;
      chunks.push(value);
      received += value.length;
      onProgress(received / total);
    }
    const result = new Uint8Array(received);
    let offset = 0;
    for (const chunk of chunks) {
      result.set(chunk, offset);
      offset += chunk.length;
    }
    return result;
  }
  async function webpToPngBytes(arrayBuffer) {
    const bitmap = await createImageBitmap(
      new Blob([arrayBuffer], { type: "image/webp" })
    );
    const canvas = document.createElement("canvas");
    canvas.width = bitmap.width;
    canvas.height = bitmap.height;
    canvas.getContext("2d").drawImage(bitmap, 0, 0);
    bitmap.close();
    return new Promise((resolve, reject) => {
      canvas.toBlob(async (blob) => {
        if (!blob) {
          reject(new Error("Failed to convert avatar to PNG."));
          return;
        }
        try {
          const buf = await blob.arrayBuffer();
          resolve(new Uint8Array(buf));
        } catch (error) {
          reject(error);
        }
      }, "image/png");
    });
  }
  function replaceTokens(text, replacements) {
    if (!replacements) return asString(text);
    let output = asString(text);
    for (const [token, value] of Object.entries(replacements)) {
      if (!value) continue;
      output = output.split(token).join(value);
    }
    return output;
  }
  function normalizePronounValue$1(value) {
    return asTrimmedString(value);
  }
  function buildPronounReplacementMap(pronounMacros) {
    if (!pronounMacros || typeof pronounMacros !== "object") return null;
    return {
      "{{sub}}": normalizePronounValue$1(pronounMacros.sub),
      "{{obj}}": normalizePronounValue$1(pronounMacros.obj),
      "{{poss}}": normalizePronounValue$1(pronounMacros.poss),
      "{{poss_p}}": normalizePronounValue$1(pronounMacros.poss_p),
      "{{ref}}": normalizePronounValue$1(pronounMacros.ref)
    };
  }
  function applyPronounReplacements(value, replacements) {
    return replaceTokens(value, replacements);
  }
  function applyPronounReplacementsToArray(values, replacements) {
    if (!Array.isArray(values)) return [];
    return values.map(
      (item) => typeof item === "string" ? replaceTokens(item, replacements) : item
    );
  }
  function getDefaultExportFromCjs(x2) {
    return x2 && x2.__esModule && Object.prototype.hasOwnProperty.call(x2, "default") ? x2["default"] : x2;
  }
  var crc32 = {};
  var hasRequiredCrc32;
  function requireCrc32() {
    if (hasRequiredCrc32) return crc32;
    hasRequiredCrc32 = 1;
    (function(exports$1) {
      (function(factory) {
        if (typeof DO_NOT_EXPORT_CRC === "undefined") {
          {
            factory(exports$1);
          }
        } else {
          factory({});
        }
      })(function(CRC32) {
        CRC32.version = "0.3.0";
        function signed_crc_table() {
          var c = 0, table2 = new Array(256);
          for (var n = 0; n != 256; ++n) {
            c = n;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            c = c & 1 ? -306674912 ^ c >>> 1 : c >>> 1;
            table2[n] = c;
          }
          return typeof Int32Array !== "undefined" ? new Int32Array(table2) : table2;
        }
        var table = signed_crc_table();
        var use_buffer = typeof Buffer !== "undefined";
        function crc32_bstr(bstr) {
          if (bstr.length > 32768) {
            if (use_buffer) return crc32_buf_8(new Buffer(bstr));
          }
          var crc2 = -1, L = bstr.length - 1;
          for (var i2 = 0; i2 < L; ) {
            crc2 = table[(crc2 ^ bstr.charCodeAt(i2++)) & 255] ^ crc2 >>> 8;
            crc2 = table[(crc2 ^ bstr.charCodeAt(i2++)) & 255] ^ crc2 >>> 8;
          }
          if (i2 === L) crc2 = crc2 >>> 8 ^ table[(crc2 ^ bstr.charCodeAt(i2)) & 255];
          return crc2 ^ -1;
        }
        function crc32_buf(buf) {
          if (buf.length > 1e4) return crc32_buf_8(buf);
          for (var crc2 = -1, i2 = 0, L = buf.length - 3; i2 < L; ) {
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
          }
          while (i2 < L + 3) crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
          return crc2 ^ -1;
        }
        function crc32_buf_8(buf) {
          for (var crc2 = -1, i2 = 0, L = buf.length - 7; i2 < L; ) {
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
            crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
          }
          while (i2 < L + 7) crc2 = crc2 >>> 8 ^ table[(crc2 ^ buf[i2++]) & 255];
          return crc2 ^ -1;
        }
        function crc32_str(str) {
          for (var crc2 = -1, i2 = 0, L = str.length, c, d; i2 < L; ) {
            c = str.charCodeAt(i2++);
            if (c < 128) {
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ c) & 255];
            } else if (c < 2048) {
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (192 | c >> 6 & 31)) & 255];
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (128 | c & 63)) & 255];
            } else if (c >= 55296 && c < 57344) {
              c = (c & 1023) + 64;
              d = str.charCodeAt(i2++) & 1023;
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (240 | c >> 8 & 7)) & 255];
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (128 | c >> 2 & 63)) & 255];
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (128 | d >> 6 & 15 | c & 3)) & 255];
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (128 | d & 63)) & 255];
            } else {
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (224 | c >> 12 & 15)) & 255];
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (128 | c >> 6 & 63)) & 255];
              crc2 = crc2 >>> 8 ^ table[(crc2 ^ (128 | c & 63)) & 255];
            }
          }
          return crc2 ^ -1;
        }
        CRC32.table = table;
        CRC32.bstr = crc32_bstr;
        CRC32.buf = crc32_buf;
        CRC32.str = crc32_str;
      });
    })(crc32);
    return crc32;
  }
  var pngChunksExtract;
  var hasRequiredPngChunksExtract;
  function requirePngChunksExtract() {
    if (hasRequiredPngChunksExtract) return pngChunksExtract;
    hasRequiredPngChunksExtract = 1;
    var crc322 = requireCrc32();
    pngChunksExtract = extractChunks;
    var uint8 = new Uint8Array(4);
    var int32 = new Int32Array(uint8.buffer);
    var uint32 = new Uint32Array(uint8.buffer);
    function extractChunks(data) {
      if (data[0] !== 137) throw new Error("Invalid .png file header");
      if (data[1] !== 80) throw new Error("Invalid .png file header");
      if (data[2] !== 78) throw new Error("Invalid .png file header");
      if (data[3] !== 71) throw new Error("Invalid .png file header");
      if (data[4] !== 13) throw new Error("Invalid .png file header: possibly caused by DOS-Unix line ending conversion?");
      if (data[5] !== 10) throw new Error("Invalid .png file header: possibly caused by DOS-Unix line ending conversion?");
      if (data[6] !== 26) throw new Error("Invalid .png file header");
      if (data[7] !== 10) throw new Error("Invalid .png file header: possibly caused by DOS-Unix line ending conversion?");
      var ended = false;
      var chunks = [];
      var idx = 8;
      while (idx < data.length) {
        uint8[3] = data[idx++];
        uint8[2] = data[idx++];
        uint8[1] = data[idx++];
        uint8[0] = data[idx++];
        var length = uint32[0] + 4;
        var chunk = new Uint8Array(length);
        chunk[0] = data[idx++];
        chunk[1] = data[idx++];
        chunk[2] = data[idx++];
        chunk[3] = data[idx++];
        var name = String.fromCharCode(chunk[0]) + String.fromCharCode(chunk[1]) + String.fromCharCode(chunk[2]) + String.fromCharCode(chunk[3]);
        if (!chunks.length && name !== "IHDR") {
          throw new Error("IHDR header missing");
        }
        if (name === "IEND") {
          ended = true;
          chunks.push({
            name,
            data: new Uint8Array(0)
          });
          break;
        }
        for (var i2 = 4; i2 < length; i2++) {
          chunk[i2] = data[idx++];
        }
        uint8[3] = data[idx++];
        uint8[2] = data[idx++];
        uint8[1] = data[idx++];
        uint8[0] = data[idx++];
        var crcActual = int32[0];
        var crcExpect = crc322.buf(chunk);
        if (crcExpect !== crcActual) {
          throw new Error(
            "CRC values for " + name + " header do not match, PNG file is likely corrupted"
          );
        }
        var chunkData = new Uint8Array(chunk.buffer.slice(4));
        chunks.push({
          name,
          data: chunkData
        });
      }
      if (!ended) {
        throw new Error(".png file ended prematurely: no IEND header was found");
      }
      return chunks;
    }
    return pngChunksExtract;
  }
  var pngChunksExtractExports = requirePngChunksExtract();
  const extractPngChunks = /* @__PURE__ */ getDefaultExportFromCjs(pngChunksExtractExports);
  var sliced;
  var hasRequiredSliced;
  function requireSliced() {
    if (hasRequiredSliced) return sliced;
    hasRequiredSliced = 1;
    sliced = function(args, slice, sliceEnd) {
      var ret = [];
      var len = args.length;
      if (0 === len) return ret;
      var start = slice < 0 ? Math.max(0, slice + len) : slice || 0;
      if (sliceEnd !== void 0) {
        len = sliceEnd < 0 ? sliceEnd + len : sliceEnd;
      }
      while (len-- > start) {
        ret[len - start] = args[len];
      }
      return ret;
    };
    return sliced;
  }
  var pngChunksEncode;
  var hasRequiredPngChunksEncode;
  function requirePngChunksEncode() {
    if (hasRequiredPngChunksEncode) return pngChunksEncode;
    hasRequiredPngChunksEncode = 1;
    var sliced2 = requireSliced();
    var crc322 = requireCrc32();
    pngChunksEncode = encodeChunks;
    var uint8 = new Uint8Array(4);
    var int32 = new Int32Array(uint8.buffer);
    var uint32 = new Uint32Array(uint8.buffer);
    function encodeChunks(chunks) {
      var totalSize = 8;
      var idx = totalSize;
      var i2;
      for (i2 = 0; i2 < chunks.length; i2++) {
        totalSize += chunks[i2].data.length;
        totalSize += 12;
      }
      var output = new Uint8Array(totalSize);
      output[0] = 137;
      output[1] = 80;
      output[2] = 78;
      output[3] = 71;
      output[4] = 13;
      output[5] = 10;
      output[6] = 26;
      output[7] = 10;
      for (i2 = 0; i2 < chunks.length; i2++) {
        var chunk = chunks[i2];
        var name = chunk.name;
        var data = chunk.data;
        var size = data.length;
        var nameChars = [
          name.charCodeAt(0),
          name.charCodeAt(1),
          name.charCodeAt(2),
          name.charCodeAt(3)
        ];
        uint32[0] = size;
        output[idx++] = uint8[3];
        output[idx++] = uint8[2];
        output[idx++] = uint8[1];
        output[idx++] = uint8[0];
        output[idx++] = nameChars[0];
        output[idx++] = nameChars[1];
        output[idx++] = nameChars[2];
        output[idx++] = nameChars[3];
        for (var j = 0; j < size; ) {
          output[idx++] = data[j++];
        }
        var crcCheck = nameChars.concat(sliced2(data));
        var crc2 = crc322.buf(crcCheck);
        int32[0] = crc2;
        output[idx++] = uint8[3];
        output[idx++] = uint8[2];
        output[idx++] = uint8[1];
        output[idx++] = uint8[0];
      }
      return output;
    }
    return pngChunksEncode;
  }
  var pngChunksEncodeExports = requirePngChunksEncode();
  const encodePngChunks = /* @__PURE__ */ getDefaultExportFromCjs(pngChunksEncodeExports);
  var pngChunkText = {};
  var encode_1;
  var hasRequiredEncode;
  function requireEncode() {
    if (hasRequiredEncode) return encode_1;
    hasRequiredEncode = 1;
    encode_1 = encode;
    function encode(keyword, content) {
      keyword = String(keyword);
      content = String(content);
      if (!/^[\x00-\xFF]+$/.test(keyword) || !/^[\x00-\xFF]+$/.test(content)) {
        throw new Error("Only Latin-1 characters are permitted in PNG tEXt chunks. You might want to consider base64 encoding and/or zEXt compression");
      }
      if (keyword.length >= 80) {
        throw new Error('Keyword "' + keyword + '" is longer than the 79-character limit imposed by the PNG specification');
      }
      var totalSize = keyword.length + content.length + 1;
      var output = new Uint8Array(totalSize);
      var idx = 0;
      var code;
      for (var i2 = 0; i2 < keyword.length; i2++) {
        if (!(code = keyword.charCodeAt(i2))) {
          throw new Error("0x00 character is not permitted in tEXt keywords");
        }
        output[idx++] = code;
      }
      output[idx++] = 0;
      for (var j = 0; j < content.length; j++) {
        if (!(code = content.charCodeAt(j))) {
          throw new Error("0x00 character is not permitted in tEXt content");
        }
        output[idx++] = code;
      }
      return {
        name: "tEXt",
        data: output
      };
    }
    return encode_1;
  }
  var decode_1;
  var hasRequiredDecode;
  function requireDecode() {
    if (hasRequiredDecode) return decode_1;
    hasRequiredDecode = 1;
    decode_1 = decode;
    function decode(data) {
      if (data.data && data.name) {
        data = data.data;
      }
      var naming = true;
      var text = "";
      var name = "";
      for (var i2 = 0; i2 < data.length; i2++) {
        var code = data[i2];
        if (naming) {
          if (code) {
            name += String.fromCharCode(code);
          } else {
            naming = false;
          }
        } else {
          if (code) {
            text += String.fromCharCode(code);
          } else {
            throw new Error("Invalid NULL character found. 0x00 character is not permitted in tEXt content");
          }
        }
      }
      return {
        keyword: name,
        text
      };
    }
    return decode_1;
  }
  var hasRequiredPngChunkText;
  function requirePngChunkText() {
    if (hasRequiredPngChunkText) return pngChunkText;
    hasRequiredPngChunkText = 1;
    pngChunkText.encode = requireEncode();
    pngChunkText.decode = requireDecode();
    return pngChunkText;
  }
  var pngChunkTextExports = requirePngChunkText();
  const PngChunkText = /* @__PURE__ */ getDefaultExportFromCjs(pngChunkTextExports);
  function toBase64Utf8(value) {
    const text = typeof value === "string" ? value : JSON.stringify(value);
    if (typeof globalThis.Buffer !== "undefined") {
      return globalThis.Buffer.from(text, "utf8").toString("base64");
    }
    const bytes = new TextEncoder().encode(text);
    let binary = "";
    for (const byte of bytes) binary += String.fromCharCode(byte);
    return btoa(binary);
  }
  function toUint8Array(value) {
    if (value instanceof Uint8Array) return value;
    if (value instanceof ArrayBuffer) return new Uint8Array(value);
    if (ArrayBuffer.isView?.(value)) {
      const buffer = value.buffer;
      if (buffer && typeof value.byteLength === "number") {
        return new Uint8Array(buffer, value.byteOffset ?? 0, value.byteLength);
      }
      if (buffer) {
        return new Uint8Array(buffer);
      }
    }
    if (Object.prototype.toString.call(value) === "[object ArrayBuffer]") {
      return new Uint8Array(value);
    }
    throw new Error("PNG payload must be a Uint8Array or ArrayBuffer.");
  }
  function removeExistingCharacterTextChunks(chunks) {
    for (let index = chunks.length - 1; index >= 0; index -= 1) {
      const chunk = chunks[index];
      if (chunk?.name !== "tEXt") continue;
      try {
        const decoded = PngChunkText.decode(chunk.data);
        const keyword = String(decoded?.keyword ?? "").toLowerCase();
        if (keyword === "chara" || keyword === "ccv3") {
          chunks.splice(index, 1);
        }
      } catch {
      }
    }
  }
  function embedCardDataInPngBytes(pngBytes, cardJson, { keyword = "chara" } = {}) {
    const baseImageBytes = toUint8Array(pngBytes);
    const chunks = extractPngChunks(baseImageBytes);
    removeExistingCharacterTextChunks(chunks);
    const payload = toBase64Utf8(cardJson);
    chunks.splice(-1, 0, PngChunkText.encode(keyword, payload));
    const encoded = encodePngChunks(chunks);
    return encoded instanceof Uint8Array ? encoded : new Uint8Array(encoded);
  }
  function generateId(nowMs = Date.now()) {
    if (typeof globalThis.crypto?.randomUUID === "function") {
      return globalThis.crypto.randomUUID();
    }
    return `${nowMs.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
  }
  const EXPORT_FORMAT = Object.freeze({
    TXT: "txt",
    JSON: "json",
    PNG: "png"
  });
  function generateRunNonce(nowMs = Date.now()) {
    return generateId(nowMs);
  }
  function createRunContext({ characterId, nowMs = Date.now() }) {
    return {
      runId: generateRunNonce(nowMs),
      targetCharacterId: characterId,
      runStartMs: nowMs,
      createdChatId: null,
      runMarker: null
    };
  }
  function joinTags(...groups) {
    const values = groups.flat().filter(Boolean);
    return [...new Set(values)];
  }
  function buildCreatorValue(normalizedCharacter) {
    const name = normalizedCharacter?.creator?.name ?? "";
    const profileUrl = normalizedCharacter?.creator?.profileUrl ?? "";
    if (name && profileUrl) return `${name}
link: ${profileUrl}`;
    if (name) return name;
    if (profileUrl) return `link: ${profileUrl}`;
    return "";
  }
  function normalizeCardExportOptions(options = {}) {
    const source = options && typeof options === "object" ? options : {};
    return {
      useChatName: source.useChatName ?? false,
      includeTags: source.includeTags ?? true,
      includeCreatorNotes: source.includeCreatorNotes ?? true,
      pronounMacros: source.pronounMacros ?? null,
      fetchImpl: source.fetchImpl ?? globalThis.fetch,
      onProgress: source.onProgress
    };
  }
  function buildNormalizedCardFields(normalizedCharacter, options) {
    const {
      useChatName,
      includeTags,
      includeCreatorNotes,
      pronounMacros
    } = options;
    const cardName = pickCardTitle(normalizedCharacter, { useChatName });
    const replacements = buildPronounReplacementMap(pronounMacros);
    const description = applyPronounReplacements(
      normalizedCharacter.description ?? "",
      replacements
    );
    const personality = "";
    const creatorValue = buildCreatorValue(normalizedCharacter);
    const systemPrompt = normalizedCharacter.systemPrompt ?? "";
    const postHistoryInstructions = normalizedCharacter.postHistoryInstructions ?? "";
    const scenario = applyPronounReplacements(
      normalizedCharacter.scenario ?? "",
      replacements
    );
    const firstMes = applyPronounReplacements(
      normalizedCharacter.firstMes ?? "",
      replacements
    );
    const mesExample = applyPronounReplacements(
      normalizedCharacter.mesExample ?? "",
      replacements
    );
    const alternateGreetingsRaw = applyPronounReplacementsToArray(
      normalizedCharacter.alternateGreetings ?? [],
      replacements
    );
    const alternateGreetings = uniqueStrings(alternateGreetingsRaw, {
      allowEmpty: true
    }).filter((greeting) => greeting !== firstMes);
    const creatorNotesHtml = includeCreatorNotes ? applyPronounReplacements(
      normalizedCharacter.creatorNotesHtml ?? "",
      replacements
    ) : "";
    const tags = includeTags ? joinTags(normalizedCharacter.tags, normalizedCharacter.customTags) : [];
    return {
      alternateGreetings,
      cardName,
      creatorNotesHtml,
      creatorValue,
      description,
      firstMes,
      mesExample,
      personality,
      postHistoryInstructions,
      scenario,
      systemPrompt,
      tags
    };
  }
  function buildJsonExport(normalizedCharacter, options) {
    return {
      format: EXPORT_FORMAT.JSON,
      content: JSON.stringify(buildCardJson(normalizedCharacter, options), null, 2),
      mimeType: "application/json",
      extension: "json"
    };
  }
  function buildTxtExport(normalizedCharacter, options) {
    return {
      format: EXPORT_FORMAT.TXT,
      content: buildCardTxt(normalizedCharacter, options),
      mimeType: "text/plain;charset=utf-8",
      extension: "txt"
    };
  }
  const EXPORT_BUILDERS = {
    [EXPORT_FORMAT.TXT]: buildTxtExport,
    [EXPORT_FORMAT.PNG]: buildPngExportArtifact,
    [EXPORT_FORMAT.JSON]: buildJsonExport
  };
  function buildCardJson(normalizedCharacter, { useChatName = false, includeTags = true, includeCreatorNotes = true, pronounMacros = null } = {}) {
    const normalizedOptions = normalizeCardExportOptions({
      useChatName,
      includeTags,
      includeCreatorNotes,
      pronounMacros
    });
    const fields = buildNormalizedCardFields(normalizedCharacter, normalizedOptions);
    const data = {
      name: fields.cardName,
      description: fields.description,
      personality: fields.personality,
      scenario: fields.scenario,
      first_mes: fields.firstMes,
      mes_example: fields.mesExample,
      creator_notes: fields.creatorNotesHtml,
      system_prompt: fields.systemPrompt,
      post_history_instructions: fields.postHistoryInstructions,
      alternate_greetings: fields.alternateGreetings,
      tags: fields.tags,
      creator: fields.creatorValue,
      character_version: normalizedCharacter.characterUrl ?? "",
      extensions: {}
    };
    if (normalizedCharacter.characterBook) {
      data.character_book = normalizedCharacter.characterBook;
    }
    return {
      spec: "chara_card_v2",
      spec_version: "2.0",
      data
    };
  }
  function buildCardTxt(normalizedCharacter, { useChatName = false, includeTags = true, pronounMacros = null } = {}) {
    const normalizedOptions = normalizeCardExportOptions({
      useChatName,
      includeTags,
      pronounMacros
    });
    const fields = buildNormalizedCardFields(normalizedCharacter, normalizedOptions);
    const lines = [
      `name: ${fields.cardName}`,
      `creator: ${fields.creatorValue}`,
      `character_version: ${normalizedCharacter.characterUrl ?? ""}`,
      `tags: ${fields.tags.join(", ")}`,
      "",
      "[description]",
      fields.description,
      "",
      "[scenario]",
      fields.scenario,
      "",
      "[first_mes]",
      fields.firstMes,
      "",
      "[mes_example]",
      fields.mesExample,
      "",
      "[alternate_greetings]",
      fields.alternateGreetings.join("\n---\n")
    ];
    return lines.join("\n").trim();
  }
  async function buildPngExportArtifact(normalizedCharacter, {
    useChatName = false,
    includeTags = true,
    includeCreatorNotes = true,
    pronounMacros = null,
    fetchImpl = globalThis.fetch,
    onProgress
  } = {}) {
    const imageUrl = normalizedCharacter.avatar?.url1200 ?? null;
    if (!imageUrl) {
      throw new Error("PNG export requires an avatar URL.");
    }
    const imageBuffer = await fetchAvatarBytes(imageUrl, fetchImpl, onProgress);
    const pngBytes = await webpToPngBytes(imageBuffer);
    const cardJson = buildCardJson(normalizedCharacter, {
      useChatName,
      includeTags,
      includeCreatorNotes,
      pronounMacros
    });
    const pngWithMetadata = embedCardDataInPngBytes(pngBytes, cardJson, {
      keyword: "chara"
    });
    return {
      format: EXPORT_FORMAT.PNG,
      content: pngWithMetadata,
      mimeType: "image/png",
      extension: "png"
    };
  }
  async function buildExportContent(normalizedCharacter, {
    format = EXPORT_FORMAT.JSON,
    useChatName = false,
    includeTags = true,
    includeCreatorNotes = true,
    pronounMacros = null,
    fetchImpl,
    onProgress
  } = {}) {
    const normalizedOptions = normalizeCardExportOptions({
      useChatName,
      includeTags,
      includeCreatorNotes,
      pronounMacros,
      fetchImpl,
      onProgress
    });
    const builder = EXPORT_BUILDERS[format] ?? buildJsonExport;
    return builder(normalizedCharacter, normalizedOptions);
  }
  function buildDefaultGenerateAlphaPayload({
    chatPayload = null,
    messageResponse = null,
    generateAlphaContext = null
  } = {}) {
    return buildGenerateAlphaPayload({
      chatPayload,
      messageResponse,
      profile: generateAlphaContext?.profile,
      suggestionMode: generateAlphaContext?.suggestionMode,
      suggestionPerspective: generateAlphaContext?.suggestionPerspective,
      userConfig: generateAlphaContext?.userConfig,
      clientPlatform: generateAlphaContext?.clientPlatform,
      generateMode: generateAlphaContext?.generateMode,
      generateType: generateAlphaContext?.generateType,
      forcedPromptGenerationCacheRefetch: generateAlphaContext?.forcedPromptGenerationCacheRefetch
    });
  }
  const DELETE_CHAT_GUARD_REASON_CODES = Object.freeze({
    CHAT_ID_MISMATCH: "CHAT_ID_MISMATCH",
    CHARACTER_MISMATCH: "CHARACTER_MISMATCH",
    TIMESTAMP_OUT_OF_WINDOW: "TIMESTAMP_OUT_OF_WINDOW",
    OWNER_MISMATCH: "OWNER_MISMATCH",
    RUN_MARKER_MISSING: "RUN_MARKER_MISSING",
    NO_CREATED_CHAT_ID: "NO_CREATED_CHAT_ID"
  });
  const DELETE_CHAT_GUARD_REASON_MESSAGES = Object.freeze({
    [DELETE_CHAT_GUARD_REASON_CODES.CHAT_ID_MISMATCH]: "Chat ID mismatch (target chat is not the run-created chat).",
    [DELETE_CHAT_GUARD_REASON_CODES.CHARACTER_MISMATCH]: "Character mismatch for deletion target chat.",
    [DELETE_CHAT_GUARD_REASON_CODES.TIMESTAMP_OUT_OF_WINDOW]: "Chat creation timestamp is outside the current run window.",
    [DELETE_CHAT_GUARD_REASON_CODES.OWNER_MISMATCH]: "Ownership mismatch for deletion target chat.",
    [DELETE_CHAT_GUARD_REASON_CODES.RUN_MARKER_MISSING]: "Run marker not found in chat messages.",
    [DELETE_CHAT_GUARD_REASON_CODES.NO_CREATED_CHAT_ID]: "No run-created chat ID available for deletion."
  });
  function toTimestamp(value) {
    const time = Date.parse(asString(value));
    return Number.isFinite(time) ? time : null;
  }
  function findRunMarkerInChatMessages(chatPayload, runMarker) {
    const messages = Array.isArray(chatPayload?.chatMessages) ? chatPayload.chatMessages : [];
    const needle = asString(runMarker);
    if (!needle) return false;
    return messages.some((item) => {
      if (item?.is_bot) return false;
      const content = asString(item?.message);
      return content.includes(needle);
    });
  }
  function buildRunMarker({ runId = generateRunNonce() }) {
    return asString(runId).trim() || generateRunNonce();
  }
  function appendRunMarkerToProbeMessage(message, runMarker) {
    const base = asString(message).trim();
    const marker = asString(runMarker).trim();
    if (!marker) return base;
    if (!base) return marker;
    return `${base}
${marker}`;
  }
  function checkChatIdMatch({ runContext, chat }) {
    const expectedChatId = runContext?.createdChatId;
    const actualChatId = chat?.id ?? null;
    const sameChatId = actualChatId !== null && String(actualChatId) === String(expectedChatId);
    return {
      ok: sameChatId,
      reasonCode: DELETE_CHAT_GUARD_REASON_CODES.CHAT_ID_MISMATCH,
      reason: DELETE_CHAT_GUARD_REASON_MESSAGES[DELETE_CHAT_GUARD_REASON_CODES.CHAT_ID_MISMATCH],
      value: sameChatId
    };
  }
  function checkCharacterMatch({ expectedCharacterId, chat }) {
    const actualCharacterId = asString(chat?.character_id);
    const expectedId = asString(expectedCharacterId);
    const matchesCharacter = expectedId && actualCharacterId === expectedId;
    return {
      ok: Boolean(matchesCharacter),
      reasonCode: DELETE_CHAT_GUARD_REASON_CODES.CHARACTER_MISMATCH,
      reason: DELETE_CHAT_GUARD_REASON_MESSAGES[DELETE_CHAT_GUARD_REASON_CODES.CHARACTER_MISMATCH],
      value: Boolean(matchesCharacter)
    };
  }
  function checkTimestampWindow({
    runContext,
    chat,
    nowMs,
    maxFutureSkewMs,
    maxPastSkewMs
  }) {
    const runStartMs = Number(runContext?.runStartMs);
    const chatCreatedAtMs = toTimestamp(chat?.created_at);
    const earliestAllowedMs = Number.isFinite(runStartMs) ? runStartMs - maxPastSkewMs : null;
    const latestAllowedMs = Number.isFinite(nowMs) ? nowMs + maxFutureSkewMs : null;
    const validTimestamp = Number.isFinite(chatCreatedAtMs) && (earliestAllowedMs === null || chatCreatedAtMs >= earliestAllowedMs) && (latestAllowedMs === null || chatCreatedAtMs <= latestAllowedMs);
    return {
      ok: validTimestamp,
      reasonCode: DELETE_CHAT_GUARD_REASON_CODES.TIMESTAMP_OUT_OF_WINDOW,
      reason: DELETE_CHAT_GUARD_REASON_MESSAGES[DELETE_CHAT_GUARD_REASON_CODES.TIMESTAMP_OUT_OF_WINDOW],
      value: validTimestamp
    };
  }
  function checkOwnership({ chat, sessionUserId }) {
    if (!sessionUserId) {
      return {
        ok: true,
        reasonCode: DELETE_CHAT_GUARD_REASON_CODES.OWNER_MISMATCH,
        reason: DELETE_CHAT_GUARD_REASON_MESSAGES[DELETE_CHAT_GUARD_REASON_CODES.OWNER_MISMATCH],
        value: true
      };
    }
    const chatUserId = asString(chat?.user_id);
    const matchesOwner = Boolean(chatUserId && chatUserId === asString(sessionUserId));
    return {
      ok: matchesOwner,
      reasonCode: DELETE_CHAT_GUARD_REASON_CODES.OWNER_MISMATCH,
      reason: DELETE_CHAT_GUARD_REASON_MESSAGES[DELETE_CHAT_GUARD_REASON_CODES.OWNER_MISMATCH],
      value: matchesOwner
    };
  }
  function checkRunMarkerPresent({ chatPayload, runContext }) {
    const runMarker = runContext?.runMarker;
    const hasRunMarker = findRunMarkerInChatMessages(chatPayload, runMarker);
    return {
      ok: hasRunMarker,
      reasonCode: DELETE_CHAT_GUARD_REASON_CODES.RUN_MARKER_MISSING,
      reason: DELETE_CHAT_GUARD_REASON_MESSAGES[DELETE_CHAT_GUARD_REASON_CODES.RUN_MARKER_MISSING],
      value: hasRunMarker
    };
  }
  function evaluateDeletionSafety({
    runContext,
    chatPayload,
    expectedCharacterId,
    sessionUserId = null,
    nowMs = Date.now(),
    maxFutureSkewMs = DEFAULT_FUTURE_SKEW_MS,
    maxPastSkewMs = DEFAULT_PAST_SKEW_MS
  }) {
    const reasons = [];
    const reasonCodes = [];
    const chat = chatPayload?.chat ?? {};
    const chatIdCheck = checkChatIdMatch({ runContext, chat });
    const characterCheck = checkCharacterMatch({ expectedCharacterId, chat });
    const timestampCheck = checkTimestampWindow({
      runContext,
      chat,
      nowMs,
      maxFutureSkewMs,
      maxPastSkewMs
    });
    const ownershipCheck = checkOwnership({ chat, sessionUserId });
    const runMarkerCheck = checkRunMarkerPresent({ chatPayload, runContext });
    const checks = {
      sameChatId: chatIdCheck.value,
      matchesCharacter: characterCheck.value,
      validTimestamp: timestampCheck.value,
      hasRunMarker: runMarkerCheck.value,
      matchesOwner: ownershipCheck.value
    };
    const allChecks = [
      chatIdCheck,
      characterCheck,
      timestampCheck,
      ownershipCheck,
      runMarkerCheck
    ];
    for (const check of allChecks) {
      if (check.ok) continue;
      reasons.push(check.reason);
      reasonCodes.push(check.reasonCode);
    }
    return {
      ok: reasons.length === 0,
      reasons,
      reasonCodes,
      checks
    };
  }
  async function guardedDeleteTemporaryChat({
    client,
    runContext,
    chatPayload = null,
    expectedCharacterId,
    sessionUserId = null,
    nowMs = Date.now(),
    maxFutureSkewMs = DEFAULT_FUTURE_SKEW_MS,
    maxPastSkewMs = DEFAULT_PAST_SKEW_MS,
    logger = console,
    force = false
  }) {
    if (!runContext?.createdChatId) {
      return {
        deleted: false,
        safe: false,
        reasons: [
          DELETE_CHAT_GUARD_REASON_MESSAGES[DELETE_CHAT_GUARD_REASON_CODES.NO_CREATED_CHAT_ID]
        ],
        reasonCodes: [DELETE_CHAT_GUARD_REASON_CODES.NO_CREATED_CHAT_ID]
      };
    }
    let resolvedChatPayload;
    try {
      resolvedChatPayload = chatPayload ?? await client.getChat(runContext.createdChatId);
    } catch (fetchError) {
      if (!force) throw fetchError;
      try {
        logger?.warn?.("[jan] delete guard: chat fetch failed, force-deleting", {
          chatId: runContext.createdChatId,
          error: fetchError?.message
        });
      } catch {
      }
      await client.deleteChat(runContext.createdChatId);
      return {
        deleted: true,
        safe: false,
        forced: true,
        reasons: [`Safety eval skipped: ${fetchError?.message ?? "Chat fetch failed"}`],
        reasonCodes: [],
        checks: {},
        chatId: runContext.createdChatId
      };
    }
    const safety = evaluateDeletionSafety({
      runContext,
      chatPayload: resolvedChatPayload,
      expectedCharacterId,
      sessionUserId,
      nowMs,
      maxFutureSkewMs,
      maxPastSkewMs
    });
    if (!safety.ok) {
      try {
        logger?.warn?.(
          force ? "[jan] delete guard overridden (forced)" : "[jan] delete guard failed",
          {
            reasons: safety.reasons,
            reasonCodes: safety.reasonCodes,
            checks: safety.checks,
            chatId: runContext?.createdChatId ?? null,
            chat: resolvedChatPayload?.chat ?? null,
            chatMessages: resolvedChatPayload?.chatMessages ?? null
          }
        );
      } catch {
      }
      if (!force) {
        return {
          deleted: false,
          safe: false,
          reasons: safety.reasons,
          reasonCodes: safety.reasonCodes,
          checks: safety.checks,
          chatId: runContext.createdChatId
        };
      }
    }
    await client.deleteChat(runContext.createdChatId);
    return {
      deleted: true,
      safe: safety.ok,
      forced: force && !safety.ok,
      reasons: safety.ok ? [] : safety.reasons,
      reasonCodes: safety.ok ? [] : safety.reasonCodes,
      checks: safety.checks,
      chatId: runContext.createdChatId
    };
  }
  const PROGRESS_STAGES = Object.freeze({
    characterLoaded: 0.08,
    chatBootstrapped: 0.18,
    probeSent: 0.35,
    chatFetched: 0.45,
    generateAlphaDone: 0.62,
    cleanupDone: 0.72,
    exportDone: 0.88
  });
  function createProgressReporter(onProgress, stages = PROGRESS_STAGES) {
    const report = (stage) => {
      if (typeof onProgress !== "function") return;
      const value = stages?.[stage];
      if (typeof value === "number") {
        onProgress(value);
      }
    };
    const mapRange = (start, end) => {
      if (typeof onProgress !== "function") return null;
      return (raw) => {
        onProgress(start + raw * (end - start));
      };
    };
    return { report, mapRange };
  }
  function createProxyOffError() {
    const proxyError = new Error("PROXY OFF");
    proxyError.code = "PROXY_OFF";
    return proxyError;
  }
  function resolveCharacterId({ characterId, characterLink }) {
    const resolvedCharacterId = extractCharacterIdFromInput(characterId || characterLink);
    if (!resolvedCharacterId) {
      throw new Error("A valid characterId or characterLink is required.");
    }
    return resolvedCharacterId;
  }
  function createNowResolver(client, nowMs) {
    return typeof client?.getServerNowMs === "function" ? () => client.getServerNowMs() : () => nowMs;
  }
  function createRunContextWithMarker({ characterId, nowMs }) {
    const runContext = createRunContext({
      characterId,
      nowMs
    });
    runContext.runMarker = buildRunMarker({ runId: runContext.runId });
    return runContext;
  }
  async function resolveGenerateAlphaContext({
    client,
    generateAlphaContext,
    generateAlphaProxyConfig,
    generateAlphaPayloadBuilder
  }) {
    const explicitGenerateAlphaContext = isPlainObject(generateAlphaContext) ? generateAlphaContext : null;
    const extractedGenerateAlphaContext = !explicitGenerateAlphaContext && typeof client.getGenerateAlphaRuntimeContext === "function" ? await client.getGenerateAlphaRuntimeContext({}) : null;
    const finalGenerateAlphaContext = explicitGenerateAlphaContext ?? extractedGenerateAlphaContext;
    const mergedGenerateAlphaContext = isPlainObject(finalGenerateAlphaContext) ? { ...finalGenerateAlphaContext } : {};
    const mergedGenerateAlphaUserConfig = mergeGenerateAlphaUserConfigWithProxyConfig(
      mergedGenerateAlphaContext.userConfig,
      generateAlphaProxyConfig
    );
    if (mergedGenerateAlphaUserConfig) {
      mergedGenerateAlphaContext.userConfig = mergedGenerateAlphaUserConfig;
    } else {
      delete mergedGenerateAlphaContext.userConfig;
    }
    const resolvedGenerateAlphaContext = Object.keys(mergedGenerateAlphaContext).length > 0 ? mergedGenerateAlphaContext : null;
    if (!resolvedGenerateAlphaContext && typeof generateAlphaPayloadBuilder !== "function") {
      throw new Error("GenerateAlpha context is required for chat extraction.");
    }
    return resolvedGenerateAlphaContext;
  }
  function resolveProbeScriptIds(probeScriptIds) {
    return Array.isArray(probeScriptIds) && probeScriptIds.length > 0 ? probeScriptIds : [];
  }
  async function bootstrapTemporaryChat({ client, characterId }) {
    return client.startTemporaryChat(characterId);
  }
  async function sendProbe({
    client,
    chatId,
    characterId,
    probeMessage,
    probeScriptIds,
    runMarker
  }) {
    const probeWithRunMarker = appendRunMarkerToProbeMessage(
      probeMessage,
      runMarker
    );
    const messageResponse = await client.sendMessage(chatId, probeWithRunMarker, {
      characterId,
      scriptIds: probeScriptIds
    });
    return { probeWithRunMarker, messageResponse };
  }
  function resolveGenerateAlphaPayloadBuilder(generateAlphaPayloadBuilder) {
    return typeof generateAlphaPayloadBuilder === "function" ? generateAlphaPayloadBuilder : buildDefaultGenerateAlphaPayload;
  }
  async function runGenerateAlpha({
    client,
    builder,
    generateAlphaContext,
    payloadInputs
  }) {
    if (typeof builder !== "function") return null;
    const payload = await builder({
      ...payloadInputs,
      generateAlphaContext
    });
    if (!payload) return null;
    const response = await client.generateAlpha(payload);
    const personaAnchor = typeof payload?.profile?.name === "string" ? payload.profile.name.trim() : "";
    return {
      response,
      personaAnchor: personaAnchor || null
    };
  }
  async function normalizeAndExport({
    characterPayload,
    chatPayload,
    generateAlphaResponse,
    personaAnchor,
    exportOptions
  }) {
    const generateAlphaFields = parseGenerateAlphaFields(
      Array.isArray(generateAlphaResponse?.messages) ? generateAlphaResponse.messages : []
    );
    if (typeof personaAnchor === "string" && personaAnchor.trim()) {
      generateAlphaFields.personaAnchor = personaAnchor.trim();
    }
    const normalizedCharacter = normalizeCharacterFields({
      characterPayload,
      chatPayload,
      generateAlphaFields
    });
    const exportPayload = await buildExportContent(normalizedCharacter, exportOptions);
    return { normalizedCharacter, exportPayload };
  }
  async function executeChatScrapeFlow({
    client,
    characterId = null,
    characterLink = null,
    probeMessage = "",
    probeScriptIds = [],
    generateAlphaContext = null,
    generateAlphaProxyConfig = null,
    generateAlphaPayloadBuilder = null,
    exportFormat = "json",
    useChatName = false,
    includeTags = true,
    includeCreatorNotes = true,
    pronounMacros = null,
    sessionUserId = null,
    nowMs = Date.now(),
    onProgress = null
  }) {
    if (!client) throw new Error("executeChatScrapeFlow requires a client.");
    const resolveNowMs = createNowResolver(client, nowMs);
    const progress = createProgressReporter(onProgress);
    const resolvedCharacterId = resolveCharacterId({
      characterId,
      characterLink
    });
    const runContext = createRunContextWithMarker({
      characterId: resolvedCharacterId,
      nowMs: resolveNowMs()
    });
    let normalizedCharacter = null;
    let exportPayload = null;
    let generateAlphaResponse = null;
    let personaAnchor = null;
    let characterPayload = null;
    let chatPayload = null;
    let resolvedGenerateAlphaContext = null;
    let runtimeError = null;
    let deleteResult = null;
    async function cleanupTemporaryChat(expectedCharacterId, { force = false } = {}) {
      if (deleteResult?.deleted) return deleteResult;
      if (deleteResult && !force) return deleteResult;
      deleteResult = await guardedDeleteTemporaryChat({
        client,
        runContext,
        chatPayload,
        expectedCharacterId,
        sessionUserId,
        nowMs: resolveNowMs(),
        force
      }).catch((error) => ({
        deleted: false,
        safe: false,
        reasons: [`Cleanup failed: ${error?.message ?? "Unknown cleanup error."}`]
      }));
      return deleteResult;
    }
    try {
      characterPayload = await client.getCharacter(resolvedCharacterId);
      runContext.runStartMs = resolveNowMs();
      progress.report("characterLoaded");
      if (characterPayload?.allow_proxy === false) {
        throw createProxyOffError();
      }
      const chatBootstrap = await bootstrapTemporaryChat({
        client,
        characterId: resolvedCharacterId
      });
      runContext.createdChatId = chatBootstrap.chatId;
      progress.report("chatBootstrapped");
      resolvedGenerateAlphaContext = await resolveGenerateAlphaContext({
        client,
        generateAlphaContext,
        generateAlphaProxyConfig,
        generateAlphaPayloadBuilder
      });
      const resolvedProbeScriptIds = resolveProbeScriptIds(probeScriptIds);
      const { probeWithRunMarker, messageResponse } = await sendProbe({
        client,
        chatId: runContext.createdChatId,
        characterId: resolvedCharacterId,
        probeMessage,
        probeScriptIds: resolvedProbeScriptIds,
        runMarker: runContext.runMarker
      });
      progress.report("probeSent");
      chatPayload = await client.getChat(runContext.createdChatId);
      progress.report("chatFetched");
      const resolvedGenerateAlphaPayloadBuilder = resolveGenerateAlphaPayloadBuilder(
        generateAlphaPayloadBuilder
      );
      if (resolvedGenerateAlphaPayloadBuilder) {
        const generateAlphaResult = await runGenerateAlpha({
          client,
          builder: resolvedGenerateAlphaPayloadBuilder,
          generateAlphaContext: resolvedGenerateAlphaContext,
          payloadInputs: {
            chatId: runContext.createdChatId,
            characterId: resolvedCharacterId,
            probeMessage: probeWithRunMarker,
            probeScriptIds: resolvedProbeScriptIds,
            chatBootstrap,
            messageResponse,
            chatPayload,
            characterPayload
          }
        });
        generateAlphaResponse = generateAlphaResult?.response ?? null;
        personaAnchor = generateAlphaResult?.personaAnchor ?? null;
      }
      progress.report("generateAlphaDone");
      await cleanupTemporaryChat(chatPayload?.chat?.character_id || resolvedCharacterId);
      progress.report("cleanupDone");
      const exportOnProgress = progress.mapRange(
        PROGRESS_STAGES.cleanupDone,
        PROGRESS_STAGES.exportDone
      );
      const exportResult = await normalizeAndExport({
        characterPayload,
        chatPayload,
        generateAlphaResponse,
        personaAnchor,
        exportOptions: {
          format: exportFormat,
          useChatName,
          includeTags,
          includeCreatorNotes,
          pronounMacros,
          onProgress: exportOnProgress
        }
      });
      normalizedCharacter = exportResult.normalizedCharacter;
      exportPayload = exportResult.exportPayload;
      progress.report("exportDone");
    } catch (error) {
      runtimeError = error;
    }
    const forceCleanup = !!runtimeError && !!runContext.createdChatId;
    await cleanupTemporaryChat(
      normalizedCharacter?.characterId || chatPayload?.chat?.character_id || resolvedCharacterId,
      { force: forceCleanup }
    );
    if (runtimeError) {
      runtimeError.deleteResult = deleteResult;
      throw runtimeError;
    }
    return {
      runContext,
      normalizedCharacter,
      exportPayload,
      deleteResult
    };
  }
  function sanitizeFilename(value, fallback = DEFAULT_FILENAME, { replacement = "_", normalizeWhitespace = true } = {}) {
    const raw = typeof value === "string" ? value.trim() : "";
    const safeReplacement = typeof replacement === "string" && replacement.length > 0 ? replacement : "_";
    let cleaned = raw.replace(/[\\/:*?"<>|]/g, safeReplacement);
    if (normalizeWhitespace) {
      cleaned = cleaned.replace(/\s+/g, " ").trim();
    } else {
      cleaned = cleaned.trim();
    }
    return cleaned || fallback;
  }
  function buildBaseFilename(normalizedCharacter, { useChatName = false } = {}) {
    const title = resolveCharacterTitle(normalizedCharacter, { useChatName });
    const creator = normalizedCharacter?.creator?.name || DEFAULT_CREATOR;
    const resolvedTitle = title || DEFAULT_FILENAME;
    return sanitizeFilename(`${resolvedTitle} - ${creator}`);
  }
  function buildDownloadArtifact({
    normalizedCharacter,
    exportPayload,
    useChatName = false
  }) {
    const filenameBase = buildBaseFilename(normalizedCharacter, { useChatName });
    const filename = `${filenameBase}.${exportPayload.extension}`;
    return {
      filename,
      mimeType: exportPayload.mimeType,
      content: exportPayload.content
    };
  }
  function getLocalStorage(documentRef) {
    try {
      return documentRef?.defaultView?.localStorage ?? globalThis.localStorage ?? null;
    } catch {
      return null;
    }
  }
  function getSessionStorage(documentRef) {
    try {
      return documentRef?.defaultView?.sessionStorage ?? globalThis.sessionStorage ?? null;
    } catch {
      return null;
    }
  }
  function readString(storage, key, fallback = null) {
    if (!storage || !key) return fallback;
    try {
      const raw = storage.getItem(key);
      if (raw === null || raw === void 0 || raw === "") return fallback;
      return raw;
    } catch {
      return fallback;
    }
  }
  function writeString(storage, key, value) {
    if (!storage || !key) return false;
    try {
      storage.setItem(key, value);
      return true;
    } catch {
      return false;
    }
  }
  function readJson(storage, key, fallback = null) {
    const raw = readString(storage, key, null);
    if (raw === null) return fallback;
    try {
      return JSON.parse(raw);
    } catch {
      return fallback;
    }
  }
  function writeJson(storage, key, value) {
    if (!storage || !key) return false;
    try {
      storage.setItem(key, JSON.stringify(value));
      return true;
    } catch {
      return false;
    }
  }
  function removeStorageItem(storage, key) {
    if (!storage || !key) return false;
    try {
      storage.removeItem(key);
      return true;
    } catch {
      return false;
    }
  }
  const SETTINGS_STORAGE_PREFIX = "jan_setting_";
  const PRONOUN_STORAGE_KEY = "jan_pronoun_macros";
  const PRONOUN_PRESET_STORAGE_KEY = "jan_pronoun_preset";
  const DEFAULT_PRONOUN_PRESET = "they";
  const SETTINGS_DEFAULTS = Object.freeze({
    vis_name_display: "chat",
    vis_char_avatar: "none",
    exp_gen_name: "chat",
    exp_gen_tags: "on",
    exp_gen_notes: "on",
    exp_batch_zip: "store"
  });
  const PRONOUN_PRESETS = Object.freeze({
    she: { sub: "she", obj: "her", poss: "her", poss_p: "hers", ref: "herself" },
    he: { sub: "he", obj: "him", poss: "his", poss_p: "his", ref: "himself" },
    they: { sub: "they", obj: "them", poss: "their", poss_p: "theirs", ref: "themselves" }
  });
  const DEFAULT_PRONOUNS = Object.freeze({
    ...PRONOUN_PRESETS[DEFAULT_PRONOUN_PRESET]
  });
  function readSettingValue(key, fallback, documentRef) {
    const storage = getLocalStorage(documentRef);
    return readString(storage, `${SETTINGS_STORAGE_PREFIX}${key}`, fallback);
  }
  function normalizePronounValue(value, fallback) {
    const text = typeof value === "string" ? value.trim() : "";
    return text || fallback;
  }
  function readPronounMacros(documentRef) {
    const storage = getLocalStorage(documentRef);
    const parsed = readJson(storage, PRONOUN_STORAGE_KEY, null);
    if (!parsed) return { ...DEFAULT_PRONOUNS };
    return {
      sub: normalizePronounValue(parsed?.sub, DEFAULT_PRONOUNS.sub),
      obj: normalizePronounValue(parsed?.obj, DEFAULT_PRONOUNS.obj),
      poss: normalizePronounValue(parsed?.poss, DEFAULT_PRONOUNS.poss),
      poss_p: normalizePronounValue(parsed?.poss_p, DEFAULT_PRONOUNS.poss_p),
      ref: normalizePronounValue(parsed?.ref, DEFAULT_PRONOUNS.ref)
    };
  }
  function readViewerSettings(documentRef) {
    return {
      nameDisplay: readSettingValue(
        "vis_name_display",
        SETTINGS_DEFAULTS.vis_name_display,
        documentRef
      ),
      avatarMode: readSettingValue(
        "vis_char_avatar",
        SETTINGS_DEFAULTS.vis_char_avatar,
        documentRef
      )
    };
  }
  function readExportSettings(documentRef) {
    const nameMode = readSettingValue(
      "exp_gen_name",
      SETTINGS_DEFAULTS.exp_gen_name,
      documentRef
    );
    const tagsMode = readSettingValue(
      "exp_gen_tags",
      SETTINGS_DEFAULTS.exp_gen_tags,
      documentRef
    );
    const notesMode = readSettingValue(
      "exp_gen_notes",
      SETTINGS_DEFAULTS.exp_gen_notes,
      documentRef
    );
    return {
      useChatName: nameMode === "chat",
      includeTags: tagsMode !== "off",
      includeCreatorNotes: notesMode !== "off",
      pronounMacros: readPronounMacros(documentRef)
    };
  }
  function resolveExportOptions(options = {}, exportSettings = readExportSettings()) {
    const settings = exportSettings ?? {};
    return {
      useChatName: options?.useChatName ?? settings.useChatName,
      includeTags: options?.includeTags ?? settings.includeTags,
      includeCreatorNotes: options?.includeCreatorNotes ?? settings.includeCreatorNotes,
      pronounMacros: options?.pronounMacros ?? settings.pronounMacros
    };
  }
  function readBatchZipSetting(documentRef) {
    return readSettingValue(
      "exp_batch_zip",
      SETTINGS_DEFAULTS.exp_batch_zip,
      documentRef
    );
  }
  function normalizeScrapeRequest(rawRequest, exportSettings, now) {
    const {
      characterId = null,
      characterLink = null,
      probeMessage = "",
      probeScriptIds = [],
      generateAlphaContext = null,
      generateAlphaProxyConfig = null,
      exportFormat = "json",
      useChatName = null,
      includeTags = null,
      includeCreatorNotes = null,
      pronounMacros = null,
      sessionUserId = null,
      nowMs = null,
      onProgress = null
    } = rawRequest ?? {};
    const resolvedExportOptions = resolveExportOptions(
      { useChatName, includeTags, includeCreatorNotes, pronounMacros },
      exportSettings
    );
    const resolvedNowMs = nowMs ?? (typeof now === "function" ? now() : Date.now());
    return {
      characterId,
      characterLink,
      probeMessage,
      probeScriptIds,
      generateAlphaContext,
      generateAlphaProxyConfig,
      exportFormat,
      ...resolvedExportOptions,
      sessionUserId,
      nowMs: resolvedNowMs,
      onProgress
    };
  }
  async function runChatScrapeFlow({ client, generateAlphaPayloadBuilder, request }) {
    return executeChatScrapeFlow({
      client,
      characterId: request.characterId,
      characterLink: request.characterLink,
      probeMessage: request.probeMessage,
      probeScriptIds: request.probeScriptIds,
      generateAlphaContext: request.generateAlphaContext,
      generateAlphaProxyConfig: request.generateAlphaProxyConfig,
      generateAlphaPayloadBuilder,
      exportFormat: request.exportFormat,
      useChatName: request.useChatName,
      includeTags: request.includeTags,
      includeCreatorNotes: request.includeCreatorNotes,
      pronounMacros: request.pronounMacros,
      sessionUserId: request.sessionUserId,
      nowMs: request.nowMs,
      onProgress: request.onProgress
    });
  }
  function buildDownloadArtifactForResult(result, useChatName) {
    return buildDownloadArtifact({
      normalizedCharacter: result.normalizedCharacter,
      exportPayload: result.exportPayload,
      useChatName
    });
  }
  function createInternalOrchestrator({
    client,
    generateAlphaPayloadBuilder = null,
    now = () => Date.now()
  }) {
    if (!client) throw new Error("createInternalOrchestrator requires a client.");
    return {
      /**
       * @param {ScrapeCharacterCardRequest} rawRequest
       * @returns {Promise<ScrapeCharacterCardResult>}
       */
      async scrapeCharacterCard(rawRequest) {
        const exportSettings = readExportSettings();
        const request = normalizeScrapeRequest(rawRequest, exportSettings, now);
        const result = await runChatScrapeFlow({
          client,
          generateAlphaPayloadBuilder,
          request
        });
        const downloadArtifact = buildDownloadArtifactForResult(
          result,
          request.useChatName
        );
        return {
          ...result,
          downloadArtifact
        };
      }
    };
  }
  const DARK_MODE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path stroke-dasharray="65" stroke-dashoffset="65" d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.5s" values="65;0"/></path></svg>`;
  const LIGHT_MODE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle stroke-dasharray="26" stroke-dashoffset="26" cx="12" cy="12" r="4"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="26;0"/></circle><path stroke-dasharray="3" stroke-dashoffset="3" d="M12 2v2"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path><path stroke-dasharray="3" stroke-dashoffset="3" d="M12 20v2"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path><path stroke-dasharray="3" stroke-dashoffset="3" d="m4.93 4.93 1.41 1.41"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path><path stroke-dasharray="3" stroke-dashoffset="3" d="m17.66 17.66 1.41 1.41"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path><path stroke-dasharray="3" stroke-dashoffset="3" d="M2 12h2"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path><path stroke-dasharray="3" stroke-dashoffset="3" d="M20 12h2"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path><path stroke-dasharray="3" stroke-dashoffset="3" d="m6.34 17.66-1.41 1.41"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path><path stroke-dasharray="3" stroke-dashoffset="3" d="m19.07 4.93-1.41 1.41"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.35s" dur="0.15s" to="0"/></path></svg>`;
  const PANEL_CLOSE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path stroke-dasharray="15" stroke-dashoffset="15" d="M1 1l10 10"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="15;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M1 11l10-10"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.1s" dur="0.2s" values="15;0"/></path></svg>`;
  const HUD_CLOSE_ICON_SVG = PANEL_CLOSE_ICON_SVG;
  const TAB_CHARACTER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle stroke-dasharray="32" stroke-dashoffset="32" cx="12" cy="8" r="5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="32;0"/></circle><path stroke-dasharray="26" stroke-dashoffset="26" d="M20 21a8 8 0 0 0-16 0"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.25s" dur="0.3s" values="26;0"/></path></svg>`;
  const TAB_DOWNLOAD_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5" aria-hidden="true" focusable="false"><path stroke-dasharray="75" stroke-dashoffset="75" stroke-linejoin="round" d="M20.935 11.009V8.793a2.98 2.98 0 0 0-1.529-2.61l-5.957-3.307a2.98 2.98 0 0 0-2.898 0L4.594 6.182a2.98 2.98 0 0 0-1.529 2.611v6.414a2.98 2.98 0 0 0 1.529 2.61l5.957 3.307a2.98 2.98 0 0 0 2.898 0l2.522-1.4"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.45s" values="75;0"/></path><path stroke-dasharray="20" stroke-dashoffset="20" stroke-linejoin="round" d="M20.33 6.996L12 12L3.67 6.996"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.3s" dur="0.2s" values="20;0"/></path><path stroke-dasharray="10" stroke-dashoffset="10" stroke-linejoin="round" d="M12 21.49V12"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.3s" dur="0.2s" values="10;0"/></path><path stroke-dasharray="6" stroke-dashoffset="6" stroke-miterlimit="10" d="M19.97 19.245v-5"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.45s" dur="0.15s" values="6;0"/></path><path stroke-dasharray="7" stroke-dashoffset="7" stroke-linejoin="round" d="m17.676 17.14l1.968 1.968a.46.46 0 0 0 .65 0l1.968-1.968"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.5s" dur="0.15s" values="7;0"/></path></svg>`;
  const TAB_SETTINGS_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true" focusable="false"><circle stroke-dasharray="19" stroke-dashoffset="19" cx="12" cy="12" r="3"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="19;0"/></circle><path stroke-dasharray="120" stroke-dashoffset="120" d="M13.765 2.152C13.398 2 12.932 2 12 2s-1.398 0-1.765.152a2 2 0 0 0-1.083 1.083c-.092.223-.129.484-.143.863a1.62 1.62 0 0 1-.79 1.353a1.62 1.62 0 0 1-1.567.008c-.336-.178-.579-.276-.82-.308a2 2 0 0 0-1.478.396C4.04 5.79 3.806 6.193 3.34 7s-.7 1.21-.751 1.605a2 2 0 0 0 .396 1.479c.148.192.355.353.676.555c.473.297.777.803.777 1.361s-.304 1.064-.777 1.36c-.321.203-.529.364-.676.556a2 2 0 0 0-.396 1.479c.052.394.285.798.75 1.605c.467.807.7 1.21 1.015 1.453a2 2 0 0 0 1.479.396c.24-.032.483-.13.819-.308a1.62 1.62 0 0 1 1.567.008c.483.28.77.795.79 1.353c.014.38.05.64.143.863a2 2 0 0 0 1.083 1.083C10.602 22 11.068 22 12 22s1.398 0 1.765-.152a2 2 0 0 0 1.083-1.083c.092-.223.129-.483.143-.863c.02-.558.307-1.074.79-1.353a1.62 1.62 0 0 1 1.567-.008c.336.178.579.276.819.308a2 2 0 0 0 1.479-.396c.315-.242.548-.646 1.014-1.453s.7-1.21.751-1.605a2 2 0 0 0-.396-1.479c-.148-.192-.355-.353-.676-.555A1.62 1.62 0 0 1 19.562 12c0-.558.304-1.064.777-1.36c.321-.203.529-.364.676-.556a2 2 0 0 0 .396-1.479c-.052-.394-.285-.798-.75-1.605c-.467-.807-.7-1.21-1.015-1.453a2 2 0 0 0-1.479-.396c-.24.032-.483.13-.82.308a1.62 1.62 0 0 1-1.566-.008a1.62 1.62 0 0 1-.79-1.353c-.014-.38-.05-.64-.143-.863a2 2 0 0 0-1.083-1.083Z"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.15s" dur="0.6s" values="120;0"/></path></svg>`;
  const SETTINGS_NAV_GENERAL_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings-icon lucide-settings"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>`;
  const SETTINGS_NAV_CHARACTER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="5" /><path d="M20 21a8 8 0 0 0-16 0" /></svg>`;
  const SETTINGS_NAV_BATCH_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20.935 11.009V8.793a2.98 2.98 0 0 0-1.529-2.61l-5.957-3.307a2.98 2.98 0 0 0-2.898 0L4.594 6.182a2.98 2.98 0 0 0-1.529 2.611v6.414a2.98 2.98 0 0 0 1.529 2.61l5.957 3.307a2.98 2.98 0 0 0 2.898 0l2.522-1.4" /><path d="M20.33 6.996L12 12L3.67 6.996" /><path d="M12 21.49V12" /><path d="M19.97 19.245v-5" /><path d="m17.676 17.14l1.968 1.968a.46.46 0 0 0 .65 0l1.968-1.968" /></svg>`;
  const SETTINGS_NAV_STORAGE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/></svg>`;
  const SETTINGS_AVATAR_NONE_ICON_SVG = `<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`;
  const SETTINGS_AVATAR_BLUR_ICON_SVG = `<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21" style="opacity:0.5;"/></svg>`;
  const SETTINGS_AVATAR_OFF_ICON_SVG = `<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
  const VERIFIED_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg>`;
  const DEFINITION_HIDDEN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
  const DEFINITION_PUBLIC_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>`;
  const DEFINITION_HIDDEN_EMPTY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>`;
  const MAXIMIZE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m15 15 6 6"/><path d="m15 9 6-6"/><path d="M21 16v5h-5"/><path d="M21 8V3h-5"/><path d="M3 16v5h5"/><path d="m3 21 6-6"/><path d="M3 8V3h5"/><path d="M9 9 3 3"/></svg>`;
  const LIGHTBOX_CLOSE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m14 10 7-7"/><path d="M20 10h-6V4"/><path d="m3 21 7-7"/><path d="M4 14h6v6"/></svg>`;
  const EXPAND_DOWN_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M19 3H5"/><path d="M12 21V7"/><path d="m6 15 6 6 6-6"/></svg>`;
  const EXPAND_UP_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m18 9-6-6-6 6"/><path d="M12 3v14"/><path d="M5 21h14"/></svg>`;
  const FORMAT_PNG_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`;
  const FORMAT_JSON_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"/><path d="M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"/></svg>`;
  const FORMAT_TXT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`;
  const DOWNLOAD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><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"/></svg>`;
  const COPY_IDLE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
  const COPY_SUCCESS_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg>`;
  const DESCRIPTION_EMPTY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>`;
  const FIRST_MESSAGE_EMPTY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M13 21h8"/><path d="m15 5 4 4"/><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>`;
  const SCENARIO_EMPTY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2 6h4"/><path d="M2 10h4"/><path d="M2 14h4"/><path d="M2 18h4"/><rect width="16" height="20" x="4" y="2" rx="2"/><path d="M9.5 8h5"/><path d="M9.5 12H16"/><path d="M9.5 16H14"/></svg>`;
  const EXAMPLE_DIALOGS_EMPTY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><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 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"/></svg>`;
  const CREATOR_NOTES_EMPTY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M14 2a2 2 0 0 0-2 2v2.53"/><path d="M12 6.53a5.98 5.98 0 0 0-8.5.5 4 4 0 0 1 4.02 5.86 4 4 0 0 1-1.76 7.04C6.82 21.17 7.97 22 9 22c1.5 0 1.5-1 3-1s1.5 1 3 1c1.03 0 2.18-.83 3.24-2.07a4 4 0 0 1-1.76-7.03 4 4 0 0 1 4.02-5.87 5.99 5.99 0 0 0-8.5-.5Z"/></svg>`;
  const NO_CHARACTER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 1 0-16 0"/></svg>`;
  const INFO_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`;
  const CORNER_DOWN_LEFT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/></svg>`;
  const CHEVRON_LEFT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="15 18 9 12 15 6"/></svg>`;
  const CHEVRON_RIGHT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="9 18 15 12 9 6"/></svg>`;
  const LOG_OUT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/></svg>`;
  const REFRESH_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>`;
  const SHIELD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>`;
  const PICK_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M2 21a8 8 0 0 1 13.292-6"/><circle cx="10" cy="8" r="5"/><path d="M19 16v6"/><path d="M22 19h-6"/></svg>`;
  const PLUS_CIRCLE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>`;
  const SETTINGS_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M14 17H5M19 7h-9"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></g></svg>`;
  const BATCH_SECTION_DOWNLOAD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/></svg>`;
  const EXPORT_FILES_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 2h-4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8"/><path d="M16.706 2.706A2.4 2.4 0 0 0 15 2v5a1 1 0 0 0 1 1h5a2.4 2.4 0 0 0-.706-1.706z"/><path d="M5 7a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h8a2 2 0 0 0 1.732-1"/></svg>`;
  const EXPORT_ZIP_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M8 12v-1"/><path d="M8 18v-2"/><path d="M8 7V6"/><circle cx="8" cy="20" r="2"/></svg>`;
  const FILTER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>`;
  const BATCH_FORMAT_PNG_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`;
  const BATCH_FORMAT_JSON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1"/><path d="M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1"/></svg>`;
  const BATCH_FORMAT_TXT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>`;
  const BULK_FETCH_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>`;
  const BULK_DOWNLOAD_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 15V3"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/></svg>`;
  const TRASH_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 11v6"/><path d="M14 11v6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`;
  const VIEWER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></svg>`;
  const FETCH_ITEM_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 17V3"/><path d="m6 11 6 6 6-6"/><path d="M19 21H5"/></svg>`;
  const QUEUE_PENDING_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M9 15h6"/></svg>`;
  const QUEUE_FETCHING_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M16 22h2a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v2.85"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M8 14v2.2l1.6 1"/><circle cx="8" cy="16" r="6"/></svg>`;
  const QUEUE_ERROR_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="m14.5 12.5-5 5"/><path d="m9.5 12.5 5 5"/></svg>`;
  const QUEUE_READY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="m9 15 2 2 4-4"/></svg>`;
  const HUD_SELECT_ALL_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>`;
  const HUD_SELECT_PARTIAL_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>`;
  const HUD_SELECT_NONE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="m9 12 2 2 4-4"/></svg>`;
  function createRafScheduler$1(fn, windowRef = globalThis) {
    const raf = typeof windowRef?.requestAnimationFrame === "function" ? windowRef.requestAnimationFrame.bind(windowRef) : (cb) => (windowRef?.setTimeout ?? globalThis.setTimeout)(cb, 16);
    const cancel = typeof windowRef?.cancelAnimationFrame === "function" ? windowRef.cancelAnimationFrame.bind(windowRef) : (id) => (windowRef?.clearTimeout ?? globalThis.clearTimeout)(id);
    let rafId = null;
    let lastArgs = null;
    const schedule = (...args) => {
      lastArgs = args;
      if (rafId !== null) return;
      rafId = raf(() => {
        rafId = null;
        fn(...lastArgs ?? []);
      });
    };
    schedule.cancel = () => {
      if (rafId === null) return;
      cancel(rafId);
      rafId = null;
    };
    return schedule;
  }
  function createDebounced(fn, delayMs = 0, windowRef = globalThis) {
    const setTimer = typeof windowRef?.setTimeout === "function" ? windowRef.setTimeout.bind(windowRef) : globalThis.setTimeout;
    const clearTimer = typeof windowRef?.clearTimeout === "function" ? windowRef.clearTimeout.bind(windowRef) : globalThis.clearTimeout;
    let timerId = null;
    let lastArgs = null;
    const schedule = (...args) => {
      lastArgs = args;
      if (timerId !== null) clearTimer(timerId);
      timerId = setTimer(() => {
        timerId = null;
        fn(...lastArgs ?? []);
      }, delayMs);
    };
    schedule.flush = () => {
      if (timerId === null) return;
      clearTimer(timerId);
      timerId = null;
      fn(...lastArgs ?? []);
    };
    schedule.cancel = () => {
      if (timerId === null) return;
      clearTimer(timerId);
      timerId = null;
    };
    return schedule;
  }
  function ensureStyle(documentRef, { id, cssText }) {
    if (!documentRef) return null;
    const existing = documentRef.getElementById(id);
    if (existing) return existing;
    const style = documentRef.createElement("style");
    style.id = id;
    style.textContent = cssText;
    (documentRef.head ?? documentRef.documentElement)?.appendChild(style);
    return style;
  }
  function removeStyle(documentRef, id) {
    documentRef?.getElementById?.(id)?.remove();
  }
  async function withBusyButton(button, task) {
    if (!button || typeof task !== "function") return null;
    const wasDisabled = button.disabled;
    const hadBusy = button.hasAttribute("aria-busy");
    if (!wasDisabled) {
      button.disabled = true;
    }
    button.setAttribute("aria-busy", "true");
    try {
      return await task();
    } finally {
      if (!wasDisabled) {
        button.disabled = false;
      }
      if (!hadBusy) {
        button.removeAttribute("aria-busy");
      }
    }
  }
  function isElementNode$1(node) {
    return !!node && node.nodeType === 1;
  }
  function createMutationObserver(documentRef, onMutations, { root, options } = {}) {
    const Observer = documentRef?.defaultView?.MutationObserver ?? globalThis.MutationObserver;
    if (typeof Observer !== "function") return null;
    const observer = new Observer(onMutations);
    const target = root ?? documentRef?.documentElement ?? documentRef?.body;
    if (target) {
      observer.observe(target, {
        childList: true,
        subtree: true,
        ...options ?? {}
      });
    }
    return observer;
  }
  function createRafScheduler(documentRef, callback) {
    const windowRef = documentRef?.defaultView ?? globalThis;
    return createRafScheduler$1(callback, windowRef);
  }
  function scheduleRetries(delays, callback, { isActive } = {}) {
    const shouldRun = typeof isActive === "function" ? isActive : () => true;
    const timers = delays.map(
      (delay) => setTimeout(() => {
        if (!shouldRun()) return;
        callback();
      }, delay)
    );
    return () => {
      timers.forEach((timer) => clearTimeout(timer));
    };
  }
  const GOOGLE_FONTS_PRECONNECT_ID = "jan-font-preconnect-google";
  const GSTATIC_FONTS_PRECONNECT_ID = "jan-font-preconnect-gstatic";
  function ensureLink(documentRef, { id, rel, href, crossOrigin = null }) {
    if (!documentRef?.head || !id || !rel || !href) return;
    if (documentRef.getElementById(id)) return;
    const link = documentRef.createElement("link");
    link.id = id;
    link.rel = rel;
    link.href = href;
    if (crossOrigin) link.crossOrigin = crossOrigin;
    documentRef.head.appendChild(link);
  }
  function ensureGoogleFontStylesheet(documentRef, { id, href }) {
    if (!documentRef?.head) return;
    ensureLink(documentRef, {
      id: GOOGLE_FONTS_PRECONNECT_ID,
      rel: "preconnect",
      href: "https://fonts.googleapis.com"
    });
    ensureLink(documentRef, {
      id: GSTATIC_FONTS_PRECONNECT_ID,
      rel: "preconnect",
      href: "https://fonts.gstatic.com",
      crossOrigin: "anonymous"
    });
    ensureLink(documentRef, {
      id,
      rel: "stylesheet",
      href
    });
  }
  const CHARACTER_VIEWER_TAB_STYLE_ID = "jan-character-viewer-tab-style";
  const CHARACTER_VIEWER_FONT_STYLESHEET_ID = "jan-character-viewer-font-stylesheet";
  const CHARACTER_VIEWER_FONT_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&family=JetBrains+Mono:wght@400&display=swap";
  const CHARACTER_VIEWER_AVATAR_WIDTH = 272;
  const CHARACTER_VIEWER_AVATAR_COLLAPSED_HEIGHT = 248;
  const CHARACTER_VIEWER_AVATAR_MAX_HEIGHT = 500;
  function ensureCharacterViewerTabStyle(documentRef) {
    if (!documentRef?.head) return;
    ensureGoogleFontStylesheet(documentRef, {
      id: CHARACTER_VIEWER_FONT_STYLESHEET_ID,
      href: CHARACTER_VIEWER_FONT_STYLESHEET_URL
    });
    ensureStyle(documentRef, {
      id: CHARACTER_VIEWER_TAB_STYLE_ID,
      cssText: `
    .jan-character-viewer-view {
      --bg-color: #050505;
      --panel-bg: linear-gradient(180deg, #0a0a0a 0%, #000000 100%);
      --border-color: rgba(255, 255, 255, 0.07);
      --text-color: #e8e8ec;
      --text-muted: rgba(255, 255, 255, 0.5);
      --accent: #fff;
      --accent-glow: rgba(255, 255, 255, 0.2);
      --success: #4CAF50;
      --error: #f44336;
      --warning: #ffb74d;
      --hover-bg: rgba(255, 255, 255, 0.05);
      --pod-bg: linear-gradient(180deg, #181818 0%, #000000 100%);
      --font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      --tok-personality: #7ba5e8;
      --tok-firstmsg: #6ec9a5;
      --tok-scenario: #e0a86e;
      --tok-dialogs: #c47ede;
      color: var(--text-color);
      display: none;
      flex-direction: column;
      font-family: var(--font-family);
      height: 100%;
      min-height: 0;
      overflow: hidden;
      position: relative;
      width: 100%;
    }

    .jan-character-viewer-view .avatar-section:hover .avatar-gradient { opacity: 1; }


    .jan-character-viewer-view.is-active {
      display: flex;
    }

    .jan-character-viewer-view::before {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      height: 1px;
      background: rgba(255, 255, 255, 0.08);
      pointer-events: none;
      z-index: 5;
    }

    .jan-character-viewer-view .main-content {
      display: flex;
      flex: 1;
      min-height: 0;
      overflow: hidden;
      position: relative;
      width: 100%;
      z-index: 1;
    }

    .jan-character-viewer-view .meta-sidebar {
      width: 272px;
      flex-shrink: 0;
      box-shadow: 1px 0 0 rgba(255, 255, 255, 0.05);
      display: flex;
      flex-direction: column;
      overflow: hidden;
      overscroll-behavior: contain;
      background: rgba(0, 0, 0, 0.15);
    }

    .jan-character-viewer-view .avatar-section {
      position: relative;
      width: 272px;
      flex-shrink: 0;
      overflow: hidden;
      cursor: pointer;
      transition: height 0.38s cubic-bezier(0.4, 0, 0.2, 1);
    }

    .jan-character-viewer-view .avatar-section::before {
      content: '';
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      height: 12px;
      background: linear-gradient(180deg, rgba(10, 10, 10, 0.58) 0%, rgba(10, 10, 10, 0.22) 42%, rgba(10, 10, 10, 0) 100%);
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s ease;
      z-index: 2;
    }

    .jan-character-viewer-view .avatar-section:hover::before { opacity: 1; }

    .jan-character-viewer-view .avatar-maximize-btn {
      position: absolute;
      top: 8px;
      right: 8px;
      opacity: 0;
      transition: opacity 0.22s ease;
      cursor: pointer;
      color: rgba(255, 255, 255, 0.9);
      background: rgba(0, 0, 0, 0.5);
      border-radius: 5px;
      padding: 4px;
      border: none;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 4;
      line-height: 0;
    }

    .jan-character-viewer-view .avatar-section:hover .avatar-maximize-btn { opacity: 1; }

    .jan-character-viewer-view .avatar-expand-toggle {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 5px;
      padding: 7px 0 10px;
      cursor: pointer;
      opacity: 0;
      transition: opacity 0.22s ease;
      font-size: 0.73rem;
      font-weight: 500;
      color: rgba(255, 255, 255, 0.85);
      letter-spacing: 0.04em;
      z-index: 3;
      user-select: none;
    }

    .jan-character-viewer-view .avatar-section:hover .avatar-expand-toggle { opacity: 1; }

    .jan-character-viewer-view .character-avatar {
      width: 100%;
      height: 100%;
      object-fit: cover;
      object-position: center top;
      display: block;
    }

    .jan-character-viewer-view .avatar-gradient {
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      height: 96px;
      background: linear-gradient(transparent, rgba(0, 0, 0, 0.72));
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s ease;
    }

    .jan-character-viewer-view .meta-body {
      flex: 1;
      overflow-y: auto;
      overscroll-behavior: contain;
      padding: 14px 15px;
      display: flex;
      flex-direction: column;
      gap: 14px;
    }

    .jan-character-viewer-view .meta-body::-webkit-scrollbar { width: 4px; }
    .jan-character-viewer-view .meta-body::-webkit-scrollbar-track { background: transparent; }
    .jan-character-viewer-view .meta-body::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 0; }
    .jan-character-viewer-view .meta-body::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }

    .jan-character-viewer-view .meta-section {
      display: flex;
      flex-direction: column;
      gap: 7px;
    }

    .jan-character-viewer-view .section-title {
      display: flex;
      align-items: center;
      justify-content: space-between;
      font-size: 0.7rem;
      text-transform: uppercase;
      letter-spacing: 0.1em;
      color: var(--text-muted);
      font-weight: 600;
      margin-bottom: 1px;
    }

    .jan-character-viewer-view .section-title-right {
      font-size: 0.82rem;
      color: var(--text-color);
      font-weight: 500;
      letter-spacing: 0;
      text-transform: none;
    }

    .jan-character-viewer-view .meta-kv {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .jan-character-viewer-view .meta-key {
      font-size: 0.8rem;
      color: var(--text-muted);
      flex-shrink: 0;
    }

    .jan-character-viewer-view .meta-value {
      font-size: 0.82rem;
      color: var(--text-color);
      text-align: right;
    }

    .jan-character-viewer-view .meta-key-toggle {
      cursor: pointer;
      user-select: none;
      text-decoration-line: underline;
      text-decoration-style: dashed;
      text-decoration-color: rgba(255, 255, 255, 0.2);
      transition: color 0.15s, text-decoration-color 0.15s;
    }

    .jan-character-viewer-view .meta-key-toggle:hover {
      color: rgba(255, 255, 255, 0.75);
      text-decoration-color: rgba(255, 255, 255, 0.45);
    }

    .jan-character-viewer-view .meta-key-toggle.is-alt {
      color: rgba(255, 255, 255, 0.65);
      text-decoration-color: rgba(255, 255, 255, 0.4);
    }

    .jan-character-viewer-view .meta-link {
      font-size: 0.82rem;
      color: rgba(255, 255, 255, 0.72);
      text-decoration: none;
      text-align: right;
      display: flex;
      align-items: center;
      gap: 2px;
      transition: color 0.15s;
    }

    .jan-character-viewer-view .meta-link:hover { color: var(--text-color); }

    .jan-character-viewer-view .meta-link.id-link {
      font-family: 'JetBrains Mono', monospace;
      font-size: 0.73rem;
      color: rgba(255, 255, 255, 0.38);
      letter-spacing: 0;
    }

    .jan-character-viewer-view .meta-link.id-link:hover { color: rgba(255, 255, 255, 0.65); }

    .jan-character-viewer-view .verified-icon { color: #5a9cf5; flex-shrink: 0; }

    .jan-character-viewer-view .tag-list {
      display: flex;
      flex-wrap: wrap;
      gap: 5px;
    }

    .jan-character-viewer-view .tag-pill {
      font-size: 0.7rem;
      font-weight: 500;
      padding: 3px 9px;
      border-radius: 100px;
      background: rgba(255, 255, 255, 0.06);
      border: 1px solid rgba(255, 255, 255, 0.08);
      color: rgba(255, 255, 255, 0.6);
      white-space: nowrap;
    }

    .jan-character-viewer-view .tag-nsfw {
      background: rgba(220, 50, 47, 0.12);
      border-color: rgba(220, 50, 47, 0.28);
      color: #e05450;
    }

    .jan-character-viewer-view .tag-custom {
      background: rgba(195, 119, 224, 0.1);
      border-color: rgba(195, 119, 224, 0.22);
      color: rgba(195, 119, 224, 0.88);
    }

    .jan-character-viewer-view .stats-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 7px;
    }

    .jan-character-viewer-view .stat-block {
      background: rgba(255, 255, 255, 0.02);
      border: 1px solid rgba(255, 255, 255, 0.05);
      border-radius: 8px;
      padding: 9px 11px;
      display: flex;
      flex-direction: column;
      gap: 3px;
    }

    .jan-character-viewer-view .stat-val {
      font-size: 1.05rem;
      font-weight: 500;
      color: var(--text-color);
      line-height: 1;
    }

    .jan-character-viewer-view .stat-lbl {
      font-size: 0.7rem;
      color: var(--text-muted);
    }

    .jan-character-viewer-view .token-bar {
      height: 6px;
      border-radius: 6px;
      overflow: hidden;
      background: rgba(255, 255, 255, 0.05);
      display: flex;
    }

    .jan-character-viewer-view .token-seg { height: 100%; }
    .jan-character-viewer-view .seg-personality { background: var(--tok-personality); }
    .jan-character-viewer-view .seg-firstmsg { background: var(--tok-firstmsg); }
    .jan-character-viewer-view .seg-scenario { background: var(--tok-scenario); }
    .jan-character-viewer-view .seg-dialogs { background: var(--tok-dialogs); }

    .jan-character-viewer-view .token-legend {
      display: flex;
      flex-direction: column;
      gap: 5px;
      margin-top: 9px;
    }

    .jan-character-viewer-view .legend-row {
      display: flex;
      align-items: center;
      gap: 7px;
    }

    .jan-character-viewer-view .legend-dot {
      width: 7px;
      height: 7px;
      border-radius: 50%;
      flex-shrink: 0;
    }

    .jan-character-viewer-view .legend-key {
      font-size: 0.77rem;
      color: var(--text-muted);
      flex: 1;
    }

    .jan-character-viewer-view .legend-val {
      font-size: 0.77rem;
      color: var(--text-color);
      font-weight: 500;
    }

    .jan-character-viewer-view .legend-row.zero .legend-dot { background: rgba(255, 255, 255, 0.1) !important; }
    .jan-character-viewer-view .legend-row.zero .legend-key { color: rgba(255, 255, 255, 0.22); }
    .jan-character-viewer-view .legend-row.zero .legend-val { color: rgba(255, 255, 255, 0.22); }

    .jan-character-viewer-view .sidebar-footer {
      padding: 11px 14px;
      box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.05);
      flex-shrink: 0;
    }

    .jan-character-viewer-view .action-block {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 6px;
      position: relative;
    }

    .jan-character-viewer-view .format-picker-group {
      display: flex;
      align-items: center;
      background: rgba(0, 0, 0, 0.5);
      border-radius: 8px;
      padding: 2px;
      transition: opacity 0.3s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
    }
    
    .jan-character-viewer-view .format-picker-group.dl-hidden {
      opacity: 0;
      pointer-events: none;
      transform: scale(0.9);
    }

    .jan-character-viewer-view .format-option { position: relative; min-width: 0; }
    .jan-character-viewer-view .format-option input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; }

    .jan-character-viewer-view .format-label {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 4px;
      padding: 6px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 0.65rem;
      font-weight: 600;
      letter-spacing: 0.03em;
      color: rgba(255, 255, 255, 0.3);
      transition: background 0.2s ease, color 0.2s ease, padding 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease;
      user-select: none;
    }

    .jan-character-viewer-view .format-label svg {
      width: 12px;
      height: 12px;
      opacity: 0.7;
      flex-shrink: 0;
      transition: opacity 0.2s;
    }

    .jan-character-viewer-view .format-label .fmt-text {
      display: inline-block;
      max-width: 0;
      overflow: hidden;
      opacity: 0;
      white-space: nowrap;
      transition: max-width 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease;
    }

    .jan-character-viewer-view .format-option:has(input[value="png"]):hover .format-label { color: #5a9cf5; }
    .jan-character-viewer-view .format-option:has(input[value="json"]):hover .format-label { color: #d4893a; }
    .jan-character-viewer-view .format-option:has(input[value="txt"]):hover .format-label { color: #8fa8be; }
    .jan-character-viewer-view .format-option:hover .format-label svg { opacity: 1; }

    .jan-character-viewer-view .format-option input[value="png"]:checked ~ .format-label {
      padding: 6px 8px;
      background: rgba(90, 156, 245, 0.1);
      color: #5a9cf5;
      box-shadow: inset 0 0 0 2px rgba(90, 156, 245, 0.55);
    }
    .jan-character-viewer-view .format-option input[value="json"]:checked ~ .format-label {
      padding: 6px 8px;
      background: rgba(212, 137, 58, 0.1);
      color: #d4893a;
      box-shadow: inset 0 0 0 2px rgba(212, 137, 58, 0.55);
    }
    .jan-character-viewer-view .format-option input[value="txt"]:checked ~ .format-label {
      padding: 6px 8px;
      background: rgba(143, 168, 190, 0.1);
      color: #8fa8be;
      box-shadow: inset 0 0 0 2px rgba(143, 168, 190, 0.55);
    }
    .jan-character-viewer-view .format-option input:checked ~ .format-label svg { opacity: 1; }
    .jan-character-viewer-view .format-option input:checked ~ .format-label .fmt-text {
      max-width: 40px;
      opacity: 1;
    }

    .jan-character-viewer-view .btn-download {
      flex: 0 0 auto;
      padding: 7px 12px;
      background: transparent;
      border: none;
      border-radius: 50px;
      color: rgba(255, 255, 255, 0.45);
      cursor: pointer;
      font-size: 0.65rem;
      font-weight: 600;
      font-family: inherit;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      transition: all 0.2s ease;
      white-space: nowrap;
    }

    .jan-character-viewer-view .btn-download:hover {
      color: #fff;
      background: rgba(255, 255, 255, 0.06);
    }

    .jan-character-viewer-view .btn-download:active { transform: scale(0.97); }

    .jan-character-viewer-view .btn-download.dl-hidden {
      opacity: 0;
      pointer-events: none;
      transform: scale(0.9);
    }

    .jan-character-viewer-view .dl-core {
      position: relative;
    }

    .jan-character-viewer-view .physics-container {
      position: absolute;
      top: 50%;
      left: 0;
      transform: translateY(-50%);
      display: flex;
      align-items: center;
      justify-content: center;
      height: 30px;
      width: 100%;
      pointer-events: none;
      overflow: hidden;
    }

    .jan-character-viewer-view .physics-mask {
      position: absolute;
      top: 0; left: 0; right: 0; bottom: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
      mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
    }

    .jan-character-viewer-view .physics-text {
      display: inline-flex;
      font-family: 'JetBrains Mono', monospace;
      font-size: 0.72rem;
      font-weight: 400;
      color: #c5d6e8;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }

    .jan-character-viewer-view .char-slot {
      display: inline-block;
      width: 1ch;
      text-align: center;
      position: relative;
      overflow: hidden;
      height: 1.5em;
      transition: width 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
      opacity: 1;
    }

    .jan-character-viewer-view .char-slot.collapsed {
      width: 0px;
      opacity: 0;
    }

    .jan-character-viewer-view .char-column {
      display: flex;
      flex-direction: column;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      will-change: transform;
      backface-visibility: hidden;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }

    .jan-character-viewer-view .char-column span {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 1.5em;
      line-height: 1;
    }

    .jan-character-viewer-view .content-section {
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    .jan-character-viewer-view .tab-bar {
      display: flex;
      align-items: stretch;
      padding: 0 22px;
      box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05);
      flex-shrink: 0;
      gap: 0;
    }

    .jan-character-viewer-view .tab-btn {
      background: transparent;
      border: none;
      border-bottom: 2px solid transparent;
      color: var(--text-muted);
      font-family: inherit;
      font-size: 0.84rem;
      font-weight: 400;
      padding: 14px 13px 12px;
      cursor: pointer;
      transition: all 0.15s ease;
      display: flex;
      align-items: center;
      gap: 5px;
      white-space: nowrap;
      margin-bottom: -1px;
    }

    .jan-character-viewer-view .tab-btn:hover { color: rgba(255, 255, 255, 0.8); }
    .jan-character-viewer-view .tab-btn:hover .tab-count { color: rgba(255, 255, 255, 0.30); }
    .jan-character-viewer-view .tab-btn:hover .tab-dash { color: rgba(255, 255, 255, 0.26); }

    .jan-character-viewer-view .tab-btn.active {
      color: var(--text-color);
      border-bottom-color: var(--accent);
      font-weight: 500;
    }

    .jan-character-viewer-view .tab-count {
      font-size: 0.7rem;
      color: rgba(255, 255, 255, 0.22);
      font-weight: 400;
      transition: color 0.15s ease;
    }

    .jan-character-viewer-view .tab-btn.active .tab-count { color: rgba(255, 255, 255, 0.38); }

    .jan-character-viewer-view .tab-dash {
      font-size: 0.7rem;
      color: rgba(255, 255, 255, 0.2);
      font-weight: 400;
      transition: color 0.15s ease;
    }

    .jan-character-viewer-view .tab-content-wrap {
      flex: 1;
      overflow: hidden;
      position: relative;
    }

    .jan-character-viewer-view .tab-pane {
      display: none;
      position: absolute;
      inset: 0;
      overflow-y: auto;
      overscroll-behavior: contain;
      flex-direction: column;
    }

    .jan-character-viewer-view .tab-pane.active { display: flex; }

    .jan-character-viewer-view .tab-pane::-webkit-scrollbar { width: 4px; }
    .jan-character-viewer-view .tab-pane::-webkit-scrollbar-track { background: transparent; margin: 0; }
    .jan-character-viewer-view .tab-pane::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 0; }
    .jan-character-viewer-view .tab-pane::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }

    .jan-character-viewer-view .field-wrapper {
      flex: 1;
      display: flex;
      flex-direction: column;
      padding: 18px 22px 22px;
    }

    .jan-character-viewer-view .field-header {
      display: flex;
      justify-content: flex-end;
      margin-bottom: 10px;
    }

    .jan-character-viewer-view .field-text {
      margin: 0;
      font-family: var(--font-family);
      font-size: 0.82rem;
      line-height: 1.75;
      color: rgba(255, 255, 255, 0.75);
      white-space: pre-wrap;
      word-break: break-word;
      background: rgba(0, 0, 0, 0.22);
      border: 1px solid rgba(255, 255, 255, 0.04);
      border-radius: 8px;
      padding: 15px 17px;
    }

    .jan-character-viewer-view .copy-btn {
      background: none;
      border: none;
      color: rgba(255, 255, 255, 0.3);
      font-family: inherit;
      font-size: 0.72rem;
      font-weight: 500;
      cursor: pointer;
      padding: 2px 0;
      display: flex;
      align-items: center;
      gap: 5px;
      position: relative;
      transition: color 0.18s;
    }

    .jan-character-viewer-view .copy-btn::after {
      content: '';
      position: absolute;
      bottom: 0;
      left: 0;
      right: 0;
      height: 1px;
      background: currentColor;
      transform: scaleX(0);
      transform-origin: left;
      transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1);
    }

    .jan-character-viewer-view .copy-btn:hover {
      color: rgba(255, 255, 255, 0.6);
    }

    .jan-character-viewer-view .copy-btn:hover::after {
      transform: scaleX(1);
    }

    .jan-character-viewer-view .copy-btn.copied {
      color: var(--success);
    }

    .jan-character-viewer-view .copy-btn.copied::after {
      transform: scaleX(1);
      background: var(--success);
    }

    .jan-character-viewer-view .empty-state {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 13px;
      color: rgba(255, 255, 255, 0.18);
      padding: 40px;
    }

    .jan-character-viewer-view .empty-state-text {
      font-size: 0.85rem;
      text-align: center;
    }

    .jan-character-viewer-view .html-display {
      padding: 22px 26px 26px;
      font-size: 0.86rem;
      line-height: 1.72;
      color: rgba(255, 255, 255, 0.72);
    }

    .jan-character-viewer-view .html-display h1,
    .jan-character-viewer-view .html-display h2,
    .jan-character-viewer-view .html-display h3 {
      color: var(--text-color);
      margin: 18px 0 6px;
      font-weight: 600;
      font-size: 0.96rem;
      line-height: 1.4;
    }

    .jan-character-viewer-view .html-display h1:first-child,
    .jan-character-viewer-view .html-display h2:first-child { margin-top: 0; }

    .jan-character-viewer-view .html-display p { margin: 5px 0; }
    .jan-character-viewer-view .html-display p:empty { display: none; }

    .jan-character-viewer-view .html-display a {
      color: #7ba5e8;
      text-decoration: none;
    }

    .jan-character-viewer-view .html-display a:hover { text-decoration: underline; }

    .jan-character-viewer-view .cv-no-character {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 11px;
      padding: 40px;
      text-align: center;
      color: rgba(255, 255, 255, 0.16);
    }

    .jan-character-viewer-view .cv-no-character-title {
      font-size: 0.88rem;
      font-weight: 500;
      color: rgba(255, 255, 255, 0.22);
      margin-top: 2px;
    }

    .jan-character-viewer-view .cv-no-character-sub {
      font-size: 0.77rem;
      color: rgba(255, 255, 255, 0.13);
      max-width: 220px;
      line-height: 1.65;
    }

    .jan-character-viewer-view .cv-load-form {
      width: 280px;
      margin-top: 8px;
      display: flex;
      flex-direction: column;
      gap: 6px;
    }

    .jan-character-viewer-view .cv-id-input {
      width: 100%;
      background: rgba(255, 255, 255, 0.04);
      border: 1px solid rgba(255, 255, 255, 0.12);
      border-radius: 6px;
      color: var(--text-color);
      font-family: 'JetBrains Mono', monospace;
      font-size: 0.72rem;
      padding: 8px 11px;
      outline: none;
      transition: border-color 0.15s;
    }

    .jan-character-viewer-view .cv-id-input::placeholder { color: rgba(255, 255, 255, 0.16); }
    .jan-character-viewer-view .cv-id-input:focus {
      border-color: rgba(255, 255, 255, 0.24);
    }

    .jan-character-viewer-view .cv-enter-btn {
      width: 100%;
      background: transparent;
      border: 1px solid rgba(255, 255, 255, 0.12);
      border-radius: 6px;
      color: rgba(255, 255, 255, 0.38);
      cursor: pointer;
      font-family: inherit;
      font-size: 0.72rem;
      font-weight: 600;
      letter-spacing: 0.03em;
      padding: 8px 12px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 6px;
      transition: color 0.18s, background 0.18s, border-color 0.15s;
    }

    .jan-character-viewer-view .cv-enter-btn:hover {
      color: rgba(255, 255, 255, 0.72);
      background: rgba(255, 255, 255, 0.04);
    }

    .jan-character-viewer-view .cv-enter-btn:focus { outline: none; }

    .jan-character-viewer-view .cv-enter-btn:active { transform: scale(0.98); }

    .jan-character-viewer-view .cv-load-error {
      font-size: 0.72rem;
      color: rgba(220, 50, 47, 0.88);
      margin-top: 6px;
      text-align: center;
      min-height: 1.4em;
    }

    .jan-character-viewer-view .cv-info-row {
      display: flex;
      align-items: center;
      gap: 5px;
      color: rgba(255, 255, 255, 0.18);
      font-size: 0.71rem;
      margin-top: 6px;
    }

    .jan-character-viewer-view .cv-info-row span {
      margin-top: 1px;
    }

    .jan-character-viewer-view .cv-load-form.is-loading .cv-id-input,
    .jan-character-viewer-view .cv-load-form.is-loading .cv-enter-btn {
      opacity: 0.45;
      pointer-events: none;
    }

    .jan-character-viewer-view .cv-load-form.is-error .cv-id-input {
      border-color: rgba(220, 50, 47, 0.45);
    }

    /* --- Greeting Carousel --- */
    @keyframes jan-greet-out-left {
      from { transform: translateX(0); opacity: 1; }
      to   { transform: translateX(-28px); opacity: 0; }
    }
    @keyframes jan-greet-out-right {
      from { transform: translateX(0); opacity: 1; }
      to   { transform: translateX(28px); opacity: 0; }
    }
    @keyframes jan-greet-in-right {
      from { transform: translateX(28px); opacity: 0; }
      to   { transform: translateX(0); opacity: 1; }
    }
    @keyframes jan-greet-in-left {
      from { transform: translateX(-28px); opacity: 0; }
      to   { transform: translateX(0); opacity: 1; }
    }

    .jan-character-viewer-view .greeting-field-header {
      justify-content: space-between;
      align-items: center;
    }

    .jan-character-viewer-view .greeting-nav {
      display: flex;
      align-items: center;
      gap: 5px;
    }

    .jan-character-viewer-view .greeting-nav-btn {
      background: none;
      border: 1px solid rgba(255, 255, 255, 0.1);
      border-radius: 5px;
      color: rgba(255, 255, 255, 0.35);
      cursor: pointer;
      width: 22px;
      height: 22px;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0;
      line-height: 0;
      transition: color 0.15s, border-color 0.15s, background 0.15s;
      flex-shrink: 0;
    }

    .jan-character-viewer-view .greeting-nav-btn:hover:not(:disabled) {
      color: rgba(255, 255, 255, 0.8);
      border-color: rgba(255, 255, 255, 0.22);
      background: rgba(255, 255, 255, 0.05);
    }

    .jan-character-viewer-view .greeting-nav-btn:disabled {
      opacity: 0.22;
      cursor: default;
    }

    .jan-character-viewer-view .greeting-counter {
      font-family: 'JetBrains Mono', monospace;
      font-size: 0.7rem;
      color: rgba(255, 255, 255, 0.28);
      min-width: 36px;
      text-align: center;
      user-select: none;
    }

    .jan-character-viewer-view .greeting-text-wrap {
      position: relative;
      overflow: hidden;
      width: 100%;
    }

    .jan-character-viewer-view .greeting-dots {
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 6px;
      padding: 10px 0 8px;
    }

    .jan-character-viewer-view .greeting-dot {
      position: relative;
      width: 7px;
      height: 7px;
      border-radius: 50%;
      background: rgba(255, 255, 255, 0.14);
      cursor: pointer;
      transition: background 0.2s, transform 0.2s;
      flex-shrink: 0;
    }

    .jan-character-viewer-view .greeting-dot::after {
      content: '';
      position: absolute;
      inset: -7px;
    }

    .jan-character-viewer-view .greeting-dot:hover {
      background: rgba(255, 255, 255, 0.32);
    }

    .jan-character-viewer-view .greeting-dot.active {
      background: rgba(255, 255, 255, 0.55);
      transform: scale(1.35);
      cursor: default;
    }


    .jan-character-viewer-lightbox.lightbox-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.82);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      z-index: 100000;
      display: flex;
      align-items: center;
      justify-content: center;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.22s ease;
    }

    .jan-character-viewer-lightbox.lightbox-overlay.active {
      opacity: 1;
      pointer-events: all;
    }

    .jan-character-viewer-lightbox.lightbox-overlay .lightbox-img {
      max-width: 88vw;
      max-height: 88vh;
      object-fit: contain;
      border-radius: 10px;
      box-shadow: 0 32px 100px rgba(0, 0, 0, 0.9);
    }

    .jan-character-viewer-lightbox.lightbox-overlay .lightbox-close {
      position: fixed;
      top: 18px;
      right: 18px;
      background: rgba(220, 50, 47, 0.88);
      border: none;
      border-radius: 7px;
      color: #fff;
      width: 34px;
      height: 34px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      z-index: 3;
      transition: background 0.15s;
      line-height: 0;
    }

    .jan-character-viewer-lightbox.lightbox-overlay .lightbox-close:hover { background: rgba(220, 50, 47, 1); }

    .jan-character-viewer-view .unload-btn {
      background: none;
      border: none;
      color: rgba(255, 255, 255, 0.2);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      align-self: center;
      padding: 0;
      margin-left: auto;
      margin-right: 10px;
      line-height: 0;
      flex-shrink: 0;
      transition: color 0.15s;
    }

    .jan-character-viewer-view .unload-btn:hover {
      color: rgba(255, 255, 255, 0.7);
    }

    .jan-character-viewer-view .cv-fetched-key {
      display: flex;
      align-items: center;
      gap: 5px;
    }

    .jan-character-viewer-view .cv-refresh-btn {
      background: none;
      border: none;
      color: rgba(255, 255, 255, 0.25);
      cursor: pointer;
      padding: 2px;
      line-height: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 4px;
      flex-shrink: 0;
      transition: color 0.15s;
    }

    .jan-character-viewer-view .cv-refresh-btn:hover {
      color: rgba(255, 255, 255, 0.7);
    }

    /* --- Proxy Status Overlay (Glowing Shield) --- */
    .jan-character-viewer-view .action-block .proxy-status-overlay {
      display: none;
    }

    .jan-character-viewer-view .action-block.is-proxy-off .format-picker-group,
    .jan-character-viewer-view .action-block.is-proxy-off .dl-core {
      filter: blur(1.5px) opacity(60%);
      pointer-events: none;
    }

    .jan-character-viewer-view .action-block.is-proxy-off .proxy-status-overlay {
      position: absolute;
      inset: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      background: radial-gradient(circle, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.2) 100%);
      border-radius: 8px;
      z-index: 10;
      pointer-events: auto;
      user-select: none;
      -webkit-user-select: none;
    }

    .jan-character-viewer-view .action-block.is-proxy-off .shield-icon-container {
      display: flex;
      align-items: center;
      justify-content: center;
      color: #ff5252;
      filter: drop-shadow(0 0 6px rgba(255, 82, 82, 0.6));
      animation: jan-proxy-pulse-glow 2s infinite alternate ease-in-out;
      position: relative;
      top: -1.2px;
    }

    @keyframes jan-proxy-pulse-glow {
      0% { filter: drop-shadow(0 0 2px rgba(255, 82, 82, 0.4)); opacity: 0.8; }
      100% { filter: drop-shadow(0 0 8px rgba(255, 82, 82, 0.8)); opacity: 1; transform: scale(1.05); }
    }

    .jan-character-viewer-view .action-block.is-proxy-off .shield-icon-container svg {
      width: 15px;
      height: 15px;
    }

    .jan-character-viewer-view .action-block.is-proxy-off .proxy-text {
      color: #fff;
      font-size: 0.75rem;
      font-weight: 500;
      letter-spacing: 0.06em;
      text-shadow: 0 1px 2px rgba(0,0,0,1);
    }
  `
    });
  }
  function escapeHtmlText(value) {
    if (value == null) return "";
    return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }
  function escapeHtmlAttr(value) {
    if (value == null) return "";
    return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
  }
  function formatRelativeTime(timestampMs, { emptyLabel = null, prefix = "", justNowLabel = "just now" } = {}) {
    if (!timestampMs) return emptyLabel;
    const elapsed = Date.now() - timestampMs;
    if (!Number.isFinite(elapsed)) return emptyLabel;
    const prefixText = prefix ? `${prefix} ` : "";
    const seconds = Math.floor(elapsed / 1e3);
    if (seconds < 10) return `${prefixText}${justNowLabel}`;
    if (seconds < 60) return `${prefixText}${seconds}s ago`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${prefixText}${minutes}m ago`;
    const hours = Math.floor(minutes / 60);
    if (hours < 24) return `${prefixText}${hours}h ago`;
    const days = Math.floor(hours / 24);
    return `${prefixText}${days}d ago`;
  }
  const VIEW_TABS = [
    { key: "description", label: "Description" },
    { key: "first-message", label: "First Message" },
    { key: "scenario", label: "Scenario" },
    { key: "example-dialogs", label: "Example Dialogs" },
    { key: "creator-notes", label: "Creator Notes" }
  ];
  function formatStatNumber(n) {
    if (n == null) return "—";
    if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
    if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
    return String(n);
  }
  function formatDate(isoString) {
    if (!isoString) return "—";
    try {
      return new Date(isoString).toLocaleDateString("en-US", {
        year: "numeric",
        month: "short",
        day: "numeric"
      });
    } catch {
      return "—";
    }
  }
  function formatFetchedTime(fetchedAt) {
    return formatRelativeTime(fetchedAt);
  }
  function calcTokenWidth(tokens, total) {
    if (!total || !tokens) return "0%";
    return `${(tokens / total * 100).toFixed(2)}%`;
  }
  function hasMeaningfulText(text) {
    return typeof text === "string" && stripJanitorAttribution(text).trim().length > 0;
  }
  function normalizeCreatorNotesText(html) {
    if (typeof html !== "string") return "";
    return html.replace(/<!--[\s\S]*?-->/g, "").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|h[1-6])>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&nbsp;|&#160;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'").replace(/\r\n/g, "\n").trim();
  }
  function getTokenRow(character, key) {
    return character?.tokenRows?.find((row) => row.key === key) ?? null;
  }
  function getDefinitionFieldText(value) {
    return typeof value === "string" ? stripJanitorAttribution(value) : null;
  }
  function mapApiResponseToCharacter(data) {
    const characterId = data?.id != null ? String(data.id) : "";
    const creatorId = data?.creator_id != null ? String(data.creator_id) : "";
    const avatarFilename = typeof data?.avatar === "string" ? data.avatar : "";
    const total = data?.token_counts?.total_tokens ?? 0;
    const hasDefinition = data?.showdefinition === true;
    const personalityTokens = data?.token_counts?.personality_tokens ?? 0;
    const firstMsgTokens = data?.token_counts?.first_message_tokens ?? 0;
    const scenarioTokens = data?.token_counts?.scenario_tokens ?? 0;
    const dialogTokens = data?.token_counts?.example_dialog_tokens ?? 0;
    const tags = [];
    if (data?.is_nsfw) tags.push({ label: "NSFW", variant: "nsfw" });
    for (const t of data?.custom_tags ?? []) tags.push({ label: t, variant: "custom" });
    for (const t of data?.tags ?? []) tags.push({ label: t.name, variant: "default" });
    const rawName = (() => {
      const name = typeof data?.name === "string" ? data.name.trim() : "";
      if (name) return data.name;
      const chatName2 = typeof data?.chat_name === "string" ? data.chat_name.trim() : "";
      if (chatName2) return data.chat_name;
      return "Unknown";
    })();
    const chatName = typeof data?.chat_name === "string" && data.chat_name.trim().length > 0 ? data.chat_name : null;
    const creatorName = typeof data?.creator_name === "string" && data.creator_name.trim().length > 0 ? data.creator_name : "Unknown";
    const rawMsgArray = Array.isArray(data?.first_messages) ? data.first_messages : data?.first_message != null ? [data.first_message] : [];
    const seenMsgs = /* @__PURE__ */ new Set();
    const uniqueFirstMessages = [];
    let sawEmptyGreeting = false;
    for (const msg of rawMsgArray) {
      if (typeof msg !== "string" || seenMsgs.has(msg)) continue;
      seenMsgs.add(msg);
      const stripped = stripJanitorAttribution(msg);
      if (stripped.trim().length === 0) {
        if (!sawEmptyGreeting) {
          uniqueFirstMessages.push("");
          sawEmptyGreeting = true;
        }
        continue;
      }
      uniqueFirstMessages.push(stripped);
    }
    return {
      id: data?.id ?? null,
      name: rawName,
      chatName,
      creator: creatorName,
      creatorVerified: data?.creator_verified ?? false,
      creatorUrl: buildProfileUrl(creatorId) ?? "",
      characterUrl: buildCharacterUrl(characterId) ?? "",
      avatarUrl: buildAvatarUrl(avatarFilename, { width: 1200 }) ?? "",
      avatarDownloadUrl: buildAvatarUrl(avatarFilename) ?? "",
      fileName: rawName.replace(/[^a-zA-Z0-9_\-]/g, "_"),
      isNsfw: Boolean(data.is_nsfw),
      chats: formatStatNumber(data.stats?.chat),
      chatsRaw: data?.stats?.chat ?? 0,
      messages: formatStatNumber(data.stats?.message),
      messagesRaw: data?.stats?.message ?? 0,
      totalTokens: total > 0 ? total.toLocaleString() : "—",
      totalTokensRaw: total,
      publishedLabel: formatDate(data.first_published_at),
      publishedValue: typeof data?.first_published_at === "string" ? data.first_published_at.split("T")[0] : "",
      updatedLabel: formatDate(data.updated_at),
      updatedValue: typeof data?.updated_at === "string" ? data.updated_at.split("T")[0] : "",
      tags,
      tokenRows: [
        {
          key: "personality",
          label: "Personality",
          value: personalityTokens ? personalityTokens.toLocaleString() : "&mdash;",
          width: calcTokenWidth(personalityTokens, total),
          zero: !personalityTokens
        },
        {
          key: "first-message",
          label: "First Message",
          value: firstMsgTokens ? firstMsgTokens.toLocaleString() : "&mdash;",
          width: calcTokenWidth(firstMsgTokens, total),
          zero: !firstMsgTokens
        },
        {
          key: "scenario",
          label: "Scenario",
          value: scenarioTokens ? scenarioTokens.toLocaleString() : "&mdash;",
          width: calcTokenWidth(scenarioTokens, total),
          zero: !scenarioTokens
        },
        {
          key: "example-dialogs",
          label: "Example Dialogs",
          value: dialogTokens ? dialogTokens.toLocaleString() : "&mdash;",
          width: calcTokenWidth(dialogTokens, total),
          zero: !dialogTokens
        }
      ],
      hasDefinition,
      descriptionText: getDefinitionFieldText(data?.personality),
      firstMessages: uniqueFirstMessages,
      firstMessageText: uniqueFirstMessages[0] ?? null,
      scenarioText: getDefinitionFieldText(data?.scenario),
      exampleDialogsText: getDefinitionFieldText(data?.example_dialogs),
      creatorNotesHtml: typeof data?.description === "string" ? data.description : "",
      allowProxy: typeof data?.allow_proxy === "boolean" ? data.allow_proxy : null
    };
  }
  function createTagMarkup(tag) {
    const classNames = ["tag-pill"];
    if (tag.variant === "nsfw") {
      classNames.push("tag-nsfw");
    }
    if (tag.variant === "custom") {
      classNames.push("tag-custom");
    }
    return `<span class="${classNames.join(" ")}">${escapeHtmlText(tag.label)}</span>`;
  }
  function createTokenSegmentsMarkup(tokenRows) {
    return tokenRows.filter((row) => row.width !== "0%").map((row) => {
      const className = row.key === "personality" ? "seg-personality" : row.key === "first-message" ? "seg-firstmsg" : row.key === "scenario" ? "seg-scenario" : "seg-dialogs";
      return `<div class="token-seg ${className}" style="width: ${row.width}"></div>`;
    }).join("");
  }
  function createTokenLegendMarkup(tokenRows) {
    return tokenRows.map((row) => {
      const dotClassName = row.key === "personality" ? "seg-personality" : row.key === "first-message" ? "seg-firstmsg" : row.key === "scenario" ? "seg-scenario" : "seg-dialogs";
      return `
        <div class="legend-row${row.zero ? " zero" : ""}">
          <span class="legend-dot ${dotClassName}"></span>
          <span class="legend-key">${row.label}</span>
          <span class="legend-val">${row.value}</span>
        </div>
      `;
    }).join("");
  }
  function createNoCharacterMarkup() {
    return `
    <div class="cv-no-character">
      ${NO_CHARACTER_ICON_SVG}
      <div class="cv-no-character-title">No character loaded</div>
      <div class="cv-load-form" id="cvLoadForm">
        <input
          class="cv-id-input"
          id="cvIdInput"
          type="text"
          placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
          autocomplete="off"
          spellcheck="false"
        />
        <button class="cv-enter-btn" id="cvEnterBtn" type="button">
          <span>Enter</span>
          ${CORNER_DOWN_LEFT_SVG}
        </button>
      </div>
      <div class="cv-load-error" id="cvLoadError"></div>
      <div class="cv-info-row">
        ${INFO_ICON_SVG}
        <span>Accepts a character URL or UUID</span>
      </div>
    </div>
  `;
  }
  function buildDefinitionHiddenPane(paneId, isActive = false) {
    return `
    <div class="tab-pane${isActive ? " active" : ""}" id="${paneId}">
      <div class="empty-state">
        ${DEFINITION_HIDDEN_EMPTY_ICON_SVG}
        <div class="empty-state-text">Definition is not publicly visible for this character</div>
      </div>
    </div>
  `;
  }
  function animateGreetingSlide(pre, newText, direction) {
    const outName = direction > 0 ? "jan-greet-out-left" : "jan-greet-out-right";
    const inName = direction > 0 ? "jan-greet-in-right" : "jan-greet-in-left";
    const dur = 170;
    pre.style.animation = `${outName} ${dur}ms ease forwards`;
    setTimeout(() => {
      pre.innerHTML = escapeHtmlText(newText);
      pre.style.animation = `${inName} ${dur}ms ease forwards`;
      pre.addEventListener("animationend", () => {
        pre.style.animation = "";
      }, { once: true });
    }, dur);
  }
  function buildTextFieldPane(paneId, copyTargetId, text, isActive = false) {
    return `
    <div class="tab-pane${isActive ? " active" : ""}" id="${paneId}">
      <div class="field-wrapper">
        <div class="field-header">
          <button class="copy-btn" type="button" data-copy-target="${copyTargetId}">
            ${COPY_IDLE_SVG}<span>Copy</span>
          </button>
        </div>
        <pre class="field-text" id="${copyTargetId}">${escapeHtmlText(text)}</pre>
      </div>
    </div>
  `;
  }
  function buildEmptyPane(paneId, iconSvg, message, isActive = false) {
    return `
    <div class="tab-pane${isActive ? " active" : ""}" id="${paneId}">
      <div class="empty-state">
        ${iconSvg}
        <div class="empty-state-text">${message}</div>
      </div>
    </div>
  `;
  }
  function buildDefinitionFieldPane({
    paneId,
    copyTargetId,
    text,
    tokenRow,
    emptyIcon,
    emptyMessage,
    isActive = false
  }) {
    if (text !== null) {
      return hasMeaningfulText(text) ? buildTextFieldPane(paneId, copyTargetId, text, isActive) : buildEmptyPane(paneId, emptyIcon, emptyMessage, isActive);
    }
    if (tokenRow?.zero) {
      return buildEmptyPane(paneId, emptyIcon, emptyMessage, isActive);
    }
    return buildDefinitionHiddenPane(paneId, isActive);
  }
  function buildFirstMessageCarouselPane(messages, isActive = false) {
    const total = messages.length;
    const showDots = total <= 8;
    const dotsMarkup = showDots ? messages.map((_, i2) => `<span class="greeting-dot${i2 === 0 ? " active" : ""}" data-greeting-idx="${i2}"></span>`).join("") : "";
    return `
    <div class="tab-pane${isActive ? " active" : ""}" id="tab-first-message">
      <div class="field-wrapper">
        <div class="field-header greeting-field-header">
          <button class="copy-btn greeting-copy-btn" type="button">
            ${COPY_IDLE_SVG}<span>Copy</span>
          </button>
          <div class="greeting-nav">
            <button class="greeting-nav-btn" id="greetingPrev" type="button" disabled>
              ${CHEVRON_LEFT_SVG}
            </button>
            <span class="greeting-counter" id="greetingCounter">1 / ${total}</span>
            <button class="greeting-nav-btn" id="greetingNext" type="button">
              ${CHEVRON_RIGHT_SVG}
            </button>
          </div>
        </div>
        <div class="greeting-text-wrap" id="greetingTextWrap">
          <pre class="field-text" id="firstMesText">${escapeHtmlText(messages[0])}</pre>
        </div>
        ${showDots ? `<div class="greeting-dots" id="greetingDots">${dotsMarkup}</div>` : ""}
      </div>
    </div>
  `;
  }
  function createTabButtonsMarkup(character) {
    const metaMap = character ? {
      "description": character.tokenRows.find((r) => r.key === "personality"),
      "first-message": character.tokenRows.find((r) => r.key === "first-message"),
      "scenario": character.tokenRows.find((r) => r.key === "scenario"),
      "example-dialogs": character.tokenRows.find((r) => r.key === "example-dialogs")
    } : {};
    return VIEW_TABS.map((tab, index) => {
      const activeClass = index === 0 ? " active" : "";
      const rowData = metaMap[tab.key];
      const metaMarkup = rowData ? rowData.zero ? `<span class="tab-dash">&mdash;</span>` : `<span class="tab-count">${rowData.value}</span>` : "";
      return `
      <button class="tab-btn${activeClass}" data-tab="${tab.key}">
        ${tab.label}
        ${metaMarkup}
      </button>
    `;
    }).join("");
  }
  function createCharacterViewerMarkup(character) {
    const descriptionTokenRow = getTokenRow(character, "personality");
    const firstMessageTokenRow = getTokenRow(character, "first-message");
    const scenarioTokenRow = getTokenRow(character, "scenario");
    const exampleDialogsTokenRow = getTokenRow(character, "example-dialogs");
    const definitionRow = character.hasDefinition ? `<div class="meta-kv">
        <span class="meta-key">Definition</span>
        <span style="display:flex;align-items:center;gap:4px;color:rgba(255,255,255,0.45)">
          ${DEFINITION_PUBLIC_ICON_SVG}
          <span style="font-size:0.82rem">Public</span>
        </span>
      </div>` : `<div class="meta-kv">
        <span class="meta-key">Definition</span>
        <span style="display:flex;align-items:center;gap:4px;color:rgba(255,255,255,0.28)" title="Character definition is hidden">
          ${DEFINITION_HIDDEN_ICON_SVG}
          <span style="font-size:0.82rem">Hidden</span>
        </span>
      </div>`;
    const descPane = buildDefinitionFieldPane({
      paneId: "tab-description",
      copyTargetId: "descriptionText",
      text: character.descriptionText,
      tokenRow: descriptionTokenRow,
      emptyIcon: DESCRIPTION_EMPTY_ICON_SVG,
      emptyMessage: "No description defined",
      isActive: true
    });
    const hasExplicitEmptyFirstMessage = character.firstMessages.length === 1 && character.firstMessages[0] === "";
    const firstMsgPane = character.firstMessages.length > 1 ? buildFirstMessageCarouselPane(character.firstMessages) : hasExplicitEmptyFirstMessage ? buildTextFieldPane("tab-first-message", "firstMesText", "") : buildDefinitionFieldPane({
      paneId: "tab-first-message",
      copyTargetId: "firstMesText",
      text: character.firstMessageText,
      tokenRow: firstMessageTokenRow,
      emptyIcon: FIRST_MESSAGE_EMPTY_ICON_SVG,
      emptyMessage: "No first message defined"
    });
    const scenarioPane = buildDefinitionFieldPane({
      paneId: "tab-scenario",
      copyTargetId: "scenarioText",
      text: character.scenarioText,
      tokenRow: scenarioTokenRow,
      emptyIcon: SCENARIO_EMPTY_ICON_SVG,
      emptyMessage: "No scenario defined for this character"
    });
    const exDialogsPane = buildDefinitionFieldPane({
      paneId: "tab-example-dialogs",
      copyTargetId: "exDialogsText",
      text: character.exampleDialogsText,
      tokenRow: exampleDialogsTokenRow,
      emptyIcon: EXAMPLE_DIALOGS_EMPTY_ICON_SVG,
      emptyMessage: "No example dialogs defined for this character"
    });
    const creatorNotesText = normalizeCreatorNotesText(character.creatorNotesHtml);
    const creatorNotesPane = creatorNotesText ? `<div class="tab-pane" id="tab-creator-notes"><div class="html-display">${escapeHtmlText(creatorNotesText).replace(/\n/g, "<br>")}</div></div>` : buildEmptyPane("tab-creator-notes", CREATOR_NOTES_EMPTY_ICON_SVG, "No creator notes");
    return `
    <div class="main-content">

      <div class="meta-sidebar">

        <div class="avatar-section" id="avatarSection" style="height:${CHARACTER_VIEWER_AVATAR_COLLAPSED_HEIGHT}px">
          <img class="character-avatar" id="avatarImg"
            src="${escapeHtmlAttr(character.avatarUrl ?? "")}"
            alt="${escapeHtmlAttr(character.name)}" style="height:100%;object-fit:cover;object-position:center top">
          <div class="avatar-gradient"></div>
          <button class="avatar-maximize-btn" id="avatarMaxBtn" title="View full image" type="button">
            ${MAXIMIZE_ICON_SVG}
          </button>
          <div class="avatar-expand-toggle" id="avatarExpandToggle">
            <span id="expandIconDown" style="line-height:0">${EXPAND_DOWN_ICON_SVG}</span>
            <span id="expandIconUp" style="display:none;line-height:0">${EXPAND_UP_ICON_SVG}</span>
            <span id="expandToggleText">Expand</span>
          </div>
        </div>

        <div class="meta-body">

          <div class="meta-section">
            <div class="meta-kv">
              <span class="meta-key${character.chatName && character.chatName !== character.name ? " meta-key-toggle" : ""}" id="nameKeyLabel">Name</span>
              <a href="${escapeHtmlAttr(character.characterUrl ?? "")}"
                target="_blank" class="meta-link"
                title="${escapeHtmlAttr(String(character.id ?? ""))}" id="nameMetaValue">${escapeHtmlText(character.name)}</a>
            </div>
            <div class="meta-kv">
              <span class="meta-key">Creator</span>
              <a href="${escapeHtmlAttr(character.creatorUrl ?? "")}"
                target="_blank" class="meta-link">
                ${escapeHtmlText(character.creator)}
                ${character.creatorVerified ? `<span class="verified-icon">${VERIFIED_ICON_SVG}</span>` : ""}
              </a>
            </div>
            ${definitionRow}
          </div>

          <div class="meta-section">
            <div class="section-title">TAGS</div>
            <div class="tag-list">
              ${character.tags.map(createTagMarkup).join("")}
            </div>
          </div>

          <div class="meta-section">
            <div class="section-title">STATS</div>
            <div class="stats-grid">
              <div class="stat-block">
                <span class="stat-val">${character.chats}</span>
                <span class="stat-lbl">Chats</span>
              </div>
              <div class="stat-block">
                <span class="stat-val">${character.messages}</span>
                <span class="stat-lbl">Messages</span>
              </div>
            </div>
          </div>

          <div class="meta-section">
            <div class="section-title">
              <span>TOKENS</span>
              <span class="section-title-right">${character.totalTokens}</span>
            </div>
            <div class="token-bar">
              ${createTokenSegmentsMarkup(character.tokenRows)}
            </div>
            <div class="token-legend">
              ${createTokenLegendMarkup(character.tokenRows)}
            </div>
          </div>

          <div class="meta-section">
            <div class="section-title">DATES</div>
            <div class="meta-kv">
              <span class="meta-key">Published</span>
              <span class="meta-value">${character.publishedLabel}</span>
            </div>
            <div class="meta-kv">
              <span class="meta-key">Updated</span>
              <span class="meta-value">${character.updatedLabel}</span>
            </div>
            ${character.fetchedAt ? `
            <div class="meta-kv">
              <span class="meta-key cv-fetched-key">Fetched <button class="cv-refresh-btn" id="cvRefreshBtn" type="button" title="Refetch character data">${REFRESH_ICON_SVG}</button></span>
              <span class="meta-value" id="cvFetchedTime">${formatFetchedTime(character.fetchedAt)}</span>
            </div>` : ""}
          </div>

        </div>

        <div class="sidebar-footer">
          <div class="action-block${character.allowProxy === false ? " is-proxy-off" : ""}">
            <div class="format-picker-group" id="formatPickerGroup">
              <label class="format-option">
                <input type="radio" name="dlFormat" value="png" checked>
                <span class="format-label">
                  ${FORMAT_PNG_ICON_SVG}
                  <span class="fmt-text">PNG</span>
                </span>
              </label>
              <label class="format-option">
                <input type="radio" name="dlFormat" value="json">
                <span class="format-label">
                  ${FORMAT_JSON_ICON_SVG}
                  <span class="fmt-text">JSON</span>
                </span>
              </label>
              <label class="format-option">
                <input type="radio" name="dlFormat" value="txt">
                <span class="format-label">
                  ${FORMAT_TXT_ICON_SVG}
                  <span class="fmt-text">TXT</span>
                </span>
              </label>
            </div>
            <div class="dl-core">
              <button class="btn-download" id="downloadBtn" type="button">
                ${DOWNLOAD_ICON_SVG}
                Download
              </button>
            </div>
            <div class="physics-container">
              <div class="physics-mask">
                <div class="physics-text" id="dlPhysicsText"></div>
              </div>
            </div>
            <div class="proxy-status-overlay" aria-hidden="true">
              <div class="shield-icon-container">
                ${SHIELD_ICON_SVG}
              </div>
              <span class="proxy-text">PROXY DISABLED</span>
            </div>
          </div>
        </div>
      </div>

      <div class="content-section">

        <div class="tab-bar" style="user-select: none; -webkit-user-select: none;">
          ${createTabButtonsMarkup(character)}
          <button class="unload-btn" id="unloadBtn" type="button">
            ${LOG_OUT_ICON_SVG}
          </button>
        </div>

        <div class="tab-content-wrap">
          ${descPane}
          ${firstMsgPane}
          ${scenarioPane}
          ${exDialogsPane}
          ${creatorNotesPane}
        </div>
      </div>
    </div>
  `;
  }
  function createLightboxMarkup(character) {
    return `
    <button class="lightbox-close" id="lightboxClose" title="Close" type="button">
      ${LIGHTBOX_CLOSE_ICON_SVG}
    </button>
    <img class="lightbox-img"
      src="${escapeHtmlAttr(character.avatarUrl ?? "")}"
      alt="${escapeHtmlAttr(character.name)}">
  `;
  }
  class LiquidSyncOdometer {
    constructor(container, documentRef = globalThis.document) {
      this.container = container;
      this.docRef = documentRef;
      this.slots = [];
    }
    setText(text, options = false) {
      if (!this.container || !this.docRef) return;
      const isSuccess = typeof options === "object" ? options?.success : options;
      const isError = typeof options === "object" ? options?.error : false;
      const value = typeof text === "string" ? text : "";
      this.container.style.color = isError ? "#AF4C50" : isSuccess ? "#4CAF50" : "#c5d6e8";
      const isInitial = this.slots.length === 0;
      while (this.slots.length < value.length) {
        const slot = this.docRef.createElement("div");
        slot.className = isInitial ? "char-slot" : "char-slot collapsed";
        const col = this.docRef.createElement("div");
        col.className = "char-column";
        const blank = this.docRef.createElement("span");
        blank.textContent = " ";
        col.appendChild(blank);
        slot.appendChild(col);
        this.container.appendChild(slot);
        this.slots.push({
          slotEl: slot,
          colEl: col,
          targetChar: " ",
          active: isInitial
        });
      }
      for (let i2 = 0; i2 < this.slots.length; i2++) {
        const state2 = this.slots[i2];
        const isActive = i2 < value.length;
        const char = isActive ? value[i2] : " ";
        if (isActive && !state2.active) {
          state2.slotEl.classList.remove("collapsed");
          state2.active = true;
        } else if (!isActive && state2.active) {
          state2.slotEl.classList.add("collapsed");
          state2.active = false;
        }
        if (state2.targetChar === char) continue;
        state2.targetChar = char;
        const span = this.docRef.createElement("span");
        span.textContent = char === " " ? " " : char;
        state2.colEl.appendChild(span);
        const targetIndex = state2.colEl.children.length - 1;
        void state2.colEl.offsetWidth;
        state2.colEl.style.transition = "transform 0.42s cubic-bezier(0.16, 1, 0.3, 1)";
        state2.colEl.style.transform = `translateY(-${targetIndex * 1.5}em)`;
      }
    }
    async clear() {
      if (!this.container) return;
      const promises = [];
      for (let i2 = 0; i2 < this.slots.length; i2++) {
        const state2 = this.slots[i2];
        if (!state2.active) continue;
        state2.targetChar = " ";
        state2.active = false;
        promises.push(
          new Promise((res) => {
            const delay = Math.random() * 0.1;
            state2.colEl.style.transition = `transform 0.3s cubic-bezier(0.55, 0.085, 0.68, 0.53) ${delay}s, opacity 0.3s ${delay}s`;
            state2.colEl.style.transform += " translateY(1.8em)";
            state2.colEl.style.opacity = "0";
            globalThis.setTimeout(() => {
              state2.slotEl.classList.add("collapsed");
              res();
            }, 300 + delay * 1e3);
          })
        );
      }
      await Promise.all(promises);
      this.container.textContent = "";
      this.slots = [];
    }
  }
  function buildDownloadBlob$1(downloadArtifact) {
    const content = downloadArtifact?.content;
    const mimeType = typeof downloadArtifact?.mimeType === "string" && downloadArtifact.mimeType.trim() ? downloadArtifact.mimeType : "application/octet-stream";
    if (typeof content !== "string" && !(content instanceof Uint8Array) && !(content instanceof ArrayBuffer) && !ArrayBuffer.isView(content)) {
      throw new Error("Unsupported download artifact content.");
    }
    return new Blob([content], { type: mimeType });
  }
  function downloadBlob({
    blob,
    filename,
    documentRef = globalThis.document,
    mountEl = null
  }) {
    if (!blob) {
      throw new Error("Download blob is required.");
    }
    if (!filename) {
      throw new Error("Filename is required to trigger download.");
    }
    if (!documentRef?.createElement) {
      throw new Error("Document reference is required to trigger download.");
    }
    const url = globalThis.URL.createObjectURL(blob);
    const link = documentRef.createElement("a");
    try {
      link.href = url;
      link.download = filename;
      (documentRef.body ?? mountEl)?.appendChild(link);
      link.click();
    } finally {
      link.remove();
      globalThis.URL.revokeObjectURL(url);
    }
  }
  function triggerArtifactDownload$1({
    downloadArtifact,
    documentRef = globalThis.document,
    mountEl = null
  }) {
    if (!downloadArtifact?.filename) {
      throw new Error("Download artifact is missing a filename.");
    }
    const blob = buildDownloadBlob$1(downloadArtifact);
    downloadBlob({
      blob,
      filename: downloadArtifact.filename,
      documentRef,
      mountEl
    });
  }
  const SVG_CACHE = /* @__PURE__ */ new Map();
  function resolveDocument$1(documentRef) {
    return documentRef ?? globalThis.document ?? null;
  }
  function createParser(documentRef) {
    const win = documentRef?.defaultView ?? globalThis;
    const Parser = win?.DOMParser ?? globalThis.DOMParser;
    if (typeof Parser !== "function") return null;
    return new Parser();
  }
  function parseSvg(svgString, documentRef) {
    const parser = createParser(documentRef);
    if (!parser) return null;
    const parsed = parser.parseFromString(svgString, "image/svg+xml");
    const svg = parsed?.documentElement ?? null;
    if (!svg || svg.nodeName?.toLowerCase?.() !== "svg") return null;
    return svg;
  }
  function createSvgNode(svgString, documentRef = globalThis.document) {
    if (typeof svgString !== "string" || !svgString) return null;
    const doc = resolveDocument$1(documentRef);
    if (!doc) return null;
    let cached = SVG_CACHE.get(svgString);
    if (!cached) {
      cached = parseSvg(svgString, doc);
      if (!cached) return null;
      SVG_CACHE.set(svgString, cached);
    }
    return doc.importNode(cached, true);
  }
  function appendSvg(target, svgString, documentRef) {
    if (!target) return null;
    const doc = resolveDocument$1(documentRef ?? target.ownerDocument);
    const svgNode = createSvgNode(svgString, doc);
    if (svgNode) {
      target.appendChild(svgNode);
    }
    return svgNode;
  }
  function replaceSvgContent(target, svgString, documentRef) {
    if (!target) return null;
    const doc = resolveDocument$1(documentRef ?? target.ownerDocument);
    const svgNode = createSvgNode(svgString, doc);
    target.replaceChildren();
    if (svgNode) {
      target.appendChild(svgNode);
    }
    return svgNode;
  }
  const CHARACTER_VIEWER_CONTROLLER_KEY = "__janCharacterViewerController__";
  const CV_FORMAT_STORAGE_KEY = "jan-cv-format";
  const CV_CHARACTER_STORAGE_KEY = "jan-cv-loaded-character";
  const CV_NAME_TOGGLE_STORAGE_KEY = "jan-cv-name-toggle";
  const CV_GREETING_INDEX_STORAGE_KEY = "jan-cv-greeting-index";
  const createSharedState = () => ({
    scroll: {
      tabScrolls: {},
      activeTab: null,
      metaBodyScroll: 0
    },
    download: {
      active: false,
      label: "",
      success: false
    },
    downloadUi: null
  });
  const sharedStateByDocument = /* @__PURE__ */ new WeakMap();
  const documentListenerControllers = /* @__PURE__ */ new WeakMap();
  function resolveDocument(documentRef) {
    return documentRef?.defaultView?.document ?? documentRef ?? globalThis.document ?? null;
  }
  function getSharedState(documentRef) {
    const doc = resolveDocument(documentRef);
    if (!doc || typeof doc !== "object") return createSharedState();
    let state2 = sharedStateByDocument.get(doc);
    if (!state2) {
      state2 = createSharedState();
      sharedStateByDocument.set(doc, state2);
    }
    return state2;
  }
  function createDocumentListenerController(documentRef) {
    const doc = resolveDocument(documentRef);
    if (!doc || typeof doc !== "object") return new AbortController();
    const existing = documentListenerControllers.get(doc);
    if (existing) existing.abort();
    const controller = new AbortController();
    documentListenerControllers.set(doc, controller);
    return controller;
  }
  function getGreetingIndexStorageKey(characterId) {
    return characterId ? `${CV_GREETING_INDEX_STORAGE_KEY}:${characterId}` : null;
  }
  function randomizeCustomTagColors(view) {
    const tags = view.querySelectorAll(".tag-custom");
    const base = Math.floor(Math.random() * 360);
    tags.forEach((tag, index) => {
      const hue = (base + index * 137.508) % 360;
      tag.style.background = `hsla(${hue}, 52%, 54%, 0.13)`;
      tag.style.borderColor = `hsla(${hue}, 55%, 65%, 0.26)`;
      tag.style.color = `hsla(${hue}, 70%, 78%, 0.92)`;
    });
  }
  function setCopyButtonState(button, copied) {
    if (!button) return;
    button.classList.toggle("copied", copied);
    button.replaceChildren();
    const doc = button.ownerDocument ?? globalThis.document;
    const iconSvg = copied ? COPY_SUCCESS_SVG : COPY_IDLE_SVG;
    const icon = createSvgNode(iconSvg, doc);
    if (icon) {
      button.appendChild(icon);
    }
    const label = doc?.createElement?.("span");
    if (label) {
      label.textContent = copied ? "Copied!" : "Copy";
      button.appendChild(label);
    }
  }
  function setupTabSwitching(view) {
    view.querySelectorAll(".tab-btn").forEach((button) => {
      button.addEventListener("click", () => {
        view.querySelectorAll(".tab-btn").forEach((tab) => tab.classList.remove("active"));
        view.querySelectorAll(".tab-pane").forEach((pane) => pane.classList.remove("active"));
        button.classList.add("active");
        view.querySelector(`#tab-${button.dataset.tab}`)?.classList.add("active");
      });
    });
  }
  function setupCopyButtons(view) {
    view.querySelectorAll("[data-copy-target]").forEach((button) => {
      button.addEventListener("click", async () => {
        const targetId = button.getAttribute("data-copy-target");
        const text = targetId ? view.querySelector(`#${targetId}`)?.textContent ?? "" : "";
        if (button._timer) {
          clearTimeout(button._timer);
        }
        try {
          await globalThis.navigator?.clipboard?.writeText(text);
          setCopyButtonState(button, true);
          button._timer = globalThis.setTimeout(() => {
            setCopyButtonState(button, false);
            button._timer = null;
          }, 2e3);
        } catch {
        }
      });
    });
  }
  function setupScrollContainment(view) {
    const stopScrollChaining = (container) => {
      if (!container) return;
      container.addEventListener("wheel", (event) => {
        if (event.ctrlKey) return;
        if (!event.deltaY) return;
        const maxScrollTop = container.scrollHeight - container.clientHeight;
        if (maxScrollTop <= 0) {
          event.preventDefault();
          event.stopPropagation();
          return;
        }
        const atTop = container.scrollTop <= 0;
        const atBottom = container.scrollTop >= maxScrollTop - 1;
        if (event.deltaY < 0 && atTop || event.deltaY > 0 && atBottom) {
          event.preventDefault();
          event.stopPropagation();
        }
      }, { passive: false });
    };
    view.querySelectorAll(".meta-body, .tab-pane").forEach(stopScrollChaining);
    const metaSidebar = view.querySelector(".meta-sidebar");
    const metaBody = view.querySelector(".meta-body");
    const contentSection = view.querySelector(".content-section");
    if (!metaSidebar || !metaBody) return;
    metaSidebar.addEventListener("wheel", (event) => {
      if (event.ctrlKey) return;
      if (!event.deltaY || metaBody.contains(event.target)) return;
      event.preventDefault();
      event.stopPropagation();
    }, { passive: false });
    contentSection?.addEventListener("wheel", (event) => {
      if (event.ctrlKey) return;
      const activeTabPane = view.querySelector(".tab-pane.active");
      if (!event.deltaY || activeTabPane?.contains(event.target)) return;
      event.preventDefault();
      event.stopPropagation();
    }, { passive: false });
  }
  function setupAvatarAndLightbox({ view, lightbox }) {
    const avatarSection = view.querySelector("#avatarSection");
    const avatarImg = view.querySelector("#avatarImg");
    const avatarMaxBtn = view.querySelector("#avatarMaxBtn");
    const expandToggle = view.querySelector("#avatarExpandToggle");
    const expandIconDown = view.querySelector("#expandIconDown");
    const expandIconUp = view.querySelector("#expandIconUp");
    const expandText = view.querySelector("#expandToggleText");
    const lightboxClose = lightbox.querySelector("#lightboxClose");
    if (!avatarSection || !avatarImg || !avatarMaxBtn || !expandToggle || !expandIconDown || !expandIconUp || !expandText || !lightboxClose) {
      return;
    }
    let avatarExpanded = false;
    function setExpand(expand) {
      avatarExpanded = expand;
      if (expand) {
        const width = avatarImg.naturalWidth || CHARACTER_VIEWER_AVATAR_WIDTH;
        const height = avatarImg.naturalHeight || 408;
        const targetHeight = Math.min(
          Math.round(CHARACTER_VIEWER_AVATAR_WIDTH * height / width),
          CHARACTER_VIEWER_AVATAR_MAX_HEIGHT
        );
        avatarSection.style.height = `${targetHeight}px`;
        avatarImg.style.height = "auto";
        avatarImg.style.objectFit = "contain";
        avatarImg.style.objectPosition = "center center";
        expandIconDown.style.display = "none";
        expandIconUp.style.display = "inline";
        expandText.textContent = "Collapse";
        return;
      }
      avatarSection.style.height = `${CHARACTER_VIEWER_AVATAR_COLLAPSED_HEIGHT}px`;
      avatarImg.style.height = "100%";
      avatarImg.style.objectFit = "cover";
      avatarImg.style.objectPosition = "center top";
      expandIconDown.style.display = "inline";
      expandIconUp.style.display = "none";
      expandText.textContent = "Expand";
    }
    function openLightbox() {
      lightbox.classList.add("active");
      lightbox.setAttribute("aria-hidden", "false");
      lightbox.focus();
    }
    function closeLightbox() {
      lightbox.classList.remove("active");
      lightbox.setAttribute("aria-hidden", "true");
    }
    expandToggle.addEventListener("click", (event) => {
      event.stopPropagation();
      setExpand(!avatarExpanded);
    });
    avatarMaxBtn.addEventListener("click", (event) => {
      event.stopPropagation();
      openLightbox();
    });
    avatarSection.addEventListener("click", () => openLightbox());
    lightbox.addEventListener("click", (event) => {
      if (event.target === lightbox) closeLightbox();
    });
    lightbox.addEventListener("keydown", (event) => {
      if (event.key === "Escape") closeLightbox();
    });
    lightboxClose.addEventListener("click", closeLightbox);
    setExpand(false);
  }
  function setupDownload(view, documentRef, character, scrapeCharacterCard, sharedState) {
    const downloadButton = view.querySelector("#downloadBtn");
    const physicsText = view.querySelector("#dlPhysicsText");
    const formatGroup = view.querySelector("#formatPickerGroup");
    if (!downloadButton || !physicsText || typeof scrapeCharacterCard !== "function") return;
    if (character.allowProxy === false) {
      downloadButton.disabled = true;
      downloadButton.setAttribute("tabindex", "-1");
      return;
    }
    const MILESTONE_MAP = [
      { threshold: 0.08, label: "Loading character..." },
      { threshold: 0.18, label: "Opening session..." },
      { threshold: 0.35, label: "Fetching definition..." },
      { threshold: 0.62, label: "Extracting fields..." },
      { threshold: 0.72, label: "Closing session..." },
      { threshold: 0.88, label: "Building card..." },
      { threshold: 0.9, label: "Saving file..." }
    ];
    const odometer = new LiquidSyncOdometer(physicsText, documentRef);
    const downloadState = sharedState?.download ?? createSharedState().download;
    const ui = { odometer, downloadButton, formatGroup };
    if (sharedState) sharedState.downloadUi = ui;
    const getUi = () => sharedState?.downloadUi ?? ui;
    const setLabel = (label, success = false) => {
      downloadState.label = label;
      downloadState.success = success;
      getUi()?.odometer?.setText(label, success);
    };
    const setBusy = (busy) => {
      const current = getUi();
      if (!current?.downloadButton) return;
      current.downloadButton.disabled = busy;
      if (busy) current.downloadButton.setAttribute("aria-busy", "true");
      else current.downloadButton.removeAttribute("aria-busy");
      current.downloadButton.classList.toggle("dl-hidden", busy);
      current.formatGroup?.classList.toggle("dl-hidden", busy);
    };
    if (downloadState.active || downloadState.label) {
      setBusy(true);
      if (downloadState.label) {
        getUi()?.odometer?.setText(downloadState.label, downloadState.success);
      }
    }
    downloadButton.addEventListener("click", async () => {
      if (downloadState.active) return;
      const exportFormat = view.querySelector('input[name="dlFormat"]:checked')?.value ?? "png";
      downloadState.active = true;
      downloadState.success = false;
      setBusy(true);
      await new Promise((r) => globalThis.setTimeout(r, 300));
      setLabel(MILESTONE_MAP[0].label, false);
      let lastMilestoneIndex = 0;
      const onProgress = (progress) => {
        for (let i2 = MILESTONE_MAP.length - 1; i2 >= 0; i2--) {
          if (progress >= MILESTONE_MAP[i2].threshold && i2 > lastMilestoneIndex) {
            lastMilestoneIndex = i2;
            setLabel(MILESTONE_MAP[i2].label, false);
            break;
          }
        }
      };
      try {
        const result = await scrapeCharacterCard({
          characterId: character.id,
          exportFormat,
          onProgress
        });
        if (!result?.downloadArtifact) {
          throw new Error("Scrape/export pipeline did not return a download artifact.");
        }
        setLabel("Saving file...", false);
        triggerArtifactDownload$1({
          downloadArtifact: result.downloadArtifact,
          documentRef,
          mountEl: view
        });
        setLabel("Completed!", true);
        await new Promise((r) => globalThis.setTimeout(r, 1600));
        await getUi()?.odometer?.clear();
      } catch (error) {
        globalThis.console?.error?.(
          "[Jan Character Viewer] Failed to export character card.",
          error
        );
        getUi()?.odometer?.setText("Failed", { error: true });
        await new Promise((r) => globalThis.setTimeout(r, 1600));
        await getUi()?.odometer?.clear();
      } finally {
        downloadState.active = false;
        downloadState.label = "";
        downloadState.success = false;
        setBusy(false);
      }
    });
  }
  function setupFirstMessageCarousel(view, character, documentRef) {
    const messages = character?.firstMessages;
    if (!messages || messages.length <= 1) return;
    const pane = view.querySelector("#tab-first-message");
    if (!pane) return;
    const pre = pane.querySelector("#firstMesText");
    const prevBtn = pane.querySelector("#greetingPrev");
    const nextBtn = pane.querySelector("#greetingNext");
    const counter = pane.querySelector("#greetingCounter");
    const dotsCont = pane.querySelector("#greetingDots");
    const copyBtn = pane.querySelector(".greeting-copy-btn");
    const textWrap = pane.querySelector("#greetingTextWrap");
    if (!pre || !prevBtn || !nextBtn || !counter) return;
    const total = messages.length;
    let currentIndex = 0;
    const storage = getSessionStorage(documentRef);
    const storageKey = getGreetingIndexStorageKey(character?.id);
    if (storage && storageKey) {
      const stored = readString(storage, storageKey, null);
      const parsed = Number.parseInt(stored ?? "", 10);
      if (Number.isFinite(parsed)) {
        currentIndex = Math.max(0, Math.min(parsed, total - 1));
      }
    }
    function persistIndex() {
      if (!storage || !storageKey) return;
      writeString(storage, storageKey, String(currentIndex));
    }
    function updateNav() {
      counter.textContent = `${currentIndex + 1} / ${total}`;
      prevBtn.disabled = currentIndex === 0;
      nextBtn.disabled = currentIndex === total - 1;
      if (dotsCont) {
        dotsCont.querySelectorAll(".greeting-dot").forEach((dot, i2) => {
          dot.classList.toggle("active", i2 === currentIndex);
        });
      }
    }
    function applyIndex() {
      pre.textContent = messages[currentIndex] ?? "";
      updateNav();
      persistIndex();
    }
    function goTo(newIndex, direction) {
      if (newIndex < 0 || newIndex >= total || newIndex === currentIndex) return;
      const dir = direction ?? (newIndex > currentIndex ? 1 : -1);
      currentIndex = newIndex;
      updateNav();
      animateGreetingSlide(pre, messages[currentIndex], dir);
      persistIndex();
    }
    applyIndex();
    prevBtn.addEventListener("click", () => goTo(currentIndex - 1, -1));
    nextBtn.addEventListener("click", () => goTo(currentIndex + 1, 1));
    if (dotsCont) {
      dotsCont.querySelectorAll(".greeting-dot").forEach((dot, i2) => {
        dot.addEventListener("click", () => goTo(i2));
      });
    }
    if (copyBtn) {
      let copyTimer = null;
      copyBtn.addEventListener("click", async () => {
        const text = pre.textContent ?? "";
        if (copyTimer) clearTimeout(copyTimer);
        try {
          await globalThis.navigator?.clipboard?.writeText(text);
          setCopyButtonState(copyBtn, true);
          copyTimer = globalThis.setTimeout(() => {
            setCopyButtonState(copyBtn, false);
            copyTimer = null;
          }, 2e3);
        } catch {
        }
      });
    }
    if (textWrap) {
      let touchStartX = 0;
      let touchStartY = 0;
      let touchStartTime = 0;
      textWrap.addEventListener("touchstart", (e) => {
        touchStartX = e.touches[0].clientX;
        touchStartY = e.touches[0].clientY;
        touchStartTime = Date.now();
      }, { passive: true });
      textWrap.addEventListener("touchend", (e) => {
        const dx = e.changedTouches[0].clientX - touchStartX;
        const dy = e.changedTouches[0].clientY - touchStartY;
        const dt = Date.now() - touchStartTime;
        if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40 && dt < 400) {
          if (dx < 0) goTo(currentIndex + 1, 1);
          else goTo(currentIndex - 1, -1);
        }
      }, { passive: true });
    }
    pane.addEventListener("keydown", (e) => {
      if (e.key === "ArrowLeft") {
        e.preventDefault();
        goTo(currentIndex - 1, -1);
      }
      if (e.key === "ArrowRight") {
        e.preventDefault();
        goTo(currentIndex + 1, 1);
      }
    });
  }
  function setupUnloadButton(view, onUnload) {
    const btn = view.querySelector("#unloadBtn");
    if (!btn || typeof onUnload !== "function") return;
    btn.addEventListener("click", onUnload);
  }
  function setupRefreshButton(view, onRefresh) {
    const btn = view.querySelector("#cvRefreshBtn");
    if (!btn || typeof onRefresh !== "function") return;
    btn.addEventListener("click", (event) => {
      event.stopPropagation();
      onRefresh();
    });
  }
  function setupNameToggle(view, character, documentRef) {
    if (!character.chatName || character.chatName === character.name) return;
    const keyLabel = view.querySelector("#nameKeyLabel");
    const valueEl = view.querySelector("#nameMetaValue");
    if (!keyLabel || !valueEl) return;
    const storage = getSessionStorage(documentRef);
    const storageKey = character?.id ? `${CV_NAME_TOGGLE_STORAGE_KEY}:${character.id}` : null;
    let showingChatName = false;
    const storedPreference = storage && storageKey ? readString(storage, storageKey, null) : null;
    if (storedPreference === "chat") {
      showingChatName = true;
    } else if (storedPreference === "name") {
      showingChatName = false;
    } else {
      const { nameDisplay } = readViewerSettings(documentRef);
      showingChatName = nameDisplay === "chat";
    }
    function applyToggleState() {
      if (showingChatName) {
        keyLabel.textContent = "Chat Name";
        keyLabel.classList.add("is-alt");
        valueEl.textContent = character.chatName;
      } else {
        keyLabel.textContent = "Name";
        keyLabel.classList.remove("is-alt");
        valueEl.textContent = character.name;
      }
    }
    applyToggleState();
    keyLabel.addEventListener("click", () => {
      showingChatName = !showingChatName;
      applyToggleState();
      if (storage && storageKey) {
        writeString(storage, storageKey, showingChatName ? "chat" : "name");
      }
    });
  }
  function applyAvatarDisplaySetting(view, lightbox, character, documentRef) {
    const { avatarMode } = readViewerSettings(documentRef);
    const avatarSection = view.querySelector("#avatarSection");
    if (!avatarSection) return false;
    if (avatarMode === "off") {
      avatarSection.remove();
      lightbox?.remove();
      return true;
    }
    if (avatarMode === "blur" && character.isNsfw) {
      const avatarImg = view.querySelector("#avatarImg");
      if (avatarImg) avatarImg.style.filter = "blur(16px)";
      const lightboxImg = lightbox?.querySelector(".lightbox-img");
      if (lightboxImg) lightboxImg.style.filter = "blur(22px)";
    }
    return false;
  }
  function setupFormatPickerPersistence(view, documentRef) {
    const storage = getLocalStorage(documentRef);
    if (!storage) return;
    const saved = readString(storage, CV_FORMAT_STORAGE_KEY, null);
    if (saved) {
      const radio = view.querySelector(`input[name="dlFormat"][value="${saved}"]`);
      if (radio) radio.checked = true;
    }
    view.querySelectorAll('input[name="dlFormat"]').forEach((radio) => {
      radio.addEventListener("change", () => {
        writeString(storage, CV_FORMAT_STORAGE_KEY, radio.value);
      });
    });
  }
  function loadCharacterIntoView(view, character, documentRef, backdropEl, onUnload, scrapeCharacterCard, onRefresh, sharedState) {
    view.innerHTML = createCharacterViewerMarkup(character);
    const lightboxHost = backdropEl ?? view;
    lightboxHost.querySelector(".jan-character-viewer-lightbox")?.remove();
    const lightbox = documentRef.createElement("div");
    lightbox.className = "jan-character-viewer-lightbox lightbox-overlay";
    lightbox.id = "lightboxOverlay";
    lightbox.setAttribute("aria-hidden", "true");
    lightbox.tabIndex = -1;
    lightbox.innerHTML = createLightboxMarkup(character);
    lightboxHost.appendChild(lightbox);
    randomizeCustomTagColors(view);
    setupTabSwitching(view);
    setupCopyButtons(view);
    setupScrollContainment(view);
    const avatarHidden = applyAvatarDisplaySetting(
      view,
      lightbox,
      character,
      documentRef
    );
    if (!avatarHidden) {
      setupAvatarAndLightbox({ view, lightbox });
    }
    setupDownload(view, documentRef, character, scrapeCharacterCard, sharedState);
    setupFormatPickerPersistence(view, documentRef);
    setupFirstMessageCarousel(view, character, documentRef);
    setupUnloadButton(view, onUnload);
    setupRefreshButton(view, onRefresh);
    setupNameToggle(view, character, documentRef);
  }
  function getLoadFormRefs(view) {
    return {
      input: view.querySelector("#cvIdInput"),
      btn: view.querySelector("#cvEnterBtn"),
      form: view.querySelector("#cvLoadForm"),
      errorEl: view.querySelector("#cvLoadError")
    };
  }
  function setLoadFormError(view, message) {
    const { form, errorEl } = getLoadFormRefs(view);
    form?.classList.add("is-error");
    if (errorEl) {
      errorEl.textContent = message;
    }
  }
  function clearLoadFormError(view) {
    const { form, errorEl } = getLoadFormRefs(view);
    form?.classList.remove("is-error");
    if (errorEl) {
      errorEl.textContent = "";
    }
  }
  function setLoadFormLoading(view, loading) {
    const { input, btn, form } = getLoadFormRefs(view);
    form?.classList.toggle("is-loading", loading);
    if (input) {
      input.disabled = loading;
    }
    if (btn) {
      btn.disabled = loading;
    }
  }
  function setupLoadForm(view, loadCharacterFromInput) {
    const input = view.querySelector("#cvIdInput");
    const btn = view.querySelector("#cvEnterBtn");
    if (!input || !btn) return;
    const handleSubmit = () => loadCharacterFromInput(input.value);
    btn.addEventListener("click", handleSubmit);
    input.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        handleSubmit();
      }
    });
  }
  function createCharacterViewerTab({
    documentRef = globalThis.document,
    backdropEl = null,
    janitorClient = null,
    scrapeCharacterCard = null,
    generateAlphaProxyConfig = null
  } = {}) {
    ensureCharacterViewerTabStyle(documentRef);
    const view = documentRef.createElement("section");
    view.className = "jan-panel-tab-view jan-character-viewer-view";
    view.innerHTML = createNoCharacterMarkup();
    const viewerClient = janitorClient ?? createJanitorClient({
      documentRef
    });
    const baseScrapeCharacterCard = typeof scrapeCharacterCard === "function" ? scrapeCharacterCard : createInternalOrchestrator({ client: viewerClient }).scrapeCharacterCard;
    const scrapeCharacterCardImpl = (options = {}) => baseScrapeCharacterCard({
      ...options,
      generateAlphaProxyConfig
    });
    const sharedState = getSharedState(documentRef);
    const documentController = createDocumentListenerController(documentRef);
    const documentSignal = documentController.signal;
    let activeLoadRequestId = 0;
    let _cvFetchedAtTicker = null;
    let _cvCurrentFetchedAt = null;
    let _cvCurrentCharacterId = null;
    documentSignal?.addEventListener?.("abort", () => {
      if (_cvFetchedAtTicker != null) {
        globalThis.clearInterval(_cvFetchedAtTicker);
        _cvFetchedAtTicker = null;
      }
    }, { once: true });
    function clearNameToggleForCharacter(characterId) {
      if (!characterId) return;
      removeStorageItem(
        getSessionStorage(documentRef),
        `${CV_NAME_TOGGLE_STORAGE_KEY}:${characterId}`
      );
    }
    function clearGreetingIndexForCharacter(characterId) {
      if (!characterId) return;
      const storageKey = getGreetingIndexStorageKey(characterId);
      if (!storageKey) return;
      removeStorageItem(getSessionStorage(documentRef), storageKey);
    }
    function saveCharacterToSession(raw, character) {
      writeJson(getSessionStorage(documentRef), CV_CHARACTER_STORAGE_KEY, { raw, character });
    }
    function loadCharacterFromSession() {
      const parsed = readJson(getSessionStorage(documentRef), CV_CHARACTER_STORAGE_KEY, null);
      if (!parsed?.character?.id) return null;
      return parsed;
    }
    function clearCharacterFromSession() {
      removeStorageItem(getSessionStorage(documentRef), CV_CHARACTER_STORAGE_KEY);
    }
    function setupCvScrollListeners() {
      view.querySelector(".meta-body")?.addEventListener("scroll", (e) => {
        sharedState.scroll.metaBodyScroll = e.currentTarget.scrollTop;
      }, { passive: true });
      view.querySelectorAll(".tab-pane").forEach((pane) => {
        const tabName = pane.id.replace(/^tab-/, "");
        pane.addEventListener("scroll", () => {
          sharedState.scroll.activeTab = tabName;
          sharedState.scroll.tabScrolls[tabName] = pane.scrollTop;
        }, { passive: true });
      });
      view.querySelectorAll(".tab-btn").forEach((btn) => {
        btn.addEventListener("click", () => {
          const tab = btn.dataset.tab;
          sharedState.scroll.activeTab = tab;
          const pane = view.querySelector(`#tab-${tab}`);
          if (pane && sharedState.scroll.tabScrolls[tab]) {
            pane.scrollTop = sharedState.scroll.tabScrolls[tab];
          }
        });
      });
    }
    if (!sharedState.scroll.activeTab) {
      const initialTab = view.querySelector(".tab-btn.active")?.dataset?.tab ?? view.querySelector(".tab-pane.active")?.id?.replace(/^tab-/, "") ?? null;
      if (initialTab) {
        sharedState.scroll.activeTab = initialTab;
      }
    }
    function clearCvScroll() {
      Object.keys(sharedState.scroll.tabScrolls).forEach((k) => {
        delete sharedState.scroll.tabScrolls[k];
      });
      sharedState.scroll.activeTab = null;
      sharedState.scroll.metaBodyScroll = 0;
    }
    const unloadCharacterFn = () => {
      activeLoadRequestId++;
      if (_cvFetchedAtTicker != null) {
        globalThis.clearInterval(_cvFetchedAtTicker);
        _cvFetchedAtTicker = null;
      }
      _cvCurrentFetchedAt = null;
      clearNameToggleForCharacter(_cvCurrentCharacterId);
      clearGreetingIndexForCharacter(_cvCurrentCharacterId);
      _cvCurrentCharacterId = null;
      const lightboxHost = backdropEl ?? view;
      lightboxHost.querySelector(".jan-character-viewer-lightbox")?.remove();
      view.innerHTML = createNoCharacterMarkup();
      setupLoadForm(view, loadCharacterFromInput);
      clearCharacterFromSession();
      clearCvScroll();
      pendingRestore = false;
    };
    const loadCharacterFromInput = async (rawInput) => {
      const raw = typeof rawInput === "string" ? rawInput.trim() : "";
      const { input } = getLoadFormRefs(view);
      if (input) {
        input.value = raw;
      }
      if (!raw) {
        setLoadFormError(view, "Please enter a character ID or URL");
        return false;
      }
      const charId = extractCharacterIdFromInput(raw);
      if (!charId) {
        setLoadFormError(view, "Please enter a valid character ID or URL");
        return false;
      }
      clearNameToggleForCharacter(charId);
      clearGreetingIndexForCharacter(charId);
      const requestId = ++activeLoadRequestId;
      clearLoadFormError(view);
      setLoadFormLoading(view, true);
      try {
        const data = await viewerClient.getCharacter(charId);
        if (requestId !== activeLoadRequestId || view.isConnected === false) {
          return false;
        }
        const character = mapApiResponseToCharacter(data);
        character.fetchedAt = Date.now();
        _cvCurrentFetchedAt = character.fetchedAt;
        _cvCurrentCharacterId = character.id;
        loadCharacterIntoView(
          view,
          character,
          documentRef,
          backdropEl,
          unloadCharacterFn,
          scrapeCharacterCardImpl,
          async () => {
            unloadCharacterFn();
            await loadCharacterFromInput(character.id);
          },
          sharedState
        );
        saveCharacterToSession(raw, character);
        clearCvScroll();
        setupCvScrollListeners();
        if (_cvFetchedAtTicker != null) globalThis.clearInterval(_cvFetchedAtTicker);
        _cvFetchedAtTicker = character.fetchedAt ? globalThis.setInterval(() => {
          if (documentRef.hidden || view.offsetHeight === 0) return;
          const el = view.querySelector("#cvFetchedTime");
          if (el) el.textContent = formatFetchedTime(character.fetchedAt);
        }, 1e4) : null;
        return true;
      } catch (error) {
        if (requestId !== activeLoadRequestId || view.isConnected === false) {
          return false;
        }
        setLoadFormLoading(view, false);
        setLoadFormError(
          view,
          error?.data?.message ?? "Character not found or failed to load"
        );
        return false;
      }
    };
    let pendingRestore = !!loadCharacterFromSession();
    function restoreFromCache() {
      if (!pendingRestore) return;
      pendingRestore = false;
      const cached = loadCharacterFromSession();
      if (!cached?.character) return;
      loadCharacterIntoView(
        view,
        cached.character,
        documentRef,
        backdropEl,
        unloadCharacterFn,
        scrapeCharacterCardImpl,
        async () => {
          unloadCharacterFn();
          await loadCharacterFromInput(cached.character.id);
        },
        sharedState
      );
      setupCvScrollListeners();
      if (_cvFetchedAtTicker != null) globalThis.clearInterval(_cvFetchedAtTicker);
      _cvFetchedAtTicker = cached.character.fetchedAt ? globalThis.setInterval(() => {
        if (documentRef.hidden || view.offsetHeight === 0) return;
        const el = view.querySelector("#cvFetchedTime");
        if (el) el.textContent = formatFetchedTime(cached.character.fetchedAt);
      }, 1e4) : null;
      _cvCurrentFetchedAt = cached.character.fetchedAt ?? null;
      _cvCurrentCharacterId = cached.character.id ?? null;
      const win = documentRef.defaultView ?? globalThis;
      win.requestAnimationFrame(() => {
        const metaBody = view.querySelector(".meta-body");
        if (metaBody && sharedState.scroll.metaBodyScroll) {
          metaBody.scrollTop = sharedState.scroll.metaBodyScroll;
        }
        if (sharedState.scroll.activeTab) {
          const tabBtn = view.querySelector(`.tab-btn[data-tab="${sharedState.scroll.activeTab}"]`);
          if (tabBtn && !tabBtn.classList.contains("active")) {
            tabBtn.click();
          } else {
            const pane = view.querySelector(`#tab-${sharedState.scroll.activeTab}`);
            const storedScroll = sharedState.scroll.tabScrolls[sharedState.scroll.activeTab];
            if (pane && storedScroll) pane.scrollTop = storedScroll;
          }
        }
      });
    }
    function notifyTabActive() {
      restoreFromCache();
      tickFetchedAtNow();
    }
    view[CHARACTER_VIEWER_CONTROLLER_KEY] = {
      loadCharacterFromInput,
      unloadCharacter: unloadCharacterFn,
      notifyTabActive,
      destroy: () => documentController.abort()
    };
    setupLoadForm(view, loadCharacterFromInput);
    function tickFetchedAtNow() {
      if (!_cvCurrentFetchedAt || view.offsetHeight === 0) return;
      const el = view.querySelector("#cvFetchedTime");
      if (el) el.textContent = formatFetchedTime(_cvCurrentFetchedAt);
    }
    const activeObserver = new MutationObserver(() => {
      if (view.classList.contains("is-active")) tickFetchedAtNow();
    });
    activeObserver.observe(view, { attributeFilter: ["class"] });
    documentSignal?.addEventListener?.("abort", () => activeObserver.disconnect(), { once: true });
    documentRef.addEventListener("visibilitychange", () => {
      if (!documentRef.hidden) tickFetchedAtNow();
    }, { signal: documentSignal });
    documentRef.addEventListener("jan-settings-changed", (event) => {
      const key = event?.detail?.key;
      if (key !== "vis_name_display" && key !== "vis_char_avatar") return;
      if (sharedState.download.active) return;
      const cached = loadCharacterFromSession();
      if (!cached?.character || view.isConnected === false) return;
      loadCharacterIntoView(
        view,
        cached.character,
        documentRef,
        backdropEl,
        unloadCharacterFn,
        scrapeCharacterCardImpl,
        async () => {
          unloadCharacterFn();
          await loadCharacterFromInput(cached.character.id);
        },
        sharedState
      );
      setupCvScrollListeners();
    }, { signal: documentSignal });
    return view;
  }
  async function loadCharacterViewerInput(view, input) {
    const loader = view?.[CHARACTER_VIEWER_CONTROLLER_KEY]?.loadCharacterFromInput;
    if (typeof loader !== "function") return false;
    return await loader(input);
  }
  function unloadCharacterViewer(view) {
    const unloader = view?.[CHARACTER_VIEWER_CONTROLLER_KEY]?.unloadCharacter;
    if (typeof unloader === "function") unloader();
  }
  function notifyCharacterViewerTabActive(view) {
    const fn = view?.[CHARACTER_VIEWER_CONTROLLER_KEY]?.notifyTabActive;
    if (typeof fn === "function") fn();
  }
  function createBatchQueue() {
    let items = [];
    const listeners = /* @__PURE__ */ new Set();
    function notify() {
      const snapshot = [...items];
      for (const fn of listeners) {
        try {
          fn(snapshot);
        } catch {
        }
      }
    }
    function findIndex(id) {
      return items.findIndex((item) => item.id === id);
    }
    return {
      /**
       * Subscribe to queue changes. Returns an unsubscribe function.
       * @param {(items: QueueItem[]) => void} listener
       * @returns {() => void}
       */
      subscribe(listener) {
        listeners.add(listener);
        return () => listeners.delete(listener);
      },
      /** @returns {QueueItem[]} */
      getItems() {
        return [...items];
      },
      /** @returns {QueueItem|null} */
      getItem(id) {
        return items.find((item) => item.id === id) ?? null;
      },
      /**
       * Add a character to the queue. Skips if the same characterId is already queued.
       * @returns {QueueItem|null} The created entry, or null if duplicate.
       */
      addItem(characterId, title, characterUrl = null, {
        autoFetch = false,
        autoExport = false,
        batchId = null
      } = {}) {
        const exists = items.some((item) => item.characterId === characterId);
        if (exists) return null;
        const entry = {
          id: generateId(),
          characterId,
          title: title || characterId,
          characterUrl,
          status: "pending",
          progress: 0,
          error: null,
          downloadArtifact: null,
          normalizedCharacter: null,
          artifactFormat: null,
          autoFetch,
          autoExport,
          batchId,
          retrying: false,
          retryCount: 0
        };
        items = [...items, entry];
        notify();
        return entry;
      },
      /**
       * Merge a partial update into an existing entry.
       * @param {string} id
       * @param {Partial<QueueItem>} patch
       */
      updateItem(id, patch) {
        const idx = findIndex(id);
        if (idx === -1) return;
        items = items.map((item, i2) => i2 === idx ? { ...item, ...patch } : item);
        notify();
      },
      /** Remove a single entry by ID. */
      removeItem(id) {
        const before = items.length;
        items = items.filter((item) => item.id !== id);
        if (items.length !== before) notify();
      },
      /** Remove multiple entries by ID set. */
      removeItems(ids) {
        const idSet = new Set(ids);
        const before = items.length;
        items = items.filter((item) => !idSet.has(item.id));
        if (items.length !== before) notify();
      },
      /** Remove all entries. */
      clear() {
        if (items.length === 0) return;
        items = [];
        notify();
      },
      /** @returns {{ total: number, pending: number, fetching: number, ready: number, error: number }} */
      getStats() {
        let pending = 0;
        let fetching = 0;
        let ready = 0;
        let error = 0;
        for (const item of items) {
          if (item.status === "pending") pending += 1;
          else if (item.status === "fetching") fetching += 1;
          else if (item.status === "ready") ready += 1;
          else if (item.status === "error") error += 1;
        }
        return { total: items.length, pending, fetching, ready, error };
      }
    };
  }
  const SELECTION_MODE_CLASS = "jan-panel-group--selection-mode";
  const SELECTION_MODE_BACKDROP_CLASS = "jan-panel-backdrop--selection-mode";
  const SELECTION_MODE_PAGE_CLASS = "jan-selection-mode-active";
  const SELECTION_STORAGE_KEY = "jan-batch-selection";
  const QUEUE_STORAGE_KEY = "jan-batch-queue";
  const SETTINGS_STORAGE_KEY = "jan-batch-settings";
  const CHECKED_STORAGE_KEY = "jan-batch-checked-ids";
  const SELECTION_OVERLAY_CLASS = "jan-batch-pick-overlay";
  const SELECTION_STYLE_ID = "jan-batch-pick-style";
  const PAGE_CARD_WRAPPER_SELECTOR = [
    ".pp-cc-wrapper",
    ".profile-character-card-wrapper",
    '[class*="_chatCard_"]'
  ].join(", ");
  const BATCH_TAB_STYLE_ID = "jan-batch-tab-style";
  const BATCH_TAB_FONT_STYLESHEET_ID = "jan-batch-font-stylesheet";
  const BATCH_TAB_FONT_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap";
  function ensureBatchDownloadTabStyle(documentRef) {
    if (!documentRef?.head) return;
    ensureGoogleFontStylesheet(documentRef, {
      id: BATCH_TAB_FONT_STYLESHEET_ID,
      href: BATCH_TAB_FONT_STYLESHEET_URL
    });
    if (documentRef.getElementById(BATCH_TAB_STYLE_ID)) return;
    const style = documentRef.createElement("style");
    style.id = BATCH_TAB_STYLE_ID;
    style.textContent = `

    #jan-panel-backdrop .jan-batch-view,
    #jan-panel-backdrop .jan-batch-selection-hud {
      --jan-batch-bg: rgba(0, 0, 0, 0.14);
      --jan-batch-bg-soft: rgba(255, 255, 255, 0.02);
      --jan-batch-bg-softer: rgba(255, 255, 255, 0.015);
      --jan-batch-border: rgba(255, 255, 255, 0.06);
      --jan-batch-border-soft: rgba(255, 255, 255, 0.05);
      --jan-batch-border-strong: rgba(255, 255, 255, 0.12);
      --jan-batch-text: #e8e8ec;
      --jan-batch-muted: rgba(255, 255, 255, 0.5);
      --jan-batch-accent: #ffffff;
      --jan-batch-success: #4caf50;
      --jan-batch-error: #f44336;
      --jan-batch-warning: #ffb74d;
      --jan-batch-pod-bg: linear-gradient(180deg, #181818 0%, #000000 100%);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-view,
    #jan-panel-backdrop.jan-panel--light .jan-batch-selection-hud {
      --jan-batch-bg: rgba(0, 0, 0, 0.035);
      --jan-batch-bg-soft: rgba(0, 0, 0, 0.02);
      --jan-batch-bg-softer: rgba(0, 0, 0, 0.03);
      --jan-batch-border: rgba(0, 0, 0, 0.08);
      --jan-batch-border-soft: rgba(0, 0, 0, 0.08);
      --jan-batch-border-strong: rgba(0, 0, 0, 0.14);
      --jan-batch-text: #1c1c1e;
      --jan-batch-muted: rgba(0, 0, 0, 0.48);
      --jan-batch-accent: #111111;
      --jan-batch-success: #2e7d32;
      --jan-batch-error: #c62828;
      --jan-batch-warning: #b26a00;
      --jan-batch-pod-bg: linear-gradient(180deg, #ffffff 0%, #f1f1f3 100%);
    }

    #jan-panel-backdrop.${SELECTION_MODE_BACKDROP_CLASS} {
      background: transparent !important;
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
      pointer-events: none !important;
    }

    .${SELECTION_MODE_CLASS} {
      display: none !important;
    }

    .${SELECTION_MODE_PAGE_CLASS} .jan-cv-btn {
      opacity: 0 !important;
      pointer-events: none !important;
      visibility: hidden !important;
    }

    .jan-batch-view.is-active {
      display: flex;
    }

    .jan-batch-view {
      color: var(--jan-batch-text);
      font-family: 'Outfit', sans-serif;
      height: 100%;
      width: 100%;
    }

    .jan-batch-sidebar {
      background: var(--jan-batch-bg);
      box-shadow: 1px 0 0 var(--jan-batch-border-soft);
      box-sizing: border-box;
      display: flex;
      flex: 0 0 338px;
      flex-direction: column;
      gap: 24px;
      overflow-y: auto;
      padding: 24px;
      width: 338px;
    }

    .jan-batch-queue-section {
      box-sizing: border-box;
      display: flex;
      flex: 1 1 auto;
      flex-direction: column;
      gap: 16px;
      min-width: 0;
      overflow: hidden;
      padding: 24px 30px;
    }

    .jan-batch-selection-cta-card {
      align-items: center;
      background: var(--jan-batch-bg-soft);
      border: 1.5px dashed var(--jan-batch-border-strong);
      border-radius: 12px;
      display: flex;
      flex-direction: column;
      gap: 14px;
      padding: 20px;
      text-align: center;
    }

    .jan-batch-selection-icon-ring {
      align-items: center;
      background: rgba(255, 255, 255, 0.04);
      border-radius: 50%;
      box-shadow: 0 0 0 1px var(--jan-batch-border);
      display: flex;
      height: 48px;
      justify-content: center;
      width: 48px;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-selection-icon-ring {
      background: rgba(0, 0, 0, 0.04);
    }

    .jan-batch-selection-copy {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }

    .jan-batch-selection-title {
      font-size: 1rem;
      font-weight: 500;
    }

    .jan-batch-selection-description {
      color: var(--jan-batch-muted);
      font-size: 0.8rem;
      line-height: 1.4;
    }

    .jan-batch-select-chars-btn {
      align-items: center;
      background: transparent;
      border: none;
      border-radius: 8px;
      box-shadow: inset 0 0 0 1px var(--jan-batch-border);
      color: var(--jan-batch-muted);
      cursor: pointer;
      display: flex;
      font: inherit;
      font-size: 0.88rem;
      font-weight: 500;
      gap: 10px;
      justify-content: center;
      letter-spacing: 0.02em;
      padding: 11px 0;
      transition: background 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
      -webkit-user-select: none;
      user-select: none;
      width: 100%;
    }

    .jan-batch-select-chars-btn:hover {
      background: rgba(255, 255, 255, 0.04);
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
      color: var(--jan-batch-text);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-select-chars-btn:hover {
      background: rgba(0, 0, 0, 0.05);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.14);
    }

    .jan-batch-select-chars-btn svg {
      opacity: 0.6;
      transition: opacity 0.2s ease;
    }

    .jan-batch-select-chars-btn:hover svg {
      opacity: 1;
    }

    .jan-batch-settings-block {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    .jan-batch-section-title {
      align-items: center;
      color: var(--jan-batch-muted);
      display: flex;
      font-size: 0.75rem;
      font-weight: 600;
      gap: 6px;
      letter-spacing: 0.1em;
      margin-bottom: 4px;
      text-transform: uppercase;
    }

    .jan-batch-setting-row {
      align-items: center;
      display: flex;
      gap: 12px;
      justify-content: space-between;
      padding: 4px 0;
      transition: opacity 0.25s ease;
    }

    .jan-batch-setting-label {
      align-items: center;
      color: var(--jan-batch-text);
      display: flex;
      font-size: 0.85rem;
      gap: 6px;
      -webkit-user-select: none;
      user-select: none;
    }

    .jan-batch-toggle-switch {
      display: inline-block;
      height: 22px;
      position: relative;
      width: 40px;
    }

    .jan-batch-toggle-switch input {
      height: 0;
      opacity: 0;
      width: 0;
    }

    .jan-batch-toggle-slider {
      background-color: rgba(255, 255, 255, 0.06);
      border: 1px solid var(--jan-batch-border);
      border-radius: 22px;
      bottom: 0;
      cursor: pointer;
      left: 0;
      position: absolute;
      right: 0;
      top: 0;
      transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-toggle-slider {
      background-color: rgba(0, 0, 0, 0.06);
    }

    .jan-batch-toggle-slider::before {
      background-color: rgba(255, 255, 255, 0.35);
      border-radius: 50%;
      bottom: 2px;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
      content: "";
      height: 16px;
      left: 2px;
      position: absolute;
      transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      width: 16px;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-toggle-slider::before {
      background-color: rgba(0, 0, 0, 0.35);
    }

    .jan-batch-toggle-switch input:checked + .jan-batch-toggle-slider {
      background-color: rgba(255, 255, 255, 0.12);
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-toggle-switch input:checked + .jan-batch-toggle-slider {
      background-color: rgba(0, 0, 0, 0.08);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
    }

    .jan-batch-toggle-switch input:checked + .jan-batch-toggle-slider::before {
      background-color: var(--jan-batch-accent);
      box-shadow: 0 0 6px rgba(255, 255, 255, 0.3);
      transform: translateX(18px);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-toggle-switch input:checked + .jan-batch-toggle-slider::before {
      box-shadow: 0 0 6px rgba(0, 0, 0, 0.18);
    }

    .jan-batch-inline-segment {
      background: rgba(0, 0, 0, 0.3);
      border-radius: 6px;
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 1px var(--jan-batch-border-soft);
      display: flex;
      padding: 3px;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-inline-segment {
      background: rgba(0, 0, 0, 0.06);
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.06);
    }

    .jan-batch-inline-segment label {
      cursor: pointer;
      margin: 0;
    }

    .jan-batch-inline-segment input {
      display: none;
    }

    .jan-batch-inline-seg-btn {
      align-items: center;
      border-radius: 4px;
      color: var(--jan-batch-muted);
      display: flex;
      font-size: 0.75rem;
      font-weight: 500;
      gap: 4px;
      justify-content: center;
      padding: 4px 10px;
      transition: all 0.2s ease;
      user-select: none;
    }

    .jan-batch-inline-segment input:checked + .jan-batch-inline-seg-btn {
      background: rgba(255, 255, 255, 0.1);
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 0 0 1px var(--jan-batch-border-soft);
      color: var(--jan-batch-text);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-inline-segment input:checked + .jan-batch-inline-seg-btn {
      background: rgba(0, 0, 0, 0.08);
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.08);
    }

    .jan-batch-inline-segment input[value="files"]:checked + .jan-batch-inline-seg-btn {
      background: rgba(90, 156, 245, 0.1);
      box-shadow: inset 0 0 0 1px rgba(90, 156, 245, 0.2);
      color: #5a9cf5;
    }

    .jan-batch-inline-segment input[value="zip"]:checked + .jan-batch-inline-seg-btn {
      background: rgba(195, 119, 224, 0.1);
      box-shadow: inset 0 0 0 1px rgba(195, 119, 224, 0.2);
      color: #c377e0;
    }

    .jan-batch-format-grid {
      display: grid;
      gap: 8px;
      grid-template-columns: repeat(3, 1fr);
      width: 100%;
    }

    .jan-batch-format-card {
      align-items: center;
      background: var(--jan-batch-bg-softer);
      border: 1px solid var(--jan-batch-border);
      border-radius: 10px;
      cursor: pointer;
      display: flex;
      flex-direction: column;
      gap: 8px;
      justify-content: center;
      padding: 12px 4px;
      transition: all 0.2s ease;
    }

    .jan-batch-format-card:hover {
      background: rgba(255, 255, 255, 0.04);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-format-card:hover {
      background: rgba(0, 0, 0, 0.04);
    }

    .jan-batch-format-card input {
      display: none;
    }

    .jan-batch-format-svg {
      align-items: center;
      color: rgba(255, 255, 255, 0.35);
      display: flex;
      justify-content: center;
      transition: all 0.2s ease;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-format-svg {
      color: rgba(0, 0, 0, 0.35);
    }

    .jan-batch-format-badge {
      background: rgba(0, 0, 0, 0.35);
      border: 1px solid rgba(255, 255, 255, 0.04);
      border-radius: 4px;
      color: var(--jan-batch-muted);
      font-size: 0.75rem;
      font-weight: 600;
      letter-spacing: 0.05em;
      padding: 2px 8px;
      transition: all 0.2s ease;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-format-badge {
      background: rgba(0, 0, 0, 0.06);
      border-color: rgba(0, 0, 0, 0.06);
    }

    .jan-batch-format-card.is-active {
      background: rgba(255, 255, 255, 0.06);
      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2), 0 0 12px rgba(255, 255, 255, 0.02);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-format-card.is-active {
      background: rgba(0, 0, 0, 0.06);
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.14), 0 0 12px rgba(0, 0, 0, 0.04);
    }

    .jan-batch-format-card.is-active .jan-batch-format-svg {
      color: var(--jan-batch-text);
    }

    .jan-batch-format-card.is-active .jan-batch-format-badge {
      background: var(--jan-batch-accent);
      border-color: var(--jan-batch-accent);
      color: #000000;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-format-card.is-active .jan-batch-format-badge {
      color: #ffffff;
    }

    .jan-batch-format-card.is-png.is-active {
      background: rgba(90, 156, 245, 0.06);
      border-color: rgba(90, 156, 245, 0.28);
      box-shadow: 0 0 0 1px rgba(90, 156, 245, 0.35);
    }

    .jan-batch-format-card.is-png.is-active .jan-batch-format-svg,
    .jan-batch-format-card.is-png:hover .jan-batch-format-svg {
      color: #5a9cf5;
    }

    .jan-batch-format-card.is-png.is-active .jan-batch-format-badge {
      background: #5a9cf5;
      border-color: #5a9cf5;
      color: #ffffff;
    }

    .jan-batch-format-card.is-png:hover {
      border-color: rgba(90, 156, 245, 0.2);
    }

    .jan-batch-format-card.is-json.is-active {
      background: rgba(212, 137, 58, 0.06);
      border-color: rgba(212, 137, 58, 0.28);
      box-shadow: 0 0 0 1px rgba(212, 137, 58, 0.35);
    }

    .jan-batch-format-card.is-json.is-active .jan-batch-format-svg,
    .jan-batch-format-card.is-json:hover .jan-batch-format-svg {
      color: #d4893a;
    }

    .jan-batch-format-card.is-json.is-active .jan-batch-format-badge {
      background: #d4893a;
      border-color: #d4893a;
      color: #ffffff;
    }

    .jan-batch-format-card.is-json:hover {
      border-color: rgba(212, 137, 58, 0.2);
    }

    .jan-batch-format-card.is-txt.is-active {
      background: rgba(143, 168, 190, 0.06);
      border-color: rgba(143, 168, 190, 0.25);
      box-shadow: 0 0 0 1px rgba(143, 168, 190, 0.32);
    }

    .jan-batch-format-card.is-txt.is-active .jan-batch-format-svg,
    .jan-batch-format-card.is-txt:hover .jan-batch-format-svg {
      color: #8fa8be;
    }

    .jan-batch-format-card.is-txt.is-active .jan-batch-format-badge {
      background: #8fa8be;
      border-color: #8fa8be;
      color: #ffffff;
    }

    .jan-batch-format-card.is-txt:hover {
      border-color: rgba(143, 168, 190, 0.2);
    }

    .jan-batch-queue-top-area {
      align-items: flex-end;
      display: flex;
      justify-content: space-between;
      margin-bottom: 8px;
    }

    .jan-batch-progress-info {
      display: flex;
      flex-direction: column;
      gap: 8px;
      width: 300px;
    }

    .jan-batch-progress-stats {
      color: var(--jan-batch-muted);
      display: flex;
      font-size: 0.85rem;
      justify-content: space-between;
    }

    .jan-batch-progress-stats strong {
      color: var(--jan-batch-text);
      font-weight: 500;
    }

    .jan-batch-global-progress-bar {
      background: rgba(255, 255, 255, 0.06);
      border-radius: 3px;
      height: 3px;
      overflow: hidden;
      width: 100%;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-global-progress-bar {
      background: rgba(0, 0, 0, 0.08);
    }

    .jan-batch-global-progress-fill {
      background: linear-gradient(90deg, rgba(255, 255, 255, 0.6), var(--jan-batch-accent));
      border-radius: 3px;
      box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
      height: 100%;
      transition: width 0.4s cubic-bezier(0.16, 1, 0.3, 1);
      width: 0%;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-global-progress-fill {
      background: linear-gradient(90deg, rgba(0, 0, 0, 0.2), var(--jan-batch-accent));
      box-shadow: none;
    }

    .jan-batch-bulk-actions-group {
      align-items: center;
      background: var(--jan-batch-bg-soft);
      border-radius: 10px;
      box-shadow: 0 0 0 1px var(--jan-batch-border);
      display: flex;
      gap: 6px;
      padding: 4px 8px;
      transition: all 0.2s ease;
    }

    .jan-batch-bulk-divider {
      background: var(--jan-batch-border);
      height: 14px;
      margin: 0 4px;
      width: 1px;
    }

    .jan-batch-select-all-wrapper {
      align-items: center;
      color: var(--jan-batch-muted);
      display: flex;
      font-size: 0.85rem;
      gap: 8px;
      margin-right: 8px;
      position: relative;
    }

    .jan-batch-filter-toggle {
      align-items: center;
      background: var(--jan-batch-bg-soft);
      border: 1px solid var(--jan-batch-border-soft);
      border-radius: 6px;
      color: var(--jan-batch-muted);
      cursor: pointer;
      display: flex;
      font: inherit;
      gap: 6px;
      padding: 4px 8px;
      transition: all 0.2s ease;
      user-select: none;
    }

    .jan-batch-filter-toggle:hover {
      background: rgba(255, 255, 255, 0.06);
      color: var(--jan-batch-text);
      border-color: var(--jan-batch-border-strong);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-filter-toggle {
      background: rgba(0, 0, 0, 0.05);
      border-color: rgba(0, 0, 0, 0.08);
      color: var(--jan-batch-muted);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-filter-toggle:hover {
      background: rgba(0, 0, 0, 0.08);
      border-color: rgba(0, 0, 0, 0.12);
      color: var(--jan-batch-text);
    }

    .jan-batch-filter-toggle svg {
      opacity: 0.6;
      transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease;
    }

    .jan-batch-filter-toggle.open svg {
      transform: rotate(180deg);
    }

    .jan-batch-filter-text {
      display: none;
    }

    .jan-batch-filter-dot {
      border-radius: 50%;
      display: inline-block;
      height: 8px;
      width: 8px;
    }

    .jan-batch-filter-menu {
      background: rgba(15, 15, 15, 0.95);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      border: 1px solid var(--jan-batch-border);
      border-radius: 8px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.02) inset;
      display: flex;
      flex-direction: column;
      gap: 2px;
      left: 24px;
      margin-top: 8px;
      min-width: 140px;
      opacity: 0;
      padding: 6px;
      pointer-events: none;
      position: absolute;
      top: 100%;
      transform: translateY(-5px) scale(0.98);
      transform-origin: top left;
      transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
      z-index: 100;
    }

    .jan-batch-filter-menu.show {
      opacity: 1;
      pointer-events: auto;
      transform: translateY(0) scale(1);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-filter-menu {
      background: rgba(255, 255, 255, 0.96);
      border-color: rgba(0, 0, 0, 0.1);
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.02) inset;
    }

    .jan-batch-filter-item {
      align-items: center;
      background: transparent;
      border: none;
      border-radius: 5px;
      color: var(--jan-batch-muted);
      cursor: pointer;
      display: flex;
      font-size: 0.85rem;
      font-weight: 500;
      justify-content: space-between;
      padding: 8px 10px;
      transition: all 0.15s ease;
    }

    .jan-batch-filter-label {
      align-items: center;
      display: flex;
      gap: 8px;
    }

    .jan-batch-filter-item:hover {
      background: rgba(255, 255, 255, 0.06);
      color: var(--jan-batch-text);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-filter-item:hover {
      background: rgba(0, 0, 0, 0.06);
    }

    .jan-batch-filter-item.is-active {
      background: rgba(255, 255, 255, 0.1);
      color: var(--jan-batch-accent);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-filter-item.is-active {
      background: rgba(0, 0, 0, 0.08);
      color: var(--jan-batch-accent);
    }

    .jan-batch-filter-check {
      opacity: 0;
      transform: scale(0.8);
      transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
    }

    .jan-batch-filter-item.is-active .jan-batch-filter-check {
      opacity: 1;
      transform: scale(1);
    }
    .jan-batch-select-all-wrapper,
    .jan-batch-format-card,
    .jan-batch-progress-stats,
    .jan-batch-hud-confirm-btn,
    .jan-batch-item-title,
    .jan-batch-item-status-text {
      -webkit-user-select: none;
      user-select: none;
    }

    .jan-batch-custom-checkbox {
      appearance: none;
      background: rgba(0, 0, 0, 0.4);
      border: none;
      border-radius: 4px;
      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
      cursor: pointer;
      flex-shrink: 0;
      height: 16px;
      position: relative;
      transition: all 0.15s ease;
      width: 16px;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-custom-checkbox {
      background: rgba(0, 0, 0, 0.08);
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18);
    }

    .jan-batch-custom-checkbox:checked,
    .jan-batch-custom-checkbox:indeterminate {
      background: var(--jan-batch-accent);
      box-shadow: none;
    }

    .jan-batch-custom-checkbox:checked::after {
      background: #000000;
      content: "";
      inset: 0;
      -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
      -webkit-mask-position: center;
      -webkit-mask-repeat: no-repeat;
      -webkit-mask-size: 12px 12px;
      mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
      mask-position: center;
      mask-repeat: no-repeat;
      mask-size: 12px 12px;
      position: absolute;
    }

    .jan-batch-custom-checkbox:indeterminate::after {
      background: #000000;
      content: "";
      inset: 0;
      -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
      -webkit-mask-position: center;
      -webkit-mask-repeat: no-repeat;
      -webkit-mask-size: 12px 12px;
      mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
      mask-position: center;
      mask-repeat: no-repeat;
      mask-size: 12px 12px;
      position: absolute;
    }

    .jan-batch-icon-btn {
      align-items: center;
      background: transparent;
      border: none;
      border-radius: 6px;
      color: var(--jan-batch-muted);
      cursor: pointer;
      display: flex;
      justify-content: center;
      padding: 6px;
      transition: all 0.15s ease;
    }

    .jan-batch-icon-btn:hover:not(:disabled) {
      background: rgba(255, 255, 255, 0.1);
      color: var(--jan-batch-text);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-icon-btn:hover:not(:disabled) {
      background: rgba(0, 0, 0, 0.08);
    }

    .jan-batch-icon-btn.is-danger:hover:not(:disabled) {
      background: rgba(244, 67, 54, 0.1);
      color: var(--jan-batch-error);
    }

    .jan-batch-icon-btn.is-primary:hover:not(:disabled) {
      background: rgba(255, 255, 255, 0.1);
      color: var(--jan-batch-accent);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-icon-btn.is-primary:hover:not(:disabled) {
      background: rgba(0, 0, 0, 0.08);
    }

    .jan-batch-icon-btn.is-success:hover:not(:disabled) {
      background: rgba(76, 175, 80, 0.1);
      color: var(--jan-batch-success);
    }

    .jan-batch-icon-btn:disabled {
      background: transparent !important;
      color: var(--jan-batch-muted) !important;
      opacity: 0.15;
      pointer-events: none;
    }

    .jan-batch-queue-box {
      background: var(--jan-batch-bg);
      border-radius: 10px;
      box-shadow: 0 0 0 1px var(--jan-batch-border-soft);
      display: flex;
      flex: 1 1 auto;
      flex-direction: column;
      gap: 6px;
      min-height: 0;
      overflow-y: auto;
      padding: 10px;
    }

    .jan-batch-queue-box::-webkit-scrollbar {
      width: 5px;
    }

    .jan-batch-queue-box::-webkit-scrollbar-track {
      background: transparent;
    }

    .jan-batch-queue-box::-webkit-scrollbar-thumb {
      background: rgba(255, 255, 255, 0.1);
      border-radius: 10px;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-queue-box::-webkit-scrollbar-thumb {
      background: rgba(0, 0, 0, 0.15);
    }

    .jan-batch-queue-item {
      align-items: center;
      background: var(--jan-batch-bg-soft);
      border: none;
      border-radius: 10px;
      box-shadow: inset 0 0 0 1px var(--jan-batch-border-soft);
      cursor: pointer;
      display: flex;
      padding: 12px 14px;
      transition: background 0.15s ease, box-shadow 0.2s ease;
    }

    .jan-batch-queue-item:hover {
      background: rgba(255, 255, 255, 0.04);
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-queue-item:hover {
      background: rgba(0, 0, 0, 0.04);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
    }

    .jan-batch-queue-item.is-selected {
      background: rgba(255, 255, 255, 0.065);
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.13);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-queue-item.is-selected {
      background: rgba(0, 0, 0, 0.06);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.13);
    }

    .jan-batch-queue-item .jan-batch-custom-checkbox {
      display: none;
    }

    .jan-batch-item-icon {
      align-items: center;
      background: rgba(255, 255, 255, 0.05);
      border-radius: 8px;
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
      color: rgba(255, 255, 255, 0.5);
      display: flex;
      flex: 0 0 auto;
      height: 32px;
      justify-content: center;
      margin-right: 14px;
      position: relative;
      width: 32px;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-item-icon {
      background: rgba(0, 0, 0, 0.05);
      box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.04);
      color: rgba(0, 0, 0, 0.45);
    }

    .jan-batch-item-icon::after {
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
      background-position: center;
      background-repeat: no-repeat;
      background-size: contain;
      bottom: -4px;
      content: "";
      filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.9)) drop-shadow(0 0 1px black);
      height: 15px;
      opacity: 0;
      pointer-events: none;
      position: absolute;
      right: -4px;
      transform: translateY(2px) scale(0.8);
      transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
      width: 15px;
    }

    .jan-batch-queue-item.is-selected .jan-batch-item-icon::after {
      opacity: 1;
      transform: translateY(0) scale(1);
    }

    .jan-batch-queue-item.status-ready .jan-batch-item-icon {
      background: rgba(120, 140, 185, 0.08);
      box-shadow: inset 0 0 0 1px rgba(120, 140, 185, 0.1);
      color: rgba(160, 178, 220, 0.65);
    }

    .jan-batch-queue-item.status-fetching .jan-batch-item-icon {
      background: rgba(255, 183, 77, 0.1);
      box-shadow: inset 0 0 0 1px rgba(255, 183, 77, 0.15);
      color: var(--jan-batch-warning);
    }

    .jan-batch-queue-item.status-success .jan-batch-item-icon {
      background: rgba(76, 175, 80, 0.1);
      box-shadow: inset 0 0 0 1px rgba(76, 175, 80, 0.15);
      color: var(--jan-batch-success);
    }

    .jan-batch-queue-item.status-error .jan-batch-item-icon {
      background: rgba(244, 67, 54, 0.08);
      box-shadow: inset 0 0 0 1px rgba(244, 67, 54, 0.12);
      color: var(--jan-batch-error);
    }

    .jan-batch-item-info {
      display: flex;
      flex: 1 1 auto;
      flex-direction: column;
      gap: 6px;
      min-width: 0;
    }

    .jan-batch-item-title {
      align-self: flex-start;
      color: var(--jan-batch-text);
      display: inline-block;
      font-size: 0.95rem;
      font-weight: 500;
      max-width: 100%;
      overflow: hidden;
      text-decoration: none;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .jan-batch-item-title:hover {
      text-decoration: underline;
    }

    .jan-batch-selection-hud.is-active .jan-batch-hud-floating-bar {
      pointer-events: auto;
    }

    .jan-batch-item-status-row {
      align-items: center;
      display: flex;
      gap: 12px;
    }

    .jan-batch-item-status-text {
      color: var(--jan-batch-muted);
      flex-shrink: 0;
      font-size: 0.75rem;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      width: 120px;
    }

    .jan-batch-queue-item.status-error .jan-batch-item-status-text:not([data-original-text]) {
      cursor: pointer;
    }

    .jan-batch-queue-item.status-error .jan-batch-item-status-text:not([data-original-text]):hover {
      text-decoration: underline;
      text-decoration-style: dotted;
    }

    .jan-batch-item-status-text[data-original-text] {
      pointer-events: none;
    }

    .jan-batch-queue-item.status-success .jan-batch-item-status-text {
      color: var(--jan-batch-success);
    }

    .jan-batch-queue-item.status-error .jan-batch-item-status-text {
      color: var(--jan-batch-error);
    }

    .jan-batch-queue-item.status-fetching .jan-batch-item-status-text {
      color: var(--jan-batch-warning);
    }

    .jan-batch-item-min-progress {
      background: rgba(255, 255, 255, 0.06);
      border-radius: 2px;
      flex: 1 1 auto;
      height: 2px;
      overflow: hidden;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-item-min-progress {
      background: rgba(0, 0, 0, 0.08);
    }

    .jan-batch-item-min-progress-fill {
      background: rgba(255, 255, 255, 0.25);
      border-radius: 2px;
      height: 100%;
      width: 0%;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-item-min-progress-fill {
      background: rgba(0, 0, 0, 0.22);
    }

    .jan-batch-queue-item.status-success .jan-batch-item-min-progress-fill {
      background: var(--jan-batch-success);
      width: 100% !important;
    }

    .jan-batch-queue-item.status-error .jan-batch-item-min-progress-fill {
      background: var(--jan-batch-error);
      width: 100% !important;
    }

    .jan-batch-queue-item.status-fetching .jan-batch-item-min-progress-fill {
      background: var(--jan-batch-warning);
      box-shadow: 0 0 8px rgba(255, 183, 77, 0.2);
    }

    .jan-batch-item-actions {
      align-items: center;
      display: flex;
      gap: 4px;
      margin-left: 12px;
    }

    .jan-panel-group--selection-mode > #jan-panel,
    .jan-panel-group--selection-mode > .jan-panel-top-tab,
    .jan-panel-group--selection-mode > .jan-bar {
      opacity: 0 !important;
      pointer-events: none !important;
    }

    .jan-panel-group--selection-mode > #jan-panel {
      transform: scale(0.95);
    }

    .jan-batch-selection-hud {
      align-items: flex-end;
      display: flex;
      inset: 0;
      justify-content: center;
      opacity: 0;
      padding-bottom: 40px;
      pointer-events: none;
      position: fixed;
      transition: opacity 0.3s ease;
      visibility: hidden;
      z-index: 100001;
    }

    .jan-batch-selection-hud.is-active {
      background: transparent;
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
      opacity: 1;
      pointer-events: none;
      visibility: visible;
    }

    .jan-batch-hud-floating-bar {
      align-items: center;
      display: flex;
      gap: 12px;
      pointer-events: none;
      white-space: nowrap;
    }

    .jan-batch-hud-icon-btn,
    .jan-batch-hud-close-btn {
      align-items: center;
      background: transparent;
      border: none;
      border-radius: 50%;
      color: rgba(255, 255, 255, 0.4);
      cursor: pointer;
      display: inline-flex;
      height: 42px;
      justify-content: center;
      padding: 0;
      transition: color 150ms ease, background 150ms ease;
      width: 42px;
    }

    .jan-batch-hud-icon-btn:hover,
    .jan-batch-hud-close-btn:hover {
      background: rgba(255, 255, 255, 0.1);
      color: #ffffff;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-hud-icon-btn,
    #jan-panel-backdrop.jan-panel--light .jan-batch-hud-close-btn {
      color: rgba(0, 0, 0, 0.45);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-hud-icon-btn:hover,
    #jan-panel-backdrop.jan-panel--light .jan-batch-hud-close-btn:hover {
      background: rgba(0, 0, 0, 0.1);
      color: #111111;
    }

    .jan-batch-hud-close-btn {
      color: #ff5555;
    }

    .jan-batch-hud-close-btn:hover {
      background: rgba(255, 60, 60, 0.2);
      color: #ff8888;
    }

    .jan-batch-hud-confirm-btn {
      background: transparent;
      border: none;
      border-radius: 9999px;
      color: rgba(255, 255, 255, 0.7);
      cursor: pointer;
      font: inherit;
      font-size: 0.85rem;
      font-weight: 600;
      height: 30px;
      margin: 0 4px;
      padding: 0 20px;
      transition: background 150ms ease, color 150ms ease;
    }

    .jan-batch-hud-confirm-btn:hover {
      color: #ffffff;
    }
    #jan-panel-backdrop.jan-panel--light .jan-batch-selection-hud .jan-pod {
      background: #ffffff;
      border-color: #8e8e93;
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-hud-confirm-btn {
      color: rgba(0, 0, 0, 0.72);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-hud-confirm-btn:hover {
      color: #111111;
    }

    .jan-batch-auto-retry-controls {
      align-items: center;
      display: flex;
      gap: 8px;
    }

    .jan-batch-auto-retry-count {
      -moz-appearance: textfield;
      appearance: textfield;
      background: rgba(0, 0, 0, 0.3);
      border: none;
      border-radius: 6px;
      box-sizing: border-box;
      color: var(--jan-batch-text);
      font-family: inherit;
      font-size: 0.8rem;
      height: 26px;
      outline: none;
      padding: 0 8px;
      text-align: center;
      transition: background 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
      width: 58px;
    }

    .jan-batch-auto-retry-count::-webkit-inner-spin-button,
    .jan-batch-auto-retry-count::-webkit-outer-spin-button {
      opacity: 0.35;
    }

    .jan-batch-auto-retry-count:focus {
      background: rgba(0, 0, 0, 0.4);
      box-shadow: 0 0 0 1px var(--jan-batch-border-strong);
    }

    .jan-batch-auto-retry-count::placeholder {
      color: rgba(255, 255, 255, 0.3);
    }

    .jan-batch-auto-retry-count:disabled {
      opacity: 0.3;
      pointer-events: none;
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-auto-retry-count {
      background: rgba(0, 0, 0, 0.05);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-auto-retry-count:focus {
      background: rgba(0, 0, 0, 0.07);
      box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
    }

    #jan-panel-backdrop.jan-panel--light .jan-batch-auto-retry-count::placeholder {
      color: rgba(0, 0, 0, 0.3);
    }

    .jan-batch-setting-row.is-dimmed {
      opacity: 0.35;
      pointer-events: none;
      user-select: none;
    }
    .jan-batch-setting-row.is-dimmed .jan-batch-toggle-switch {
      cursor: default;
    }
  `;
    documentRef.head.appendChild(style);
  }
  function ensureSelectionOverlayStyle(documentRef) {
    if (documentRef.getElementById(SELECTION_STYLE_ID)) return;
    const style = documentRef.createElement("style");
    style.id = SELECTION_STYLE_ID;
    style.textContent = `
    .${SELECTION_OVERLAY_CLASS} {
      position: absolute;
      inset: 0;
      background: rgba(76, 175, 80, 0.18);
      border: 2px solid rgba(76, 175, 80, 0.6);
      border-radius: inherit;
      pointer-events: none;
      z-index: 200;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .${SELECTION_OVERLAY_CLASS}::after {
      content: '';
      width: 24px;
      height: 24px;
      background: rgba(76, 175, 80, 0.9);
      border-radius: 50%;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
      background-size: 14px 14px;
      background-repeat: no-repeat;
      background-position: center;
      box-shadow: 0 2px 8px rgba(0,0,0,0.3);
    }
  `;
    (documentRef.head ?? documentRef.documentElement).appendChild(style);
  }
  const CHARACTER_LINK_SELECTORS = [
    'a[href*="/characters/"]',
    'a[href*="/character/"]'
  ];
  function matchesSelector(node, selector) {
    return typeof node?.matches === "function" && node.matches(selector);
  }
  function findCharacterLinkElement(wrapper) {
    if (!wrapper) return null;
    for (const selector of CHARACTER_LINK_SELECTORS) {
      const found = wrapper.querySelector?.(selector);
      if (found) return found;
      if (matchesSelector(wrapper, selector)) return wrapper;
    }
    return null;
  }
  function hasCharacterLink(wrapper) {
    return Boolean(findCharacterLinkElement(wrapper));
  }
  function resolveCharacterLinkHref(linkEl, documentRef = globalThis.document) {
    if (!linkEl) return "";
    const directHref = typeof linkEl?.href === "string" ? linkEl.href.trim() : "";
    if (directHref) return directHref;
    const rawHref = typeof linkEl?.getAttribute === "function" ? (linkEl.getAttribute("href") ?? "").trim() : "";
    if (!rawHref) return "";
    try {
      return new URL(rawHref, documentRef?.baseURI ?? "https://janitorai.com").href;
    } catch {
      return rawHref;
    }
  }
  function getCharacterInputFromWrapper(wrapper, documentRef = globalThis.document) {
    const link = findCharacterLinkElement(wrapper);
    if (!link) return null;
    const href = resolveCharacterLinkHref(link, documentRef);
    return href || null;
  }
  function getCharacterIdFromPath(pathname) {
    const path = typeof pathname === "string" ? pathname : "";
    const match = path.match(/\/characters?\/([^/?#]+)/);
    return match?.[1] ?? null;
  }
  function buildCharacterInput(characterId, documentRef = globalThis.document) {
    try {
      return new URL(
        `/characters/${characterId}`,
        documentRef?.baseURI ?? "https://janitorai.com"
      ).href;
    } catch {
      return `https://janitorai.com/characters/${characterId}`;
    }
  }
  function truncateStatusText(text, maxLen = 23) {
    const safeText = typeof text === "string" ? text : String(text ?? "");
    if (safeText.length <= maxLen) return safeText;
    return safeText.slice(0, maxLen) + "...";
  }
  function resolveQueueTitle(entry) {
    return entry?.title?.trim() ?? "";
  }
  function resolveQueueCharacterUrl(entry) {
    return entry?.characterUrl ?? buildCharacterInput(entry?.characterId) ?? "";
  }
  function formatFetchedAt(fetchedAt) {
    return formatRelativeTime(fetchedAt, {
      emptyLabel: "Ready to Export",
      prefix: "Fetched",
      justNowLabel: "just now"
    });
  }
  const STATUS_MAP = {
    pending: {
      visualStatus: "status-ready",
      iconSvg: QUEUE_PENDING_ICON_SVG,
      primaryButtonClass: "is-primary",
      primaryButtonTitle: "Fetch definition",
      primaryButtonIcon: FETCH_ITEM_ICON_SVG,
      primaryAction: "fetch",
      getStatusText: () => "Pending"
    },
    fetching: {
      visualStatus: "status-fetching",
      iconSvg: QUEUE_FETCHING_ICON_SVG,
      primaryButtonClass: "",
      primaryButtonTitle: "Fetching...",
      primaryButtonIcon: VIEWER_ICON_SVG,
      primaryAction: "none",
      getStatusText: (entry) => `Fetching... ${entry.progress}%`
    },
    ready: {
      visualStatus: "status-success",
      iconSvg: QUEUE_READY_ICON_SVG,
      primaryButtonClass: "is-primary",
      primaryButtonTitle: "Open in Character Viewer",
      primaryButtonIcon: VIEWER_ICON_SVG,
      primaryAction: "view",
      getStatusText: (entry) => formatFetchedAt(entry.fetchedAt)
    },
    error: {
      visualStatus: "status-error",
      iconSvg: QUEUE_ERROR_ICON_SVG,
      primaryButtonClass: "is-primary",
      primaryButtonTitle: "Retry Fetch",
      primaryButtonIcon: BULK_FETCH_ICON_SVG,
      primaryAction: "fetch",
      getStatusText: (entry) => entry.retrying ? "Auto-retry..." : truncateStatusText(entry.error ? `Failed: ${entry.error}` : "Failed")
    }
  };
  function mapQueueEntryToMarkupItem(entry) {
    const mapped = STATUS_MAP[entry.status] ?? STATUS_MAP.pending;
    const statusText = mapped.getStatusText ? mapped.getStatusText(entry) : "";
    const fullError = entry.status === "error" && entry.error ? `Failed: ${entry.error}` : "";
    return {
      queueId: entry.id,
      characterId: entry.characterId,
      title: resolveQueueTitle(entry),
      characterUrl: resolveQueueCharacterUrl(entry),
      visualStatus: mapped.visualStatus,
      actionStatus: entry.status,
      statusText,
      fullError,
      progress: entry.progress,
      checked: false,
      primaryButtonClass: mapped.primaryButtonClass,
      primaryButtonTitle: mapped.primaryButtonTitle,
      primaryButtonIcon: mapped.primaryButtonIcon,
      iconSvg: mapped.iconSvg,
      primaryAction: mapped.primaryAction
    };
  }
  function buildQueueItemMarkupWithId(item) {
    const primaryButtonClassName = item.primaryButtonClass ? ` jan-batch-icon-btn ${item.primaryButtonClass}` : " jan-batch-icon-btn";
    const checkedAttr = item.checked ? " checked" : "";
    const selectedClass = item.checked ? " is-selected" : "";
    const safeQueueId = escapeHtmlAttr(item.queueId);
    const safeActionStatus = escapeHtmlAttr(item.actionStatus);
    const safeTitle = escapeHtmlText(item.title);
    const safeCharacterUrl = escapeHtmlAttr(item.characterUrl ?? "");
    const safeStatusText = escapeHtmlText(item.statusText);
    const safeFullError = escapeHtmlAttr(item.fullError ?? "");
    const safePrimaryTitle = escapeHtmlAttr(item.primaryButtonTitle);
    const safePrimaryAction = escapeHtmlAttr(item.primaryAction);
    const progressValue = Number.isFinite(item.progress) ? item.progress : 0;
    const titleMarkup = safeCharacterUrl ? `<a class="jan-batch-item-title" href="${safeCharacterUrl}" target="_blank" rel="noopener noreferrer">${safeTitle}</a>` : `<div class="jan-batch-item-title">${safeTitle}</div>`;
    return `
    <label class="jan-batch-queue-item ${item.visualStatus}${selectedClass}" data-queue-id="${safeQueueId}">
      <input type="checkbox" class="jan-batch-custom-checkbox jan-batch-cb-row" data-status="${safeActionStatus}" data-queue-id="${safeQueueId}"${checkedAttr}>
      <div class="jan-batch-item-icon">
        ${item.iconSvg}
      </div>
      <div class="jan-batch-item-info">
        ${titleMarkup}
        <div class="jan-batch-item-status-row">
          <span class="jan-batch-item-status-text"${safeFullError ? ` title="${safeFullError}" data-full-error="${safeFullError}"` : ""}>${safeStatusText}</span>
          <div class="jan-batch-item-min-progress">
            <div class="jan-batch-item-min-progress-fill" style="width: ${progressValue}%"></div>
          </div>
        </div>
      </div>
      <div class="jan-batch-item-actions">
        <button type="button" class="${primaryButtonClassName.trim()}" title="${safePrimaryTitle}" data-action="${safePrimaryAction}" data-queue-id="${safeQueueId}">
          ${item.primaryButtonIcon}
        </button>
        <button type="button" class="jan-batch-icon-btn is-danger" title="Remove" data-action="remove" data-queue-id="${safeQueueId}">
          ${TRASH_ICON_SVG}
        </button>
      </div>
    </label>
  `;
  }
  function createBatchViewMarkup() {
    return `
    <div class="jan-batch-sidebar">
      <div class="jan-batch-selection-cta-card">
        <div class="jan-batch-selection-icon-ring">
          ${PICK_ICON_SVG}
        </div>
        <div class="jan-batch-selection-copy">
          <div class="jan-batch-selection-title">Pick from Page</div>
          <div class="jan-batch-selection-description">Enter selection mode to click on character cards around the website.</div>
        </div>
        <button type="button" class="jan-batch-select-chars-btn">
          ${PLUS_CIRCLE_SVG}
          <span>Select Characters</span>
        </button>
      </div>

      <div class="jan-batch-settings-block">
        <div class="jan-batch-section-title">
          ${SETTINGS_ICON_SVG}
          <span>Settings</span>
        </div>

        <div class="jan-batch-setting-row">
          <div class="jan-batch-setting-label">Auto-fetch</div>
          <label class="jan-batch-toggle-switch">
            <input type="checkbox" class="jan-batch-auto-fetch" name="jan-batch-auto-fetch" checked>
            <span class="jan-batch-toggle-slider"></span>
          </label>
        </div>

        <div class="jan-batch-setting-row">
          <div class="jan-batch-setting-label">Auto-export</div>
          <label class="jan-batch-toggle-switch">
            <input type="checkbox" class="jan-batch-auto-export" name="jan-batch-auto-export">
            <span class="jan-batch-toggle-slider"></span>
          </label>
        </div>

        <div class="jan-batch-setting-row">
          <div class="jan-batch-setting-label">Auto-retry</div>
          <div class="jan-batch-auto-retry-controls">
            <input type="number" class="jan-batch-auto-retry-count" name="jan-batch-auto-retry-count" min="1" max="20" placeholder="1-20">
            <label class="jan-batch-toggle-switch">
              <input type="checkbox" class="jan-batch-auto-retry" name="jan-batch-auto-retry">
              <span class="jan-batch-toggle-slider"></span>
            </label>
          </div>
        </div>

        <div class="jan-batch-setting-row">
          <div class="jan-batch-setting-label">Export As</div>
          <div class="jan-batch-inline-segment">
            <label>
              <input type="radio" name="jan-batch-download-mode" value="files" checked>
              <div class="jan-batch-inline-seg-btn">
                ${EXPORT_FILES_ICON_SVG}
                <span>Files</span>
              </div>
            </label>
            <label>
              <input type="radio" name="jan-batch-download-mode" value="zip">
              <div class="jan-batch-inline-seg-btn">
                ${EXPORT_ZIP_ICON_SVG}
                <span>ZIP</span>
              </div>
            </label>
          </div>
        </div>
      </div>

      <div class="jan-batch-settings-block">
        <div class="jan-batch-section-title">
          ${BATCH_SECTION_DOWNLOAD_ICON_SVG}
          <span>Format</span>
        </div>

        <div class="jan-batch-format-grid">
          <label class="jan-batch-format-card is-png">
            <input type="radio" name="jan-batch-export-format" value="png" checked>
            <div class="jan-batch-format-svg">${BATCH_FORMAT_PNG_SVG}</div>
            <div class="jan-batch-format-badge">PNG</div>
          </label>
          <label class="jan-batch-format-card is-json">
            <input type="radio" name="jan-batch-export-format" value="json">
            <div class="jan-batch-format-svg">${BATCH_FORMAT_JSON_SVG}</div>
            <div class="jan-batch-format-badge">JSON</div>
          </label>
          <label class="jan-batch-format-card is-txt">
            <input type="radio" name="jan-batch-export-format" value="txt">
            <div class="jan-batch-format-svg">${BATCH_FORMAT_TXT_SVG}</div>
            <div class="jan-batch-format-badge">TXT</div>
          </label>
        </div>
      </div>

      <div style="flex: 1;"></div>
    </div>

    <div class="jan-batch-queue-section">
      <div class="jan-batch-queue-top-area">
        <div class="jan-batch-progress-info">
          <div class="jan-batch-progress-stats">
            <strong>Download Queue</strong>
            <span>0 / 0 Tasks</span>
          </div>
          <div class="jan-batch-global-progress-bar">
            <div class="jan-batch-global-progress-fill"></div>
          </div>
        </div>

        <div class="jan-batch-bulk-actions-group">
          <div class="jan-batch-select-all-wrapper" title="Select Filtered">
            <input type="checkbox" class="jan-batch-custom-checkbox jan-batch-select-all">
            <button type="button" class="jan-batch-filter-toggle" title="Filter Selection" aria-expanded="false">
              <span class="jan-batch-filter-dot jan-batch-filter-dot--current" style="background: var(--jan-batch-accent); box-shadow: 0 0 6px rgba(255, 255, 255, 0.4);"></span>
              <span class="jan-batch-filter-text">All</span>
              ${FILTER_ICON_SVG}
            </button>

            <div class="jan-batch-filter-menu">
              <div class="jan-batch-filter-item is-active" data-filter="all">
                <div class="jan-batch-filter-label">
                  <span class="jan-batch-filter-dot" style="background: var(--jan-batch-accent); box-shadow: 0 0 6px rgba(255, 255, 255, 0.4);"></span>
                  All
                </div>
                <svg class="jan-batch-filter-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M20 6 9 17l-5-5" />
                </svg>
              </div>
              <div class="jan-batch-filter-item" data-filter="ready">
                <div class="jan-batch-filter-label">
                  <span class="jan-batch-filter-dot" style="background: var(--jan-batch-success); box-shadow: 0 0 6px rgba(76, 175, 80, 0.4);"></span>
                  Success
                </div>
                <svg class="jan-batch-filter-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M20 6 9 17l-5-5" />
                </svg>
              </div>
              <div class="jan-batch-filter-item" data-filter="error">
                <div class="jan-batch-filter-label">
                  <span class="jan-batch-filter-dot" style="background: var(--jan-batch-error); box-shadow: 0 0 6px rgba(244, 67, 54, 0.4);"></span>
                  Error
                </div>
                <svg class="jan-batch-filter-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M20 6 9 17l-5-5" />
                </svg>
              </div>
              <div class="jan-batch-filter-item" data-filter="pending">
                <div class="jan-batch-filter-label">
                  <span class="jan-batch-filter-dot" style="background: rgba(160, 178, 220, 0.65);"></span>
                  Idle
                </div>
                <svg class="jan-batch-filter-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
                  <path d="M20 6 9 17l-5-5" />
                </svg>
              </div>
            </div>
          </div>

          <div class="jan-batch-bulk-divider"></div>

          <button type="button" class="jan-batch-icon-btn is-primary jan-batch-bulk-fetch" title="Fetch Features for Selected" disabled>
            ${BULK_FETCH_ICON_SVG}
          </button>
          <button type="button" class="jan-batch-icon-btn is-success jan-batch-bulk-download" title="Download Selected" disabled>
            ${BULK_DOWNLOAD_ICON_SVG}
          </button>
          <button type="button" class="jan-batch-icon-btn is-danger jan-batch-bulk-remove" title="Remove Selected" disabled>
            ${TRASH_ICON_SVG}
          </button>
        </div>
      </div>

      <div class="jan-batch-queue-box">

      </div>
    </div>
  `;
  }
  function createSelectionHudMarkup() {
    return `
    <div class="jan-batch-hud-floating-bar">
      <div class="jan-pod jan-pod--circle">
        <button type="button" class="jan-batch-hud-icon-btn jan-batch-hud-select-btn" title="Select All">
          <span class="jan-batch-hud-icon jan-batch-hud-icon--all">${HUD_SELECT_ALL_ICON_SVG}</span>
          <span class="jan-batch-hud-icon jan-batch-hud-icon--partial" hidden>${HUD_SELECT_PARTIAL_ICON_SVG}</span>
          <span class="jan-batch-hud-icon jan-batch-hud-icon--none" hidden>${HUD_SELECT_NONE_ICON_SVG}</span>
        </button>
      </div>
      <div class="jan-pod">
        <button type="button" class="jan-batch-hud-confirm-btn">Confirm (0)</button>
      </div>
      <div class="jan-pod jan-pod--circle jan-pod--close">
        <button type="button" class="jan-batch-hud-close-btn" title="Close">
          ${HUD_CLOSE_ICON_SVG}
        </button>
      </div>
    </div>
  `;
  }
  function syncFormatCards(viewEl) {
    const formatCards = Array.from(viewEl.querySelectorAll(".jan-batch-format-card"));
    formatCards.forEach((card) => {
      const input = card.querySelector('input[name="jan-batch-export-format"]');
      card.classList.toggle("is-active", Boolean(input?.checked));
    });
  }
  function updateSelectionHudIcons(hudSelectBtn, { allChecked, someChecked }) {
    const iconAll = hudSelectBtn.querySelector(".jan-batch-hud-icon--all");
    const iconPartial = hudSelectBtn.querySelector(".jan-batch-hud-icon--partial");
    const iconNone = hudSelectBtn.querySelector(".jan-batch-hud-icon--none");
    if (!iconAll || !iconPartial || !iconNone) return;
    iconAll.hidden = true;
    iconPartial.hidden = true;
    iconNone.hidden = true;
    if (allChecked) {
      iconNone.hidden = false;
      hudSelectBtn.title = "Unselect All";
      return;
    }
    if (someChecked) {
      iconPartial.hidden = false;
      hudSelectBtn.title = "Select All";
      return;
    }
    iconAll.hidden = false;
    hudSelectBtn.title = "Select All";
  }
  function updateBulkActions({ selectedCheckboxes, bulkFetchBtn, bulkDownloadBtn, bulkRemoveBtn }) {
    let fetchable = 0;
    let downloadable = 0;
    selectedCheckboxes.forEach((checkbox) => {
      const status = checkbox.getAttribute("data-status");
      if (status === "pending" || status === "error" || status === "ready") fetchable += 1;
      if (status === "ready") downloadable += 1;
    });
    bulkFetchBtn.disabled = fetchable === 0;
    bulkFetchBtn.title = fetchable > 0 ? `Fetch ${fetchable} card(s)` : "Select items to fetch";
    bulkDownloadBtn.disabled = downloadable === 0;
    bulkDownloadBtn.title = downloadable > 0 ? `Download ${downloadable} ready card(s)` : "Select ready items to download";
    bulkRemoveBtn.disabled = selectedCheckboxes.length === 0;
    bulkRemoveBtn.title = selectedCheckboxes.length > 0 ? `Remove ${selectedCheckboxes.length} card(s)` : "Select items to remove";
  }
  function createBatchFetchController({
    queue,
    documentRef,
    mountEl,
    scrapeCharacterCard,
    getSelectedExportFormat,
    getSelectedDownloadMode,
    triggerArtifactDownload: triggerArtifactDownload2,
    downloadBulkAsZip: downloadBulkAsZip2,
    isAutoRetryEnabled = () => false,
    getAutoRetryCount = () => null
  }) {
    let isFetchingBulk = false;
    let isDestroyed = false;
    const autoRetryTimers = /* @__PURE__ */ new Map();
    const downloadedBatchIds = /* @__PURE__ */ new Set();
    function cancelAutoRetryTimer(queueId) {
      const timerId = autoRetryTimers.get(queueId);
      if (timerId == null) return;
      clearTimeout(timerId);
      autoRetryTimers.delete(queueId);
      const entry = queue.getItem(queueId);
      if (entry?.retrying) {
        queue.updateItem(queueId, { retrying: false });
      }
    }
    function scheduleAutoRetry(queueId, delayMs) {
      if (isDestroyed) return;
      queue.updateItem(queueId, { retrying: true });
      const timerId = setTimeout(async () => {
        autoRetryTimers.delete(queueId);
        if (isDestroyed) return;
        const entry = queue.getItem(queueId);
        if (!entry || entry.status !== "error") return;
        queue.updateItem(queueId, { retryCount: entry.retryCount + 1, retrying: false });
        await fetchSingleItem(queueId, { isAutoRetry: true });
      }, delayMs);
      autoRetryTimers.set(queueId, timerId);
    }
    function resolveAutoRetryLimit() {
      if (!isAutoRetryEnabled?.()) return null;
      const raw = typeof getAutoRetryCount === "function" ? getAutoRetryCount() : null;
      const parsed = Number.parseInt(raw, 10);
      if (!Number.isFinite(parsed) || parsed < 1) return null;
      return Math.min(parsed, 20);
    }
    function checkBatchAndTriggerZip(batchId) {
      if (isDestroyed) return;
      if (!batchId || downloadedBatchIds.has(batchId)) return;
      const batchItems = queue.getItems().filter((item) => item.batchId === batchId && item.autoExport);
      const allSettled = batchItems.every((item) => {
        if (item.status === "ready") return true;
        if (item.status === "error" && !item.retrying) return true;
        return false;
      });
      if (!allSettled) return;
      const readyItems = batchItems.filter((item) => item.status === "ready");
      if (readyItems.length === 0) return;
      downloadedBatchIds.add(batchId);
      void downloadBulkAsZip2({
        entries: readyItems,
        exportFormat: getSelectedExportFormat(),
        documentRef,
        mountEl
      }).catch(() => {
      });
    }
    async function fetchSingleItem(queueId, { isAutoRetry = false } = {}) {
      if (isDestroyed) return;
      cancelAutoRetryTimer(queueId);
      const entry = queue.getItem(queueId);
      if (!entry || entry.status === "fetching") return;
      if (typeof scrapeCharacterCard !== "function") {
        queue.updateItem(queueId, { status: "error", error: "Scrape not available", progress: 0 });
        return;
      }
      const exportFormat = getSelectedExportFormat();
      const currentRetryCount = Number.isFinite(entry.retryCount) ? entry.retryCount : 0;
      const nextRetryCount = !isAutoRetry && currentRetryCount > 0 ? 0 : currentRetryCount;
      queue.updateItem(queueId, {
        status: "fetching",
        progress: 0,
        error: null,
        retryCount: nextRetryCount,
        retrying: false
      });
      try {
        const result = await scrapeCharacterCard({
          characterId: entry.characterId,
          exportFormat,
          onProgress: (p) => {
            if (isDestroyed) return;
            queue.updateItem(queueId, { progress: Math.round(p * 100) });
          }
        });
        if (isDestroyed) return;
        if (!result?.downloadArtifact) {
          throw new Error("Pipeline did not return a download artifact.");
        }
        const normalizedCharacter = result.normalizedCharacter ?? null;
        const resolvedTitle = normalizedCharacter?.name ?? null;
        const resolvedCharacterUrl = entry.characterUrl ?? normalizedCharacter?.characterUrl ?? null;
        queue.updateItem(queueId, {
          status: "ready",
          progress: 100,
          downloadArtifact: result.downloadArtifact,
          normalizedCharacter,
          title: resolvedTitle || entry.title,
          characterUrl: resolvedCharacterUrl,
          artifactFormat: exportFormat,
          fetchedAt: Date.now()
        });
        const updatedEntry = queue.getItem(queueId);
        if (updatedEntry?.autoExport) {
          const mode = getSelectedDownloadMode();
          if (mode === "files") {
            await triggerArtifactDownload2({
              downloadArtifact: result.downloadArtifact,
              documentRef,
              mountEl,
              normalizedCharacter,
              exportFormat,
              exportOptions: readExportSettings(documentRef)
            });
          } else if (mode === "zip") {
            checkBatchAndTriggerZip(updatedEntry.batchId);
          }
        }
      } catch (error) {
        if (isDestroyed) return;
        const message = error?.data?.message ?? error?.message ?? "Unknown error";
        const isProxyOff = error?.code === "PROXY_OFF" || message === "PROXY OFF";
        queue.updateItem(queueId, {
          status: "error",
          progress: 0,
          error: isProxyOff ? "PROXY OFF" : message.length > 100 ? message.slice(0, 100) + "..." : message
        });
        if (!isProxyOff) {
          const failedEntry = queue.getItem(queueId);
          const retryLimit = resolveAutoRetryLimit();
          const shouldAutoRetry = failedEntry && retryLimit != null && failedEntry.retryCount < retryLimit;
          if (shouldAutoRetry) {
            scheduleAutoRetry(queueId, 3e3);
          } else if (failedEntry?.autoExport && getSelectedDownloadMode() === "zip") {
            checkBatchAndTriggerZip(failedEntry.batchId);
          }
        }
      }
    }
    async function fetchBulk(queueIds) {
      if (isDestroyed) return;
      if (isFetchingBulk) return;
      isFetchingBulk = true;
      for (const id of queueIds) {
        const entry = queue.getItem(id);
        if (!entry) continue;
        if (entry.status === "fetching") continue;
        await fetchSingleItem(id);
      }
      isFetchingBulk = false;
    }
    async function tryAutoFetch() {
      if (isDestroyed) return;
      if (isFetchingBulk) return;
      const pending = queue.getItems().filter((e) => e.status === "pending" && e.autoFetch === true);
      if (pending.length === 0) return;
      await fetchBulk(pending.map((e) => e.id));
    }
    function destroy() {
      if (isDestroyed) return;
      isDestroyed = true;
      for (const timerId of autoRetryTimers.values()) {
        clearTimeout(timerId);
      }
      autoRetryTimers.clear();
      downloadedBatchIds.clear();
    }
    return {
      fetchSingleItem,
      fetchBulk,
      tryAutoFetch,
      cancelAutoRetryTimer,
      isFetchingBulk: () => isFetchingBulk,
      destroy
    };
  }
  var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
  var fleb = new u8([
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    0,
    1,
    1,
    1,
    1,
    2,
    2,
    2,
    2,
    3,
    3,
    3,
    3,
    4,
    4,
    4,
    4,
    5,
    5,
    5,
    5,
    0,
    /* unused */
    0,
    0,
    /* impossible */
    0
  ]);
  var fdeb = new u8([
    0,
    0,
    0,
    0,
    1,
    1,
    2,
    2,
    3,
    3,
    4,
    4,
    5,
    5,
    6,
    6,
    7,
    7,
    8,
    8,
    9,
    9,
    10,
    10,
    11,
    11,
    12,
    12,
    13,
    13,
    /* unused */
    0,
    0
  ]);
  var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
  var freb = function(eb, start) {
    var b = new u16(31);
    for (var i2 = 0; i2 < 31; ++i2) {
      b[i2] = start += 1 << eb[i2 - 1];
    }
    var r = new i32(b[30]);
    for (var i2 = 1; i2 < 30; ++i2) {
      for (var j = b[i2]; j < b[i2 + 1]; ++j) {
        r[j] = j - b[i2] << 5 | i2;
      }
    }
    return { b, r };
  };
  var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
  fl[28] = 258, revfl[258] = 28;
  var _b = freb(fdeb, 0), revfd = _b.r;
  var rev = new u16(32768);
  for (var i = 0; i < 32768; ++i) {
    var x = (i & 43690) >> 1 | (i & 21845) << 1;
    x = (x & 52428) >> 2 | (x & 13107) << 2;
    x = (x & 61680) >> 4 | (x & 3855) << 4;
    rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
  }
  var hMap = (function(cd, mb, r) {
    var s = cd.length;
    var i2 = 0;
    var l = new u16(mb);
    for (; i2 < s; ++i2) {
      if (cd[i2])
        ++l[cd[i2] - 1];
    }
    var le = new u16(mb);
    for (i2 = 1; i2 < mb; ++i2) {
      le[i2] = le[i2 - 1] + l[i2 - 1] << 1;
    }
    var co;
    if (r) {
      co = new u16(1 << mb);
      var rvb = 15 - mb;
      for (i2 = 0; i2 < s; ++i2) {
        if (cd[i2]) {
          var sv = i2 << 4 | cd[i2];
          var r_1 = mb - cd[i2];
          var v = le[cd[i2] - 1]++ << r_1;
          for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
            co[rev[v] >> rvb] = sv;
          }
        }
      }
    } else {
      co = new u16(s);
      for (i2 = 0; i2 < s; ++i2) {
        if (cd[i2]) {
          co[i2] = rev[le[cd[i2] - 1]++] >> 15 - cd[i2];
        }
      }
    }
    return co;
  });
  var flt = new u8(288);
  for (var i = 0; i < 144; ++i)
    flt[i] = 8;
  for (var i = 144; i < 256; ++i)
    flt[i] = 9;
  for (var i = 256; i < 280; ++i)
    flt[i] = 7;
  for (var i = 280; i < 288; ++i)
    flt[i] = 8;
  var fdt = new u8(32);
  for (var i = 0; i < 32; ++i)
    fdt[i] = 5;
  var flm = /* @__PURE__ */ hMap(flt, 9, 0);
  var fdm = /* @__PURE__ */ hMap(fdt, 5, 0);
  var shft = function(p) {
    return (p + 7) / 8 | 0;
  };
  var slc = function(v, s, e) {
    if (e == null || e > v.length)
      e = v.length;
    return new u8(v.subarray(s, e));
  };
  var ec = [
    "unexpected EOF",
    "invalid block type",
    "invalid length/literal",
    "invalid distance",
    "stream finished",
    "no stream handler",
    ,
    "no callback",
    "invalid UTF-8 data",
    "extra field too long",
    "date not in range 1980-2099",
    "filename too long",
    "stream finishing",
    "invalid zip data"
    // determined by unknown compression method
  ];
  var err = function(ind, msg, nt) {
    var e = new Error(msg || ec[ind]);
    e.code = ind;
    if (Error.captureStackTrace)
      Error.captureStackTrace(e, err);
    if (!nt)
      throw e;
    return e;
  };
  var wbits = function(d, p, v) {
    v <<= p & 7;
    var o = p / 8 | 0;
    d[o] |= v;
    d[o + 1] |= v >> 8;
  };
  var wbits16 = function(d, p, v) {
    v <<= p & 7;
    var o = p / 8 | 0;
    d[o] |= v;
    d[o + 1] |= v >> 8;
    d[o + 2] |= v >> 16;
  };
  var hTree = function(d, mb) {
    var t = [];
    for (var i2 = 0; i2 < d.length; ++i2) {
      if (d[i2])
        t.push({ s: i2, f: d[i2] });
    }
    var s = t.length;
    var t2 = t.slice();
    if (!s)
      return { t: et, l: 0 };
    if (s == 1) {
      var v = new u8(t[0].s + 1);
      v[t[0].s] = 1;
      return { t: v, l: 1 };
    }
    t.sort(function(a, b) {
      return a.f - b.f;
    });
    t.push({ s: -1, f: 25001 });
    var l = t[0], r = t[1], i0 = 0, i1 = 1, i22 = 2;
    t[0] = { s: -1, f: l.f + r.f, l, r };
    while (i1 != s - 1) {
      l = t[t[i0].f < t[i22].f ? i0++ : i22++];
      r = t[i0 != i1 && t[i0].f < t[i22].f ? i0++ : i22++];
      t[i1++] = { s: -1, f: l.f + r.f, l, r };
    }
    var maxSym = t2[0].s;
    for (var i2 = 1; i2 < s; ++i2) {
      if (t2[i2].s > maxSym)
        maxSym = t2[i2].s;
    }
    var tr = new u16(maxSym + 1);
    var mbt = ln(t[i1 - 1], tr, 0);
    if (mbt > mb) {
      var i2 = 0, dt = 0;
      var lft = mbt - mb, cst = 1 << lft;
      t2.sort(function(a, b) {
        return tr[b.s] - tr[a.s] || a.f - b.f;
      });
      for (; i2 < s; ++i2) {
        var i2_1 = t2[i2].s;
        if (tr[i2_1] > mb) {
          dt += cst - (1 << mbt - tr[i2_1]);
          tr[i2_1] = mb;
        } else
          break;
      }
      dt >>= lft;
      while (dt > 0) {
        var i2_2 = t2[i2].s;
        if (tr[i2_2] < mb)
          dt -= 1 << mb - tr[i2_2]++ - 1;
        else
          ++i2;
      }
      for (; i2 >= 0 && dt; --i2) {
        var i2_3 = t2[i2].s;
        if (tr[i2_3] == mb) {
          --tr[i2_3];
          ++dt;
        }
      }
      mbt = mb;
    }
    return { t: new u8(tr), l: mbt };
  };
  var ln = function(n, l, d) {
    return n.s == -1 ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) : l[n.s] = d;
  };
  var lc = function(c) {
    var s = c.length;
    while (s && !c[--s])
      ;
    var cl = new u16(++s);
    var cli = 0, cln = c[0], cls = 1;
    var w = function(v) {
      cl[cli++] = v;
    };
    for (var i2 = 1; i2 <= s; ++i2) {
      if (c[i2] == cln && i2 != s)
        ++cls;
      else {
        if (!cln && cls > 2) {
          for (; cls > 138; cls -= 138)
            w(32754);
          if (cls > 2) {
            w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305);
            cls = 0;
          }
        } else if (cls > 3) {
          w(cln), --cls;
          for (; cls > 6; cls -= 6)
            w(8304);
          if (cls > 2)
            w(cls - 3 << 5 | 8208), cls = 0;
        }
        while (cls--)
          w(cln);
        cls = 1;
        cln = c[i2];
      }
    }
    return { c: cl.subarray(0, cli), n: s };
  };
  var clen = function(cf, cl) {
    var l = 0;
    for (var i2 = 0; i2 < cl.length; ++i2)
      l += cf[i2] * cl[i2];
    return l;
  };
  var wfblk = function(out, pos, dat) {
    var s = dat.length;
    var o = shft(pos + 2);
    out[o] = s & 255;
    out[o + 1] = s >> 8;
    out[o + 2] = out[o] ^ 255;
    out[o + 3] = out[o + 1] ^ 255;
    for (var i2 = 0; i2 < s; ++i2)
      out[o + i2 + 4] = dat[i2];
    return (o + 4 + s) * 8;
  };
  var wblk = function(dat, out, final, syms, lf, df, eb, li, bs, bl, p) {
    wbits(out, p++, final);
    ++lf[256];
    var _a2 = hTree(lf, 15), dlt = _a2.t, mlb = _a2.l;
    var _b2 = hTree(df, 15), ddt = _b2.t, mdb = _b2.l;
    var _c = lc(dlt), lclt = _c.c, nlc = _c.n;
    var _d = lc(ddt), lcdt = _d.c, ndc = _d.n;
    var lcfreq = new u16(19);
    for (var i2 = 0; i2 < lclt.length; ++i2)
      ++lcfreq[lclt[i2] & 31];
    for (var i2 = 0; i2 < lcdt.length; ++i2)
      ++lcfreq[lcdt[i2] & 31];
    var _e = hTree(lcfreq, 7), lct = _e.t, mlcb = _e.l;
    var nlcc = 19;
    for (; nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc)
      ;
    var flen = bl + 5 << 3;
    var ftlen = clen(lf, flt) + clen(df, fdt) + eb;
    var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18];
    if (bs >= 0 && flen <= ftlen && flen <= dtlen)
      return wfblk(out, p, dat.subarray(bs, bs + bl));
    var lm, ll, dm, dl;
    wbits(out, p, 1 + (dtlen < ftlen)), p += 2;
    if (dtlen < ftlen) {
      lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt;
      var llm = hMap(lct, mlcb, 0);
      wbits(out, p, nlc - 257);
      wbits(out, p + 5, ndc - 1);
      wbits(out, p + 10, nlcc - 4);
      p += 14;
      for (var i2 = 0; i2 < nlcc; ++i2)
        wbits(out, p + 3 * i2, lct[clim[i2]]);
      p += 3 * nlcc;
      var lcts = [lclt, lcdt];
      for (var it = 0; it < 2; ++it) {
        var clct = lcts[it];
        for (var i2 = 0; i2 < clct.length; ++i2) {
          var len = clct[i2] & 31;
          wbits(out, p, llm[len]), p += lct[len];
          if (len > 15)
            wbits(out, p, clct[i2] >> 5 & 127), p += clct[i2] >> 12;
        }
      }
    } else {
      lm = flm, ll = flt, dm = fdm, dl = fdt;
    }
    for (var i2 = 0; i2 < li; ++i2) {
      var sym = syms[i2];
      if (sym > 255) {
        var len = sym >> 18 & 31;
        wbits16(out, p, lm[len + 257]), p += ll[len + 257];
        if (len > 7)
          wbits(out, p, sym >> 23 & 31), p += fleb[len];
        var dst = sym & 31;
        wbits16(out, p, dm[dst]), p += dl[dst];
        if (dst > 3)
          wbits16(out, p, sym >> 5 & 8191), p += fdeb[dst];
      } else {
        wbits16(out, p, lm[sym]), p += ll[sym];
      }
    }
    wbits16(out, p, lm[256]);
    return p + ll[256];
  };
  var deo = /* @__PURE__ */ new i32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]);
  var et = /* @__PURE__ */ new u8(0);
  var dflt = function(dat, lvl, plvl, pre, post, st) {
    var s = st.z || dat.length;
    var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7e3)) + post);
    var w = o.subarray(pre, o.length - post);
    var lst = st.l;
    var pos = (st.r || 0) & 7;
    if (lvl) {
      if (pos)
        w[0] = st.r >> 3;
      var opt = deo[lvl - 1];
      var n = opt >> 13, c = opt & 8191;
      var msk_1 = (1 << plvl) - 1;
      var prev = st.p || new u16(32768), head = st.h || new u16(msk_1 + 1);
      var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1;
      var hsh = function(i3) {
        return (dat[i3] ^ dat[i3 + 1] << bs1_1 ^ dat[i3 + 2] << bs2_1) & msk_1;
      };
      var syms = new i32(25e3);
      var lf = new u16(288), df = new u16(32);
      var lc_1 = 0, eb = 0, i2 = st.i || 0, li = 0, wi = st.w || 0, bs = 0;
      for (; i2 + 2 < s; ++i2) {
        var hv = hsh(i2);
        var imod = i2 & 32767, pimod = head[hv];
        prev[imod] = pimod;
        head[hv] = imod;
        if (wi <= i2) {
          var rem = s - i2;
          if ((lc_1 > 7e3 || li > 24576) && (rem > 423 || !lst)) {
            pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i2 - bs, pos);
            li = lc_1 = eb = 0, bs = i2;
            for (var j = 0; j < 286; ++j)
              lf[j] = 0;
            for (var j = 0; j < 30; ++j)
              df[j] = 0;
          }
          var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767;
          if (rem > 2 && hv == hsh(i2 - dif)) {
            var maxn = Math.min(n, rem) - 1;
            var maxd = Math.min(32767, i2);
            var ml = Math.min(258, rem);
            while (dif <= maxd && --ch_1 && imod != pimod) {
              if (dat[i2 + l] == dat[i2 + l - dif]) {
                var nl = 0;
                for (; nl < ml && dat[i2 + nl] == dat[i2 + nl - dif]; ++nl)
                  ;
                if (nl > l) {
                  l = nl, d = dif;
                  if (nl > maxn)
                    break;
                  var mmd = Math.min(dif, nl - 2);
                  var md = 0;
                  for (var j = 0; j < mmd; ++j) {
                    var ti = i2 - dif + j & 32767;
                    var pti = prev[ti];
                    var cd = ti - pti & 32767;
                    if (cd > md)
                      md = cd, pimod = ti;
                  }
                }
              }
              imod = pimod, pimod = prev[imod];
              dif += imod - pimod & 32767;
            }
          }
          if (d) {
            syms[li++] = 268435456 | revfl[l] << 18 | revfd[d];
            var lin = revfl[l] & 31, din = revfd[d] & 31;
            eb += fleb[lin] + fdeb[din];
            ++lf[257 + lin];
            ++df[din];
            wi = i2 + l;
            ++lc_1;
          } else {
            syms[li++] = dat[i2];
            ++lf[dat[i2]];
          }
        }
      }
      for (i2 = Math.max(i2, wi); i2 < s; ++i2) {
        syms[li++] = dat[i2];
        ++lf[dat[i2]];
      }
      pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i2 - bs, pos);
      if (!lst) {
        st.r = pos & 7 | w[pos / 8 | 0] << 3;
        pos -= 7;
        st.h = head, st.p = prev, st.i = i2, st.w = wi;
      }
    } else {
      for (var i2 = st.w || 0; i2 < s + lst; i2 += 65535) {
        var e = i2 + 65535;
        if (e >= s) {
          w[pos / 8 | 0] = lst;
          e = s;
        }
        pos = wfblk(w, pos + 1, dat.subarray(i2, e));
      }
      st.i = s;
    }
    return slc(o, 0, pre + shft(pos) + post);
  };
  var crct = /* @__PURE__ */ (function() {
    var t = new Int32Array(256);
    for (var i2 = 0; i2 < 256; ++i2) {
      var c = i2, k = 9;
      while (--k)
        c = (c & 1 && -306674912) ^ c >>> 1;
      t[i2] = c;
    }
    return t;
  })();
  var crc = function() {
    var c = -1;
    return {
      p: function(d) {
        var cr = c;
        for (var i2 = 0; i2 < d.length; ++i2)
          cr = crct[cr & 255 ^ d[i2]] ^ cr >>> 8;
        c = cr;
      },
      d: function() {
        return ~c;
      }
    };
  };
  var dopt = function(dat, opt, pre, post, st) {
    if (!st) {
      st = { l: 1 };
      if (opt.dictionary) {
        var dict = opt.dictionary.subarray(-32768);
        var newDat = new u8(dict.length + dat.length);
        newDat.set(dict);
        newDat.set(dat, dict.length);
        dat = newDat;
        st.w = dict.length;
      }
    }
    return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? st.l ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 20 : 12 + opt.mem, pre, post, st);
  };
  var mrg = function(a, b) {
    var o = {};
    for (var k in a)
      o[k] = a[k];
    for (var k in b)
      o[k] = b[k];
    return o;
  };
  var wbytes = function(d, b, v) {
    for (; v; ++b)
      d[b] = v, v >>>= 8;
  };
  function deflateSync(data, opts) {
    return dopt(data, opts || {}, 0, 0);
  }
  var fltn = function(d, p, t, o) {
    for (var k in d) {
      var val = d[k], n = p + k, op = o;
      if (Array.isArray(val))
        op = mrg(o, val[1]), val = val[0];
      if (val instanceof u8)
        t[n] = [val, op];
      else {
        t[n += "/"] = [new u8(0), op];
        fltn(val, n, t, o);
      }
    }
  };
  var te = typeof TextEncoder != "undefined" && /* @__PURE__ */ new TextEncoder();
  var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
  var tds = 0;
  try {
    td.decode(et, { stream: true });
    tds = 1;
  } catch (e) {
  }
  function strToU8(str, latin1) {
    var i2;
    if (te)
      return te.encode(str);
    var l = str.length;
    var ar = new u8(str.length + (str.length >> 1));
    var ai = 0;
    var w = function(v) {
      ar[ai++] = v;
    };
    for (var i2 = 0; i2 < l; ++i2) {
      if (ai + 5 > ar.length) {
        var n = new u8(ai + 8 + (l - i2 << 1));
        n.set(ar);
        ar = n;
      }
      var c = str.charCodeAt(i2);
      if (c < 128 || latin1)
        w(c);
      else if (c < 2048)
        w(192 | c >> 6), w(128 | c & 63);
      else if (c > 55295 && c < 57344)
        c = 65536 + (c & 1023 << 10) | str.charCodeAt(++i2) & 1023, w(240 | c >> 18), w(128 | c >> 12 & 63), w(128 | c >> 6 & 63), w(128 | c & 63);
      else
        w(224 | c >> 12), w(128 | c >> 6 & 63), w(128 | c & 63);
    }
    return slc(ar, 0, ai);
  }
  var exfl = function(ex) {
    var le = 0;
    if (ex) {
      for (var k in ex) {
        var l = ex[k].length;
        if (l > 65535)
          err(9);
        le += l + 4;
      }
    }
    return le;
  };
  var wzh = function(d, b, f, fn, u, c, ce, co) {
    var fl2 = fn.length, ex = f.extra, col = co && co.length;
    var exl = exfl(ex);
    wbytes(d, b, ce != null ? 33639248 : 67324752), b += 4;
    if (ce != null)
      d[b++] = 20, d[b++] = f.os;
    d[b] = 20, b += 2;
    d[b++] = f.flag << 1 | (c < 0 && 8), d[b++] = u && 8;
    d[b++] = f.compression & 255, d[b++] = f.compression >> 8;
    var dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980;
    if (y < 0 || y > 119)
      err(10);
    wbytes(d, b, y << 25 | dt.getMonth() + 1 << 21 | dt.getDate() << 16 | dt.getHours() << 11 | dt.getMinutes() << 5 | dt.getSeconds() >> 1), b += 4;
    if (c != -1) {
      wbytes(d, b, f.crc);
      wbytes(d, b + 4, c < 0 ? -c - 2 : c);
      wbytes(d, b + 8, f.size);
    }
    wbytes(d, b + 12, fl2);
    wbytes(d, b + 14, exl), b += 16;
    if (ce != null) {
      wbytes(d, b, col);
      wbytes(d, b + 6, f.attrs);
      wbytes(d, b + 10, ce), b += 14;
    }
    d.set(fn, b);
    b += fl2;
    if (exl) {
      for (var k in ex) {
        var exf = ex[k], l = exf.length;
        wbytes(d, b, +k);
        wbytes(d, b + 2, l);
        d.set(exf, b + 4), b += 4 + l;
      }
    }
    if (col)
      d.set(co, b), b += col;
    return b;
  };
  var wzf = function(o, b, c, d, e) {
    wbytes(o, b, 101010256);
    wbytes(o, b + 8, c);
    wbytes(o, b + 10, c);
    wbytes(o, b + 12, d);
    wbytes(o, b + 16, e);
  };
  function zipSync(data, opts) {
    if (!opts)
      opts = {};
    var r = {};
    var files = [];
    fltn(data, "", r, opts);
    var o = 0;
    var tot = 0;
    for (var fn in r) {
      var _a2 = r[fn], file = _a2[0], p = _a2[1];
      var compression = p.level == 0 ? 0 : 8;
      var f = strToU8(fn), s = f.length;
      var com = p.comment, m = com && strToU8(com), ms = m && m.length;
      var exl = exfl(p.extra);
      if (s > 65535)
        err(11);
      var d = compression ? deflateSync(file, p) : file, l = d.length;
      var c = crc();
      c.p(file);
      files.push(mrg(p, {
        size: file.length,
        crc: c.d(),
        c: d,
        f,
        m,
        u: s != fn.length || m && com.length != ms,
        o,
        compression
      }));
      o += 30 + s + exl + l;
      tot += 76 + 2 * (s + exl) + (ms || 0) + l;
    }
    var out = new u8(tot + 22), oe = o, cdl = tot - o;
    for (var i2 = 0; i2 < files.length; ++i2) {
      var f = files[i2];
      wzh(out, f.o, f, f.f, f.u, f.c.length);
      var badd = 30 + f.f.length + exfl(f.extra);
      out.set(f.c, f.o + badd);
      wzh(out, o, f, f.f, f.u, f.c.length, f.o, f.m), o += 16 + badd + (f.m ? f.m.length : 0);
    }
    wzf(out, o, files.length, cdl, oe);
    return out;
  }
  async function buildDownloadBlob(downloadArtifact, normalizedCharacter, exportFormat, artifactFormat, exportOptions = null) {
    if (normalizedCharacter && exportFormat) {
      if (artifactFormat === exportFormat && downloadArtifact?.content) {
        return buildDownloadBlob$1(downloadArtifact);
      }
      const built = await buildExportContent(normalizedCharacter, {
        format: exportFormat,
        ...exportOptions ?? {}
      });
      return new Blob([built.content], { type: built.mimeType });
    }
    return buildDownloadBlob$1(downloadArtifact);
  }
  async function triggerArtifactDownload({
    downloadArtifact,
    documentRef,
    mountEl,
    normalizedCharacter,
    exportFormat,
    exportOptions
  }) {
    let blob, filename;
    if (normalizedCharacter && exportFormat) {
      blob = await buildDownloadBlob(
        null,
        normalizedCharacter,
        exportFormat,
        null,
        exportOptions
      );
      const baseName = downloadArtifact?.filename?.replace(/\.[^/.]+$/, "") ?? "character";
      const ext = exportFormat === "png" ? ".png" : exportFormat === "json" ? ".json" : ".txt";
      filename = `${sanitizeFilename(baseName, "character")}${ext}`;
    } else {
      if (!downloadArtifact?.filename) {
        throw new Error("Download artifact is missing a filename.");
      }
      blob = await buildDownloadBlob(downloadArtifact);
      filename = sanitizeFilename(downloadArtifact.filename, "character");
    }
    downloadBlob({ documentRef, mountEl, blob, filename });
  }
  function getEntryFilename(entry, exportFormat) {
    const artifactName = typeof entry?.downloadArtifact?.filename === "string" ? entry.downloadArtifact.filename.trim() : "";
    if (entry?.normalizedCharacter && exportFormat) {
      const baseName = artifactName ? artifactName.replace(/\.[^/.]+$/, "") : entry.title || entry.characterId || "character";
      const ext2 = exportFormat === "png" ? ".png" : exportFormat === "json" ? ".json" : ".txt";
      return `${sanitizeFilename(baseName, "character")}${ext2}`;
    }
    if (artifactName) return sanitizeFilename(artifactName, "character");
    const fallbackBase = sanitizeFilename(entry?.title || entry?.characterId, "character");
    const ext = exportFormat === "png" ? ".png" : exportFormat === "json" ? ".json" : ".txt";
    return `${fallbackBase}${ext}`;
  }
  function dedupeFilename(name, usedNames) {
    if (!usedNames.has(name)) {
      usedNames.add(name);
      return name;
    }
    const match = name.match(/^(.*?)(\.[^.]*)?$/);
    const base = match?.[1] ?? name;
    const ext = match?.[2] ?? "";
    let i2 = 2;
    let candidate = `${base} (${i2})${ext}`;
    while (usedNames.has(candidate)) {
      i2 += 1;
      candidate = `${base} (${i2})${ext}`;
    }
    usedNames.add(candidate);
    return candidate;
  }
  function getZipCompressionLevel(documentRef, exportFormat) {
    if (exportFormat === "png") return 0;
    const levelSetting = readBatchZipSetting(documentRef);
    const levels = {
      store: 0,
      fast: 1,
      standard: 6,
      max: 9
    };
    return levels[levelSetting] ?? 0;
  }
  async function downloadBulkAsZip({
    entries,
    exportFormat,
    documentRef,
    mountEl,
    exportOptions = null
  }) {
    if (entries.length === 0) return;
    const resolvedExportOptions = exportOptions ?? readExportSettings(documentRef);
    const usedNames = /* @__PURE__ */ new Set();
    const zipLevel = getZipCompressionLevel(documentRef, exportFormat);
    const zipEntries = {};
    for (const entry of entries) {
      const blob = await buildDownloadBlob(
        entry.downloadArtifact,
        entry.normalizedCharacter,
        exportFormat,
        entry.artifactFormat,
        resolvedExportOptions
      );
      const filename = dedupeFilename(getEntryFilename(entry, exportFormat), usedNames);
      const buffer = await blob.arrayBuffer();
      zipEntries[filename] = [new Uint8Array(buffer), { level: zipLevel }];
    }
    const zipBuffer = zipSync(zipEntries);
    const zipBlob = new Blob([zipBuffer], { type: "application/zip" });
    downloadBlob({
      documentRef,
      mountEl,
      blob: zipBlob,
      filename: "jan-batch-export.zip"
    });
  }
  async function downloadBulkEntries({
    entries,
    exportMode,
    exportFormat,
    documentRef,
    mountEl,
    exportOptions = null
  }) {
    if (entries.length === 0) return;
    if (exportMode === "zip") {
      await downloadBulkAsZip({
        entries,
        exportFormat,
        documentRef,
        mountEl,
        exportOptions
      });
      return;
    }
    const resolvedExportOptions = exportOptions ?? readExportSettings(documentRef);
    for (const entry of entries) {
      await triggerArtifactDownload({
        downloadArtifact: entry.downloadArtifact,
        documentRef,
        mountEl,
        normalizedCharacter: entry.normalizedCharacter,
        exportFormat,
        exportOptions: resolvedExportOptions
      });
    }
  }
  function getCharacterNameFromWrapper(wrapper) {
    const nameEl = wrapper.querySelector(
      '[class*="charName"], [class*="name"], a[href*="/characters/"]'
    );
    return nameEl?.textContent?.trim() || null;
  }
  function setupBatchSelection({
    documentRef,
    groupEl,
    backdropEl,
    hud,
    selectCharsBtn,
    hudSelectBtn,
    hudConfirmBtn,
    hudCloseBtn,
    queue,
    isAutoFetchEnabled,
    isAutoExportEnabled,
    tryAutoFetch
  }) {
    const selectedCards = /* @__PURE__ */ new Map();
    let selectionObserver = null;
    let selectionSyncRaf = null;
    const wrapperPositionCache = /* @__PURE__ */ new WeakMap();
    const controller = new AbortController();
    const { signal } = controller;
    function getSelectionStorage() {
      return getSessionStorage(documentRef);
    }
    function loadStoredSelections() {
      selectedCards.clear();
      const storage = getSelectionStorage();
      if (!storage) return;
      const parsed = readJson(storage, SELECTION_STORAGE_KEY, null);
      if (!Array.isArray(parsed)) {
        if (parsed != null) removeStorageItem(storage, SELECTION_STORAGE_KEY);
        return;
      }
      parsed.forEach((item) => {
        if (!item?.characterId) return;
        selectedCards.set(item.characterId, {
          characterId: item.characterId,
          title: item.title || item.characterId,
          characterUrl: item.characterUrl ?? null
        });
      });
    }
    function persistSelections() {
      const storage = getSelectionStorage();
      if (!storage) return;
      if (selectedCards.size === 0) {
        removeStorageItem(storage, SELECTION_STORAGE_KEY);
        return;
      }
      writeJson(storage, SELECTION_STORAGE_KEY, Array.from(selectedCards.values()));
    }
    function clearStoredSelections() {
      selectedCards.clear();
      const storage = getSelectionStorage();
      if (!storage) return;
      removeStorageItem(storage, SELECTION_STORAGE_KEY);
    }
    function getEntryFromWrapper(wrapper) {
      const charInput = getCharacterInputFromWrapper(wrapper, documentRef);
      if (!charInput) return null;
      const characterId = extractCharacterIdFromInput(charInput);
      if (!characterId) return null;
      return {
        wrapper,
        characterId,
        characterUrl: charInput,
        title: getCharacterNameFromWrapper(wrapper) || characterId
      };
    }
    function getVisibleWrapperEntries() {
      return Array.from(documentRef.querySelectorAll(PAGE_CARD_WRAPPER_SELECTOR)).map((wrapper) => getEntryFromWrapper(wrapper)).filter(Boolean);
    }
    function getSelectionObserverTargets() {
      const wrappers = Array.from(documentRef.querySelectorAll(PAGE_CARD_WRAPPER_SELECTOR));
      const fallback = documentRef.body ?? documentRef.documentElement;
      if (wrappers.length === 0) {
        return fallback ? [fallback] : [];
      }
      const targets = /* @__PURE__ */ new Set();
      wrappers.forEach((wrapper) => {
        const target = wrapper.parentElement ?? fallback;
        if (target) targets.add(target);
      });
      return Array.from(targets);
    }
    function ensureSelectionOverlay(wrapper) {
      if (wrapper.querySelector(`.${SELECTION_OVERLAY_CLASS}`)) return;
      const win = documentRef.defaultView ?? globalThis;
      const pos = win.getComputedStyle(wrapper).position;
      if (!pos || pos === "static") {
        if (!wrapperPositionCache.has(wrapper)) {
          wrapperPositionCache.set(wrapper, wrapper.style.position);
        }
        wrapper.style.position = "relative";
      }
      const overlay = documentRef.createElement("div");
      overlay.className = SELECTION_OVERLAY_CLASS;
      wrapper.appendChild(overlay);
    }
    function removeSelectionOverlay(wrapper) {
      wrapper.querySelector(`.${SELECTION_OVERLAY_CLASS}`)?.remove();
      if (wrapperPositionCache.has(wrapper)) {
        wrapper.style.position = wrapperPositionCache.get(wrapper) ?? "";
        wrapperPositionCache.delete(wrapper);
      }
    }
    function syncSelectionOverlays() {
      const entries = getVisibleWrapperEntries();
      entries.forEach(({ wrapper, characterId }) => {
        if (selectedCards.has(characterId)) {
          ensureSelectionOverlay(wrapper);
        } else {
          removeSelectionOverlay(wrapper);
        }
      });
    }
    function scheduleSelectionSync() {
      if (selectionSyncRaf) return;
      const win = documentRef.defaultView ?? globalThis;
      selectionSyncRaf = win.requestAnimationFrame(() => {
        selectionSyncRaf = null;
        syncSelectionOverlays();
        updateHudCount();
      });
    }
    function startSelectionObserver() {
      stopSelectionObserver();
      const targets = getSelectionObserverTargets();
      if (targets.length === 0) return;
      selectionObserver = new MutationObserver(() => scheduleSelectionSync());
      targets.forEach((target) => {
        selectionObserver.observe(target, { childList: true, subtree: true });
      });
    }
    function stopSelectionObserver() {
      selectionObserver?.disconnect();
      selectionObserver = null;
      if (selectionSyncRaf) {
        const win = documentRef.defaultView ?? globalThis;
        win.cancelAnimationFrame?.(selectionSyncRaf);
        selectionSyncRaf = null;
      }
    }
    function setSelectionMode(active) {
      groupEl.classList.toggle(SELECTION_MODE_CLASS, active);
      backdropEl.classList.toggle(SELECTION_MODE_BACKDROP_CLASS, active);
      documentRef.documentElement?.classList.toggle(SELECTION_MODE_PAGE_CLASS, active);
      hud.classList.toggle("is-active", active);
      hud.setAttribute("aria-hidden", active ? "false" : "true");
      if (active) {
        backdropEl.style.pointerEvents = "none";
        hud.style.pointerEvents = "";
        enterPageSelectionMode();
      } else {
        backdropEl.style.pointerEvents = "";
        hud.style.pointerEvents = "";
        exitPageSelectionMode();
      }
    }
    function updateHudCount() {
      if (hudConfirmBtn) {
        hudConfirmBtn.textContent = `Confirm (${selectedCards.size})`;
      }
      if (hudSelectBtn) {
        const entries = getVisibleWrapperEntries();
        const selectedVisible = entries.filter((entry) => selectedCards.has(entry.characterId)).length;
        const allChecked = entries.length > 0 && selectedVisible === entries.length;
        const someChecked = selectedVisible > 0 && !allChecked;
        updateSelectionHudIcons(hudSelectBtn, { allChecked, someChecked });
      }
    }
    hud.addEventListener("click", (event) => {
      event.stopPropagation();
    }, { signal });
    function onPageCardClick(event) {
      let target = event.target;
      if (!target || typeof target.closest !== "function") {
        target = target?.parentElement;
      }
      const wrapper = target?.closest?.(PAGE_CARD_WRAPPER_SELECTOR);
      if (!wrapper) return;
      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
      const entry = getEntryFromWrapper(wrapper);
      if (!entry) return;
      if (selectedCards.has(entry.characterId)) {
        selectedCards.delete(entry.characterId);
        removeSelectionOverlay(wrapper);
        persistSelections();
        updateHudCount();
        return;
      }
      selectedCards.set(entry.characterId, {
        characterId: entry.characterId,
        title: entry.title,
        characterUrl: entry.characterUrl
      });
      ensureSelectionOverlay(wrapper);
      persistSelections();
      updateHudCount();
    }
    let pageClickHandler = null;
    function enterPageSelectionMode() {
      if (pageClickHandler) exitPageSelectionMode();
      loadStoredSelections();
      syncSelectionOverlays();
      updateHudCount();
      pageClickHandler = (event) => {
        if (hud.contains(event.target) || groupEl.contains(event.target)) return;
        onPageCardClick(event);
      };
      documentRef.addEventListener("click", pageClickHandler, { capture: true, signal });
      startSelectionObserver();
    }
    function exitPageSelectionMode() {
      if (pageClickHandler) {
        documentRef.removeEventListener("click", pageClickHandler, { capture: true });
        pageClickHandler = null;
      }
      stopSelectionObserver();
      documentRef.querySelectorAll(`.${SELECTION_OVERLAY_CLASS}`).forEach((el) => {
        const wrapper = el.parentElement;
        if (wrapper) removeSelectionOverlay(wrapper);
        else el.remove();
      });
    }
    function confirmSelection() {
      const batchId = generateId();
      const doAutoFetch = isAutoFetchEnabled();
      const doAutoExport = doAutoFetch && isAutoExportEnabled();
      let addedCount = 0;
      for (const [, data] of selectedCards) {
        const added = queue.addItem(data.characterId, data.title, data.characterUrl, {
          autoFetch: doAutoFetch,
          autoExport: doAutoExport,
          batchId: doAutoFetch ? batchId : null
        });
        if (added) addedCount += 1;
      }
      clearStoredSelections();
      setSelectionMode(false);
      if (addedCount > 0) {
        tryAutoFetch();
      }
    }
    function cancelSelection() {
      setSelectionMode(false);
    }
    selectCharsBtn?.addEventListener("click", () => {
      setSelectionMode(true);
    }, { signal });
    hudConfirmBtn?.addEventListener("click", () => {
      confirmSelection();
    }, { signal });
    hudCloseBtn?.addEventListener("click", () => {
      cancelSelection();
    }, { signal });
    hudSelectBtn?.addEventListener("click", () => {
      const entries = getVisibleWrapperEntries();
      const selectedVisible = entries.filter((entry) => selectedCards.has(entry.characterId)).length;
      const allVisibleSelected = entries.length > 0 && selectedVisible === entries.length;
      if (allVisibleSelected) {
        entries.forEach(({ wrapper, characterId }) => {
          if (!selectedCards.has(characterId)) return;
          selectedCards.delete(characterId);
          removeSelectionOverlay(wrapper);
        });
      } else {
        entries.forEach((entry) => {
          if (!selectedCards.has(entry.characterId)) {
            selectedCards.set(entry.characterId, {
              characterId: entry.characterId,
              title: entry.title,
              characterUrl: entry.characterUrl
            });
          }
          ensureSelectionOverlay(entry.wrapper);
        });
      }
      persistSelections();
      updateHudCount();
    }, { signal });
    function destroy() {
      setSelectionMode(false);
      stopSelectionObserver();
      controller.abort();
    }
    return { destroy };
  }
  const scrollCacheByDocument = /* @__PURE__ */ new WeakMap();
  function getScrollCache(documentRef) {
    const doc = documentRef?.defaultView?.document ?? documentRef ?? globalThis.document;
    if (!doc || typeof doc !== "object") {
      return { sidebar: 0, queue: 0 };
    }
    let cache = scrollCacheByDocument.get(doc);
    if (!cache) {
      cache = { sidebar: 0, queue: 0 };
      scrollCacheByDocument.set(doc, cache);
    }
    return cache;
  }
  function createBatchQueueUI({
    documentRef,
    view,
    queue,
    queueBox,
    selectAllCheckbox,
    bulkFetchBtn,
    bulkDownloadBtn,
    bulkRemoveBtn,
    progressStatsSpan,
    progressFill,
    onAction,
    onSelectionChange
  }) {
    let checkedCharacterIds = /* @__PURE__ */ new Set();
    let initializing = false;
    const scrollCache = getScrollCache(documentRef);
    const controller = new AbortController();
    const { signal } = controller;
    let fetchedAtIntervalId = null;
    let activeObserver = null;
    const rowCache = /* @__PURE__ */ new Map();
    const filterToggleBtn = view.querySelector(".jan-batch-filter-toggle");
    const filterMenu = view.querySelector(".jan-batch-filter-menu");
    const filterItems = Array.from(view.querySelectorAll(".jan-batch-filter-item"));
    const currentFilterText = view.querySelector(".jan-batch-filter-text");
    const currentFilterDot = view.querySelector(".jan-batch-filter-dot--current");
    const filterItemsByValue = /* @__PURE__ */ new Map();
    let currentFilter = "all";
    filterItems.forEach((item) => {
      const value = item.getAttribute("data-filter");
      if (value) filterItemsByValue.set(value, item);
    });
    const initialFilterItem = filterItems.find((item) => item.classList.contains("is-active")) ?? filterItems[0];
    if (initialFilterItem) {
      currentFilter = initialFilterItem.getAttribute("data-filter") || "all";
      syncFilterIndicator(initialFilterItem);
    }
    function setInitializing(value) {
      initializing = value;
    }
    function setCheckedCharacterIds(ids) {
      checkedCharacterIds = new Set(ids);
    }
    function getCheckedCharacterIds() {
      return new Set(checkedCharacterIds);
    }
    function createQueueRowElement(item) {
      const doc = documentRef?.defaultView?.document ?? documentRef ?? globalThis.document;
      if (!doc) return null;
      const template = doc.createElement("template");
      template.innerHTML = buildQueueItemMarkupWithId(item).trim();
      return template.content.firstElementChild;
    }
    function updateQueueRowElement(row, item) {
      if (!row) return;
      const statusClass = item.visualStatus ? ` ${item.visualStatus}` : "";
      const selectedClass = item.checked ? " is-selected" : "";
      row.className = `jan-batch-queue-item${statusClass}${selectedClass}`;
      row.setAttribute("data-queue-id", item.queueId);
      const checkbox = row.querySelector(".jan-batch-cb-row");
      if (checkbox) {
        checkbox.checked = Boolean(item.checked);
        checkbox.setAttribute("data-status", item.actionStatus ?? "");
        checkbox.setAttribute("data-queue-id", item.queueId);
      }
      const titleEl = row.querySelector(".jan-batch-item-title");
      if (titleEl) {
        titleEl.textContent = String(item.title ?? "");
        if (titleEl.tagName?.toLowerCase() === "a") {
          if (item.characterUrl) {
            titleEl.setAttribute("href", item.characterUrl);
            titleEl.setAttribute("target", "_blank");
            titleEl.setAttribute("rel", "noopener noreferrer");
          } else {
            titleEl.removeAttribute("href");
            titleEl.removeAttribute("target");
            titleEl.removeAttribute("rel");
          }
        }
      }
      const statusEl = row.querySelector(".jan-batch-item-status-text");
      if (statusEl) {
        statusEl.textContent = String(item.statusText ?? "");
        if (item.fullError) {
          statusEl.setAttribute("title", item.fullError);
          statusEl.setAttribute("data-full-error", item.fullError);
        } else {
          statusEl.removeAttribute("title");
          statusEl.removeAttribute("data-full-error");
        }
      }
      const progressFill2 = row.querySelector(".jan-batch-item-min-progress-fill");
      if (progressFill2) {
        const progressValue = Number.isFinite(item.progress) ? item.progress : 0;
        progressFill2.style.width = `${progressValue}%`;
      }
      const iconEl = row.querySelector(".jan-batch-item-icon");
      if (iconEl) {
        replaceSvgContent(iconEl, item.iconSvg, documentRef);
      }
      const primaryButton = row.querySelector('.jan-batch-item-actions button[data-action]:not([data-action="remove"])');
      if (primaryButton) {
        const primaryClass = item.primaryButtonClass ? `jan-batch-icon-btn ${item.primaryButtonClass}`.trim() : "jan-batch-icon-btn";
        primaryButton.className = primaryClass;
        primaryButton.title = String(item.primaryButtonTitle ?? "");
        primaryButton.setAttribute("data-action", item.primaryAction ?? "none");
        primaryButton.setAttribute("data-queue-id", item.queueId);
        replaceSvgContent(primaryButton, item.primaryButtonIcon, documentRef);
      }
      const removeButton = row.querySelector('button[data-action="remove"]');
      if (removeButton) {
        removeButton.setAttribute("data-queue-id", item.queueId);
      }
    }
    function getRowCheckboxes() {
      return Array.from(queueBox.querySelectorAll(".jan-batch-cb-row"));
    }
    function getCheckedQueueIds() {
      return getRowCheckboxes().filter((cb) => cb.checked).map((cb) => cb.getAttribute("data-queue-id")).filter(Boolean);
    }
    function getFilteredCheckboxes(rowCheckboxes) {
      if (currentFilter === "all") return rowCheckboxes;
      return rowCheckboxes.filter((cb) => cb.getAttribute("data-status") === currentFilter);
    }
    function syncFilterIndicator(item) {
      filterItems.forEach((i2) => i2.classList.toggle("is-active", i2 === item));
      const labelText = item?.querySelector(".jan-batch-filter-label")?.textContent?.trim();
      if (currentFilterText) currentFilterText.textContent = labelText || "All";
      const itemDot = item?.querySelector(".jan-batch-filter-dot");
      if (itemDot && currentFilterDot) {
        currentFilterDot.style.background = itemDot.style.background;
        currentFilterDot.style.boxShadow = itemDot.style.boxShadow;
      }
    }
    function closeFilterMenu() {
      filterMenu?.classList.remove("show");
      filterToggleBtn?.classList.remove("open");
      filterToggleBtn?.setAttribute("aria-expanded", "false");
    }
    function applyFilterSelection() {
      const rowCheckboxes = getRowCheckboxes();
      rowCheckboxes.forEach((cb) => {
        const status = cb.getAttribute("data-status");
        const shouldBeChecked = currentFilter === "all" || status === currentFilter;
        if (cb.checked !== shouldBeChecked) cb.checked = shouldBeChecked;
      });
      syncSelectionState();
    }
    function setCurrentFilter(filter, { applySelection = false } = {}) {
      if (!filter) return;
      currentFilter = filter;
      const item = filterItemsByValue.get(filter);
      if (item) syncFilterIndicator(item);
      if (applySelection) {
        applyFilterSelection();
      } else {
        syncSelectionState();
      }
    }
    function syncCheckedCharacterIdsFromDom() {
      const currentUuids = new Set(getCheckedQueueIds());
      const queueItems = queue.getItems();
      checkedCharacterIds = new Set(
        queueItems.filter((qi) => currentUuids.has(qi.id)).map((qi) => qi.characterId)
      );
    }
    function syncSelectionState() {
      const rowCheckboxes = getRowCheckboxes();
      const selectedCheckboxes = rowCheckboxes.filter((cb) => cb.checked);
      const targetCheckboxes = getFilteredCheckboxes(rowCheckboxes);
      const allChecked = targetCheckboxes.length > 0 && targetCheckboxes.every((cb) => cb.checked);
      const someChecked = targetCheckboxes.some((cb) => cb.checked) && !allChecked;
      rowCheckboxes.forEach((cb) => {
        cb.closest(".jan-batch-queue-item")?.classList.toggle("is-selected", cb.checked);
      });
      if (selectAllCheckbox) {
        selectAllCheckbox.checked = allChecked;
        selectAllCheckbox.indeterminate = someChecked;
      }
      if (bulkFetchBtn && bulkDownloadBtn && bulkRemoveBtn) {
        updateBulkActions({
          selectedCheckboxes,
          bulkFetchBtn,
          bulkDownloadBtn,
          bulkRemoveBtn
        });
      }
      syncCheckedCharacterIdsFromDom();
      if (!initializing) {
        onSelectionChange?.(getCheckedCharacterIds());
      }
    }
    function renderQueue(items) {
      const rowCheckboxesExist = !initializing && queueBox.querySelector(".jan-batch-cb-row") !== null;
      if (rowCheckboxesExist) {
        syncCheckedCharacterIdsFromDom();
      }
      const markupItems = items.map(mapQueueEntryToMarkupItem);
      markupItems.forEach((item) => {
        item.checked = checkedCharacterIds.has(item.characterId);
      });
      const savedQueueScroll = queueBox.scrollTop;
      const nextIds = new Set(markupItems.map((item) => item.queueId));
      for (const [queueId, row] of rowCache.entries()) {
        if (!nextIds.has(queueId)) {
          row.remove();
          rowCache.delete(queueId);
        }
      }
      markupItems.forEach((item) => {
        let row = rowCache.get(item.queueId);
        if (!row) {
          row = createQueueRowElement(item);
          if (!row) return;
          rowCache.set(item.queueId, row);
        } else {
          updateQueueRowElement(row, item);
        }
        queueBox.appendChild(row);
      });
      if (savedQueueScroll) queueBox.scrollTop = savedQueueScroll;
      const stats = queue.getStats();
      const completed = stats.ready + stats.error;
      if (progressStatsSpan) {
        progressStatsSpan.textContent = `${completed} / ${stats.total} Tasks`;
      }
      if (progressFill) {
        const pct = stats.total > 0 ? (completed / stats.total * 100).toFixed(2) : 0;
        progressFill.style.width = `${pct}%`;
      }
      syncSelectionState();
    }
    function attachEventListeners() {
      filterToggleBtn?.addEventListener("click", (event) => {
        event.preventDefault();
        event.stopPropagation();
        const willOpen = !filterMenu?.classList.contains("show");
        if (willOpen) {
          filterMenu?.classList.add("show");
          filterToggleBtn.classList.add("open");
          filterToggleBtn.setAttribute("aria-expanded", "true");
        } else {
          closeFilterMenu();
        }
      }, { signal });
      filterItems.forEach((item) => {
        item.addEventListener("click", (event) => {
          event.preventDefault();
          event.stopPropagation();
          const filterValue = item.getAttribute("data-filter") || "all";
          setCurrentFilter(filterValue, { applySelection: true });
          closeFilterMenu();
        }, { signal });
      });
      documentRef.addEventListener("click", (event) => {
        if (!filterMenu || !filterToggleBtn) return;
        if (filterMenu.contains(event.target) || filterToggleBtn.contains(event.target)) return;
        closeFilterMenu();
      }, { signal });
      queueBox.addEventListener("change", (event) => {
        if (event.target?.classList?.contains("jan-batch-cb-row")) {
          const status = event.target.getAttribute("data-status");
          if (event.target.checked && currentFilter !== "all" && status && status !== currentFilter) {
            setCurrentFilter("all", { applySelection: false });
          }
          syncSelectionState();
        }
      }, { signal });
      queueBox.addEventListener("click", (event) => {
        if (event.target?.closest("a.jan-batch-item-title")) {
          event.stopPropagation();
          return;
        }
        const actionBtn = event.target.closest("[data-action][data-queue-id]");
        if (!actionBtn) return;
        event.preventDefault();
        event.stopPropagation();
        const action = actionBtn.getAttribute("data-action");
        const queueId = actionBtn.getAttribute("data-queue-id");
        if (!action || !queueId) return;
        onAction?.({ action, queueId });
      }, { signal });
      const copyTimers = /* @__PURE__ */ new WeakMap();
      queueBox.addEventListener("click", (event) => {
        const statusEl = event.target.closest(".jan-batch-item-status-text[data-full-error]");
        if (!statusEl) return;
        event.preventDefault();
        event.stopPropagation();
        const fullError = statusEl.getAttribute("data-full-error");
        if (!fullError) return;
        if (copyTimers.has(statusEl)) return;
        const win = documentRef.defaultView ?? globalThis;
        win.navigator?.clipboard?.writeText(fullError).then(() => {
          if (!statusEl.hasAttribute("data-original-text")) {
            statusEl.setAttribute("data-original-text", statusEl.textContent);
          }
          statusEl.textContent = "Copied!";
          const timerId = setTimeout(() => {
            copyTimers.delete(statusEl);
            const original = statusEl.getAttribute("data-original-text") ?? "";
            statusEl.textContent = original;
            statusEl.removeAttribute("data-original-text");
          }, 1200);
          copyTimers.set(statusEl, timerId);
        }).catch(() => {
          win.prompt?.("Copy error:", fullError);
        });
      }, { signal });
      selectAllCheckbox?.addEventListener("change", () => {
        const rowCheckboxes = getRowCheckboxes();
        rowCheckboxes.forEach((cb) => {
          const status = cb.getAttribute("data-status");
          const shouldApply = currentFilter === "all" || status === currentFilter;
          if (shouldApply) cb.checked = selectAllCheckbox.checked;
        });
        syncSelectionState();
      }, { signal });
    }
    function tickBatchFetchedAtNow() {
      if (documentRef.hidden || view.offsetHeight === 0) return;
      queueBox.querySelectorAll(".jan-batch-queue-item.status-success .jan-batch-item-status-text").forEach((el) => {
        const queueId = el.closest("[data-queue-id]")?.getAttribute("data-queue-id");
        const entry = queueId ? queue.getItem(queueId) : null;
        if (entry?.fetchedAt) el.textContent = formatFetchedAt(entry.fetchedAt);
      });
    }
    function setupFetchedAtTicker() {
      if (fetchedAtIntervalId != null) {
        globalThis.clearInterval(fetchedAtIntervalId);
      }
      fetchedAtIntervalId = globalThis.setInterval(() => {
        tickBatchFetchedAtNow();
      }, 1e4);
      activeObserver?.disconnect();
      activeObserver = new MutationObserver(() => {
        if (view.classList.contains("is-active")) tickBatchFetchedAtNow();
      });
      activeObserver.observe(view, { attributeFilter: ["class"] });
      documentRef.addEventListener("visibilitychange", () => {
        if (!documentRef.hidden) tickBatchFetchedAtNow();
      }, { signal });
    }
    function attachScrollCache() {
      view.querySelector(".jan-batch-sidebar")?.addEventListener("scroll", (e) => {
        scrollCache.sidebar = e.currentTarget.scrollTop;
      }, { passive: true, signal });
      queueBox?.addEventListener("scroll", () => {
        scrollCache.queue = queueBox.scrollTop;
      }, { passive: true, signal });
    }
    function restoreScroll() {
      if (scrollCache.sidebar || scrollCache.queue) {
        const win = documentRef.defaultView ?? globalThis;
        win.requestAnimationFrame(() => {
          const sidebar = view.querySelector(".jan-batch-sidebar");
          if (sidebar && scrollCache.sidebar) sidebar.scrollTop = scrollCache.sidebar;
          if (queueBox && scrollCache.queue) queueBox.scrollTop = scrollCache.queue;
        });
      }
    }
    function destroy() {
      controller.abort();
      if (fetchedAtIntervalId != null) {
        globalThis.clearInterval(fetchedAtIntervalId);
        fetchedAtIntervalId = null;
      }
      activeObserver?.disconnect();
      activeObserver = null;
      rowCache.clear();
    }
    return {
      attachEventListeners,
      attachScrollCache,
      destroy,
      getCheckedCharacterIds,
      getCheckedQueueIds,
      renderQueue,
      restoreScroll,
      setCheckedCharacterIds,
      setInitializing,
      setupFetchedAtTicker,
      syncSelectionState
    };
  }
  function saveQueueToSession({ documentRef, queue }) {
    const storage = getSessionStorage(documentRef);
    if (!storage) return;
    const serializable = queue.getItems().map((item) => {
      const base = {
        id: item.id,
        characterId: item.characterId,
        title: item.title,
        characterUrl: item.characterUrl,
        autoFetch: item.autoFetch,
        autoExport: item.autoExport,
        batchId: item.batchId,
        retrying: false,
        retryCount: item.retryCount
      };
      if (item.status === "ready" && item.normalizedCharacter) {
        return {
          ...base,
          status: "ready",
          progress: 100,
          error: null,
          normalizedCharacter: item.normalizedCharacter,
          artifactFormat: item.artifactFormat ?? null,
          artifactFilename: item.downloadArtifact?.filename ?? null,
          fetchedAt: item.fetchedAt ?? null
        };
      }
      if (item.status === "error") {
        return {
          ...base,
          status: "error",
          progress: 0,
          error: item.error ?? null,
          artifactFormat: null
        };
      }
      return {
        ...base,
        status: "pending",
        progress: 0,
        error: null,
        artifactFormat: null
      };
    });
    writeJson(storage, QUEUE_STORAGE_KEY, serializable);
  }
  function restoreQueueFromSession({ documentRef, queue }) {
    const storage = getSessionStorage(documentRef);
    if (!storage) return;
    const items = readJson(storage, QUEUE_STORAGE_KEY, null);
    if (!Array.isArray(items)) return;
    items.forEach((item) => {
      if (!item?.characterId) return;
      queue.addItem(item.characterId, item.title, item.characterUrl, {
        autoFetch: false,
        // don't re-trigger auto-fetch on restore
        autoExport: false,
        batchId: item.batchId ?? null
      });
      const added = queue.getItems().find((q) => q.characterId === item.characterId);
      if (!added) return;
      if (item.status === "ready" && item.normalizedCharacter) {
        queue.updateItem(added.id, {
          status: "ready",
          progress: 100,
          normalizedCharacter: item.normalizedCharacter,
          artifactFormat: item.artifactFormat ?? null,
          downloadArtifact: item.artifactFilename ? { filename: item.artifactFilename, content: null, mimeType: null } : null,
          fetchedAt: item.fetchedAt ?? null
        });
      } else if (item.status === "error" && item.error) {
        queue.updateItem(added.id, {
          status: "error",
          error: item.error,
          retryCount: item.retryCount ?? 0
        });
      }
    });
  }
  function loadSettings({ documentRef, view, autoFetchToggle, autoExportToggle, autoRetryToggle, autoRetryCountInput }) {
    const storage = getLocalStorage(documentRef);
    if (!storage) return;
    const s = readJson(storage, SETTINGS_STORAGE_KEY, null);
    if (!s) return;
    if (typeof s.autoFetch === "boolean" && autoFetchToggle) autoFetchToggle.checked = s.autoFetch;
    if (typeof s.autoExport === "boolean" && autoExportToggle) autoExportToggle.checked = s.autoExport;
    if (typeof s.autoRetry === "boolean" && autoRetryToggle) autoRetryToggle.checked = s.autoRetry;
    if (typeof s.autoRetryCount === "number" && autoRetryCountInput) {
      autoRetryCountInput.value = Math.min(20, Math.max(1, s.autoRetryCount));
    }
    if (s.exportMode) {
      const modeInput = view.querySelector(`input[name="jan-batch-download-mode"][value="${s.exportMode}"]`);
      if (modeInput) modeInput.checked = true;
    }
    if (s.format) {
      const fmtInput = view.querySelector(`input[name="jan-batch-export-format"][value="${s.format}"]`);
      if (fmtInput) fmtInput.checked = true;
    }
  }
  function saveSettings({
    documentRef,
    autoFetchToggle,
    autoExportToggle,
    autoRetryToggle,
    autoRetryCountInput,
    getSelectedDownloadMode,
    getSelectedExportFormat
  }) {
    const storage = getLocalStorage(documentRef);
    if (!storage) return;
    const parsedAutoRetryCount = Number.parseInt(autoRetryCountInput?.value ?? "", 10);
    const autoRetryCount = Number.isFinite(parsedAutoRetryCount) && parsedAutoRetryCount >= 1 ? Math.min(20, parsedAutoRetryCount) : null;
    writeJson(storage, SETTINGS_STORAGE_KEY, {
      autoFetch: autoFetchToggle?.checked ?? true,
      autoExport: autoExportToggle?.checked ?? false,
      autoRetry: autoRetryToggle?.checked ?? false,
      autoRetryCount: Number.isFinite(autoRetryCount) ? autoRetryCount : null,
      exportMode: getSelectedDownloadMode(),
      format: getSelectedExportFormat()
    });
  }
  function loadCheckedIds({ documentRef }) {
    const storage = getSessionStorage(documentRef);
    if (!storage) return /* @__PURE__ */ new Set();
    const raw = readJson(storage, CHECKED_STORAGE_KEY, null);
    if (Array.isArray(raw)) return new Set(raw);
    return /* @__PURE__ */ new Set();
  }
  function saveCheckedIds({ documentRef, checkedCharacterIds }) {
    const storage = getSessionStorage(documentRef);
    if (!storage) return;
    if (!checkedCharacterIds || checkedCharacterIds.size === 0) {
      removeStorageItem(storage, CHECKED_STORAGE_KEY);
      return;
    }
    writeJson(storage, CHECKED_STORAGE_KEY, Array.from(checkedCharacterIds));
  }
  function createBatchDownloadTab({
    documentRef,
    groupEl,
    backdropEl,
    janitorClient = null,
    scrapeCharacterCard = null,
    generateAlphaProxyConfig = null
  }) {
    ensureBatchDownloadTabStyle(documentRef);
    ensureSelectionOverlayStyle(documentRef);
    const tabController = new AbortController();
    const { signal } = tabController;
    let isDestroyed = false;
    const viewerClient = janitorClient ?? createJanitorClient({
      documentRef
    });
    const baseScrapeCharacterCard = typeof scrapeCharacterCard === "function" ? scrapeCharacterCard : createInternalOrchestrator({ client: viewerClient }).scrapeCharacterCard;
    const scrapeCharacterCardImpl = (options = {}) => baseScrapeCharacterCard({
      ...options,
      generateAlphaProxyConfig
    });
    const queue = createBatchQueue();
    const view = documentRef.createElement("section");
    view.className = "jan-panel-tab-view jan-batch-view";
    view.innerHTML = createBatchViewMarkup();
    const hud = documentRef.createElement("div");
    hud.className = "jan-batch-selection-hud";
    hud.setAttribute("aria-hidden", "true");
    hud.innerHTML = createSelectionHudMarkup();
    backdropEl.appendChild(hud);
    const selectAllCheckbox = view.querySelector(".jan-batch-select-all");
    const bulkFetchBtn = view.querySelector(".jan-batch-bulk-fetch");
    const bulkDownloadBtn = view.querySelector(".jan-batch-bulk-download");
    const bulkRemoveBtn = view.querySelector(".jan-batch-bulk-remove");
    const selectCharsBtn = view.querySelector(".jan-batch-select-chars-btn");
    const queueBox = view.querySelector(".jan-batch-queue-box");
    const progressStatsSpan = view.querySelector(".jan-batch-progress-stats span");
    const progressFill = view.querySelector(".jan-batch-global-progress-fill");
    const autoFetchToggle = view.querySelector(".jan-batch-auto-fetch");
    const autoExportToggle = view.querySelector(".jan-batch-auto-export");
    const autoRetryToggle = view.querySelector(".jan-batch-auto-retry");
    const autoRetryCountInput = view.querySelector(".jan-batch-auto-retry-count");
    const hudSelectBtn = hud.querySelector(".jan-batch-hud-select-btn");
    const hudConfirmBtn = hud.querySelector(".jan-batch-hud-confirm-btn");
    const hudCloseBtn = hud.querySelector(".jan-batch-hud-close-btn");
    function getSelectedExportFormat() {
      return view.querySelector('input[name="jan-batch-export-format"]:checked')?.value ?? "png";
    }
    function getSelectedDownloadMode() {
      return view.querySelector('input[name="jan-batch-download-mode"]:checked')?.value ?? "files";
    }
    function isAutoRetryEnabled() {
      return autoRetryToggle?.checked ?? false;
    }
    function getAutoRetryCount() {
      if (!autoRetryCountInput) return null;
      const raw = autoRetryCountInput.value.trim();
      if (!raw) return null;
      const parsed = Number.parseInt(raw, 10);
      if (!Number.isFinite(parsed) || parsed < 1) return null;
      return Math.min(parsed, 20);
    }
    function persistSettings() {
      saveSettings({
        documentRef,
        autoFetchToggle,
        autoExportToggle,
        autoRetryToggle,
        autoRetryCountInput,
        getSelectedDownloadMode,
        getSelectedExportFormat
      });
    }
    function syncAutoExportRowState() {
      const autoExportRow = autoExportToggle?.closest(".jan-batch-setting-row");
      const enabled = autoFetchToggle?.checked ?? true;
      autoExportRow?.classList.toggle("is-dimmed", !enabled);
      if (!enabled && autoExportToggle?.checked) {
        autoExportToggle.checked = false;
      }
    }
    function syncAutoRetryRowState() {
      if (autoRetryCountInput) {
        autoRetryCountInput.disabled = !autoRetryToggle?.checked;
      }
    }
    autoFetchToggle?.addEventListener("change", () => {
      syncAutoExportRowState();
      syncAutoRetryRowState();
      persistSettings();
    }, { signal });
    autoExportToggle?.addEventListener("change", () => {
      persistSettings();
    }, { signal });
    autoRetryToggle?.addEventListener("change", () => {
      syncAutoRetryRowState();
      persistSettings();
    }, { signal });
    autoRetryCountInput?.addEventListener("input", () => {
      const raw = autoRetryCountInput.value;
      if (raw !== "") {
        const val = parseInt(raw, 10);
        if (isNaN(val) || val < 1) {
          autoRetryCountInput.value = 1;
        } else if (val > 20) {
          autoRetryCountInput.value = 20;
        }
      }
      persistSettings();
    }, { signal });
    syncAutoExportRowState();
    syncAutoRetryRowState();
    const formatInputs = Array.from(view.querySelectorAll('input[name="jan-batch-export-format"]'));
    formatInputs.forEach((input) => {
      input.addEventListener("change", () => {
        syncFormatCards(view);
        persistSettings();
      }, { signal });
    });
    Array.from(view.querySelectorAll('input[name="jan-batch-download-mode"]')).forEach((radio) => {
      radio.addEventListener("change", () => persistSettings(), { signal });
    });
    syncFormatCards(view);
    const fetchController = createBatchFetchController({
      queue,
      documentRef,
      mountEl: view,
      scrapeCharacterCard: scrapeCharacterCardImpl,
      getSelectedExportFormat,
      getSelectedDownloadMode,
      triggerArtifactDownload,
      downloadBulkAsZip,
      isAutoRetryEnabled,
      getAutoRetryCount
    });
    const queueUi = createBatchQueueUI({
      documentRef,
      view,
      queue,
      queueBox,
      selectAllCheckbox,
      bulkFetchBtn,
      bulkDownloadBtn,
      bulkRemoveBtn,
      progressStatsSpan,
      progressFill,
      onAction: ({ action, queueId }) => {
        if (action === "remove") {
          queue.removeItem(queueId);
          return;
        }
        if (action === "fetch") {
          fetchController.cancelAutoRetryTimer(queueId);
          fetchController.fetchSingleItem(queueId);
          return;
        }
        if (action === "view") {
          const entry = queue.getItem(queueId);
          if (entry?.characterUrl) {
            const viewerTab = documentRef.querySelector(".jan-character-viewer-view");
            if (viewerTab) {
              const viewBtn = documentRef.querySelector('.jan-bar-tab[aria-label="Character Viewer"]');
              if (viewBtn) viewBtn.click();
              unloadCharacterViewer(viewerTab);
              loadCharacterViewerInput(viewerTab, entry.characterUrl);
            }
          }
        }
      },
      onSelectionChange: (checkedCharacterIds) => {
        saveCheckedIds({ documentRef, checkedCharacterIds });
      }
    });
    queueUi.attachEventListeners();
    queueUi.attachScrollCache();
    queueUi.setupFetchedAtTicker();
    const selectionController = setupBatchSelection({
      documentRef,
      groupEl,
      backdropEl,
      hud,
      selectCharsBtn,
      hudSelectBtn,
      hudConfirmBtn,
      hudCloseBtn,
      queue,
      isAutoFetchEnabled: () => autoFetchToggle?.checked ?? true,
      isAutoExportEnabled: () => autoExportToggle?.checked ?? false,
      tryAutoFetch: fetchController.tryAutoFetch
    });
    bulkFetchBtn?.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      const ids = queueUi.getCheckedQueueIds();
      fetchController.fetchBulk(ids);
    }, { signal });
    bulkDownloadBtn?.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      const ids = queueUi.getCheckedQueueIds();
      const entries = ids.map((id) => queue.getItem(id)).filter((entry) => entry && entry.status === "ready" && entry.downloadArtifact);
      downloadBulkEntries({
        entries,
        exportMode: getSelectedDownloadMode(),
        exportFormat: getSelectedExportFormat(),
        documentRef,
        mountEl: view
      });
    }, { signal });
    bulkRemoveBtn?.addEventListener("click", (event) => {
      event.preventDefault();
      event.stopPropagation();
      const ids = queueUi.getCheckedQueueIds();
      queue.removeItems(ids);
    }, { signal });
    loadSettings({ documentRef, view, autoFetchToggle, autoExportToggle, autoRetryToggle, autoRetryCountInput });
    syncAutoExportRowState();
    syncAutoRetryRowState();
    syncFormatCards(view);
    queueUi.setInitializing(true);
    restoreQueueFromSession({ documentRef, queue });
    queueUi.setCheckedCharacterIds(loadCheckedIds({ documentRef }));
    queueUi.renderQueue(queue.getItems());
    queueUi.setInitializing(false);
    const win = documentRef.defaultView ?? globalThis;
    const scheduleRenderQueue = createRafScheduler$1((items) => queueUi.renderQueue(items), win);
    const schedulePersistQueue = createDebounced(
      () => saveQueueToSession({ documentRef, queue }),
      350,
      win
    );
    const unsubscribeQueue = queue.subscribe((items) => {
      scheduleRenderQueue(items);
      schedulePersistQueue();
    });
    queueUi.restoreScroll();
    function destroy() {
      if (isDestroyed) return;
      isDestroyed = true;
      schedulePersistQueue.flush?.();
      schedulePersistQueue.cancel?.();
      scheduleRenderQueue.cancel?.();
      unsubscribeQueue?.();
      queueUi.destroy?.();
      fetchController.destroy?.();
      selectionController?.destroy?.();
      tabController.abort();
      hud.remove();
    }
    view.__janDestroy = destroy;
    return view;
  }
  function renderBatchDownloadPane() {
    return `
        <div class="settings-pane" id="pane-batch-download">

          <div class="setting-group">
            <div class="setting-group-title">Export Options</div>
            
            <div class="setting-row">
              <div class="setting-label-block">
                <span class="setting-label">ZIP Compression Level</span>
                <span id="hint-exp_batch_zip" class="setting-desc">No compression so it's the fastest.</span>
              </div>
              <div class="inline-segment">
                <label><input type="radio" class="setting-control" data-key="exp_batch_zip" name="exp_batch_zip" value="store" data-hint="No compression so it's the fastest." checked><span class="inline-seg-btn">Store</span></label>
                <label><input type="radio" class="setting-control" data-key="exp_batch_zip" name="exp_batch_zip" value="fast" data-hint="Level 1. Very fast compression with small size savings."><span class="inline-seg-btn">Fast</span></label>
                <label><input type="radio" class="setting-control" data-key="exp_batch_zip" name="exp_batch_zip" value="standard" data-hint="Level 6. Balanced speed and file size."><span class="inline-seg-btn">Standard</span></label>
                <label><input type="radio" class="setting-control" data-key="exp_batch_zip" name="exp_batch_zip" value="max" data-hint="Level 9. Smallest files, slowest export."><span class="inline-seg-btn">Maximum</span></label>
              </div>
            </div>
          </div>

        </div>
  `;
  }
  function renderCharacterViewerPane() {
    return `
        <div class="settings-pane" id="pane-character-viewer">

          <div class="setting-group">
            <div class="setting-group-title">Visual</div>
            
            <div class="setting-row">
              <div class="setting-label-block">
                <span class="setting-label">Name Display</span>
                <span class="setting-desc">Choose which name shows in the viewer sidebar</span>
              </div>
              <div class="inline-segment">
                <label><input type="radio" class="setting-control" data-key="vis_name_display" name="vis_name_display" value="chat" checked><span class="inline-seg-btn">Always Chat Name</span></label>
                <label><input type="radio" class="setting-control" data-key="vis_name_display" name="vis_name_display" value="name"><span class="inline-seg-btn">Always Name</span></label>
              </div>
            </div>
            
            <div class="setting-row col">
              <div class="setting-label-block">
                <span class="setting-label">Character Avatar</span>
                <span class="setting-desc">Choose how avatars are shown</span>
              </div>
              <div class="choice-cards-grid">
                <label class="choice-card">
                  <input type="radio" class="setting-control" data-key="vis_char_avatar" name="vis_char_avatar" value="none" checked>
                  <div class="choice-card-icon">${SETTINGS_AVATAR_NONE_ICON_SVG}</div>
                  <span class="choice-card-title">None</span>
                  <span class="choice-card-desc">No filter</span>
                </label>
                <label class="choice-card">
                  <input type="radio" class="setting-control" data-key="vis_char_avatar" name="vis_char_avatar" value="blur">
                  <div class="choice-card-icon">${SETTINGS_AVATAR_BLUR_ICON_SVG}</div>
                  <span class="choice-card-title">Blur NSFW</span>
                  <span class="choice-card-desc">Blur NSFW avatars</span>
                </label>
                <label class="choice-card">
                  <input type="radio" class="setting-control" data-key="vis_char_avatar" name="vis_char_avatar" value="off">
                  <div class="choice-card-icon">${SETTINGS_AVATAR_OFF_ICON_SVG}</div>
                  <span class="choice-card-title">OFF</span>
                  <span class="choice-card-desc">Hide avatar section</span>
                </label>
              </div>
            </div>
          </div>

        </div>
  `;
  }
  function renderGeneralPane() {
    return `
        <div class="settings-pane active" id="pane-general">

          <div class="setting-group">
            <div class="setting-group-title">Pronoun Macros</div>
            <div class="setting-row col">
              <div style="width:100%; display:flex; justify-content:space-between; align-items:flex-start;">
                <div class="setting-label-block">
                  <span class="setting-label">Persona Pronouns</span>
                </div>
                <div class="inline-segment">
                  <label><input type="radio" class="setting-control" name="pron_preset" value="she"><span class="inline-seg-btn">She/Her</span></label>
                  <label><input type="radio" class="setting-control" name="pron_preset" value="he"><span class="inline-seg-btn">He/Him</span></label>
                  <label><input type="radio" class="setting-control" name="pron_preset" value="they" checked><span class="inline-seg-btn">They/Them</span></label>
                  <label><input type="radio" class="setting-control" name="pron_preset" value="custom"><span class="inline-seg-btn">Custom</span></label>
                </div>
              </div>
              <div class="pronoun-inputs-grid">
                <div class="pronoun-input-col">
                  <span class="pronoun-input-label">Subjective</span>
                  <input type="text" class="setting-input pron-input" data-key="sub" placeholder="they">
                </div>
                <div class="pronoun-input-col">
                  <span class="pronoun-input-label">Objective</span>
                  <input type="text" class="setting-input pron-input" data-key="obj" placeholder="them">
                </div>
                <div class="pronoun-input-col">
                  <span class="pronoun-input-label">Possessive</span>
                  <input type="text" class="setting-input pron-input" data-key="poss" placeholder="their">
                </div>
                <div class="pronoun-input-col">
                  <span class="pronoun-input-label">Poss. Pronoun</span>
                  <input type="text" class="setting-input pron-input" data-key="poss_p" placeholder="theirs">
                </div>
                <div class="pronoun-input-col">
                  <span class="pronoun-input-label">Reflexive</span>
                  <input type="text" class="setting-input pron-input" data-key="ref" placeholder="themselves">
                </div>
              </div>
            </div>
          </div>

          <div class="setting-group">
            <div class="setting-group-title">Export Options</div>
            
            <div class="setting-row">
              <div class="setting-label-block">
                <span class="setting-label">Name Field</span>
                <span class="setting-desc">Choose which name is saved in the V2 name field</span>
              </div>
              <div class="inline-segment">
                <label><input type="radio" class="setting-control" data-key="exp_gen_name" name="exp_gen_name" value="chat" checked><span class="inline-seg-btn">Chat Name</span></label>
                <label><input type="radio" class="setting-control" data-key="exp_gen_name" name="exp_gen_name" value="name"><span class="inline-seg-btn">Name</span></label>
              </div>
            </div>
            
            <div class="setting-row">
              <div class="setting-label-block">
                <span class="setting-label">Export Tags</span>
                <span class="setting-desc">Include character tags in the V2 export</span>
              </div>
              <div class="inline-segment">
                <label><input type="radio" class="setting-control" data-key="exp_gen_tags" name="exp_gen_tags" value="on" checked><span class="inline-seg-btn">Tags</span></label>
                <label><input type="radio" class="setting-control" data-key="exp_gen_tags" name="exp_gen_tags" value="off"><span class="inline-seg-btn">No Tags</span></label>
              </div>
            </div>
            
            <div class="setting-row">
              <div class="setting-label-block">
                <span class="setting-label">Export Creator Notes</span>
                <span class="setting-desc">Include creator notes/character bio in the V2 export</span>
              </div>
              <div class="inline-segment">
                <label><input type="radio" class="setting-control" data-key="exp_gen_notes" name="exp_gen_notes" value="on" checked><span class="inline-seg-btn">Creator Notes</span></label>
                <label><input type="radio" class="setting-control" data-key="exp_gen_notes" name="exp_gen_notes" value="off"><span class="inline-seg-btn">No Creator Notes</span></label>
              </div>
            </div>
          </div>

        </div>
  `;
  }
  function renderStoragePane() {
    return `
        <div class="settings-pane" id="pane-storage">

          <div class="setting-group">
            <div class="setting-group-title">Data</div>
            <div class="danger-zone">
              <div>
                <div class="danger-label">Reset all settings</div>
                <div class="danger-hint">Restore every setting to its default. This cannot be undone.
                </div>
              </div>
              <button class="btn-danger">Reset</button>
            </div>
          </div>

        </div>
  `;
  }
  const SETTINGS_TAB_STYLE_ID = "jan-settings-tab-style";
  const SETTINGS_FONT_STYLESHEET_ID = "jan-settings-font-stylesheet";
  const SETTINGS_FONT_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600&display=swap";
  const SETTINGS_PANE_STORAGE_KEY = "jan-settings-active-pane";
  function ensureSettingsTabStyle(documentRef) {
    if (!documentRef?.head) return;
    ensureGoogleFontStylesheet(documentRef, {
      id: SETTINGS_FONT_STYLESHEET_ID,
      href: SETTINGS_FONT_STYLESHEET_URL
    });
    if (documentRef.getElementById(SETTINGS_TAB_STYLE_ID)) return;
    const style = documentRef.createElement("style");
    style.id = SETTINGS_TAB_STYLE_ID;
    style.textContent = `
    .jan-settings-view {
      --bg-color: #050505;
      --panel-bg: linear-gradient(180deg, #0a0a0a 0%, #000000 100%);
      --border-color: rgba(255, 255, 255, 0.07);
      --text-color: #e8e8ec;
      --text-muted: rgba(255, 255, 255, 0.5);
      --accent: #fff;
      --hover-bg: rgba(255, 255, 255, 0.05);
      --pod-bg: linear-gradient(180deg, #181818 0%, #000000 100%);
      --font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      --success: #4CAF50;
      --error: #f44336;
      color: var(--text-color);
      display: none;
      flex-direction: column;
      font-family: var(--font-family);
      font-size: 16px;
      line-height: 1.2;
      height: 100%;
      min-height: 0;
      margin: 0;
      overflow: hidden;
      padding: 0;
      position: relative;
      width: 100%;
    }

    .jan-settings-view,
    .jan-settings-view * {
      box-sizing: border-box;
    }

    .jan-settings-view.is-active {
      display: flex;
    }

    .jan-panel-tab-view.jan-settings-view.is-active {
      display: flex;
    }

    .jan-settings-view .main-content {
      display: flex;
      flex: 1;
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden;
      min-height: 0;
    }

    /* ══════════════════════════════════════
       LEFT SETTINGS NAV
    ══════════════════════════════════════ */
    .jan-settings-view .settings-nav {
      box-sizing: content-box;
      flex: 0 0 auto;
      width: 178px;
      max-width: 178px;
      box-shadow: 1px 0 0 rgba(255, 255, 255, 0.05);
      background: rgba(0, 0, 0, 0.15);
      margin: 0;
      padding: 18px 10px;
      display: flex;
      flex-direction: column;
      gap: 2px;
      align-self: stretch;
    }

    .jan-settings-view .nav-item {
      -webkit-appearance: none;
      appearance: none;
      display: flex !important;
      align-items: center;
      gap: 9px;
      width: 100% !important;
      box-sizing: content-box !important;
      white-space: nowrap;
      padding: 8px 11px !important;
      margin: 0 !important;
      border: none !important;
      border-radius: 7px;
      outline: none;
      background: transparent;
      box-shadow: none !important;
      color: var(--text-muted);
      font-family: inherit;
      font-size: 13px !important;
      font-weight: 400 !important;
      line-height: normal;
      text-align: left;
      text-decoration: none;
      white-space: nowrap;
      cursor: pointer;
      transition: background 0.15s, color 0.15s;
    }

    .jan-settings-view .nav-item svg {
      flex-shrink: 0;
      opacity: 0.55;
      transition: opacity 0.15s;
    }

    .jan-settings-view .nav-item:hover {
      background: transparent !important;
      color: var(--text-color);
    }

    .jan-settings-view .nav-item:hover svg {
      opacity: 0.9;
    }

    .jan-settings-view .nav-item.active {
      background: transparent !important;
      color: var(--text-color);
      font-weight: 500;
    }

    .jan-settings-view .nav-item.active svg {
      opacity: 1;
    }

    .jan-settings-view .nav-spacer {
      flex: 1;
    }

    /* ══════════════════════════════════════
       RIGHT SETTINGS CONTENT
    ══════════════════════════════════════ */
    .jan-settings-view .settings-content {
      flex: 1;
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden;
      position: relative;
    }

    .jan-settings-view .settings-pane {
      display: none;
      position: absolute;
      inset: 0;
      overflow-y: auto;
      padding: 28px 32px;
      flex-direction: column;
      gap: 26px;
    }

    .jan-settings-view .settings-pane.active {
      display: flex;
    }

    .jan-settings-view .settings-pane::-webkit-scrollbar {
      width: 5px;
    }

    .jan-settings-view .settings-pane::-webkit-scrollbar-track {
      background: transparent;
    }

    .jan-settings-view .settings-pane::-webkit-scrollbar-thumb {
      background: rgba(255, 255, 255, 0.1);
      border-radius: 10px;
    }

    .jan-settings-view .settings-pane::-webkit-scrollbar-thumb:hover {
      background: rgba(255, 255, 255, 0.2);
    }

    /* ── Setting Group ── */
    .jan-settings-view .setting-group {
      display: flex;
      flex-direction: column;
      gap: 6px;
    }

    .jan-settings-view .setting-group-title {
      font-size: 0.7rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.1em;
      color: var(--text-muted);
      margin-bottom: 3px;
    }

    /* ── Setting Row ── */
    .jan-settings-view .setting-row {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 20px;
      padding: 12px 15px;
      background: rgba(255, 255, 255, 0.02);
      border-radius: 9px;
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
    }

    .jan-settings-view .setting-row.col {
      flex-direction: column;
      align-items: flex-start;
      gap: 13px;
    }

    .jan-settings-view .setting-label-block {
      display: flex;
      flex-direction: column;
      gap: 3px;
    }

    .jan-settings-view .setting-label {
      font-size: 0.875rem;
      color: var(--text-color);
      font-weight: 400;
    }

    .jan-settings-view .setting-desc {
      font-size: 0.775rem;
      color: rgba(255, 255, 255, 0.36);
      line-height: 1.45;
    }

    .jan-settings-view .setting-hint {
      font-size: 0.72rem;
      color: rgba(255, 255, 255, 0.45);
      margin-top: 3px;
    }

    /* ── Toggle Switch ── */
    .jan-settings-view .toggle-switch {
      position: relative;
      display: inline-block;
      width: 40px;
      height: 22px;
      flex-shrink: 0;
    }

    .jan-settings-view .toggle-switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }

    .jan-settings-view .toggle-slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: rgba(255, 255, 255, 0.06);
      transition: .3s cubic-bezier(0.4, 0.0, 0.2, 1);
      border-radius: 22px;
      border: 1px solid rgba(255, 255, 255, 0.06);
    }

    .jan-settings-view .toggle-slider:before {
      position: absolute;
      content: "";
      height: 16px;
      width: 16px;
      left: 2px;
      bottom: 2px;
      background-color: rgba(255, 255, 255, 0.35);
      transition: .3s cubic-bezier(0.4, 0.0, 0.2, 1);
      border-radius: 50%;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    }

    .jan-settings-view input:checked + .toggle-slider {
      background-color: rgba(255, 255, 255, 0.12);
      box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
    }

    .jan-settings-view input:checked + .toggle-slider:before {
      transform: translateX(18px);
      background-color: var(--accent);
      box-shadow: 0 0 6px rgba(255, 255, 255, 0.3);
    }

    /* ── Inline Segment ── */
    .jan-settings-view .inline-segment {
      display: flex;
      background: rgba(0, 0, 0, 0.3);
      border-radius: 6px;
      padding: 3px;
      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.05);
      flex-shrink: 0;
    }

    .jan-settings-view .inline-segment label {
      cursor: pointer;
      margin: 0;
      position: relative;
    }

    /* Subtle separator line between buttons (except the last) */
    .jan-settings-view .inline-segment label:not(:last-child)::after {
      content: '';
      position: absolute;
      right: -1px; /* offset for thicker line */
      top: 25%;
      bottom: 25%;
      width: 2px;
      border-radius: 2px;
      background: rgba(255, 255, 255, 0.08); /* faint divider line */
      z-index: 1;
      transition: opacity 0.2s;
    }

    /* Hide the separator nearest to the currently checked segment so it doesn't clash with the checked pill shadow */
    .jan-settings-view .inline-segment label:has(input:checked)::after {
      opacity: 0;
    }
    .jan-settings-view .inline-segment label:has(+ label input:checked)::after {
      opacity: 0;
    }

    .jan-settings-view .inline-segment input {
      display: none;
    }

    .jan-settings-view .inline-seg-btn {
      padding: 5px 12px;
      font-size: 0.78rem;
      font-weight: 500;
      color: var(--text-muted);
      border-radius: 4px;
      transition: all 0.2s;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 5px;
      user-select: none;
      font-family: inherit;
    }

    .jan-settings-view .inline-segment input:checked + .inline-seg-btn {
      background: rgba(255, 255, 255, 0.1);
      color: var(--text-color);
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.05);
    }

    /* Theme segment */
    .jan-settings-view .inline-segment input[value="dark"]:checked + .inline-seg-btn {
      color: #b39ddb;
      background: rgba(179, 157, 219, 0.1);
      box-shadow: inset 0 0 0 1px rgba(179, 157, 219, 0.22);
    }

    .jan-settings-view .inline-segment input[value="light"]:checked + .inline-seg-btn {
      color: #ffcc80;
      background: rgba(255, 204, 128, 0.1);
      box-shadow: inset 0 0 0 1px rgba(255, 204, 128, 0.22);
    }

    /* Files/ZIP segment */
    .jan-settings-view .inline-segment input[value="files"]:checked + .inline-seg-btn {
      color: #5a9cf5;
      background: rgba(90, 156, 245, 0.1);
      box-shadow: inset 0 0 0 1px rgba(90, 156, 245, 0.2);
    }

    .jan-settings-view .inline-segment input[value="zip"]:checked + .inline-seg-btn {
      color: #c377e0;
      background: rgba(195, 119, 224, 0.1);
      box-shadow: inset 0 0 0 1px rgba(195, 119, 224, 0.2);
    }

    /* ── Choice Cards ── */
    .jan-settings-view .choice-cards-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 10px;
      width: 100%;
    }

    .jan-settings-view .choice-card {
      display: flex;
      flex-direction: column;
      gap: 8px;
      background: rgba(255, 255, 255, 0.015);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 10px;
      padding: 16px;
      cursor: pointer;
      transition: all 0.2s;
    }

    .jan-settings-view .choice-card:hover {
      background: rgba(255, 255, 255, 0.03);
      border-color: rgba(255, 255, 255, 0.12);
    }

    .jan-settings-view .choice-card:has(input:checked) {
      background: rgba(255, 255, 255, 0.06);
      border-color: rgba(255, 255, 255, 0.28);
      box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05);
    }

    .jan-settings-view .choice-card input {
      display: none;
    }

    .jan-settings-view .choice-card-icon {
      color: rgba(255, 255, 255, 0.4);
      display: flex;
      align-items: center;
    }

    .jan-settings-view .choice-card:has(input:checked) .choice-card-icon {
      color: #fff;
    }

    .jan-settings-view .choice-card-title {
      font-weight: 500;
      font-size: 0.85rem;
      color: #fff;
    }

    .jan-settings-view .choice-card-desc {
      font-size: 0.7rem;
      color: rgba(255, 255, 255, 0.5);
      line-height: 1.4;
    }

    /* ── Text Input ── */
    .jan-settings-view .setting-input {
      background: rgba(0, 0, 0, 0.2);
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 6px;
      color: var(--text-color);
      font-family: inherit;
      font-size: 0.82rem;
      padding: 8px 10px;
      width: 100%;
      box-sizing: border-box;
      transition: border-color 0.2s, background 0.2s;
    }

    .jan-settings-view .setting-input:focus {
      outline: none;
      border-color: rgba(255, 255, 255, 0.25);
      background: rgba(0, 0, 0, 0.3);
    }

    .jan-settings-view .setting-input::placeholder {
      color: rgba(255, 255, 255, 0.2);
    }

    .jan-settings-view .pronoun-inputs-grid {
      display: grid;
      grid-template-columns: repeat(5, 1fr);
      gap: 10px;
      width: 100%;
    }

    .jan-settings-view .pronoun-input-col {
      display: flex;
      flex-direction: column;
      gap: 6px;
    }

    .jan-settings-view .pronoun-input-label {
      font-size: 0.73rem;
      color: rgba(255, 255, 255, 0.4);
      font-weight: 500;
      text-align: center;
    }

    /* ── Format Cards ── */
    .jan-settings-view .format-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 8px;
      max-width: 300px;
    }

    .jan-settings-view .format-card {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 8px;
      padding: 13px 4px;
      background: rgba(255, 255, 255, 0.015);
      border: 1px solid rgba(255, 255, 255, 0.06);
      border-radius: 10px;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .jan-settings-view .format-card:hover {
      background: rgba(255, 255, 255, 0.04);
    }

    .jan-settings-view .format-png:hover {
      border-color: rgba(90, 156, 245, 0.2);
    }

    .jan-settings-view .format-png:hover .format-svg {
      color: rgba(90, 156, 245, 0.7);
    }

    .jan-settings-view .format-json:hover {
      border-color: rgba(212, 137, 58, 0.2);
    }

    .jan-settings-view .format-json:hover .format-svg {
      color: rgba(212, 137, 58, 0.7);
    }

    .jan-settings-view .format-txt:hover {
      border-color: rgba(143, 168, 190, 0.2);
    }

    .jan-settings-view .format-txt:hover .format-svg {
      color: rgba(143, 168, 190, 0.7);
    }

    .jan-settings-view .format-card input {
      display: none;
    }

    .jan-settings-view .format-svg {
      display: flex;
      align-items: center;
      justify-content: center;
      color: rgba(255, 255, 255, 0.35);
      transition: all 0.2s ease;
    }

    .jan-settings-view .format-badge {
      font-size: 0.75rem;
      font-weight: 600;
      color: var(--text-muted);
      letter-spacing: 0.05em;
      background: rgba(0, 0, 0, 0.35);
      padding: 2px 8px;
      border-radius: 4px;
      border: 1px solid rgba(255, 255, 255, 0.04);
      transition: all 0.2s ease;
    }

    .jan-settings-view .format-png:has(input:checked) {
      background: rgba(90, 156, 245, 0.06);
      border-color: rgba(90, 156, 245, 0.28);
      box-shadow: 0 0 0 1px rgba(90, 156, 245, 0.35);
    }

    .jan-settings-view .format-png input:checked ~ .format-svg {
      color: #5a9cf5;
    }

    .jan-settings-view .format-png input:checked ~ .format-badge {
      color: #fff;
      background: #5a9cf5;
      border-color: #5a9cf5;
    }

    .jan-settings-view .format-json:has(input:checked) {
      background: rgba(212, 137, 58, 0.06);
      border-color: rgba(212, 137, 58, 0.28);
      box-shadow: 0 0 0 1px rgba(212, 137, 58, 0.35);
    }

    .jan-settings-view .format-json input:checked ~ .format-svg {
      color: #d4893a;
    }

    .jan-settings-view .format-json input:checked ~ .format-badge {
      color: #fff;
      background: #d4893a;
      border-color: #d4893a;
    }

    .jan-settings-view .format-txt:has(input:checked) {
      background: rgba(143, 168, 190, 0.06);
      border-color: rgba(143, 168, 190, 0.25);
      box-shadow: 0 0 0 1px rgba(143, 168, 190, 0.32);
    }

    .jan-settings-view .format-txt input:checked ~ .format-svg {
      color: #8fa8be;
    }

    .jan-settings-view .format-txt input:checked ~ .format-badge {
      color: #fff;
      background: #8fa8be;
      border-color: #8fa8be;
    }

    /* ── Definition Info Card ── */
    .jan-settings-view .def-info-card {
      background: rgba(255, 255, 255, 0.02);
      border: 1px solid rgba(255, 255, 255, 0.06);
      border-radius: 9px;
      padding: 14px 16px;
      display: flex;
      flex-direction: column;
      gap: 11px;
    }

    .jan-settings-view .def-info-title {
      font-size: 0.72rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.08em;
      color: rgba(255, 255, 255, 0.3);
    }

    .jan-settings-view .def-info-row {
      display: flex;
      align-items: flex-start;
      gap: 11px;
    }

    .jan-settings-view .def-info-icon {
      flex-shrink: 0;
      margin-top: 1px;
    }

    .jan-settings-view .def-info-text {
      display: flex;
      flex-direction: column;
      gap: 3px;
    }

    .jan-settings-view .def-info-label {
      font-size: 0.83rem;
      font-weight: 500;
    }

    .jan-settings-view .def-info-desc {
      font-size: 0.78rem;
      color: rgba(255, 255, 255, 0.4);
      line-height: 1.5;
    }

    .jan-settings-view .def-info-divider {
      height: 1px;
      background: rgba(255, 255, 255, 0.05);
    }

    /* ── Danger zone ── */
    .jan-settings-view .danger-zone {
      background: rgba(244, 67, 54, 0.04);
      border: 1px solid rgba(244, 67, 54, 0.1);
      border-radius: 9px;
      padding: 13px 15px;
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 20px;
    }

    .jan-settings-view .danger-label {
      font-size: 0.875rem;
      color: var(--text-color);
    }

    .jan-settings-view .danger-hint {
      font-size: 0.775rem;
      color: rgba(255, 255, 255, 0.32);
      margin-top: 2px;
    }

    .jan-settings-view .btn-danger {
      padding: 7px 15px;
      background: rgba(244, 67, 54, 0.06);
      border: 1px solid rgba(244, 67, 54, 0.18);
      border-radius: 7px;
      color: rgba(244, 67, 54, 0.75);
      font-family: inherit;
      font-size: 0.78rem;
      font-weight: 500;
      letter-spacing: 0.03em;
      cursor: pointer;
      transition: color 0.15s, background 0.15s, border-color 0.15s, transform 0.1s;
      white-space: nowrap;
      flex-shrink: 0;
    }

    .jan-settings-view .btn-danger:hover {
      background: rgba(244, 67, 54, 0.1);
      border-color: rgba(244, 67, 54, 0.32);
      color: #f44336;
    }

    /* ── Confirm Modal ── */
    .jan-settings-view .confirm-modal-overlay {
      position: absolute;
      inset: 0;
      background: rgba(0, 0, 0, 0.55);
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 50;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.15s;
    }

    .jan-settings-view .confirm-modal-overlay.is-open {
      opacity: 1;
      pointer-events: auto;
    }

    .jan-settings-view .confirm-modal {
      background: #050505;
      border: 1px solid rgba(255, 255, 255, 0.08);
      border-radius: 10px;
      padding: 22px 24px 20px;
      width: 220px;
      display: flex;
      flex-direction: column;
      gap: 16px;
      transform: scale(0.96);
      transition: transform 0.15s cubic-bezier(0.2, 0, 0, 1);
    }

    .jan-settings-view .confirm-modal-overlay.is-open .confirm-modal {
      transform: scale(1);
    }

    .jan-settings-view .confirm-modal-title {
      font-size: 0.875rem;
      font-weight: 500;
      color: var(--text-color);
      text-align: center;
    }

    .jan-settings-view .confirm-modal-actions {
      display: flex;
      gap: 7px;
      justify-content: center;
    }

    .jan-settings-view .btn-modal-cancel {
      padding: 6px 14px;
      background: transparent;
      border: 1px solid rgba(255, 255, 255, 0.09);
      border-radius: 6px;
      color: rgba(255, 255, 255, 0.45);
      font-family: inherit;
      font-size: 0.78rem;
      font-weight: 400;
      cursor: pointer;
      transition: border-color 0.15s, color 0.15s;
    }

    .jan-settings-view .btn-modal-cancel:hover {
      border-color: rgba(255, 255, 255, 0.18);
      color: rgba(255, 255, 255, 0.75);
    }

    .jan-settings-view .btn-modal-confirm {
      padding: 6px 14px;
      background: rgba(244, 67, 54, 0.07);
      border: 1px solid rgba(244, 67, 54, 0.2);
      border-radius: 6px;
      color: rgba(244, 67, 54, 0.8);
      font-family: inherit;
      font-size: 0.78rem;
      font-weight: 500;
      cursor: pointer;
      transition: background 0.15s, border-color 0.15s, color 0.15s;
    }

    .jan-settings-view .btn-modal-confirm:hover {
      background: rgba(244, 67, 54, 0.12);
      border-color: rgba(244, 67, 54, 0.32);
      color: #f44336;
    }
  `;
    documentRef.head.appendChild(style);
  }
  function getSettingsStorage(documentRef) {
    try {
      return documentRef?.defaultView?.localStorage ?? globalThis.localStorage ?? null;
    } catch {
      return null;
    }
  }
  function createSettingsMarkup() {
    return `
    <div class="main-content">

      <!-- ── LEFT NAV ── -->
      <div class="settings-nav">
        <button class="nav-item active" data-pane="general">
          ${SETTINGS_NAV_GENERAL_ICON_SVG}
          General
        </button>
        <button class="nav-item" data-pane="character-viewer">
          ${SETTINGS_NAV_CHARACTER_ICON_SVG}
          Character Viewer
        </button>
        <button class="nav-item" data-pane="batch-download">
          ${SETTINGS_NAV_BATCH_ICON_SVG}
          Batch Download
        </button>
        <div class="nav-spacer"></div>
        <button class="nav-item" data-pane="storage">
          ${SETTINGS_NAV_STORAGE_ICON_SVG}
          Storage
        </button>
      </div>

      <!-- ── RIGHT CONTENT ── -->
      <div class="settings-content">

        ${renderGeneralPane()}
        ${renderCharacterViewerPane()}
        ${renderBatchDownloadPane()}
        ${renderStoragePane()}

      </div>
    </div>

    <div class="confirm-modal-overlay" id="jan-reset-modal">
      <div class="confirm-modal">
        <div class="confirm-modal-title">Are you sure?</div>
        <div class="confirm-modal-actions">
          <button class="btn-modal-cancel">Cancel</button>
          <button class="btn-modal-confirm">Reset</button>
        </div>
      </div>
    </div>
  `;
  }
  function updateRadioHint(view, el) {
    if (el.type !== "radio" || !el.checked || !el.dataset.hint) return;
    const hintEl = view.querySelector(`#hint-${el.dataset.key}`);
    if (hintEl) hintEl.textContent = el.dataset.hint;
  }
  function emitSettingsChanged(documentRef, key, value) {
    const EventCtor = documentRef?.defaultView?.CustomEvent ?? globalThis.CustomEvent;
    if (!EventCtor) return;
    documentRef.dispatchEvent(new EventCtor("jan-settings-changed", {
      detail: { key, value: String(value) }
    }));
  }
  function setupSettingsControls(view, documentRef) {
    const storage = getSettingsStorage(documentRef);
    view.querySelectorAll(".setting-control[data-key]").forEach((el) => {
      const key = el.dataset.key;
      if (!key) return;
      const stored = storage?.getItem(`${SETTINGS_STORAGE_PREFIX}${key}`);
      const val = stored !== null ? stored : SETTINGS_DEFAULTS[key];
      if (el.type === "radio") {
        if (el.value === val) {
          el.checked = true;
          updateRadioHint(view, el);
        }
      } else if (el.type === "checkbox") {
        el.checked = val === "true";
      }
      el.addEventListener("change", () => {
        let toStore = el.value;
        if (el.type === "checkbox") toStore = el.checked ? "true" : "false";
        storage?.setItem(`${SETTINGS_STORAGE_PREFIX}${key}`, toStore);
        if (el.type === "radio") {
          view.querySelectorAll(`.setting-control[name="${el.name}"]`).forEach((radio) => {
            if (radio.checked) updateRadioHint(view, radio);
          });
        }
        emitSettingsChanged(documentRef, key, toStore);
      });
    });
  }
  function setupPronounMacros(view, documentRef) {
    const storage = getSettingsStorage(documentRef);
    const pronRadios = view.querySelectorAll('input[name="pron_preset"]');
    const pronInputsArray = view.querySelectorAll(".pron-input");
    const pronInputs = {};
    pronInputsArray.forEach((input) => {
      pronInputs[input.dataset.key] = input;
    });
    function savePronouns() {
      const data = {};
      Object.keys(pronInputs).forEach((k) => {
        data[k] = pronInputs[k].value;
      });
      storage?.setItem(PRONOUN_STORAGE_KEY, JSON.stringify(data));
      const activeRadio = view.querySelector('input[name="pron_preset"]:checked');
      if (activeRadio) storage?.setItem(PRONOUN_PRESET_STORAGE_KEY, activeRadio.value);
    }
    pronRadios.forEach((radio) => {
      radio.addEventListener("change", (event) => {
        const val = event.target.value;
        if (PRONOUN_PRESETS[val]) {
          Object.keys(pronInputs).forEach((k) => {
            pronInputs[k].value = PRONOUN_PRESETS[val][k];
          });
          savePronouns();
        }
      });
    });
    pronInputsArray.forEach((input) => {
      input.addEventListener("input", () => {
        const customOpt = view.querySelector('input[name="pron_preset"][value="custom"]');
        if (customOpt) customOpt.checked = true;
        savePronouns();
      });
    });
    function loadPronouns() {
      const preset = storage?.getItem(PRONOUN_PRESET_STORAGE_KEY) || DEFAULT_PRONOUN_PRESET;
      const radio = view.querySelector(`input[name="pron_preset"][value="${preset}"]`);
      if (radio) radio.checked = true;
      const dataStr = storage?.getItem(PRONOUN_STORAGE_KEY);
      let data = PRONOUN_PRESETS[DEFAULT_PRONOUN_PRESET];
      if (dataStr) {
        try {
          data = JSON.parse(dataStr);
        } catch {
        }
      } else if (PRONOUN_PRESETS[preset]) {
        data = PRONOUN_PRESETS[preset];
      }
      Object.keys(pronInputs).forEach((k) => {
        if (data[k]) pronInputs[k].value = data[k];
      });
    }
    loadPronouns();
  }
  function setupResetButton(view, documentRef) {
    const resetBtn = view.querySelector(".btn-danger");
    const modal = view.querySelector("#jan-reset-modal");
    if (!resetBtn || !modal) return;
    const cancelBtn = modal.querySelector(".btn-modal-cancel");
    const confirmBtn = modal.querySelector(".btn-modal-confirm");
    function openModal() {
      modal.classList.add("is-open");
    }
    function closeModal() {
      modal.classList.remove("is-open");
    }
    function doReset() {
      const storage = getSettingsStorage(documentRef);
      Object.keys(SETTINGS_DEFAULTS).forEach((key) => {
        storage?.removeItem?.(`${SETTINGS_STORAGE_PREFIX}${key}`);
      });
      view.querySelectorAll(".setting-control[data-key]").forEach((el) => {
        const key = el.dataset.key;
        if (!key) return;
        const defaultValue = SETTINGS_DEFAULTS[key];
        if (defaultValue == null) return;
        if (el.type === "radio") {
          el.checked = el.value === defaultValue;
          if (el.checked) updateRadioHint(view, el);
        } else if (el.type === "checkbox") {
          el.checked = String(defaultValue) === "true";
        }
      });
      const presetData = PRONOUN_PRESETS[DEFAULT_PRONOUN_PRESET] ?? PRONOUN_PRESETS.they ?? {};
      const presetRadio = view.querySelector(`input[name="pron_preset"][value="${DEFAULT_PRONOUN_PRESET}"]`);
      if (presetRadio) presetRadio.checked = true;
      view.querySelectorAll(".pron-input").forEach((input) => {
        const key = input.dataset.key;
        input.value = presetData?.[key] ?? "";
      });
      storage?.removeItem?.(PRONOUN_STORAGE_KEY);
      storage?.removeItem?.(PRONOUN_PRESET_STORAGE_KEY);
      Object.entries(SETTINGS_DEFAULTS).forEach(([key, value]) => {
        emitSettingsChanged(documentRef, key, value);
      });
    }
    resetBtn.addEventListener("click", openModal);
    cancelBtn.addEventListener("click", closeModal);
    confirmBtn.addEventListener("click", () => {
      doReset();
      closeModal();
    });
    modal.addEventListener("click", (e) => {
      if (e.target === modal) closeModal();
    });
  }
  function setupNavPersistence(view, documentRef) {
    const storage = getSettingsStorage(documentRef);
    const navItems = Array.from(view.querySelectorAll(".nav-item"));
    const panes = Array.from(view.querySelectorAll(".settings-pane"));
    const normalizePaneId = (paneId) => paneId === "about" ? "storage" : paneId;
    function setActivePane(paneId) {
      const normalized = normalizePaneId(paneId);
      if (!normalized) return;
      navItems.forEach((item) => {
        item.classList.toggle("active", item.dataset.pane === normalized);
      });
      panes.forEach((pane) => {
        pane.classList.toggle("active", pane.id === `pane-${normalized}`);
      });
      storage?.setItem(SETTINGS_PANE_STORAGE_KEY, normalized);
    }
    navItems.forEach((item) => {
      item.addEventListener("click", () => {
        const paneId = item.dataset.pane;
        if (paneId) setActivePane(paneId);
      });
    });
    const storedRaw = storage?.getItem(SETTINGS_PANE_STORAGE_KEY);
    const stored = normalizePaneId(storedRaw);
    const validStored = stored && panes.some((pane) => pane.id === `pane-${stored}`);
    setActivePane(validStored ? stored : "general");
  }
  function createSettingsTab({ documentRef = globalThis.document } = {}) {
    ensureSettingsTabStyle(documentRef);
    const view = documentRef.createElement("section");
    view.className = "jan-panel-tab-view jan-settings-view";
    view.innerHTML = createSettingsMarkup();
    setupSettingsControls(view, documentRef);
    setupPronounMacros(view, documentRef);
    setupNavPersistence(view, documentRef);
    setupResetButton(view, documentRef);
    return view;
  }
  const CONFIG = {
    dims: {
      width: "1200px",
      // edit panel width here
      height: "720px",
      // edit panel height here
      offsetY: "80px",
      // edit vertical offset here
      blur: "4px"
      // edit backdrop blur strength here
    },
    anim: {
      inDuration: 240,
      // ms open animation duration
      outDuration: 180,
      // ms close animation duration
      scaleFrom: 0.8
      // edit open animation start scale here (0.0 - 1.0)
    },
    colors: {
      dark: {
        start: "#0a0a0a",
        // top of panel / bottom of top-tab
        end: "#000000"
        // bottom of panel
      }
    },
    ids: {
      backdrop: "jan-panel-backdrop",
      panel: "jan-panel",
      style: "jan-panel-style"
    },
    storage: {
      theme: "jan-panel-theme",
      tab: "jan-panel-active-tab"
    }
  };
  const TAB_DEFS = [
    { svg: TAB_CHARACTER_SVG, label: "Character Viewer" },
    { svg: TAB_DOWNLOAD_SVG, label: "Batch Download" },
    { svg: TAB_SETTINGS_SVG, label: "Settings" }
  ];
  const BROWSER = (() => {
    const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
    if (ua.includes("Firefox/")) return "firefox";
    if (ua.includes("Edg/")) return "edge";
    return "other";
  })();
  const TOP_TAB_HEIGHT = BROWSER === "firefox" ? 44 : 42;
  const blockWheel = (event) => {
    if (event.ctrlKey || !event.deltaY) return;
    event.preventDefault();
    event.stopPropagation();
  };
  const state = {
    isOpen: false,
    currentTheme: "dark",
    activeAnim: null,
    // Animation object for cancellation
    panelRuntime: null,
    // { backdrop, group, setActiveTab, characterViewerView }
    elements: {
      backdrop: null,
      group: null
    }
  };
  const safeStorage = {
    get: (type, doc) => {
      try {
        return doc?.defaultView?.[type] ?? globalThis[type] ?? null;
      } catch {
        return null;
      }
    },
    getItem: (type, key, doc) => safeStorage.get(type, doc)?.getItem(key),
    setItem: (type, key, val, doc) => {
      try {
        safeStorage.get(type, doc)?.setItem(key, String(val));
      } catch {
      }
    }
  };
  const Storage = {
    loadTheme: (doc) => safeStorage.getItem("localStorage", CONFIG.storage.theme, doc) === "1" ? "light" : "dark",
    saveTheme: (theme, doc) => safeStorage.setItem("localStorage", CONFIG.storage.theme, theme === "light" ? "1" : "0", doc),
    loadTabIndex: (doc) => {
      const raw = safeStorage.getItem("sessionStorage", CONFIG.storage.tab, doc);
      const idx = parseInt(raw, 10);
      return Number.isFinite(idx) && idx >= 0 ? idx : 0;
    },
    saveTabIndex: (idx, doc) => safeStorage.setItem("sessionStorage", CONFIG.storage.tab, idx, doc)
  };
  function ensurePanelStyle(documentRef) {
    if (!documentRef?.head || documentRef.getElementById(CONFIG.ids.style)) return;
    const style = documentRef.createElement("style");
    style.id = CONFIG.ids.style;
    style.textContent = `
    :root {
      --jp-bg: linear-gradient(180deg, ${CONFIG.colors.dark.start} 0%, ${CONFIG.colors.dark.end} 100%);
      --jp-border: 1px solid rgba(0, 0, 0, 0.8);
      --jp-shadow: inset 0 0.5px 0.5px rgba(255, 255, 255, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 30px 60px rgba(0, 0, 0, 0.95);
      --jp-text: #e8e8ec;

      --jp-pod-bg: linear-gradient(180deg, #181818 0%, #000000 100%);
      --jp-pod-border: 1px solid rgba(0, 0, 0, 0.9);
      --jp-pod-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 15px 35px rgba(0, 0, 0, 0.95);

      --jp-btn-text: rgba(255, 255, 255, 0.4);
      --jp-btn-hover-bg: rgba(255, 255, 255, 0.1);
      --jp-btn-hover-text: #fff;
      --jp-btn-active-bg: rgba(255, 255, 255, 0.15);
      --jp-btn-active-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);

      --jp-tab-fill: url(#gObsidian);
      --jp-tab-stroke: rgba(0, 0, 0, 0.8);
      --jp-tab-hl: rgba(255, 255, 255, 0.35);
      --jp-tab-hl-under: rgba(255, 255, 255, 0.1);
      --jp-tab-text: rgba(255, 255, 255, 0.95);
      --jp-tab-text-shadow: 0 2px 10px rgba(0,0,0,0.8), 0 1px 2px rgba(0,0,0,0.5);
      
      --jp-close-color: #ff5555;
      --jp-close-hover-bg: rgba(255, 60, 60, 0.2);
      --jp-close-hover-color: #ff8888;
      
      --jp-placeholder-color: rgba(255, 255, 255, 0.72);
      --jp-placeholder-desc: rgba(255, 255, 255, 0.48);
    }

    .jan-panel--light {
      --jp-bg: #ffffff;
      --jp-border: 1px solid #8e8e93;
      --jp-shadow: 0 15px 35px rgba(0, 0, 0, 0.08);
      --jp-text: #1c1c1e;

      --jp-pod-bg: #ffffff;
      --jp-pod-border: 1px solid #8e8e93;
      --jp-pod-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);

      --jp-btn-text: #666666;
      --jp-btn-hover-bg: #000000;
      --jp-btn-hover-text: #ffffff;
      --jp-btn-active-bg: #000000;
      --jp-btn-active-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4);

      --jp-tab-fill: #ffffff;
      --jp-tab-stroke: #8e8e93;
      --jp-tab-hl: transparent;
      --jp-tab-hl-under: transparent;
      --jp-tab-text: #1c1c1e;
      --jp-tab-text-shadow: none;
      
      --jp-close-color: #d32f2f;
      --jp-close-hover-bg: #ff3b30; /* Red engulf */
      --jp-close-hover-color: #ffffff;
      
      --jp-placeholder-color: rgba(0, 0, 0, 0.7);
      --jp-placeholder-desc: rgba(0, 0, 0, 0.5);
    }

    /* Light mode specific override for singular pod hover engulf */
    .jan-panel--light .jan-pod--circle:hover {
       background: #000000;
       border-color: #000000;
    }
    .jan-panel--light .jan-pod--circle:hover > button {
       color: #ffffff;
       background: transparent;
    }
    .jan-panel--light .jan-pod--circle.jan-pod--close:hover {
       background: #ff3b30;
       border-color: #ff3b30;
    }

    #${CONFIG.ids.backdrop} {
      align-items: center;
      backdrop-filter: blur(${CONFIG.dims.blur});
      -webkit-backdrop-filter: blur(${CONFIG.dims.blur});
      background: rgba(0, 0, 0, 0.50);
      bottom: 0; box-sizing: border-box; display: flex; justify-content: center;
      left: 0; position: fixed; right: 0; top: 0; z-index: 99999;
    }

    .jan-panel-group {
      margin-bottom: ${CONFIG.dims.offsetY};
      pointer-events: none; position: relative; transform-origin: center center;
    }

    .jan-bar {
      align-items: center; background: transparent; display: flex; gap: 12px;
      left: 50%; pointer-events: auto; position: absolute; top: calc(100% + 40px);
      transform: translateX(-50%); white-space: nowrap;
    }

    .jan-pod {
      align-items: center; background: var(--jp-pod-bg); border: var(--jp-pod-border);
      border-radius: 9999px; box-shadow: var(--jp-pod-shadow);
      box-sizing: border-box; display: flex; gap: 4px; padding: 6px;
      transition: background 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
    }

    .jan-pod--circle { border-radius: 50%; padding: 0; }

    #${CONFIG.ids.panel} {
      background: var(--jp-bg); border: var(--jp-border); border-radius: 12px;
      box-shadow: var(--jp-shadow); box-sizing: border-box;
      color: var(--jp-text); display: flex; flex-direction: column;
      font-family: inherit; height: ${CONFIG.dims.height};
      max-height: calc(90vh - 60px); max-width: 95vw; overflow: hidden;
      pointer-events: auto; width: ${CONFIG.dims.width};
      transition: background 200ms ease, color 200ms ease, border-color 200ms ease, box-shadow 200ms ease;
    }

    .jan-panel-close, .jan-panel-theme, .jan-bar-tab {
      align-items: center; background: transparent; border: none; border-radius: 50%;
      cursor: pointer; display: inline-flex; flex: 0 0 auto; height: 42px;
      justify-content: center; padding: 0; pointer-events: auto; position: relative;
      transition: color 150ms ease, background 150ms ease; width: 42px;
    }
    
    .jan-panel-theme { color: var(--jp-btn-text); }
    .jan-panel-theme:hover { background: var(--jp-btn-hover-bg); color: var(--jp-btn-hover-text); }
    .jan-panel-theme svg { display: block; height: 18px; width: 18px; }

    .jan-panel-close { color: var(--jp-close-color); }
    .jan-panel-close:hover { background: var(--jp-close-hover-bg); color: var(--jp-close-hover-color); }
    .jan-panel-close svg { display: block; height: 12px; width: 12px; }

    .jan-bar-tab { color: var(--jp-btn-text); }
    .jan-bar-tab:hover { background: var(--jp-btn-hover-bg); color: var(--jp-btn-hover-text); }
    .jan-bar-tab.active {
      background: var(--jp-btn-active-bg); box-shadow: var(--jp-btn-active-shadow);
      color: #fff;
    }
    .jan-bar-tab-icon { align-items: center; display: flex; flex-shrink: 0; justify-content: center; }
    .jan-bar-tab-icon svg { display: block; height: 18px; width: 18px; }

    .jan-panel-top-tab {
      position: absolute; top: -40px; left: 50%; transform: translateX(-50%);
      width: 284px; height: ${TOP_TAB_HEIGHT}px; z-index: 10;
      display: flex; align-items: center; justify-content: center; pointer-events: none;
    }

    .tab-shape-bg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
    .tab-fill { fill: var(--jp-tab-fill); pointer-events: auto; }
    .tab-stroke { stroke: var(--jp-tab-stroke); pointer-events: none; }
    .tab-macos-highlight-under { stroke: var(--jp-tab-hl-under); stroke-width: 1px; transform: translateY(0.5px); pointer-events: none; }
    .tab-macos-highlight { stroke: var(--jp-tab-hl); stroke-width: 0.5px; opacity: 0.8; pointer-events: none; }

    .jan-panel-tab-name-container {
      position: relative; z-index: 2; width: 240px; height: 100%;
      display: flex; align-items: center; justify-content: center;
      overflow: hidden; padding-bottom: 2px;
    }

    .jan-panel-tab-name {
      color: var(--jp-tab-text); font-family: inherit; font-size: 14px; font-weight: 600;
      letter-spacing: 0.18em; text-transform: uppercase; text-shadow: var(--jp-tab-text-shadow);
      user-select: none; -webkit-user-select: none; white-space: nowrap;
      will-change: transform, opacity, filter;
    }

    .jan-panel-body { flex: 1 1 auto; min-height: 0; overflow: hidden; position: relative; }
    .jan-panel-tab-view { display: none; height: 100%; width: 100%; }
    .jan-panel-tab-view.is-active { display: block; }
    
    /* Focus styles */
    .jan-panel-theme:focus-visible,
    .jan-bar-tab:focus-visible {
      outline: 2px solid rgba(255, 255, 255, 0.40);
      outline-offset: 2px;
    }
    .jan-panel-close:focus-visible {
      outline: 2px solid rgba(220, 50, 50, 0.50);
      outline-offset: 2px;
    }
    .jan-panel--light .jan-panel-theme:focus-visible,
    .jan-panel--light .jan-panel-close:focus-visible,
    .jan-panel--light .jan-bar-tab:focus-visible {
      outline-color: rgba(0, 0, 0, 0.35);
    }
    
    /* Placeholder */
    .jan-panel-placeholder-view.is-active { align-items: center; display: flex; justify-content: center; }
    .jan-panel-placeholder-copy {
      align-items: center; color: var(--jp-placeholder-color); display: flex;
      flex-direction: column; gap: 10px; max-width: 420px; padding: 32px; text-align: center;
    }
    .jan-panel-placeholder-copy h3 { font-size: 18px; font-weight: 600; letter-spacing: 0.03em; margin: 0; }
    .jan-panel-placeholder-copy p { color: var(--jp-placeholder-desc); line-height: 1.55; margin: 0; }
  `;
    documentRef.head.appendChild(style);
  }
  function createBackdrop(doc, theme) {
    const el = doc.createElement("div");
    el.id = CONFIG.ids.backdrop;
    el.setAttribute("aria-hidden", "true");
    if (theme === "light") el.classList.add("jan-panel--light");
    el.addEventListener("pointerdown", (e) => {
      if (e.target === el) closePanel({ documentRef: doc });
    });
    return el;
  }
  function createButton(doc, className, label, iconNode, onClick) {
    const btn = doc.createElement("button");
    btn.type = "button";
    btn.className = className;
    if (label) btn.setAttribute("aria-label", label);
    if (iconNode) btn.appendChild(iconNode);
    if (onClick) btn.addEventListener("click", onClick);
    return btn;
  }
  function createFloatingBar(doc, onThemeToggle, onTabChange, onClose) {
    const bar = doc.createElement("div");
    bar.className = "jan-bar";
    bar.addEventListener("wheel", blockWheel, { passive: false });
    const themePod = doc.createElement("div");
    themePod.className = "jan-pod jan-pod--circle";
    const themeIcon = createSvgNode(
      state.currentTheme === "dark" ? DARK_MODE_ICON_SVG : LIGHT_MODE_ICON_SVG,
      doc
    );
    const themeBtn = createButton(
      doc,
      "jan-panel-theme",
      state.currentTheme === "dark" ? "Switch to light mode" : "Switch to dark mode",
      themeIcon,
      onThemeToggle
    );
    themePod.appendChild(themeBtn);
    const tabsPod = doc.createElement("div");
    tabsPod.className = "jan-pod";
    const tabBtns = TAB_DEFS.map((def, i2) => {
      const iconWrap = doc.createElement("span");
      iconWrap.className = "jan-bar-tab-icon";
      const iconSvg = createSvgNode(def.svg, doc);
      if (iconSvg) iconWrap.appendChild(iconSvg);
      const btn = createButton(doc, "jan-bar-tab" + (i2 === 0 ? " active" : ""), def.label, iconWrap);
      btn.addEventListener("click", () => onTabChange(i2));
      return btn;
    });
    tabBtns.forEach((btn) => tabsPod.appendChild(btn));
    const closePod = doc.createElement("div");
    closePod.className = "jan-pod jan-pod--circle jan-pod--close";
    const closeBtn = createButton(
      doc,
      "jan-panel-close",
      "Close panel",
      createSvgNode(PANEL_CLOSE_ICON_SVG, doc),
      onClose
    );
    closePod.appendChild(closeBtn);
    bar.append(themePod, tabsPod, closePod);
    return { bar, themeBtn, tabBtns };
  }
  function createTopTab(doc) {
    const el = doc.createElement("div");
    el.className = "jan-panel-top-tab";
    el.addEventListener("wheel", blockWheel, { passive: false });
    const pathFillD = `M 0 40 A 12 12 0 0 0 12 28 L 12 12 A 12 12 0 0 1 24 0 L 260 0 A 12 12 0 0 1 272 12 L 272 28 A 12 12 0 0 0 284 40 L 284 ${TOP_TAB_HEIGHT} L 0 ${TOP_TAB_HEIGHT} Z`;
    const innerHighlightD = "M 0 41 A 11 11 0 0 0 11 30 L 11 12 A 13 13 0 0 1 24 1 L 260 1 A 13 13 0 0 1 273 12 L 273 30 A 11 11 0 0 0 284 41";
    const pathD = "M 0 40 A 12 12 0 0 0 12 28 L 12 12 A 12 12 0 0 1 24 0 L 260 0 A 12 12 0 0 1 272 12 L 272 28 A 12 12 0 0 0 284 40";
    el.innerHTML = `
    <svg class="tab-shape-bg" viewBox="0 0 284 ${TOP_TAB_HEIGHT}" width="284" height="${TOP_TAB_HEIGHT}" aria-hidden="true">
      <defs>
        <linearGradient id="gObsidian" x1="0" y1="1" x2="0" y2="0">
          <stop offset="0%" stop-color="${CONFIG.colors.dark.start}"/>
          <stop offset="100%" stop-color="#141414"/>
        </linearGradient>
      </defs>
      <path class="tab-fill" d="${pathFillD}" />
      <path class="tab-macos-highlight-under" d="${innerHighlightD}" fill="none" stroke-linecap="round" />
      <path class="tab-macos-highlight" d="${innerHighlightD}" fill="none" stroke-linecap="round" />
      <path class="tab-stroke" d="${pathD}" fill="none" stroke-width="1.2" stroke-linecap="round" />
    </svg>
    <div class="jan-panel-tab-name-container">
      <div class="jan-panel-tab-name">${TAB_DEFS[0].label}</div>
    </div>
  `;
    return { el, nameEl: el.querySelector(".jan-panel-tab-name") };
  }
  function prefersReducedMotion(doc) {
    try {
      return Boolean(doc?.defaultView?.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches);
    } catch {
      return false;
    }
  }
  function animateIn(backdrop, panel) {
    if (!backdrop?.animate || !panel?.animate || prefersReducedMotion(backdrop?.ownerDocument)) {
      backdrop.style.opacity = "1";
      panel.style.opacity = "1";
      panel.style.transform = "scale(1)";
      return;
    }
    const opts = { duration: CONFIG.anim.inDuration, fill: "forwards" };
    const bdAnim = backdrop.animate([{ opacity: 0 }, { opacity: 1 }], { ...opts, easing: "ease" });
    bdAnim.addEventListener("finish", () => bdAnim.cancel(), { once: true });
    const pAnim = panel.animate(
      [{ opacity: 0, transform: `scale(${CONFIG.anim.scaleFrom})` }, { opacity: 1, transform: "scale(1)" }],
      { ...opts, easing: "cubic-bezier(0.22, 1, 0.36, 1)" }
    );
    pAnim.addEventListener("finish", () => pAnim.cancel(), { once: true });
  }
  function animateOut(backdrop, panel, onDone) {
    if (!backdrop?.animate || !panel?.animate || prefersReducedMotion(backdrop?.ownerDocument)) {
      onDone?.();
      return null;
    }
    const opts = { duration: CONFIG.anim.outDuration, fill: "forwards" };
    backdrop.animate([{ opacity: 1 }, { opacity: 0 }], { ...opts, easing: "ease" });
    const pAnim = panel.animate(
      [{ opacity: 1, transform: "scale(1)" }, { opacity: 0, transform: `scale(${CONFIG.anim.scaleFrom})` }],
      { ...opts, easing: "cubic-bezier(0.55, 0, 0.45, 1)" }
    );
    pAnim.addEventListener("finish", onDone, { once: true });
    return pAnim;
  }
  function animateTabLabel(el, newLabel, direction) {
    if (prefersReducedMotion(el?.ownerDocument) || !el?.animate) {
      el.textContent = newLabel;
      return;
    }
    el.getAnimations?.().forEach((a) => a.cancel());
    const outY = direction === "next" ? "-24px" : "24px";
    const inY = direction === "next" ? "24px" : "-24px";
    el.animate(
      [
        { opacity: 1, transform: "translateY(0) scale(1)", filter: "blur(0px)" },
        { opacity: 0, transform: `translateY(${outY}) scale(0.95)`, filter: "blur(2px)" }
      ],
      { duration: 180, easing: "cubic-bezier(0.55, 0, 1, 0.45)", fill: "forwards" }
    ).addEventListener("finish", () => {
      el.textContent = newLabel;
      el.animate(
        [
          { opacity: 0, transform: `translateY(${inY}) scale(0.95)`, filter: "blur(2px)" },
          { opacity: 1, transform: "translateY(0) scale(1)", filter: "blur(0px)" }
        ],
        { duration: 250, easing: "cubic-bezier(0, 0.55, 0.45, 1)", fill: "forwards" }
      );
    }, { once: true });
  }
  function buildPanel(documentRef, onClose, { generateAlphaProxyConfig = null } = {}) {
    const backdrop = createBackdrop(documentRef, state.currentTheme);
    const group = documentRef.createElement("div");
    group.className = "jan-panel-group";
    if (state.currentTheme === "light") group.classList.add("jan-panel--light");
    const onThemeToggle = () => {
      state.currentTheme = state.currentTheme === "dark" ? "light" : "dark";
      Storage.saveTheme(state.currentTheme, documentRef);
      const isLight = state.currentTheme === "light";
      group.classList.toggle("jan-panel--light", isLight);
      backdrop.classList.toggle("jan-panel--light", isLight);
      if (barApi.themeBtn) {
        replaceSvgContent(
          barApi.themeBtn,
          isLight ? LIGHT_MODE_ICON_SVG : DARK_MODE_ICON_SVG,
          documentRef
        );
        barApi.themeBtn.setAttribute("aria-label", isLight ? "Switch to dark mode" : "Switch to light mode");
      }
    };
    let activeTabIdx = 0;
    const setActiveTab = (nextIdx) => {
      if (!Number.isInteger(nextIdx) || nextIdx < 0 || nextIdx >= TAB_DEFS.length || nextIdx === activeTabIdx) return;
      const dir = nextIdx > activeTabIdx ? "next" : "prev";
      activeTabIdx = nextIdx;
      barApi.tabBtns.forEach((btn, i2) => btn.classList.toggle("active", i2 === nextIdx));
      tabViews.forEach((view, i2) => view.classList.toggle("is-active", i2 === nextIdx));
      animateTabLabel(topTabApi.nameEl, TAB_DEFS[nextIdx].label, dir);
      Storage.saveTabIndex(nextIdx, documentRef);
      if (nextIdx === 0) notifyCharacterViewerTabActive(characterViewerView);
    };
    const barApi = createFloatingBar(documentRef, onThemeToggle, setActiveTab, onClose);
    const topTabApi = createTopTab(documentRef);
    const panel = documentRef.createElement("div");
    panel.id = CONFIG.ids.panel;
    panel.setAttribute("role", "dialog");
    panel.setAttribute("aria-modal", "true");
    panel.setAttribute("aria-label", "Jan script panel");
    const body = documentRef.createElement("div");
    body.className = "jan-panel-body";
    const characterViewerView = createCharacterViewerTab({
      documentRef,
      backdropEl: backdrop,
      generateAlphaProxyConfig
    });
    const batchDownloadView = createBatchDownloadTab({ documentRef, groupEl: group, backdropEl: backdrop });
    const settingsView = createSettingsTab({ documentRef });
    const tabViews = [
      characterViewerView,
      batchDownloadView,
      settingsView
    ];
    tabViews[0].classList.add("is-active");
    tabViews.forEach((v) => body.appendChild(v));
    panel.appendChild(body);
    group.append(panel, topTabApi.el, barApi.bar);
    backdrop.appendChild(group);
    const savedIdx = Storage.loadTabIndex(documentRef);
    if (savedIdx > 0 && savedIdx < TAB_DEFS.length) {
      activeTabIdx = savedIdx;
      barApi.tabBtns.forEach((btn, i2) => btn.classList.toggle("active", i2 === savedIdx));
      tabViews.forEach((view, i2) => view.classList.toggle("is-active", i2 === savedIdx));
      topTabApi.nameEl.textContent = TAB_DEFS[savedIdx].label;
    }
    if (activeTabIdx === 0) {
      globalThis.setTimeout(() => notifyCharacterViewerTabActive(characterViewerView), 0);
    }
    return {
      backdrop,
      group,
      setActiveTab,
      characterViewerView,
      destroy: () => {
        tabViews.forEach((view) => {
          if (typeof view?.__janDestroy === "function") {
            try {
              view.__janDestroy();
            } catch {
            }
          }
        });
      }
    };
  }
  function openPanel({
    documentRef = globalThis.document,
    generateAlphaProxyConfig = null
  } = {}) {
    if (!documentRef?.body) return;
    if (state.isOpen) return state.panelRuntime;
    state.currentTheme = Storage.loadTheme(documentRef);
    if (state.activeAnim) {
      state.activeAnim.cancel();
      state.activeAnim = null;
    }
    state.elements.backdrop?.remove();
    state.panelRuntime = null;
    ensurePanelStyle(documentRef);
    const runtime = buildPanel(
      documentRef,
      () => closePanel({ documentRef }),
      { generateAlphaProxyConfig }
    );
    state.elements.backdrop = runtime.backdrop;
    state.elements.group = runtime.group;
    state.panelRuntime = runtime;
    state.isOpen = true;
    documentRef.body.appendChild(runtime.backdrop);
    animateIn(runtime.backdrop, runtime.group);
    return runtime;
  }
  async function openCharacterViewerPanel({
    documentRef = globalThis.document,
    characterInput = null,
    generateAlphaProxyConfig = null
  } = {}) {
    const runtime = openPanel({ documentRef, generateAlphaProxyConfig });
    if (!runtime) return false;
    if (characterInput) {
      unloadCharacterViewer(runtime.characterViewerView);
    }
    runtime.setActiveTab?.(0);
    if (characterInput) {
      return await loadCharacterViewerInput(runtime.characterViewerView, characterInput);
    }
    return true;
  }
  function closePanel({ documentRef = globalThis.document } = {}) {
    if (!state.isOpen || !state.elements.backdrop || !state.elements.group) return;
    state.isOpen = false;
    state.panelRuntime?.destroy?.();
    state.activeAnim = animateOut(state.elements.backdrop, state.elements.group, () => {
      state.elements.backdrop?.remove();
      state.elements.backdrop = null;
      state.elements.group = null;
      state.activeAnim = null;
      state.panelRuntime = null;
    });
  }
  function togglePanel(options) {
    state.isOpen ? closePanel(options) : openPanel(options);
  }
  const OPENER_BUTTON_ID = "jan-opener-button";
  const OPENER_STYLE_ID = "jan-opener-style";
  const RIGHT_ACTIONS_CONTAINER_SELECTOR = ".pp-top-bar-right > div";
  const HEADER_CONTAINER_SELECTOR = ".pp-top-bar-right";
  const CLEANUP_KEY = "__janOpenerCleanup__";
  const OPENER_ICON_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
  <g fill="none" stroke="currentColor" stroke-width="1.5">
    <circle cx="12" cy="12" r="3"/>
    <path d="M13.765 2.152C13.398 2 12.932 2 12 2s-1.398 0-1.765.152a2 2 0 0 0-1.083 1.083c-.092.223-.129.484-.143.863a1.62 1.62 0 0 1-.79 1.353a1.62 1.62 0 0 1-1.567.008c-.336-.178-.579-.276-.82-.308a2 2 0 0 0-1.478.396C4.04 5.79 3.806 6.193 3.34 7s-.7 1.21-.751 1.605a2 2 0 0 0 .396 1.479c.148.192.355.353.676.555c.473.297.777.803.777 1.361s-.304 1.064-.777 1.36c-.321.203-.529.364-.676.556a2 2 0 0 0-.396 1.479c.052.394.285.798.75 1.605c.467.807.7 1.21 1.015 1.453a2 2 0 0 0 1.479.396c.24-.032.483-.13.819-.308a1.62 1.62 0 0 1 1.567.008c.483.28.77.795.79 1.353c.014.38.05.64.143.863a2 2 0 0 0 1.083 1.083C10.602 22 11.068 22 12 22s1.398 0 1.765-.152a2 2 0 0 0 1.083-1.083c.092-.223.129-.483.143-.863c.02-.558.307-1.074.79-1.353a1.62 1.62 0 0 1 1.567-.008c.336.178.579.276.819.308a2 2 0 0 0 1.479-.396c.315-.242.548-.646 1.014-1.453s.7-1.21.751-1.605a2 2 0 0 0-.396-1.479c-.148-.192-.355-.353-.676-.555A1.62 1.62 0 0 1 19.562 12c0-.558.304-1.064.777-1.36c.321-.203.529-.364.676-.556a2 2 0 0 0 .396-1.479c-.052-.394-.285-.798-.75-1.605c-.467-.807-.7-1.21-1.015-1.453a2 2 0 0 0-1.479-.396c-.24.032-.483.13-.82.308a1.62 1.62 0 0 1-1.566-.008a1.62 1.62 0 0 1-.79-1.353c-.014-.38-.05-.64-.143-.863a2 2 0 0 0-1.083-1.083Z"/>
  </g>
</svg>
`;
  let activeRuntime = null;
  function ensureOpenerStyle(documentRef) {
    if (!documentRef?.head) return;
    if (documentRef.getElementById(OPENER_STYLE_ID)) return;
    const styleElement = documentRef.createElement("style");
    styleElement.id = OPENER_STYLE_ID;
    styleElement.textContent = `
    #${OPENER_BUTTON_ID}.jan-opener-button {
      align-items: center;
      background: none;
      border: none;
      color: inherit;
      cursor: pointer;
      display: inline-flex;
      flex: 0 0 auto;
      height: 38px;
      justify-content: center;
      margin-right: 8px;
      min-width: 38px;
      opacity: 0.55;
      padding: 0;
      transition: opacity 200ms ease;
      width: 38px;
    }
    #${OPENER_BUTTON_ID}.jan-opener-button:hover {
      opacity: 1;
    }
    #${OPENER_BUTTON_ID}.jan-opener-button:focus-visible {
      border-radius: 6px;
      outline: 2px solid rgba(255, 255, 255, 0.55);
      outline-offset: 2px;
    }
    #${OPENER_BUTTON_ID}.jan-opener-button svg {
      display: block;
      height: 22px;
      transform-origin: center;
      width: 22px;
    }
  `;
    documentRef.head.appendChild(styleElement);
  }
  function getRotationDeg(el, documentRef) {
    if (!el || !documentRef) return 0;
    const win = documentRef.defaultView || globalThis;
    if (typeof win.getComputedStyle !== "function") return 0;
    const matrix = win.getComputedStyle(el).transform;
    if (!matrix || matrix === "none") return 0;
    const m = matrix.match(/matrix\(([^)]+)\)/);
    if (!m) return 0;
    const [a, b] = m[1].split(",").map((value) => Number(value.trim()));
    if (!Number.isFinite(a) || !Number.isFinite(b)) return 0;
    const deg = Math.atan2(b, a) * (180 / Math.PI);
    return deg < 0 ? deg + 360 : deg;
  }
  function canAnimate(svg) {
    return typeof svg?.animate === "function";
  }
  function makeRewindKeyframes(fromAngle, totalAngle) {
    const MAX_STEP = 120;
    const steps = Math.ceil(totalAngle / MAX_STEP);
    return Array.from({ length: steps + 1 }, (_, i2) => ({
      transform: `rotate(${fromAngle - i2 / steps * totalAngle}deg)`,
      ...i2 < steps && { easing: "linear" }
    }));
  }
  function createOpenerButton(documentRef) {
    const button = documentRef.createElement("button");
    button.type = "button";
    button.id = OPENER_BUTTON_ID;
    button.className = "jan-opener-button";
    button.setAttribute("aria-label", "Open script UI");
    appendSvg(button, OPENER_ICON_SVG, documentRef);
    const getSvg = () => button.querySelector("svg");
    let forwardAnim = null;
    let rewindAnim = null;
    const handleMouseEnter = () => {
      const svg = getSvg();
      if (!canAnimate(svg)) return;
      const startAngle = getRotationDeg(svg, documentRef);
      if (rewindAnim) {
        rewindAnim.cancel();
        rewindAnim = null;
      }
      if (forwardAnim) {
        forwardAnim.cancel();
        forwardAnim = null;
      }
      forwardAnim = svg.animate(
        [
          { transform: `rotate(${startAngle}deg)` },
          { transform: `rotate(${startAngle + 360}deg)` }
        ],
        { duration: 3e3, iterations: Infinity, easing: "linear" }
      );
    };
    const handleMouseLeave = () => {
      if (!forwardAnim) return;
      const currentTime = forwardAnim.currentTime;
      if (typeof currentTime !== "number") {
        forwardAnim.cancel();
        forwardAnim = null;
        return;
      }
      const totalAngle = currentTime * (360 / 3e3);
      const visualAngle = totalAngle % 360;
      forwardAnim.cancel();
      forwardAnim = null;
      const svg = getSvg();
      if (!canAnimate(svg)) return;
      if (totalAngle < 0.5) return;
      const duration = Math.max(500, Math.min(1200, totalAngle / 360 * 300));
      rewindAnim = svg.animate(
        makeRewindKeyframes(visualAngle, totalAngle),
        { duration, easing: "cubic-bezier(0.33, 1, 0.68, 1)", fill: "none" }
      );
      rewindAnim.addEventListener("finish", () => {
        rewindAnim = null;
      });
    };
    const handleClick = (event) => {
      event.preventDefault();
      event.stopPropagation();
      togglePanel({ documentRef });
    };
    button.addEventListener("mouseenter", handleMouseEnter);
    button.addEventListener("mouseleave", handleMouseLeave);
    button.addEventListener("click", handleClick);
    button[CLEANUP_KEY] = () => {
      forwardAnim?.cancel();
      rewindAnim?.cancel();
      button.removeEventListener("mouseenter", handleMouseEnter);
      button.removeEventListener("mouseleave", handleMouseLeave);
      button.removeEventListener("click", handleClick);
    };
    return button;
  }
  function findRightActionsContainer(documentRef) {
    return documentRef.querySelector(RIGHT_ACTIONS_CONTAINER_SELECTOR);
  }
  function isElementNode(node) {
    return node?.nodeType === 1;
  }
  function isWithinHeader(node) {
    const element = isElementNode(node) ? node : node?.parentElement;
    return Boolean(element?.closest?.(HEADER_CONTAINER_SELECTOR));
  }
  function shouldScheduleEnsure(mutations) {
    for (const mutation of mutations) {
      if (mutation.type !== "childList") continue;
      if (isWithinHeader(mutation.target)) return true;
      const nodes = [...mutation.addedNodes, ...mutation.removedNodes];
      for (const node of nodes) {
        if (!isElementNode(node)) continue;
        const element = node;
        if (element.id === OPENER_BUTTON_ID) return true;
        if (element.matches?.(RIGHT_ACTIONS_CONTAINER_SELECTOR)) return true;
        if (element.matches?.(HEADER_CONTAINER_SELECTOR)) return true;
        if (element.querySelector?.(RIGHT_ACTIONS_CONTAINER_SELECTOR)) return true;
      }
    }
    return false;
  }
  function ensureHeaderOpener(documentRef) {
    const actionsContainer = findRightActionsContainer(documentRef);
    if (!actionsContainer) {
      const staleOpener = documentRef.getElementById(OPENER_BUTTON_ID);
      staleOpener?.[CLEANUP_KEY]?.();
      staleOpener?.remove();
      return null;
    }
    const existingOpenerButton = documentRef.getElementById(OPENER_BUTTON_ID);
    const firstChild = actionsContainer.firstElementChild;
    if (existingOpenerButton) {
      if (existingOpenerButton.parentElement !== actionsContainer) {
        actionsContainer.insertBefore(existingOpenerButton, firstChild);
      } else if (actionsContainer.firstElementChild !== existingOpenerButton) {
        actionsContainer.insertBefore(existingOpenerButton, firstChild);
      }
      return existingOpenerButton;
    }
    const openerButton = createOpenerButton(documentRef);
    actionsContainer.insertBefore(openerButton, firstChild);
    return openerButton;
  }
  function initHeaderOpener({ documentRef = globalThis.document } = {}) {
    if (!documentRef?.body || typeof MutationObserver !== "function") {
      return () => {
      };
    }
    if (activeRuntime?.documentRef === documentRef) {
      activeRuntime.ensure();
      return activeRuntime.destroy;
    }
    activeRuntime?.destroy?.();
    ensureOpenerStyle(documentRef);
    const ensure = () => {
      ensureOpenerStyle(documentRef);
      ensureHeaderOpener(documentRef);
    };
    ensure();
    const scheduleEnsure = createRafScheduler$1(
      ensure,
      documentRef?.defaultView ?? globalThis
    );
    const observer = new MutationObserver((mutations) => {
      if (shouldScheduleEnsure(mutations)) {
        scheduleEnsure();
      }
    });
    observer.observe(documentRef.body, {
      childList: true,
      subtree: true
    });
    const windowRef = documentRef.defaultView;
    windowRef?.addEventListener?.("popstate", scheduleEnsure);
    windowRef?.addEventListener?.("hashchange", scheduleEnsure);
    const destroy = () => {
      observer.disconnect();
      windowRef?.removeEventListener?.("popstate", scheduleEnsure);
      windowRef?.removeEventListener?.("hashchange", scheduleEnsure);
      const openerButton = documentRef.getElementById(OPENER_BUTTON_ID);
      openerButton?.[CLEANUP_KEY]?.();
      openerButton?.remove();
      documentRef.getElementById(OPENER_STYLE_ID)?.remove();
      if (activeRuntime?.destroy === destroy) {
        activeRuntime = null;
      }
    };
    activeRuntime = {
      documentRef,
      ensure,
      destroy
    };
    return destroy;
  }
  function destroyHeaderOpener() {
    activeRuntime?.destroy?.();
  }
  function autoInitHeaderOpener() {
    if (!globalThis.document) {
      return () => {
      };
    }
    return initHeaderOpener({ documentRef: globalThis.document });
  }
  const VIEWER_ICON_DEFAULT_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/></svg>`;
  const VIEWER_ICON_HOVER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"/><path d="m21 3-9 9"/><path d="M15 3h6v6"/></svg>`;
  const CARD_VIEWER_STYLE_ID = "jan-cv-btn-style";
  const CHAT_CARD_WRAPPER_SELECTOR = '[class*="_chatCard_"]';
  const CARD_WRAPPER_SELECTOR = [
    ".pp-cc-wrapper",
    ".profile-character-card-wrapper",
    CHAT_CARD_WRAPPER_SELECTOR
  ].join(", ");
  const CARD_VIEWER_REVEAL_SELECTOR = [
    ".pp-cc-wrapper:hover .jan-cv-btn",
    ".profile-character-card-wrapper:hover .jan-cv-btn",
    `${CHAT_CARD_WRAPPER_SELECTOR}:hover .jan-cv-btn`
  ].join(",\n    ");
  const CARD_VIEWER_STYLE = `
    .jan-cv-btn {
      appearance: none;
      -webkit-appearance: none;
      position: absolute;
      top: -20px;
      left: 50%;
      transform: translateX(-50%);
      box-sizing: border-box;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 30px;
      height: 30px;
      padding: 0;
      font-size: 14px;
      border-radius: 9999px;
      background: rgba(28, 28, 28, 0.92);
      color: rgba(255, 255, 255, 0.72);
      border: none;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
      cursor: pointer;
      opacity: 0;
      pointer-events: none;
      transition: opacity 120ms ease;
      z-index: 100;
      line-height: 0;
    }
    ${CARD_VIEWER_REVEAL_SELECTOR} {
      opacity: 1;
      pointer-events: auto;
    }
    .jan-cv-btn--chat-card {
      top: -20px;
      right: auto;
      left: 50%;
      transform: translateX(-50%);
    }
    .jan-cv-icon-default,
    .jan-cv-icon-hover {
      display: flex;
      align-items: center;
      justify-content: center;
      line-height: 0;
      transition: opacity 160ms ease;
    }
    .jan-cv-icon-hover {
      position: absolute;
      inset: 0;
      opacity: 0;
    }
    .jan-cv-btn:hover .jan-cv-icon-default { opacity: 0; }
    .jan-cv-btn:hover .jan-cv-icon-hover { opacity: 1; }
    .jan-cv-btn svg { display: block; pointer-events: none; }
  `;
  function getCardWrappers(scope) {
    return [...scope.querySelectorAll(CARD_WRAPPER_SELECTOR)];
  }
  function isCardWrapper(node) {
    return typeof node?.matches === "function" && node.matches(CARD_WRAPPER_SELECTOR);
  }
  function isChatCardWrapper(node) {
    return typeof node?.matches === "function" && node.matches(CHAT_CARD_WRAPPER_SELECTOR);
  }
  function hasViewerButton(wrapper) {
    if (!wrapper?.querySelector) return false;
    try {
      return Boolean(wrapper.querySelector(":scope > .jan-cv-btn"));
    } catch {
      return Boolean(wrapper.querySelector(".jan-cv-btn"));
    }
  }
  function injectButton$1(wrapper, documentRef) {
    if (hasViewerButton(wrapper)) return;
    const win = documentRef.defaultView ?? globalThis;
    const pos = win.getComputedStyle(wrapper).position;
    if (!pos || pos === "static") {
      wrapper.style.position = "relative";
    }
    const btn = documentRef.createElement("button");
    btn.className = "jan-cv-btn";
    if (isChatCardWrapper(wrapper)) {
      btn.classList.add("jan-cv-btn--chat-card");
    }
    btn.type = "button";
    btn.title = "Open in Character Viewer";
    btn.setAttribute("aria-label", "Open in Character Viewer");
    const iconDefault = documentRef.createElement("span");
    iconDefault.className = "jan-cv-icon-default";
    appendSvg(iconDefault, VIEWER_ICON_DEFAULT_SVG, documentRef);
    const iconHover = documentRef.createElement("span");
    iconHover.className = "jan-cv-icon-hover";
    appendSvg(iconHover, VIEWER_ICON_HOVER_SVG, documentRef);
    btn.append(iconDefault, iconHover);
    wrapper.appendChild(btn);
  }
  function injectAll(scope, documentRef) {
    if (isCardWrapper(scope) && hasCharacterLink(scope)) {
      injectButton$1(scope, documentRef);
    }
    getCardWrappers(scope).forEach((wrapper) => {
      if (!hasCharacterLink(wrapper)) return;
      injectButton$1(wrapper, documentRef);
    });
  }
  function shouldScanNode(node) {
    if (!isElementNode$1(node)) return false;
    if (isCardWrapper(node)) return true;
    return Boolean(node.querySelector?.(CARD_WRAPPER_SELECTOR));
  }
  async function handleViewerButtonClick(event) {
    const target = event?.target;
    const button = target?.closest?.(".jan-cv-btn");
    if (!button || button.disabled) return;
    const documentRef = button.ownerDocument ?? globalThis.document;
    const wrapper = button.closest?.(CARD_WRAPPER_SELECTOR);
    if (!wrapper) return;
    event.preventDefault();
    event.stopPropagation();
    const characterInput = getCharacterInputFromWrapper(wrapper, documentRef);
    if (!characterInput) return;
    await withBusyButton(
      button,
      () => openCharacterViewerPanel({
        documentRef,
        characterInput
      })
    );
  }
  let _observer$1 = null;
  let _retryCleanup$1 = null;
  let _controller$1 = null;
  let _active$1 = false;
  function initCardViewerButtons({ documentRef = globalThis.document } = {}) {
    if (_active$1) return;
    if (!documentRef) return;
    _active$1 = true;
    ensureStyle(documentRef, { id: CARD_VIEWER_STYLE_ID, cssText: CARD_VIEWER_STYLE });
    injectAll(documentRef, documentRef);
    _controller$1 = new AbortController();
    documentRef.addEventListener("click", handleViewerButtonClick, {
      capture: true,
      signal: _controller$1.signal
    });
    const scheduleInject = createRafScheduler(documentRef, () => {
      if (!_active$1) return;
      injectAll(documentRef, documentRef);
    });
    _observer$1 = createMutationObserver(documentRef, (mutations) => {
      for (const m of mutations) {
        if (m.type !== "childList") continue;
        for (const node of m.addedNodes) {
          if (!shouldScanNode(node)) continue;
          scheduleInject();
          return;
        }
      }
    });
    _retryCleanup$1 = scheduleRetries(
      [250, 750, 1500, 2500],
      () => injectAll(documentRef, documentRef),
      { isActive: () => _active$1 }
    );
  }
  function destroyCardViewerButtons({ documentRef = globalThis.document } = {}) {
    _active$1 = false;
    _observer$1?.disconnect();
    _observer$1 = null;
    _retryCleanup$1?.();
    _retryCleanup$1 = null;
    _controller$1?.abort();
    _controller$1 = null;
    removeStyle(documentRef, CARD_VIEWER_STYLE_ID);
    documentRef?.querySelectorAll?.(".jan-cv-btn")?.forEach((b) => b.remove());
  }
  function autoInitCardViewerButtons() {
    if (!globalThis.document) return () => {
    };
    initCardViewerButtons({ documentRef: globalThis.document });
    return () => destroyCardViewerButtons({ documentRef: globalThis.document });
  }
  const CHAR_PAGE_BTN_ID = "jan-char-page-cv-btn";
  const CHAR_PAGE_STYLE_ID = "jan-char-page-cv-btn-style";
  const CHAR_PAGE_STYLE = `
    /* Button — panel philosophy: transparent → subtle glass on hover */
    #${CHAR_PAGE_BTN_ID} {
      appearance: none;
      -webkit-appearance: none;
      background: transparent;
      border: none;
      border-radius: 6px;
      box-sizing: border-box;
      color: rgba(255, 255, 255, 0.65);
      cursor: pointer;
      display: inline-flex;
      align-items: center;
      gap: 8px;
      font-family: inherit;
      font-size: inherit;
      font-weight: 600;
      line-height: 1.2;
      padding: 8px 14px;
      transition: color 150ms ease, background 150ms ease;
      user-select: none;
      vertical-align: middle;
      white-space: nowrap;
    }
    #${CHAR_PAGE_BTN_ID}:hover {
      background: rgba(255, 255, 255, 0.08);
      color: #fff;
    }
    #${CHAR_PAGE_BTN_ID}:focus-visible {
      outline: 2px solid rgba(255, 255, 255, 0.40);
      outline-offset: 2px;
    }
    #${CHAR_PAGE_BTN_ID}:disabled {
      cursor: not-allowed;
      opacity: 0.5;
    }
    /* Icon wrap — stacks default + hover icons, crossfades on hover */
    .jan-cp-icon-wrap {
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      line-height: 0;
      position: relative;
      width: 1em;
      height: 1em;
    }
    .jan-cp-icon-default,
    .jan-cp-icon-hover {
      display: flex;
      align-items: center;
      justify-content: center;
      transition: opacity 160ms ease;
    }
    .jan-cp-icon-hover {
      inset: 0;
      opacity: 0;
      position: absolute;
    }
    #${CHAR_PAGE_BTN_ID} svg { display: block; pointer-events: none; }
    #${CHAR_PAGE_BTN_ID}:hover .jan-cp-icon-default { opacity: 0; }
    #${CHAR_PAGE_BTN_ID}:hover .jan-cp-icon-hover   { opacity: 1; }
  `;
  function findMoreButtonsContainer(documentRef) {
    for (const h2 of documentRef.querySelectorAll("h2.chakra-heading")) {
      if (h2.textContent.trim().toLowerCase() !== "more") continue;
      let el = h2.nextElementSibling;
      while (el) {
        if (el.tagName === "DIV") return el;
        el = el.nextElementSibling;
      }
    }
    return null;
  }
  function hasCharacterPageButton(container) {
    return Boolean(container?.querySelector?.(`#${CHAR_PAGE_BTN_ID}`));
  }
  function injectButton(container, characterInput, documentRef) {
    if (!container || hasCharacterPageButton(container)) return false;
    const btn = documentRef.createElement("button");
    btn.id = CHAR_PAGE_BTN_ID;
    btn.type = "button";
    btn.title = "Open in Character Viewer";
    btn.setAttribute("aria-label", "Open in Character Viewer");
    const iconWrap = documentRef.createElement("span");
    iconWrap.className = "jan-cp-icon-wrap";
    const iconDefault = documentRef.createElement("span");
    iconDefault.className = "jan-cp-icon-default";
    appendSvg(iconDefault, VIEWER_ICON_DEFAULT_SVG, documentRef);
    const iconHover = documentRef.createElement("span");
    iconHover.className = "jan-cp-icon-hover";
    appendSvg(iconHover, VIEWER_ICON_HOVER_SVG, documentRef);
    const label = documentRef.createElement("span");
    label.textContent = "Open in Character Viewer";
    iconWrap.append(iconDefault, iconHover);
    btn.append(iconWrap, label);
    container.insertBefore(btn, container.firstElementChild);
    return true;
  }
  function tryInject(documentRef) {
    const win = documentRef.defaultView ?? globalThis;
    const characterId = getCharacterIdFromPath(win.location?.pathname);
    if (!characterId) return false;
    const container = findMoreButtonsContainer(documentRef);
    if (!container) return false;
    return injectButton(container, buildCharacterInput(characterId, documentRef), documentRef);
  }
  async function handleCharacterPageButtonClick(event) {
    const target = event?.target;
    const button = target?.closest?.(`#${CHAR_PAGE_BTN_ID}`);
    if (!button || button.disabled) return;
    const documentRef = button.ownerDocument ?? globalThis.document;
    const win = documentRef.defaultView ?? globalThis;
    const characterId = getCharacterIdFromPath(win.location?.pathname);
    if (!characterId) return;
    event.preventDefault();
    event.stopPropagation();
    const characterInput = buildCharacterInput(characterId, documentRef);
    await withBusyButton(
      button,
      () => openCharacterViewerPanel({ documentRef, characterInput })
    );
  }
  let _observer = null;
  let _retryCleanup = null;
  let _controller = null;
  let _active = false;
  function initCharacterPageButton({ documentRef = globalThis.document } = {}) {
    if (_active) return;
    if (!documentRef) return;
    _active = true;
    ensureStyle(documentRef, { id: CHAR_PAGE_STYLE_ID, cssText: CHAR_PAGE_STYLE });
    tryInject(documentRef);
    _controller = new AbortController();
    documentRef.addEventListener("click", handleCharacterPageButtonClick, {
      capture: true,
      signal: _controller.signal
    });
    const scheduleInject = createRafScheduler(documentRef, () => {
      if (!_active) return;
      if (!documentRef.getElementById(CHAR_PAGE_BTN_ID)) {
        tryInject(documentRef);
      }
    });
    _observer = createMutationObserver(documentRef, (mutations) => {
      for (const m of mutations) {
        if (m.type !== "childList") continue;
        for (const node of m.addedNodes) {
          if (!isElementNode$1(node)) continue;
          scheduleInject();
          return;
        }
      }
    });
    _retryCleanup = scheduleRetries(
      [150, 500, 1e3, 2500],
      () => tryInject(documentRef),
      { isActive: () => _active }
    );
  }
  function destroyCharacterPageButton({ documentRef = globalThis.document } = {}) {
    _active = false;
    _observer?.disconnect();
    _observer = null;
    _retryCleanup?.();
    _retryCleanup = null;
    _controller?.abort();
    _controller = null;
    removeStyle(documentRef, CHAR_PAGE_STYLE_ID);
    documentRef?.getElementById?.(CHAR_PAGE_BTN_ID)?.remove();
  }
  function autoInitCharacterPageButton() {
    if (!globalThis.document) return () => {
    };
    initCharacterPageButton({ documentRef: globalThis.document });
    return () => destroyCharacterPageButton({ documentRef: globalThis.document });
  }
  if (typeof document !== "undefined") {
    autoInitHeaderOpener();
    autoInitCardViewerButtons();
    autoInitCharacterPageButton();
  }
  exports.autoInitCardViewerButtons = autoInitCardViewerButtons;
  exports.autoInitCharacterPageButton = autoInitCharacterPageButton;
  exports.autoInitHeaderOpener = autoInitHeaderOpener;
  exports.buildDownloadArtifact = buildDownloadArtifact;
  exports.buildExportContent = buildExportContent;
  exports.buildGenerateAlphaPayload = buildGenerateAlphaPayload;
  exports.buildSendMessageRequestBody = buildSendMessageRequestBody;
  exports.createCharacterViewerTab = createCharacterViewerTab;
  exports.createInternalOrchestrator = createInternalOrchestrator;
  exports.createJanitorClient = createJanitorClient;
  exports.destroyCardViewerButtons = destroyCardViewerButtons;
  exports.destroyCharacterPageButton = destroyCharacterPageButton;
  exports.destroyHeaderOpener = destroyHeaderOpener;
  exports.extractCharacterIdFromInput = extractCharacterIdFromInput;
  exports.initCardViewerButtons = initCardViewerButtons;
  exports.initCharacterPageButton = initCharacterPageButton;
  exports.initHeaderOpener = initHeaderOpener;
  exports.normalizeCharacterFields = normalizeCharacterFields;
  exports.parseGenerateAlphaFields = parseGenerateAlphaFields;
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
  return exports;
})({});