CtrlEm DB by Strateg

Adds DB shortcuts and a DB manager to CtrlEm command pages.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         CtrlEm DB by Strateg
// @namespace    https://discord.com/channels/1465036592262676601/1505167683107160156
// @version      1.3.0
// @description  Adds DB shortcuts and a DB manager to CtrlEm command pages.
// @license      MIT
// @match        https://ctrlem.com/*
// @connect      ctrlem.com
// @connect      api.imgbb.com
// @connect      i.ibb.co
// @connect      catbox.moe
// @connect      files.catbox.moe
// @connect      upload.vidhosting.in
// @connect      stream.vidhosting.in
// @connect      *
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_getValue
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// ==/UserScript==

(function() {
  'use strict';
	var LOG_PREFIX = "[CtrlEm DB]";
	var STORAGE_KEY = "ctrlemDbState.v1";
	var UI_STORAGE_KEY = "ctrlemDbUiState.v1";
	var UPLOADER_SETTINGS_STORAGE_KEY = "ctrlemDbUploaderSettings.v1";
	var AUTO_SEND_QUEUE_STORAGE_KEY = "ctrlemDbAutoSendQueue.v1";
	var INPUT_CATEGORY_NAME = "Input";
	var NO_PREVIEWS_MARKER = "- (no previews)";
	var USER_CONFIG = Object.freeze({
		autosaveDelayMs: 500,
		autoSend: Object.freeze({
			intervalDefaultSeconds: 5,
			intervalMinSeconds: 3,
			intervalMaxSeconds: 3600,
			minimumRequestIntervalDefaultSeconds: 3,
			minimumRequestIntervalMinSeconds: 1,
			minimumRequestIntervalMaxSeconds: 60,
			queueLockTtlMs: 1200,
			heartbeatMs: 2e3,
			heartbeatTimeoutMs: 9e3,
			runnerMs: 200,
			managerRefreshMs: 200
		}),
		imagePreviews: Object.freeze({ maxItems: 100 }),
		ui: Object.freeze({
			saveDelayMs: 120,
			scrollSaveDelayMs: 120,
			domObserverDelayMs: 120,
			modalCloseDelayMs: 250,
			uploadSessionCloseDelayMs: 700
		}),
		upload: Object.freeze({
			delayMs: 3e3,
			ctrlemImageMaxBytes: 5 * 1024 * 1024,
			ctrlemSoundMaxBytes: 15 * 1024 * 1024,
			imgbbMaxBytes: 32 * 1024 * 1024,
			vidhostingMaxBytes: 100 * 1024 * 1024,
			externalRequestTimeoutMs: 12e4
		}),
		linkCheck: Object.freeze({
			timeoutMs: 15e3,
			concurrency: 4
		}),
		imageCache: Object.freeze({
			fetchTimeoutMs: 6e4,
			pruneDelayMs: 2e3
		}),
		inputCapture: Object.freeze({
			maxTextLength: 200,
			countMin: 1,
			countMax: 5
		})
	});
	var AUTOSAVE_DELAY = USER_CONFIG.autosaveDelayMs;
	var AUTO_SEND_INTERVAL_DEFAULT = USER_CONFIG.autoSend.intervalDefaultSeconds;
	var AUTO_SEND_INTERVAL_MIN = USER_CONFIG.autoSend.intervalMinSeconds;
	var AUTO_SEND_INTERVAL_MAX = USER_CONFIG.autoSend.intervalMaxSeconds;
	var AUTO_SEND_MINIMUM_REQUEST_INTERVAL_DEFAULT = USER_CONFIG.autoSend.minimumRequestIntervalDefaultSeconds;
	var AUTO_SEND_MINIMUM_REQUEST_INTERVAL_MIN = USER_CONFIG.autoSend.minimumRequestIntervalMinSeconds;
	var AUTO_SEND_MINIMUM_REQUEST_INTERVAL_MAX = USER_CONFIG.autoSend.minimumRequestIntervalMaxSeconds;
	var IMAGE_PREVIEW_MAX_ITEMS = USER_CONFIG.imagePreviews.maxItems;
	var IMAGE_CACHE_DB_NAME = "ctrlem-image-cache";
	var IMAGE_CACHE_STORE = "images";
	var IMAGE_PLACEHOLDER_URL = "data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2264%22 height=%2264%22%3E%3C/svg%3E";
	var RecordType = Object.freeze({
		LINKS: "links",
		TEXT: "text",
		IMAGE: "image",
		SOUND: "sound",
		VIDEO: "video"
	});
	var TYPE_LABELS = Object.freeze({
		[RecordType.LINKS]: "Links",
		[RecordType.TEXT]: "Text",
		[RecordType.IMAGE]: "Images",
		[RecordType.SOUND]: "Sounds",
		[RecordType.VIDEO]: "Videos"
	});
	var TYPE_ORDER = Object.freeze([
		RecordType.LINKS,
		RecordType.TEXT,
		RecordType.IMAGE,
		RecordType.SOUND,
		RecordType.VIDEO
	]);
	var CONFIG = Object.freeze({
		debug: true,
		selectors: Object.freeze({
			commandsHead: ".panel--commands .panel-head",
			resultsPanel: ".panel--results",
			resultsContainer: "#responses-container",
			resultsPagination: "#responses-pagination"
		})
	});
	var UI_IDS = Object.freeze({
		dbButton: "ctrlem-db-open-button",
		dbManager: "ctrlem-db-manager",
		dbManagerStatus: "ctrlem-db-manager-status",
		dbManagerCategoryList: "ctrlem-db-manager-category-list",
		dbManagerTextarea: "ctrlem-db-manager-textarea",
		dbManagerImport: "ctrlem-db-manager-import",
		dbManagerImportAll: "ctrlem-db-manager-import-all",
		textPrefix: "ctrlem-db-text",
		mediaPrefix: "ctrlem-db-media",
		uploadPrefix: "ctrlem-db-upload",
		imgbbKeyModal: "ctrlem-db-imgbb-key-modal"
	});
	var TEXT_COMMANDS = Object.freeze({
		openPage: Object.freeze({
			key: "openPage",
			type: RecordType.LINKS,
			panelSelector: "#acc-openPage",
			inputSelector: "#val-openPage",
			label: "Link database",
			emptyText: "No links configured"
		}),
		sendMessage: Object.freeze({
			key: "sendMessage",
			type: RecordType.TEXT,
			panelSelector: "#acc-sendMessage",
			inputSelector: "#val-sendMessage",
			label: "Phrase database",
			emptyText: "No phrases configured"
		}),
		writeForMe: Object.freeze({
			key: "writeForMe",
			type: RecordType.TEXT,
			panelSelector: "#acc-writeForMe",
			inputSelector: "#val-writeForMe-text",
			label: "Phrase database",
			emptyText: "No phrases configured"
		}),
		videoOverlay: Object.freeze({
			key: "videoOverlay",
			type: RecordType.VIDEO,
			panelSelector: "#acc-videoOverlay",
			inputSelector: "#val-videoOverlay",
			label: "Video database",
			emptyText: "No videos configured"
		}),
		popupSound: Object.freeze({
			key: "popupSound",
			type: RecordType.SOUND,
			panelSelector: "#acc-popupSound",
			inputSelector: "#val-popupSound",
			label: "Sound database",
			emptyText: "No sounds configured"
		})
	});
	var MEDIA_COMMANDS = Object.freeze({
		popupImage: Object.freeze({
			key: "popupImage",
			type: RecordType.IMAGE,
			panelSelector: "#acc-popupImage",
			inputSelector: "#val-popupImage",
			dropzoneSelector: ".upload-dropzone[data-upload-for=\"popupImage\"]",
			gallerySelector: "#gallery-popupImage"
		}),
		changeWallpaper: Object.freeze({
			key: "changeWallpaper",
			type: RecordType.IMAGE,
			panelSelector: "#acc-changeWallpaper",
			inputSelector: "#val-changeWallpaper",
			dropzoneSelector: ".upload-dropzone[data-upload-for=\"changeWallpaper\"]",
			gallerySelector: "#gallery-changeWallpaper"
		})
	});
	var UPLOAD_COMMANDS = Object.freeze({
		popupImage: Object.freeze({
			key: "popupImage",
			label: "Popup Image",
			type: RecordType.IMAGE,
			panelSelector: "#acc-popupImage",
			inputSelector: "#val-popupImage",
			nativeDropzoneSelector: ".upload-dropzone[data-upload-for=\"popupImage\"]",
			tools: Object.freeze(["ctrlem", "imgbb"])
		}),
		changeWallpaper: Object.freeze({
			key: "changeWallpaper",
			label: "Change Wallpaper",
			type: RecordType.IMAGE,
			panelSelector: "#acc-changeWallpaper",
			inputSelector: "#val-changeWallpaper",
			nativeDropzoneSelector: ".upload-dropzone[data-upload-for=\"changeWallpaper\"]",
			tools: Object.freeze(["ctrlem", "imgbb"])
		}),
		popupSound: Object.freeze({
			key: "popupSound",
			label: "Popup Sound",
			type: RecordType.SOUND,
			panelSelector: "#acc-popupSound",
			inputSelector: "#val-popupSound",
			nativeDropzoneSelector: ".upload-dropzone[data-upload-for=\"popupSound\"]",
			tools: Object.freeze(["catbox"])
		}),
		videoOverlay: Object.freeze({
			key: "videoOverlay",
			label: "Video Overlay",
			type: RecordType.VIDEO,
			panelSelector: "#acc-videoOverlay",
			inputSelector: "#val-videoOverlay",
			tools: Object.freeze(["vidhosting", "catbox"])
		})
	});
	var ACTION_COMMANDS = Object.freeze({ sendOrDelete: Object.freeze({
		key: "sendOrDelete",
		panelSelector: "#acc-sendOrDelete",
		clickOnly: true
	}) });
	var INPUT_CAPTURE_COMMANDS = Object.freeze({
		openPage: Object.freeze({
			key: "openPage",
			type: RecordType.LINKS,
			inputSelector: "#val-openPage"
		}),
		sendMessage: Object.freeze({
			key: "sendMessage",
			type: RecordType.TEXT,
			inputSelector: "#val-sendMessage"
		}),
		writeForMe: Object.freeze({
			key: "writeForMe",
			type: RecordType.TEXT,
			inputSelector: "#val-writeForMe-text",
			countSelector: "#val-writeForMe-count"
		}),
		popupImage: Object.freeze({
			key: "popupImage",
			type: RecordType.IMAGE,
			inputSelector: "#val-popupImage"
		}),
		changeWallpaper: Object.freeze({
			key: "changeWallpaper",
			type: RecordType.IMAGE,
			inputSelector: "#val-changeWallpaper"
		}),
		popupSound: Object.freeze({
			key: "popupSound",
			type: RecordType.SOUND,
			inputSelector: "#val-popupSound"
		}),
		videoOverlay: Object.freeze({
			key: "videoOverlay",
			type: RecordType.VIDEO,
			inputSelector: "#val-videoOverlay"
		})
	});
	var AUTO_SEND_COMMAND_KEYS = Object.freeze([
		"openPage",
		"sendMessage",
		"writeForMe",
		"popupImage",
		"changeWallpaper",
		"popupSound",
		"videoOverlay",
		"sendOrDelete"
	]);
	function parseLines(rawText) {
		return String(rawText || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
	}
	function isNoPreviewsMarker(value) {
		return String(value || "").trim().toLowerCase() === NO_PREVIEWS_MARKER;
	}
	function hasNoPreviewsMarker(rawText) {
		const lines = parseLines(rawText);
		return lines.length > 0 && isNoPreviewsMarker(lines[0]);
	}
	function getCategoryDataLines(rawText) {
		const lines = parseLines(rawText);
		return hasNoPreviewsMarker(rawText) ? lines.slice(1) : lines;
	}
	function parseLabeledUrlLine(rawText) {
		const text = String(rawText || "").trim();
		if (!text) return null;
		const match = text.match(/^(\S+)(?:\s+([\s\S]+))?$/);
		if (!match) return null;
		const url = match[1].trim();
		const label = String(match[2] || "").trim();
		return {
			url,
			label,
			display: label || url
		};
	}
	function getLineItem(type, rawText) {
		if (type === RecordType.VIDEO || type === RecordType.SOUND) {
			const media = parseLabeledUrlLine(rawText);
			return media ? {
				value: media.url,
				label: media.label,
				display: media.display,
				title: String(rawText || "").trim()
			} : null;
		}
		const value = String(rawText || "").trim();
		return value ? {
			value,
			display: value,
			title: value
		} : null;
	}
	function isHttpUrl(value) {
		try {
			const url = new URL(String(value || "").trim());
			return url.protocol === "http:" || url.protocol === "https:";
		} catch {
			return false;
		}
	}
	function formatCategoryContent(rawText) {
		const lines = parseLines(rawText);
		return lines.length > 0 ? `${lines.join("\n")}\n` : "";
	}
	function clampAutoSendInterval(value) {
		const parsed = Number.parseInt(String(value), 10);
		if (!Number.isFinite(parsed)) return AUTO_SEND_INTERVAL_DEFAULT;
		return Math.min(AUTO_SEND_INTERVAL_MAX, Math.max(AUTO_SEND_INTERVAL_MIN, parsed));
	}
	function clampMinimumRequestInterval(value) {
		const parsed = Number.parseInt(String(value), 10);
		if (!Number.isFinite(parsed)) return AUTO_SEND_MINIMUM_REQUEST_INTERVAL_DEFAULT;
		return Math.min(AUTO_SEND_MINIMUM_REQUEST_INTERVAL_MAX, Math.max(AUTO_SEND_MINIMUM_REQUEST_INTERVAL_MIN, parsed));
	}
	function createId(prefix = "cat") {
		if (window.crypto?.randomUUID) return `${prefix}-${window.crypto.randomUUID()}`;
		return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
	}
	function clonePlain(value) {
		return JSON.parse(JSON.stringify(value));
	}
	function normalizeType(type) {
		return TYPE_ORDER.includes(type) ? type : RecordType.TEXT;
	}
	function getUploadImageUrl(uploadId) {
		return `${window.location.origin}/api/uploads/${encodeURIComponent(uploadId)}/image`;
	}
	function getLabelFromUrl(url, fallback = "Media") {
		try {
			const lastPart = new URL(url, window.location.href).pathname.split("/").filter(Boolean).pop();
			return lastPart ? decodeURIComponent(lastPart) : fallback;
		} catch {
			return fallback;
		}
	}
	function getSafeFileName(value, fallback) {
		return String(value || fallback || "db").trim().replace(/[<>:"/\\|?*\x00-\x1F]/g, "_").replace(/\s+/g, "_").slice(0, 80) || fallback || "db";
	}
	function getCategoryNameFromFileName(fileName) {
		return String(fileName || "").replace(/\.[^.]+$/, "").trim() || "Imported";
	}
	function normalizeMediaItem(value, fallbackTitle) {
		if (typeof value === "string") {
			const url = value.trim();
			return url ? {
				url,
				previewUrl: url,
				title: fallbackTitle || getLabelFromUrl(url)
			} : null;
		}
		const url = String(value?.url || "").trim();
		if (!url) return null;
		const uploadId = String(value?.uploadId || value?.id || "").trim();
		return {
			url,
			previewUrl: String(value?.previewUrl || value?.src || url).trim(),
			title: String(value?.title || fallbackTitle || getLabelFromUrl(url)).trim(),
			uploadId,
			canDelete: Boolean(value?.canDelete || uploadId || value?.deleteButton),
			deleteButton: value?.deleteButton instanceof Element ? value.deleteButton : null
		};
	}
	function uniqueMediaItems(items) {
		const byUrl = new Map();
		items.forEach((item) => {
			const normalized = normalizeMediaItem(item);
			if (!normalized) return;
			const existing = byUrl.get(normalized.url);
			if (!existing) {
				byUrl.set(normalized.url, normalized);
				return;
			}
			if (!existing.uploadId && normalized.uploadId) existing.uploadId = normalized.uploadId;
			if (!existing.previewUrl && normalized.previewUrl) existing.previewUrl = normalized.previewUrl;
			if (!existing.canDelete && normalized.canDelete) existing.canDelete = true;
			if (!existing.deleteButton && normalized.deleteButton) existing.deleteButton = normalized.deleteButton;
		});
		return Array.from(byUrl.values());
	}
	function uniqueStrings(values) {
		return Array.from(new Set(values.filter(Boolean)));
	}
	var defaultDb_default = "{  \"version\": 2,  \"exportedAt\": \"2026-05-29T16:16:27.363Z\",  \"autoSendIntervalSeconds\": 300,  \"minimumRequestIntervalSeconds\": 3,  \"types\": {    \"links\": [      {        \"name\": \"Hypnotube: Latex\",        \"content\": \"https://hypnotube.com/video/dreaming-latex-36543.html\\n\"      }    ],    \"text\": [      {        \"name\": \"Hello\",        \"content\": \"Hey there. Ready to play?\\nClearing screens is no fun at all!\\nI wish you would think less at work\\n\"      }    ],    \"image\": [      {        \"name\": \"Girls 2D\",        \"content\": \"https://i.ibb.co/Q7dvRLXj/popup-1778972568083.jpg\\nhttps://i.ibb.co/27XX01xX/ssdfsdf.png\\n\"      },      {        \"name\": \"Feets\",        \"content\": \"https://ctrlem.com/api/uploads/520be0f5-9c14-4714-8ce5-d6e3bf0b2381/image\\n\"      },      {        \"name\": \"Wallpapers\",        \"content\": \"https://i.ibb.co/TxDKQwPL/qxqmfk8.png\\n\"      }    ],    \"sound\": [      {        \"name\": \"Kisses\",        \"content\": \"https://files.catbox.moe/b8pqxq.mp3 23 sec kisses\\n\"      }    ],    \"video\": [      {        \"name\": \"Shiny\",        \"content\": \"https://stream.vidhosting.in/videos/cb6c7a99.mp4 Soundy strength\\n\"      },      {        \"name\": \"Thights\",        \"content\": \"https://stream.vidhosting.in/videos/cb6c7a99.mp4 Soundy strength\\n\"      },      {        \"name\": \"Goth\",        \"content\": \"https://stream.vidhosting.in/videos/0387432f.mp4 goth walk with sound and clothed tits\\n\"      },      {        \"name\": \"Voice\",        \"content\": \"https://stream.vidhosting.in/videos/6447b981.mp4 Triggers you\\n\"      }    ]  }}\n";
	var EMPTY_DEFAULT_DB = Object.freeze({ types: Object.freeze({}) });
	function readDefaultDb() {
		try {
			const parsed = JSON.parse(defaultDb_default);
			return parsed && typeof parsed === "object" ? parsed : EMPTY_DEFAULT_DB;
		} catch {
			return EMPTY_DEFAULT_DB;
		}
	}
	function createEmptyCategory(type) {
		return {
			id: createId(type),
			name: "Untitled",
			content: ""
		};
	}
	function createSeedState() {
		const defaultDb = readDefaultDb();
		const types = TYPE_ORDER.reduce((result, type) => {
			result[type] = (Array.isArray(defaultDb.types?.[type]) ? defaultDb.types[type] : []).map((category) => ({
				id: createId(type),
				name: String(category?.name || "Untitled").trim() || "Untitled",
				content: formatCategoryContent(category?.content)
			}));
			if (result[type].length === 0) result[type].push(createEmptyCategory(type));
			return result;
		}, {});
		return {
			version: 2,
			activeType: RecordType.LINKS,
			autoSave: true,
			autoSendIntervalSeconds: clampAutoSendInterval(defaultDb.autoSendIntervalSeconds),
			minimumRequestIntervalSeconds: clampMinimumRequestInterval(defaultDb.minimumRequestIntervalSeconds),
			types
		};
	}
	function normalizeCategory(category, type) {
		return {
			id: String(category?.id || createId(type)),
			name: String(category?.name || "Untitled").trim() || "Untitled",
			content: formatCategoryContent(category?.content)
		};
	}
	function normalizeDbState(rawState) {
		const seed = createSeedState();
		const source = rawState && typeof rawState === "object" ? rawState : {};
		const normalized = {
			version: 2,
			activeType: source.activeType ? normalizeType(source.activeType) : RecordType.LINKS,
			autoSave: source.autoSave !== false,
			autoSendIntervalSeconds: clampAutoSendInterval(source.autoSendIntervalSeconds),
			minimumRequestIntervalSeconds: clampMinimumRequestInterval(source.minimumRequestIntervalSeconds),
			types: {}
		};
		TYPE_ORDER.forEach((type) => {
			const rawCategories = Array.isArray(source.types?.[type]) ? source.types[type] : seed.types[type];
			normalized.types[type] = rawCategories.map((category) => normalizeCategory(category, type));
			if (normalized.types[type].length === 0) normalized.types[type].push(createEmptyCategory(type));
		});
		return normalized;
	}
	function resetManagerSelections(state, selection) {
		TYPE_ORDER.forEach((type) => {
			selection.selectedCategoryByType[type] = state.types[type][0]?.id || null;
		});
	}
	function getUserCategories(state, type) {
		const normalizedType = normalizeType(type);
		if (!Array.isArray(state.types[normalizedType])) state.types[normalizedType] = [];
		if (state.types[normalizedType].length === 0) state.types[normalizedType].push(createEmptyCategory(normalizedType));
		return state.types[normalizedType];
	}
	function normalizeAllCategoryContent(state) {
		TYPE_ORDER.forEach((type) => {
			(Array.isArray(state?.types?.[type]) ? state.types[type] : []).forEach((category) => {
				category.content = formatCategoryContent(category.content);
			});
		});
	}
	function getSelectedCategory(state, selection, type) {
		const categories = getUserCategories(state, type);
		const selectedId = selection.selectedCategoryByType[type];
		let category = categories.find((item) => item.id === selectedId);
		if (!category) {
			category = categories[0];
			selection.selectedCategoryByType[type] = category?.id || null;
		}
		return category || null;
	}
	function getCategories(state, type) {
		return getUserCategories(state, type).map((category) => ({
			id: category.id,
			name: category.name,
			content: category.content,
			items: parseLines(category.content)
		}));
	}
	function getDatabaseSummary(state) {
		return TYPE_ORDER.reduce((summary, type) => {
			const categories = getUserCategories(state, type);
			summary[type] = {
				categories: categories.length,
				items: categories.reduce((count, category) => count + parseLines(category.content).length, 0)
			};
			return summary;
		}, {});
	}
	function getStaticMediaCategories(state, type) {
		return getUserCategories(state, type).map((category) => ({
			id: category.id,
			name: category.name,
			disablePreviews: type === RecordType.IMAGE && hasNoPreviewsMarker(category.content),
			items: getCategoryDataLines(category.content).map((url) => normalizeMediaItem(url)).filter(Boolean)
		}));
	}
	function getRecordKey(type, value) {
		const trimmed = String(value || "").trim();
		if (type === RecordType.VIDEO || type === RecordType.SOUND) return parseLabeledUrlLine(trimmed)?.url || "";
		return trimmed;
	}
	function hasStoredValue(state, type, value) {
		const key = getRecordKey(type, value);
		if (!key) return true;
		return getUserCategories(state, type).some((category) => parseLines(category.content).some((line) => getRecordKey(type, line) === key));
	}
	function getOrCreateInputCategory(state, type) {
		const categories = getUserCategories(state, type);
		let category = categories.find((item) => item.name.toLowerCase() === INPUT_CATEGORY_NAME.toLowerCase());
		if (!category) {
			category = {
				id: createId(type),
				name: INPUT_CATEGORY_NAME,
				content: ""
			};
			categories.push(category);
		}
		return category;
	}
	function appendInputValue(state, selection, type, value) {
		const category = getOrCreateInputCategory(state, type);
		const lines = parseLines(category.content);
		const line = String(value || "").trim();
		const key = getRecordKey(type, line);
		if (key && !lines.some((item) => getRecordKey(type, item) === key)) {
			lines.push(line);
			category.content = formatCategoryContent(lines.join("\n"));
		}
		selection.selectedCategoryByType[type] ||= category.id;
		return category;
	}
	function makeUniqueCategoryName(state, type, name, ignoreId = "") {
		const base = String(name || "Untitled").trim() || "Untitled";
		const names = new Set(getUserCategories(state, type).filter((category) => category.id !== ignoreId).map((category) => category.name));
		if (!names.has(base)) return base;
		let index = 2;
		let candidate = `${base} (${index})`;
		while (names.has(candidate)) {
			index += 1;
			candidate = `${base} (${index})`;
		}
		return candidate;
	}
	function createExportPayload(state) {
		return {
			version: 2,
			exportedAt: new Date().toISOString(),
			autoSendIntervalSeconds: clampAutoSendInterval(state.autoSendIntervalSeconds),
			minimumRequestIntervalSeconds: clampMinimumRequestInterval(state.minimumRequestIntervalSeconds),
			types: TYPE_ORDER.reduce((result, type) => {
				result[type] = getUserCategories(state, type).map((category) => ({
					name: category.name,
					content: formatCategoryContent(category.content)
				}));
				return result;
			}, {})
		};
	}
	function parseImportFile(file, text, activeType, log) {
		const nameFromFile = getCategoryNameFromFileName(file.name);
		if (!file.name.toLowerCase().endsWith(".json")) return [{
			type: activeType,
			name: nameFromFile,
			content: text
		}];
		let parsed = null;
		try {
			parsed = JSON.parse(text);
		} catch (error) {
			log("error", "Invalid JSON import", {
				name: file.name,
				message: error?.message || String(error)
			});
			return [];
		}
		const additions = [];
		const pushCategory = (type, category, fallbackName) => {
			if (!category || typeof category !== "object") return;
			const content = category.content !== void 0 ? category.content : Array.isArray(category.items) ? category.items.join("\n") : "";
			additions.push({
				type,
				name: String(category.name || fallbackName || "Imported").trim() || "Imported",
				content: String(content || "")
			});
		};
		if (Array.isArray(parsed)) {
			parsed.forEach((category, index) => {
				pushCategory(category?.type || activeType, category, `${nameFromFile}-${index + 1}`);
			});
			return additions;
		}
		if (parsed?.types && typeof parsed.types === "object") {
			TYPE_ORDER.forEach((type) => {
				(Array.isArray(parsed.types[type]) ? parsed.types[type] : []).forEach((category, index) => pushCategory(type, category, `${TYPE_LABELS[type]}-${index + 1}`));
			});
			return additions;
		}
		if (parsed?.name || parsed?.content) {
			pushCategory(parsed.type || activeType, parsed, nameFromFile);
			return additions;
		}
		log("warn", "JSON import did not contain categories", { name: file.name });
		return additions;
	}
	var MANAGER_TABS = Object.freeze({
		EDITOR: "editor",
		SETTINGS: "settings",
		INFO: "info"
	});
	function normalizeManagerTab(value) {
		const tab = String(value || "");
		return Object.values(MANAGER_TABS).includes(tab) ? tab : MANAGER_TABS.EDITOR;
	}
	function createUiState() {
		return {
			version: 2,
			manager: {
				activeTab: MANAGER_TABS.EDITOR,
				selectedCategoryByType: {},
				categoryListScrollTop: 0
			},
			pickers: {},
			uploads: {}
		};
	}
	function normalizeStringMap(value) {
		return Object.entries(value && typeof value === "object" ? value : {}).reduce((result, [key, item]) => {
			result[key] = item == null ? "" : String(item);
			return result;
		}, {});
	}
	function normalizeUiState(rawState) {
		const source = rawState && typeof rawState === "object" ? rawState : {};
		const sourceVersion = Number(source.version) || 0;
		const manager = source.manager && typeof source.manager === "object" ? source.manager : {};
		const rawPickers = source.pickers && typeof source.pickers === "object" ? source.pickers : {};
		const rawUploads = source.uploads && typeof source.uploads === "object" ? source.uploads : {};
		return {
			version: 2,
			manager: {
				activeTab: normalizeManagerTab(manager.activeTab),
				selectedCategoryByType: normalizeStringMap(manager.selectedCategoryByType),
				categoryListScrollTop: Math.max(0, Number(manager.categoryListScrollTop) || 0)
			},
			pickers: Object.entries(rawPickers).reduce((result, [commandKey, value]) => {
				const picker = value && typeof value === "object" ? value : {};
				result[commandKey] = {
					categoryId: String(picker.categoryId || ""),
					categoryName: String(picker.categoryName || ""),
					value: String(picker.value || ""),
					itemIndex: Number.isFinite(Number(picker.itemIndex)) ? Math.max(-1, Number(picker.itemIndex)) : -1,
					scrollTop: Math.max(0, Number(picker.scrollTop) || 0)
				};
				return result;
			}, {}),
			uploads: Object.entries(rawUploads).reduce((result, [commandKey, value]) => {
				result[commandKey] = { collapsed: commandKey === "popupImage" && sourceVersion < 2 ? true : (value && typeof value === "object" ? value : {}).collapsed !== false };
				return result;
			}, {})
		};
	}
	function getPickerUiState(uiState, commandKey) {
		if (!uiState.pickers[commandKey]) uiState.pickers[commandKey] = {
			categoryId: "",
			categoryName: "",
			value: "",
			itemIndex: -1,
			scrollTop: 0
		};
		return uiState.pickers[commandKey];
	}
	function getUploadUiState(uiState, commandKey) {
		if (!uiState.uploads) uiState.uploads = {};
		if (!uiState.uploads[commandKey]) uiState.uploads[commandKey] = { collapsed: true };
		return uiState.uploads[commandKey];
	}
	function isCaptureValueValid(config, value) {
		if (!value) return false;
		if (config.type === RecordType.LINKS || config.type === RecordType.IMAGE || config.type === RecordType.SOUND || config.type === RecordType.VIDEO) return isHttpUrl(value);
		if (config.key !== "writeForMe") return true;
		const count = Number.parseInt(document.querySelector(config.countSelector)?.value || "1", 10);
		return value.length <= USER_CONFIG.inputCapture.maxTextLength && count >= USER_CONFIG.inputCapture.countMin && count <= USER_CONFIG.inputCapture.countMax;
	}
	function mountInputCapture(options) {
		const { captureInputValue, log } = options;
		if (document.documentElement.dataset.ctrlemDbInputCapture === "true") return false;
		document.documentElement.dataset.ctrlemDbInputCapture = "true";
		document.addEventListener("click", (event) => {
			if (!(event.target instanceof Element)) return;
			const button = event.target.closest("[data-send]");
			if (!button) return;
			if (button.disabled) return;
			const config = INPUT_CAPTURE_COMMANDS[button.dataset.send];
			if (!config) return;
			captureInputValue(config).catch((error) => {
				log("error", "Failed to capture input", {
					command: config.key,
					message: error?.message || String(error)
				});
			});
		}, true);
		log("debug", "Input capture mounted");
		return true;
	}
	function createElement(tagName, options = {}, children = []) {
		const element = document.createElement(tagName);
		if (options.id) element.id = options.id;
		if (options.className) element.className = options.className;
		if (options.text !== void 0) element.textContent = options.text;
		if (options.value !== void 0) element.value = options.value;
		if (options.title) element.title = options.title;
		if (options.type) element.type = options.type;
		if (options.checked !== void 0) element.checked = Boolean(options.checked);
		if (options.attrs) Object.entries(options.attrs).forEach(([name, value]) => {
			element.setAttribute(name, value);
		});
		if (options.dataset) Object.entries(options.dataset).forEach(([name, value]) => {
			element.dataset[name] = value;
		});
		children.forEach((child) => element.appendChild(child));
		return element;
	}
	function downloadTextFile(filename, content, mimeType) {
		const blob = new Blob([content], { type: mimeType });
		const url = URL.createObjectURL(blob);
		const link = createElement("a", { attrs: {
			href: url,
			download: filename
		} });
		document.body.appendChild(link);
		link.click();
		link.remove();
		window.setTimeout(() => URL.revokeObjectURL(url), USER_CONFIG.ui.modalCloseDelayMs);
	}
	function getMediaPickerId(commandKey) {
		return `${UI_IDS.mediaPrefix}-${commandKey}`;
	}
	function updateMediaPicker(picker, options) {
		return typeof picker?.ctrlemDbRefresh === "function" ? picker.ctrlemDbRefresh(options) : false;
	}
	function createMediaPicker(options) {
		const { config, categories, input, uiState = {}, onSelect, onDelete, onCategoryChange, onUiStateChange, onAddCategory, onRenameCategory, onPreviewToggle, setImagePreviewSource } = options;
		const picker = createElement("div", {
			id: getMediaPickerId(config.key),
			className: "ctrlem-db-media-picker",
			attrs: { "aria-label": `${config.key} DB picker` },
			dataset: {
				command: config.key,
				type: config.type
			}
		});
		const state = {
			categories,
			input,
			uiState,
			selectedValue: "",
			selectedIndex: -1,
			hasSelectedIndex: false
		};
		if (categories.length === 0) {
			picker.appendChild(createElement("div", {
				className: "ctrlem-db-empty",
				text: "No media configured"
			}));
			return picker;
		}
		const select = createElement("select", {
			id: `${getMediaPickerId(config.key)}-category`,
			className: "form-select ctrlem-db-media-select"
		});
		const grid = createElement("div", {
			className: "ctrlem-db-media-grid",
			attrs: { role: "list" }
		});
		const syncSelection = () => {
			state.selectedValue = String(state.uiState?.value || state.input.value || "").trim();
			state.selectedIndex = Number(state.uiState?.itemIndex);
			state.hasSelectedIndex = Number.isFinite(state.selectedIndex) && state.selectedIndex >= 0;
		};
		const populateSelect = () => {
			select.replaceChildren(...state.categories.map((category, index) => createElement("option", {
				text: `${category.name} (${category.items.length})`,
				attrs: { value: String(index) }
			})));
		};
		const addButton = createElement("button", {
			className: "ctrlem-db-category-tool-button",
			text: "+",
			title: "Add category",
			type: "button",
			attrs: { "aria-label": "Add category" }
		});
		const renameButton = createElement("button", {
			className: "ctrlem-db-category-tool-button",
			text: "✎",
			title: "Edit category",
			type: "button",
			attrs: { "aria-label": "Edit category" }
		});
		const previewToggle = createElement("input", {
			className: "ctrlem-db-preview-toggle-input",
			type: "checkbox",
			checked: true,
			attrs: { "aria-label": "" }
		});
		const previewControl = createElement("label", {
			className: "ctrlem-db-preview-toggle",
			title: "Show previews",
			dataset: { tooltip: "Show previews" }
		}, [previewToggle, createElement("span", { text: "" })]);
		const toolbarChildren = config.type === RecordType.IMAGE ? [
			select,
			previewControl,
			addButton,
			renameButton
		] : [
			select,
			addButton,
			renameButton
		];
		addButton.addEventListener("click", () => onAddCategory?.());
		renameButton.addEventListener("click", () => {
			const category = state.categories[Number(select.value) || 0];
			if (category?.id && category.id !== "default") onRenameCategory?.(category.id);
		});
		previewToggle.addEventListener("change", () => {
			const categoryIndex = Number(select.value) || 0;
			const category = state.categories[categoryIndex];
			if (!category?.id || category.id === "default" || category.items.length > IMAGE_PREVIEW_MAX_ITEMS) return;
			category.disablePreviews = !previewToggle.checked;
			renderMediaCategory(categoryIndex);
			onPreviewToggle?.(category.id, previewToggle.checked);
		});
		picker.appendChild(createElement("div", { className: "ctrlem-db-media-toolbar ctrlem-db-category-toolbar" }, toolbarChildren));
		picker.appendChild(grid);
		const getCategoryIndex = () => {
			const byId = state.categories.findIndex((category) => category.id && category.id === state.uiState?.categoryId);
			if (byId >= 0) return byId;
			const byName = state.categories.findIndex((category) => category.name === state.uiState?.categoryName);
			if (byName >= 0) return byName;
			const byValue = state.categories.findIndex((category) => category.items.some((item) => item?.url === state.selectedValue));
			return byValue >= 0 ? byValue : 0;
		};
		const renderMediaCategory = (categoryIndex, renderOptions = {}) => {
			syncSelection();
			const category = state.categories[categoryIndex];
			const scrollTop = renderOptions.scrollTop;
			picker.dataset.categoryId = category?.id || "";
			picker.dataset.category = category?.name || "";
			renameButton.disabled = Boolean(!category || category.id === "default");
			if (config.type === RecordType.IMAGE) {
				const isTooLarge = Boolean(category && category.items.length > IMAGE_PREVIEW_MAX_ITEMS);
				previewToggle.checked = Boolean(category && !category.disablePreviews && !isTooLarge);
				previewToggle.disabled = Boolean(!category || category.id === "default" || isTooLarge);
				previewControl.title = isTooLarge ? `Disabled: ${IMAGE_PREVIEW_MAX_ITEMS}+ files` : "Show previews";
				previewControl.dataset.tooltip = previewControl.title;
			}
			if (!category || category.items.length === 0) {
				grid.replaceChildren(createElement("div", {
					className: "ctrlem-db-empty",
					text: "No media in this category"
				}));
				if (scrollTop !== void 0) grid.scrollTop = Math.max(0, Number(scrollTop) || 0);
				return;
			}
			const reusableTiles = new Map(Array.from(grid.querySelectorAll(".ctrlem-db-media-tile")).map((tile) => [tile.dataset.itemKey, tile]));
			const tiles = category.items.map((item, itemIndex) => {
				const tileOptions = {
					item,
					itemIndex,
					config,
					category,
					input: state.input,
					selectedValue: state.selectedValue,
					selectedIndex: state.selectedIndex,
					hasSelectedIndex: state.hasSelectedIndex,
					onDelete,
					setImagePreviewSource
				};
				const reusableTile = reusableTiles.get(getImageTileKey(category, item, itemIndex));
				if (canReuseImageTile(reusableTile, category, item)) {
					updateImageTile(reusableTile, tileOptions);
					return reusableTile;
				}
				return createImageTile(tileOptions);
			});
			grid.replaceChildren(...tiles);
			if (scrollTop !== void 0) grid.scrollTop = Math.max(0, Number(scrollTop) || 0);
		};
		select.addEventListener("change", () => {
			const categoryIndex = Number(select.value) || 0;
			renderMediaCategory(categoryIndex);
			grid.scrollTop = 0;
			const category = state.categories[categoryIndex];
			onUiStateChange?.({
				categoryId: category?.id || "",
				categoryName: category?.name || "",
				itemIndex: -1,
				scrollTop: 0
			});
			onCategoryChange?.({
				command: config.key,
				type: config.type,
				category: category?.name
			});
		});
		picker.ctrlemDbRefresh = (nextOptions = {}) => {
			const nextCategories = Array.isArray(nextOptions.categories) ? nextOptions.categories : [];
			if (!canReconcileCategoryStructure(state.categories) || !canReconcileCategoryStructure(nextCategories)) return false;
			if (nextOptions.input && nextOptions.input !== state.input) return false;
			const scrollTop = grid.scrollTop;
			state.categories = nextCategories;
			state.uiState = nextOptions.uiState || {};
			syncSelection();
			populateSelect();
			const categoryIndex = getCategoryIndex();
			select.value = String(categoryIndex);
			renderMediaCategory(categoryIndex, { scrollTop });
			return true;
		};
		let scrollTimer = 0;
		grid.addEventListener("scroll", () => {
			window.clearTimeout(scrollTimer);
			scrollTimer = window.setTimeout(() => {
				onUiStateChange?.({ scrollTop: grid.scrollTop });
			}, USER_CONFIG.ui.scrollSaveDelayMs);
		});
		grid.addEventListener("click", (event) => {
			if (!(event.target instanceof Element)) return;
			const button = event.target.closest(".ctrlem-db-media-tile");
			if (!button || !grid.contains(button)) return;
			onSelect({
				input: state.input,
				picker,
				button
			});
		});
		grid.addEventListener("keydown", (event) => {
			if (event.key !== "Enter" && event.key !== " ") return;
			if (!(event.target instanceof Element)) return;
			if (event.target.closest(".ctrlem-db-media-delete")) return;
			const button = event.target.closest(".ctrlem-db-media-tile");
			if (!button || !grid.contains(button)) return;
			event.preventDefault();
			onSelect({
				input: state.input,
				picker,
				button
			});
		});
		populateSelect();
		syncSelection();
		const initialCategoryIndex = getCategoryIndex();
		select.value = String(initialCategoryIndex);
		renderMediaCategory(initialCategoryIndex);
		window.setTimeout(() => {
			grid.scrollTop = Math.max(0, Number(state.uiState?.scrollTop) || 0);
		}, 0);
		return picker;
	}
	function getCategoryKey(category) {
		return String(category?.id || category?.name || "").trim();
	}
	function canReconcileCategoryStructure(categories) {
		if (!Array.isArray(categories) || categories.length === 0) return false;
		const keys = categories.map((category) => getCategoryKey(category));
		return keys.every(Boolean) && new Set(keys).size === keys.length;
	}
	function getImageTileKey(category, item, itemIndex) {
		return `${getCategoryKey(category)}|${item?.url || ""}|${itemIndex}`;
	}
	function shouldRenderImagePreview(category) {
		return !category.disablePreviews && category.items.length <= IMAGE_PREVIEW_MAX_ITEMS;
	}
	function canReuseImageTile(tile, category, item) {
		if (!tile) return false;
		if (tile.dataset.previewEnabled !== String(shouldRenderImagePreview(category))) return false;
		if (tile.dataset.canDelete === "true") return false;
		return tile.dataset.url === item.url;
	}
	function createImageTile(options) {
		const { item, itemIndex, config, category, input, selectedValue, selectedIndex, hasSelectedIndex, onDelete, setImagePreviewSource } = options;
		const shouldRenderPreview = shouldRenderImagePreview(category);
		const itemKey = getImageTileKey(category, item, itemIndex);
		const previewSource = item.previewUrl || item.url;
		const canDelete = Boolean(category.isDefault && item.canDelete);
		const children = shouldRenderPreview ? [createElement("img", {
			className: "ctrlem-db-media-img",
			attrs: {
				src: IMAGE_PLACEHOLDER_URL,
				alt: item.title,
				loading: "lazy",
				referrerpolicy: "no-referrer"
			}
		})] : [createElement("span", {
			className: "ctrlem-db-media-url-label",
			text: item.title || item.url
		})];
		if (canDelete) children.push(createElement("button", {
			className: "ctrlem-db-media-delete",
			text: "x",
			title: "Delete image",
			type: "button",
			attrs: { "aria-label": "Delete image" }
		}));
		const tile = createElement("div", {
			className: `ctrlem-db-media-tile ctrlem-db-media-tile-image${shouldRenderPreview ? "" : " no-preview"}${canDelete ? " has-delete" : ""}`,
			title: item.title,
			attrs: {
				role: "button",
				tabindex: "0",
				"aria-label": item.title
			},
			dataset: {
				command: config.key,
				type: config.type,
				categoryId: category.id || "",
				category: category.name,
				source: category.isDefault ? "default" : "saved",
				url: item.url,
				index: String(itemIndex),
				itemKey,
				previewEnabled: String(shouldRenderPreview),
				previewSource,
				loadedPreviewSource: previewSource,
				canDelete: String(canDelete)
			}
		}, children);
		tile.querySelector(".ctrlem-db-media-delete")?.addEventListener("click", (event) => {
			event.preventDefault();
			event.stopPropagation();
			onDelete({
				item,
				tile
			});
		});
		if ((selectedValue || input.value.trim()) === item.url && (!hasSelectedIndex || selectedIndex === itemIndex)) tile.classList.add("is-selected");
		const image = tile.querySelector(".ctrlem-db-media-img");
		if (image) setImagePreviewSource(image, previewSource, item.url);
		return tile;
	}
	function updateImageTile(tile, options) {
		const { item, itemIndex, config, category, input, selectedValue, selectedIndex, hasSelectedIndex, setImagePreviewSource } = options;
		const shouldRenderPreview = shouldRenderImagePreview(category);
		const previewSource = item.previewUrl || item.url;
		tile.title = item.title;
		tile.setAttribute("aria-label", item.title);
		tile.dataset.command = config.key;
		tile.dataset.type = config.type;
		tile.dataset.categoryId = category.id || "";
		tile.dataset.category = category.name;
		tile.dataset.source = category.isDefault ? "default" : "saved";
		tile.dataset.url = item.url;
		tile.dataset.index = String(itemIndex);
		tile.dataset.itemKey = getImageTileKey(category, item, itemIndex);
		tile.dataset.previewEnabled = String(shouldRenderPreview);
		tile.dataset.previewSource = previewSource;
		tile.dataset.canDelete = "false";
		tile.classList.toggle("is-selected", (selectedValue || input.value.trim()) === item.url && (!hasSelectedIndex || selectedIndex === itemIndex));
		const image = tile.querySelector(".ctrlem-db-media-img");
		if (image) {
			image.alt = item.title;
			if (tile.dataset.loadedPreviewSource !== previewSource) {
				setImagePreviewSource(image, previewSource, item.url);
				tile.dataset.loadedPreviewSource = previewSource;
			}
			return;
		}
		const label = tile.querySelector(".ctrlem-db-media-url-label");
		if (label) label.textContent = item.title || item.url;
	}
	function getTextPickerId(commandKey) {
		return `${UI_IDS.textPrefix}-${commandKey}`;
	}
	function createTextPicker(options) {
		const { config, input, categories, uiState = {}, onSelect, onPreview, onCategoryChange, onUiStateChange, onAddCategory, onRenameCategory } = options;
		const pickerId = getTextPickerId(config.key);
		if (categories.length === 0) return createElement("div", {
			id: pickerId,
			className: "ctrlem-db-empty",
			text: config.emptyText
		});
		const list = createElement("div", {
			id: pickerId,
			className: "ctrlem-db-phrase-list ctrlem-db-text-picker",
			attrs: { "aria-label": config.label },
			dataset: {
				command: config.key,
				type: config.type
			}
		});
		const select = createElement("select", {
			id: `${pickerId}-category`,
			className: "form-select ctrlem-db-text-select"
		});
		const rows = createElement("div", {
			className: "ctrlem-db-rows",
			attrs: { role: "list" }
		});
		const selectedValue = String(uiState.value || input.value || "").trim();
		const selectedIndex = Number(uiState.itemIndex);
		const hasSelectedIndex = Number.isFinite(selectedIndex) && selectedIndex >= 0;
		categories.forEach((category, categoryIndex) => {
			select.appendChild(createElement("option", {
				text: `${category.name} (${category.items.length})`,
				attrs: { value: String(categoryIndex) }
			}));
		});
		const addButton = createElement("button", {
			className: "ctrlem-db-category-tool-button",
			text: "+",
			title: "Add category",
			type: "button",
			attrs: { "aria-label": "Add category" }
		});
		const renameButton = createElement("button", {
			className: "ctrlem-db-category-tool-button",
			text: "✎",
			title: "Edit category",
			type: "button",
			attrs: { "aria-label": "Edit category" }
		});
		addButton.addEventListener("click", () => onAddCategory?.());
		renameButton.addEventListener("click", () => {
			const category = categories[Number(select.value) || 0];
			if (category?.id) onRenameCategory?.(category.id);
		});
		list.appendChild(createElement("div", { className: "ctrlem-db-text-toolbar ctrlem-db-category-toolbar" }, [
			select,
			addButton,
			renameButton
		]));
		list.appendChild(rows);
		const getCategoryIndex = () => {
			const byId = categories.findIndex((category) => category.id && category.id === uiState.categoryId);
			if (byId >= 0) return byId;
			const byName = categories.findIndex((category) => category.name === uiState.categoryName);
			if (byName >= 0) return byName;
			const byValue = categories.findIndex((category) => category.items.some((line) => {
				return getLineItem(config.type, line)?.value === selectedValue;
			}));
			return byValue >= 0 ? byValue : 0;
		};
		const renderCategory = (categoryIndex) => {
			const category = categories[categoryIndex];
			rows.replaceChildren();
			list.dataset.categoryIndex = String(categoryIndex);
			list.dataset.categoryId = category?.id || "";
			list.dataset.category = category?.name || "";
			if (!category || category.items.length === 0) {
				rows.appendChild(createElement("div", {
					className: "ctrlem-db-empty",
					text: config.emptyText
				}));
				return;
			}
			category.items.forEach((line, itemIndex) => {
				const item = getLineItem(config.type, line);
				if (!item) return;
				const hasPreview = Boolean(onPreview && (config.type === RecordType.SOUND || config.type === RecordType.VIDEO));
				const button = createElement(hasPreview ? "div" : "button", {
					className: `ctrlem-db-row${hasPreview ? " has-preview" : ""}`,
					title: item.title,
					type: hasPreview ? void 0 : "button",
					attrs: hasPreview ? {
						role: "button",
						tabindex: "0"
					} : { role: "listitem" },
					dataset: {
						command: config.key,
						type: config.type,
						value: item.value,
						label: item.label || "",
						index: String(itemIndex),
						categoryId: category.id || "",
						category: category.name
					}
				}, [createElement("span", {
					className: "ctrlem-db-row-label",
					text: item.display
				})]);
				if (hasPreview) button.appendChild(createElement("button", {
					className: "ctrlem-db-row-preview",
					text: "Preview",
					title: `Preview ${item.display}`,
					type: "button"
				}));
				if (selectedValue && item.value === selectedValue && (!hasSelectedIndex || selectedIndex === itemIndex)) button.classList.add("is-selected");
				rows.appendChild(button);
			});
			if (!rows.querySelector(".ctrlem-db-row")) rows.appendChild(createElement("div", {
				className: "ctrlem-db-empty",
				text: config.emptyText
			}));
		};
		select.addEventListener("change", () => {
			const categoryIndex = Number(select.value) || 0;
			renderCategory(categoryIndex);
			rows.scrollTop = 0;
			const category = categories[categoryIndex];
			onUiStateChange?.({
				categoryId: category?.id || "",
				categoryName: category?.name || "",
				itemIndex: -1,
				scrollTop: 0
			});
			onCategoryChange?.({
				command: config.key,
				type: config.type,
				category: category?.name
			});
		});
		let scrollTimer = 0;
		rows.addEventListener("scroll", () => {
			window.clearTimeout(scrollTimer);
			scrollTimer = window.setTimeout(() => {
				onUiStateChange?.({ scrollTop: rows.scrollTop });
			}, USER_CONFIG.ui.scrollSaveDelayMs);
		});
		list.addEventListener("click", (event) => {
			if (!(event.target instanceof Element)) return;
			const previewButton = event.target.closest(".ctrlem-db-row-preview");
			if (previewButton && list.contains(previewButton)) {
				const button = previewButton.closest(".ctrlem-db-row");
				if (button) onPreview?.({
					input,
					list,
					button
				});
				return;
			}
			const button = event.target.closest(".ctrlem-db-row");
			if (!button || !list.contains(button)) return;
			onSelect({
				input,
				list,
				button
			});
		});
		list.addEventListener("keydown", (event) => {
			if (event.key !== "Enter" && event.key !== " ") return;
			if (!(event.target instanceof Element)) return;
			if (event.target.closest(".ctrlem-db-row-preview")) return;
			const button = event.target.closest(".ctrlem-db-row");
			if (!button || !list.contains(button)) return;
			if (!button.classList.contains("has-preview")) return;
			event.preventDefault();
			onSelect({
				input,
				list,
				button
			});
		});
		const initialCategoryIndex = getCategoryIndex();
		select.value = String(initialCategoryIndex);
		renderCategory(initialCategoryIndex);
		window.setTimeout(() => {
			rows.scrollTop = Math.max(0, Number(uiState.scrollTop) || 0);
		}, 0);
		return list;
	}
	var MANAGER_ID = "ctrlem-db-autosend-manager";
	var TOAST_BOTTOM_PROPERTY = "--ctrlem-db-toast-bottom";
	var LOCK_TTL_MS = USER_CONFIG.autoSend.queueLockTtlMs;
	var HEARTBEAT_MS = USER_CONFIG.autoSend.heartbeatMs;
	var HEARTBEAT_TIMEOUT_MS = USER_CONFIG.autoSend.heartbeatTimeoutMs;
	var RUNNER_MS = USER_CONFIG.autoSend.runnerMs;
	var NATIVE_SEND_DISABLED_GRACE_MS = 1e3;
	var NATIVE_SEND_REQUEST_GRACE_MS = 250;
	var SEND_SETTLE_MS = 1500;
	var RATE_LIMIT_BACKOFF_MIN_MS = 5e3;
	var RATE_LIMIT_BACKOFF_MAX_MS = 2e4;
	var TASK_STATUS = Object.freeze({
		PENDING: "pending",
		SENDING: "sending"
	});
	var AutoSendController = class {
		options;
		states = new Map();
		manualTasks = new Map();
		instanceId = `tab-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
		managerTimer = 0;
		heartbeatTimer = 0;
		runnerTimer = 0;
		bypassSendButton = null;
		unlockObserver = null;
		rateLimitObserver = null;
		sendRequestCaptureDepth = 0;
		nativeSendRequestsInFlight = 0;
		nativeSendBusyUntil = 0;
		nextSendAllowedAt = 0;
		activeSendTaskId = "";
		rateLimitBackoffUntil = 0;
		managerCollapsed = false;
		constructor(options) {
			this.options = options;
			window.addEventListener("storage", (event) => {
				if (event.key !== "ctrlemDbAutoSendQueue.v1") return;
				this.applyStoredQueue(this.parseStoredQueue(event.newValue));
			});
			document.addEventListener("click", (event) => this.captureManualSend(event), true);
			window.addEventListener("beforeunload", () => this.removeOwnedTasks("tab unloaded"));
			this.installNativeSendTracker();
			this.startHeartbeat();
			this.startRunner();
			this.startButtonUnlocker();
			this.startRateLimitObserver();
		}
		getCommandConfig(commandKey) {
			return TEXT_COMMANDS[commandKey] || MEDIA_COMMANDS[commandKey] || ACTION_COMMANDS[commandKey] || null;
		}
		getIntervalMs() {
			return clampAutoSendInterval(this.options.getState()?.autoSendIntervalSeconds) * 1e3;
		}
		getIntervalSeconds() {
			return clampAutoSendInterval(this.options.getState()?.autoSendIntervalSeconds);
		}
		getMinimumRequestIntervalMs() {
			return clampMinimumRequestInterval(this.options.getState()?.minimumRequestIntervalSeconds) * 1e3;
		}
		getMinimumRequestIntervalSeconds() {
			return clampMinimumRequestInterval(this.options.getState()?.minimumRequestIntervalSeconds);
		}
		cleanText(value) {
			return String(value || "").replace(/\s+/g, " ").trim();
		}
		getProfileTitle() {
			const heading = document.querySelector(".profile-details h1");
			if (!heading) return this.getPageCode();
			const clone = heading.cloneNode(true);
			clone.querySelectorAll(".verified-badge, [title=\"Verified\"]").forEach((element) => element.remove());
			return this.cleanText(clone.textContent) || this.getPageCode();
		}
		getPageCode() {
			const code = String(document.querySelector(".profile-controlcode code")?.textContent || "").trim();
			if (code) return code;
			const pathMatch = window.location.pathname.match(/\/u\/([^/?#]+)/i);
			if (pathMatch?.[1]) return decodeURIComponent(pathMatch[1]);
			return document.title.match(/^([A-Z0-9]{3,})\b/i)?.[1] || "Current page";
		}
		getPageKey() {
			const path = window.location.pathname.replace(/\/+$/, "") || "/";
			return `${window.location.origin}${path}`;
		}
		getPageUrl() {
			return window.location.href;
		}
		getSendType(config, kind) {
			if (kind === "manual") return "Manual";
			if (config?.type && TYPE_LABELS[config.type]) return `Auto ${TYPE_LABELS[config.type]}`;
			return "Auto Action";
		}
		getTaskCategory(config, items) {
			if (items[0]?.category) return items[0].category;
			const picker = this.getCommandPicker(config);
			return String(picker?.dataset?.category || "").trim() || config.label || config.key;
		}
		getCommandPicker(config) {
			if (config.clickOnly) return null;
			return document.getElementById(getTextPickerId(config.key)) || document.getElementById(getMediaPickerId(config.key));
		}
		getVisibleItems(config) {
			const picker = this.getCommandPicker(config);
			if (!picker) return [];
			const selector = config.type === RecordType.IMAGE ? ".ctrlem-db-media-tile" : ".ctrlem-db-row";
			return Array.from(picker.querySelectorAll(selector)).map((element) => ({
				commandKey: config.key,
				type: config.type || "",
				value: element.dataset.value || element.dataset.url || "",
				categoryId: element.dataset.categoryId || picker.dataset.categoryId || "",
				category: element.dataset.category || picker.dataset.category || "",
				index: Number(element.dataset.index || -1),
				source: element.dataset.source || "saved",
				isSelected: element.classList.contains("is-selected")
			})).filter((item) => item.value);
		}
		getStartIndex(config, input, items) {
			const uiState = this.options.getPickerUiState?.(config.key) || {};
			const savedValue = String(uiState.value || input?.dataset?.ctrlemDbSelectedValue || input?.value || "").trim();
			const savedIndex = Number(uiState.itemIndex);
			if (Number.isFinite(savedIndex) && savedIndex >= 0 && items[savedIndex]?.value === savedValue) return savedIndex;
			const selectedIndex = items.findIndex((item) => item.isSelected);
			if (selectedIndex >= 0) return selectedIndex;
			const valueIndex = items.findIndex((item) => item.value === savedValue);
			return valueIndex >= 0 ? valueIndex : 0;
		}
		getItemSelector(config) {
			return config.type === RecordType.IMAGE ? ".ctrlem-db-media-tile" : ".ctrlem-db-row";
		}
		getItemValue(element) {
			return element.dataset.value || element.dataset.url || "";
		}
		findLiveItemElement(config, picker, item) {
			const selector = this.getItemSelector(config);
			const elements = Array.from(picker.querySelectorAll(selector));
			const exact = elements.find((element) => this.getItemValue(element) === item.value && (element.dataset.categoryId || picker.dataset.categoryId || "") === item.categoryId && Number(element.dataset.index || -1) === item.index);
			if (exact) return exact;
			return elements.find((element) => this.getItemValue(element) === item.value && (element.dataset.categoryId || picker.dataset.categoryId || "") === item.categoryId) || null;
		}
		selectItem(config, item, options = {}) {
			const input = document.querySelector(config.inputSelector);
			const picker = this.getCommandPicker(config);
			if (!input || !picker) return false;
			const element = this.findLiveItemElement(config, picker, item);
			if (!element) return false;
			if (element.classList.contains("ctrlem-db-media-tile")) {
				this.options.selectMediaItem(input, picker, element, options);
				return true;
			}
			this.options.selectTextItem(input, picker, element, options);
			return true;
		}
		setButtonState(button, isActive) {
			if (!button) return;
			button.classList.toggle("is-active", isActive);
			button.setAttribute("aria-pressed", String(isActive));
			button.title = isActive ? "Stop auto-send" : "Start auto-send";
		}
		setCommandButtons(commandKey, isActive) {
			document.querySelectorAll(`.ctrlem-db-auto-send-button[data-command="${commandKey}"]`).forEach((button) => {
				this.setButtonState(button, isActive);
			});
		}
		parseStoredQueue(raw) {
			if (!raw) return this.createQueueState();
			try {
				const parsed = JSON.parse(raw);
				return this.normalizeQueueState(parsed);
			} catch {
				return this.createQueueState();
			}
		}
		createQueueState() {
			return {
				tasks: [],
				lastSentAt: 0,
				nextSendAllowedAt: 0,
				activeSendTaskId: "",
				activeSendOwnerId: "",
				activeSendStartedAt: 0,
				activeSendSettleAt: 0,
				rateLimitBackoffUntil: 0,
				lock: null,
				heartbeats: {},
				updatedAt: Date.now()
			};
		}
		getHeartbeatTime(value) {
			if (value && typeof value === "object") return Math.max(0, Number(value.at) || 0);
			return Math.max(0, Number(value) || 0);
		}
		getHeartbeatPageKey(value) {
			if (value && typeof value === "object") return String(value.pageKey || "").trim();
			return "";
		}
		normalizeTask(task, index) {
			const createdAt = Math.max(0, Number(task.createdAt) || Date.now());
			const sequence = Number.isFinite(Number(task.sequence)) ? Number(task.sequence) : createdAt + index;
			const hasTaskInterval = task.taskIntervalSeconds !== void 0 && task.taskIntervalSeconds !== null && task.taskIntervalSeconds !== "";
			return {
				...task,
				id: String(task.id || ""),
				key: String(task.key || ""),
				kind: String(task.kind || ""),
				ownerId: String(task.ownerId || ""),
				commandKey: String(task.commandKey || ""),
				sendType: String(task.sendType || task.kind || ""),
				pageKey: String(task.pageKey || "").trim(),
				pageUrl: String(task.pageUrl || task.pageKey || "").trim(),
				pageCode: String(task.pageCode || "").trim(),
				categoryId: String(task.categoryId || ""),
				category: String(task.category || ""),
				itemValue: String(task.itemValue || ""),
				itemIndex: Number.isFinite(Number(task.itemIndex)) ? Number(task.itemIndex) : -1,
				nextIndex: Number.isFinite(Number(task.nextIndex)) ? Number(task.nextIndex) : -1,
				taskIntervalSeconds: clampAutoSendInterval(hasTaskInterval ? task.taskIntervalSeconds : this.getIntervalSeconds()),
				createdAt,
				dueAt: Math.max(0, Number(task.dueAt) || 0),
				sequence,
				status: task.status === TASK_STATUS.SENDING ? TASK_STATUS.SENDING : TASK_STATUS.PENDING,
				attemptedAt: Math.max(0, Number(task.attemptedAt) || 0),
				attemptOwnerId: String(task.attemptOwnerId || ""),
				nextIndexAfterAttempt: Number.isFinite(Number(task.nextIndexAfterAttempt)) ? Number(task.nextIndexAfterAttempt) : -1
			};
		}
		normalizeQueueState(queue) {
			const source = queue && typeof queue === "object" ? queue : {};
			const heartbeats = source.heartbeats && typeof source.heartbeats === "object" ? source.heartbeats : {};
			const lastSentAt = Math.max(0, Number(source.lastSentAt) || 0);
			const migratedNextAllowedAt = lastSentAt > 0 ? lastSentAt + this.getMinimumRequestIntervalMs() : 0;
			return {
				tasks: Array.isArray(source.tasks) ? source.tasks.map((task, index) => this.normalizeTask(task, index)).filter((task) => task.id && task.key && task.kind && task.commandKey) : [],
				lastSentAt,
				nextSendAllowedAt: Math.max(0, Number(source.nextSendAllowedAt) || migratedNextAllowedAt),
				activeSendTaskId: String(source.activeSendTaskId || ""),
				activeSendOwnerId: String(source.activeSendOwnerId || ""),
				activeSendStartedAt: Math.max(0, Number(source.activeSendStartedAt) || 0),
				activeSendSettleAt: Math.max(0, Number(source.activeSendSettleAt) || 0),
				rateLimitBackoffUntil: Math.max(0, Number(source.rateLimitBackoffUntil) || 0),
				lock: source.lock && typeof source.lock === "object" ? source.lock : null,
				heartbeats,
				updatedAt: Math.max(0, Number(source.updatedAt) || 0)
			};
		}
		getStoredQueue() {
			return this.parseStoredQueue(window.localStorage.getItem(AUTO_SEND_QUEUE_STORAGE_KEY));
		}
		writeQueue(queue) {
			queue.updatedAt = Date.now();
			window.localStorage.setItem(AUTO_SEND_QUEUE_STORAGE_KEY, JSON.stringify(queue));
			this.applyStoredQueue(queue);
		}
		cleanStaleTasks(queue, now = Date.now()) {
			let changed = false;
			const heartbeats = queue.heartbeats || {};
			Object.keys(heartbeats).forEach((ownerId) => {
				if (now - this.getHeartbeatTime(heartbeats[ownerId]) > HEARTBEAT_TIMEOUT_MS) {
					delete heartbeats[ownerId];
					changed = true;
				}
			});
			const before = queue.tasks.length;
			queue.tasks = queue.tasks.filter((task) => {
				if (task.kind !== "manual") return true;
				return now - this.getHeartbeatTime(heartbeats[task.ownerId]) <= HEARTBEAT_TIMEOUT_MS;
			});
			const activeTask = queue.activeSendTaskId ? queue.tasks.find((task) => task.id === queue.activeSendTaskId) : null;
			const activeOwnerId = String(queue.activeSendOwnerId || activeTask?.attemptOwnerId || activeTask?.ownerId || "");
			const activeStartedAt = Number(queue.activeSendStartedAt || activeTask?.attemptedAt || 0);
			const activeOwnerStale = activeOwnerId && activeOwnerId !== this.instanceId && now - this.getHeartbeatTime(heartbeats[activeOwnerId]) > HEARTBEAT_TIMEOUT_MS && now - activeStartedAt > HEARTBEAT_TIMEOUT_MS;
			if (queue.activeSendTaskId && (!activeTask || activeOwnerStale)) {
				if (activeTask?.status === TASK_STATUS.SENDING) {
					activeTask.status = TASK_STATUS.PENDING;
					activeTask.dueAt = Math.max(now, this.getQueueNextSendAllowedAt(queue));
					activeTask.attemptOwnerId = "";
				}
				this.clearActiveSend(queue);
				changed = true;
			}
			return changed || before !== queue.tasks.length;
		}
		tryQueueLock(callback) {
			const now = Date.now();
			const token = createId("lock");
			const ownerId = this.instanceId;
			for (let attempt = 0; attempt < 3; attempt += 1) {
				const base = this.getStoredQueue();
				const lock = base.lock;
				if (lock?.ownerId && lock.ownerId !== ownerId && Number(lock.expiresAt || 0) > now) return false;
				base.lock = {
					ownerId,
					token,
					expiresAt: now + LOCK_TTL_MS
				};
				window.localStorage.setItem(AUTO_SEND_QUEUE_STORAGE_KEY, JSON.stringify(base));
				const locked = this.getStoredQueue();
				if (locked.lock?.ownerId !== ownerId || locked.lock?.token !== token) continue;
				this.cleanStaleTasks(locked);
				callback(locked);
				if (locked.lock?.ownerId === ownerId && locked.lock?.token === token) locked.lock = null;
				this.writeQueue(locked);
				return true;
			}
			return false;
		}
		startHeartbeat() {
			const beat = () => {
				this.tryQueueLock((queue) => {
					queue.heartbeats ||= {};
					queue.heartbeats[this.instanceId] = {
						at: Date.now(),
						pageKey: this.getPageKey()
					};
				});
			};
			beat();
			this.heartbeatTimer = window.setInterval(beat, HEARTBEAT_MS);
		}
		getRequestUrl(input) {
			if (typeof input === "string") return input;
			if (input instanceof URL) return input.href;
			return input.url || "";
		}
		shouldTrackNativeSendRequest(input) {
			if (this.sendRequestCaptureDepth <= 0) return false;
			try {
				return new URL(this.getRequestUrl(input), window.location.href).origin === window.location.origin;
			} catch {
				return false;
			}
		}
		beginNativeSendRequest() {
			this.nativeSendRequestsInFlight += 1;
			this.nativeSendBusyUntil = Math.max(this.nativeSendBusyUntil, Date.now() + NATIVE_SEND_REQUEST_GRACE_MS);
		}
		endNativeSendRequest() {
			this.nativeSendRequestsInFlight = Math.max(0, this.nativeSendRequestsInFlight - 1);
			this.nativeSendBusyUntil = Date.now() + NATIVE_SEND_REQUEST_GRACE_MS;
		}
		isNativeSendBusy(now = Date.now()) {
			return this.nativeSendRequestsInFlight > 0 || now < this.nativeSendBusyUntil;
		}
		getRateLimitBackoffMs() {
			const base = this.getMinimumRequestIntervalMs() * 2;
			return Math.max(RATE_LIMIT_BACKOFF_MIN_MS, Math.min(RATE_LIMIT_BACKOFF_MAX_MS, base));
		}
		markRateLimited() {
			const now = Date.now();
			const backoffUntil = now + this.getRateLimitBackoffMs();
			let retryQueued = false;
			this.tryQueueLock((queue) => {
				const task = this.getRetryableAttemptTask(queue);
				if (!task) return;
				queue.rateLimitBackoffUntil = Math.max(Number(queue.rateLimitBackoffUntil || 0), backoffUntil);
				queue.nextSendAllowedAt = Math.max(Number(queue.nextSendAllowedAt || 0), backoffUntil);
				this.rateLimitBackoffUntil = Math.max(this.rateLimitBackoffUntil, backoffUntil);
				this.nativeSendBusyUntil = Math.max(this.nativeSendBusyUntil, backoffUntil);
				this.requeueRateLimitedTask(queue, task, backoffUntil);
				retryQueued = true;
				this.options.log("warn", "Auto-send rate limited; task will retry", {
					command: task.commandKey,
					kind: task.kind,
					retryInMs: backoffUntil - now
				});
			});
			if (!retryQueued) this.nativeSendBusyUntil = Math.max(this.nativeSendBusyUntil, now + NATIVE_SEND_REQUEST_GRACE_MS);
		}
		isRateLimitText(text) {
			const normalized = text.toLowerCase();
			return normalized.includes("too frequent") || normalized.includes("too many requests") || normalized.includes("rate limit") || normalized.includes("слишком част") || normalized.includes("частые запрос");
		}
		scanNodeForRateLimit(node) {
			const text = String(node.textContent || "").trim();
			if (text && this.isRateLimitText(text)) this.markRateLimited();
		}
		installNativeSendTrackerOn(targetWindow) {
			if (!targetWindow || targetWindow.__ctrlemDbNativeSendTrackerInstalled) return;
			targetWindow.__ctrlemDbNativeSendTrackerInstalled = true;
			const nativeFetch = targetWindow.fetch?.bind(targetWindow);
			if (nativeFetch) targetWindow.fetch = ((input, init) => {
				const tracked = this.shouldTrackNativeSendRequest(input);
				if (tracked) this.beginNativeSendRequest();
				return nativeFetch(input, init).finally(() => {
					if (tracked) this.endNativeSendRequest();
				});
			});
			const xhrPrototype = targetWindow.XMLHttpRequest?.prototype;
			if (!xhrPrototype) return;
			const nativeOpen = xhrPrototype.open;
			const nativeSend = xhrPrototype.send;
			const controller = this;
			xhrPrototype.open = function open(method, url, ...args) {
				this.__ctrlemDbRequestUrl = url;
				nativeOpen.call(this, method, url, ...args);
			};
			xhrPrototype.send = function send(...args) {
				if (controller.shouldTrackNativeSendRequest(this.__ctrlemDbRequestUrl || window.location.href)) {
					controller.beginNativeSendRequest();
					this.addEventListener("loadend", () => controller.endNativeSendRequest(), { once: true });
				}
				nativeSend.apply(this, args);
			};
		}
		installNativeSendTracker() {
			this.installNativeSendTrackerOn(window);
			const unsafeWindowRef = globalThis.unsafeWindow;
			if (unsafeWindowRef && unsafeWindowRef !== window) this.installNativeSendTrackerOn(unsafeWindowRef);
		}
		startRunner() {
			if (this.runnerTimer) return;
			this.runnerTimer = window.setInterval(() => {
				this.unlockSendButtons();
				this.processDueQueue();
			}, RUNNER_MS);
		}
		unlockSendButton(button) {
			if (!button?.matches?.("[data-send]")) return;
			if (button.disabled || button.hasAttribute("disabled")) {
				this.nativeSendBusyUntil = Math.max(this.nativeSendBusyUntil, Date.now() + NATIVE_SEND_DISABLED_GRACE_MS);
				button.disabled = false;
			}
			if (button.hasAttribute("disabled")) button.removeAttribute("disabled");
			if (button.style?.opacity === "0.5") button.style.opacity = "";
			if (button.style?.pointerEvents === "none") button.style.pointerEvents = "";
			button.classList?.remove("disabled", "is-disabled");
			button.setAttribute("aria-disabled", "false");
		}
		unlockSendButtons() {
			document.querySelectorAll("[data-send]").forEach((button) => this.unlockSendButton(button));
		}
		scheduleSendButtonUnlock(button) {
			[
				0,
				50,
				150,
				300,
				600,
				1e3,
				2e3
			].forEach((delay) => {
				window.setTimeout(() => {
					if (button?.isConnected) this.unlockSendButton(button);
					this.unlockSendButtons();
				}, delay);
			});
		}
		startButtonUnlocker() {
			this.unlockSendButtons();
			if (this.unlockObserver) return;
			this.unlockObserver = new MutationObserver((mutations) => {
				let shouldUnlock = false;
				mutations.forEach((mutation) => {
					if (shouldUnlock) return;
					const target = mutation.target;
					if (target?.matches?.("[data-send]")) {
						shouldUnlock = true;
						return;
					}
					if (target?.querySelector?.("[data-send]")) {
						shouldUnlock = true;
						return;
					}
					mutation.addedNodes.forEach((node) => {
						if (shouldUnlock || node.nodeType !== Node.ELEMENT_NODE) return;
						shouldUnlock = Boolean(node.matches?.("[data-send]") || node.querySelector?.("[data-send]"));
					});
				});
				if (shouldUnlock) window.setTimeout(() => this.unlockSendButtons(), 0);
			});
			this.unlockObserver.observe(document.documentElement, {
				subtree: true,
				childList: true,
				attributes: true,
				attributeFilter: [
					"disabled",
					"style",
					"class"
				]
			});
		}
		startRateLimitObserver() {
			if (this.rateLimitObserver) return;
			this.rateLimitObserver = new MutationObserver((mutations) => {
				mutations.forEach((mutation) => {
					if (mutation.type === "characterData") this.scanNodeForRateLimit(mutation.target);
					mutation.addedNodes.forEach((node) => this.scanNodeForRateLimit(node));
				});
			});
			this.rateLimitObserver.observe(document.body || document.documentElement, {
				subtree: true,
				childList: true,
				characterData: true
			});
		}
		stop(commandKey, reason = "stopped", options = {}) {
			const state = this.states.get(commandKey);
			if (!state) return;
			state.stopped = true;
			this.states.delete(commandKey);
			this.setButtonState(state.button, false);
			this.setCommandButtons(commandKey, false);
			if (options.publish !== false) this.removeTask(state.taskId);
			this.options.log("info", "Auto-send stopped", {
				command: commandKey,
				reason
			});
		}
		stopAll(reason = "stopped", options = {}) {
			Array.from(this.states.keys()).forEach((commandKey) => this.stop(commandKey, reason, options));
		}
		removeTask(taskId) {
			this.tryQueueLock((queue) => {
				queue.tasks = queue.tasks.filter((task) => task.id !== taskId);
				if (queue.activeSendTaskId === taskId || this.activeSendTaskId === taskId) this.clearActiveSend(queue);
			});
		}
		removeOwnedTasks(reason = "stopped") {
			this.stopAll(reason, { publish: false });
			this.manualTasks.clear();
			this.tryQueueLock((queue) => {
				queue.tasks = queue.tasks.filter((task) => task.kind !== "manual" || task.ownerId !== this.instanceId);
				if (queue.activeSendOwnerId === this.instanceId || this.activeSendTaskId) this.clearActiveSend(queue);
				if (queue.heartbeats) delete queue.heartbeats[this.instanceId];
			});
		}
		buildTaskKey(pageKey, commandKey, sendType) {
			return `${pageKey}::${commandKey}::${sendType}`;
		}
		taskMatchesCurrentPage(task) {
			const pageKey = String(task?.pageKey || "").trim();
			if (pageKey && pageKey === this.getPageKey()) return true;
			return !pageKey && String(task?.pageCode || "").trim() === this.getPageCode();
		}
		taskSortValue(task) {
			return Number(task?.sequence ?? task?.createdAt ?? 0) || 0;
		}
		sortReadyTasks(tasks) {
			return [...tasks].sort((a, b) => {
				const manualPriority = (a.kind === "manual" ? 0 : 1) - (b.kind === "manual" ? 0 : 1);
				if (manualPriority !== 0) return manualPriority;
				const dueDiff = Number(a.dueAt || 0) - Number(b.dueAt || 0);
				if (dueDiff !== 0) return dueDiff;
				return this.taskSortValue(a) - this.taskSortValue(b);
			});
		}
		getNextSequence(queue) {
			return Math.max(0, ...queue.tasks.map((task) => this.taskSortValue(task))) + 1;
		}
		isOwnerAlive(queue, ownerId, now = Date.now()) {
			if (ownerId === this.instanceId) return true;
			return now - this.getHeartbeatTime(queue.heartbeats?.[ownerId]) <= HEARTBEAT_TIMEOUT_MS;
		}
		isTaskPageActive(queue, task, now = Date.now()) {
			if (this.taskMatchesCurrentPage(task)) return true;
			const taskPageKey = String(task?.pageKey || "").trim();
			if (!taskPageKey) return false;
			return Object.values(queue.heartbeats || {}).some((heartbeat) => this.getHeartbeatPageKey(heartbeat) === taskPageKey && now - this.getHeartbeatTime(heartbeat) <= HEARTBEAT_TIMEOUT_MS);
		}
		findQueuedAutoTask(queue, config, sendType) {
			return queue.tasks.find((task) => task.kind === "auto" && task.commandKey === config.key && (task.sendType === sendType || !task.sendType) && this.taskMatchesCurrentPage(task));
		}
		ensureTaskCategoryVisible(config, task) {
			if (config.clickOnly || !task?.category) return;
			const picker = this.getCommandPicker(config);
			if (!picker || picker.dataset.category === task.category) return;
			const select = picker.querySelector("select");
			if (!select) return;
			const option = Array.from(select.options).find((item) => String(item.textContent || "").replace(/\s+\(\d+\)$/, "") === task.category);
			if (!option || select.value === option.value) return;
			select.value = option.value;
			select.dispatchEvent(new Event("change", { bubbles: true }));
		}
		start(config, sendButton, button) {
			const input = config.inputSelector ? document.querySelector(config.inputSelector) : null;
			const items = config.clickOnly ? [] : this.getVisibleItems(config);
			if (!config.clickOnly && !input) {
				this.options.notifySite("Auto-send input was not found", "error");
				return;
			}
			if (!config.clickOnly && items.length === 0) {
				this.options.notifySite("Auto-send category is empty", "error");
				return;
			}
			if (this.states.has(config.key)) {
				this.stop(config.key);
				return;
			}
			const taskId = createId("autosend");
			const category = this.getTaskCategory(config, items);
			const pageKey = this.getPageKey();
			const pageUrl = this.getPageUrl();
			const pageCode = this.getPageCode();
			const sendType = this.getSendType(config, "auto");
			const taskKey = this.buildTaskKey(pageKey, config.key, sendType);
			const startIndex = config.clickOnly ? 0 : this.getStartIndex(config, input, items);
			const startItem = config.clickOnly ? null : items[startIndex % items.length];
			const state = {
				taskId,
				commandKey: config.key,
				config,
				sendButton,
				button,
				items,
				nextIndex: startIndex,
				category,
				profileTitle: this.getProfileTitle(),
				pageKey,
				pageUrl,
				pageCode,
				stopped: false
			};
			let added = false;
			this.tryQueueLock((queue) => {
				const existing = this.findQueuedAutoTask(queue, config, sendType);
				if (existing) {
					state.taskId = existing.id;
					state.nextIndex = Number.isFinite(Number(existing.nextIndex)) && Number(existing.nextIndex) >= 0 ? Number(existing.nextIndex) : startIndex;
					existing.key ||= taskKey;
					existing.pageKey ||= pageKey;
					existing.pageUrl ||= pageUrl;
					existing.pageCode ||= pageCode;
					existing.profileTitle = state.profileTitle;
					existing.category ||= category;
					existing.categoryId ||= startItem?.categoryId || "";
					existing.taskIntervalSeconds = clampAutoSendInterval(existing.taskIntervalSeconds || this.getIntervalSeconds());
					added = true;
					return;
				}
				const now = Date.now();
				queue.tasks.push({
					id: taskId,
					key: taskKey,
					kind: "auto",
					commandKey: config.key,
					sendType,
					pageKey,
					pageUrl,
					pageCode,
					categoryId: startItem?.categoryId || "",
					category,
					profileTitle: state.profileTitle,
					itemValue: startItem?.value || "",
					itemIndex: Number.isFinite(Number(startItem?.index)) ? Number(startItem.index) : startIndex,
					nextIndex: startIndex,
					taskIntervalSeconds: this.getIntervalSeconds(),
					createdAt: now,
					dueAt: now,
					sequence: this.getNextSequence(queue),
					status: TASK_STATUS.PENDING
				});
				added = true;
			});
			if (!added) {
				this.options.notifySite("Auto-send queue is busy. Try again.", "error");
				return;
			}
			this.states.set(config.key, state);
			this.setButtonState(button, true);
			this.setCommandButtons(config.key, true);
			this.options.log("info", "Auto-send queued", {
				command: config.key,
				type: config.type,
				category: state.category,
				pageCode: state.pageCode,
				items: config.clickOnly ? null : items.length,
				intervalSeconds: this.getIntervalSeconds()
			});
			this.restoreTaskSelection(this.getStoredQueue().tasks.find((task) => task.id === state.taskId), state);
			this.processDueQueue();
		}
		toggle(config, sendButton, button) {
			if (this.states.has(config.key)) {
				this.stop(config.key);
				return;
			}
			this.start(config, sendButton, button);
		}
		createRuntimeState(task, config, sendButton, button) {
			const input = config.inputSelector ? document.querySelector(config.inputSelector) : null;
			this.ensureTaskCategoryVisible(config, task);
			const items = config.clickOnly ? [] : this.getVisibleItems(config);
			if (!config.clickOnly && (!input || items.length === 0)) return null;
			let nextIndex = Number(task.nextIndex);
			if (!Number.isFinite(nextIndex) || nextIndex < 0) nextIndex = Number.isFinite(Number(task.itemIndex)) && Number(task.itemIndex) >= 0 ? Number(task.itemIndex) : this.getStartIndex(config, input, items);
			if (!config.clickOnly && task.itemValue) {
				const itemIndex = items.findIndex((item) => item.value === task.itemValue && (!task.categoryId || item.categoryId === task.categoryId));
				if (itemIndex >= 0) nextIndex = itemIndex;
			}
			return {
				taskId: task.id,
				commandKey: config.key,
				config,
				sendButton,
				button,
				items,
				nextIndex,
				category: task.category || this.getTaskCategory(config, items),
				profileTitle: task.profileTitle || this.getProfileTitle(),
				pageKey: task.pageKey || this.getPageKey(),
				pageUrl: task.pageUrl || task.pageKey || this.getPageUrl(),
				pageCode: task.pageCode || this.getPageCode(),
				stopped: false
			};
		}
		restoreTaskSelection(task, state) {
			if (!task || !state || state.config.clickOnly || state.items.length === 0) return;
			this.ensureTaskCategoryVisible(state.config, task);
			state.items = this.getVisibleItems(state.config);
			if (state.items.length === 0) return;
			const item = state.items.find((candidate) => candidate.value === task.itemValue && (!task.categoryId || candidate.categoryId === task.categoryId)) || state.items[state.nextIndex % state.items.length];
			if (!item) return;
			const focusState = this.options.captureFocusState?.();
			if (this.selectItem(state.config, item, {
				focus: false,
				focusState
			})) this.options.restoreFocusState?.(focusState);
		}
		adoptTask(task) {
			if (!task || task.kind !== "auto" || !this.taskMatchesCurrentPage(task)) return false;
			if (this.states.get(task.commandKey)?.taskId === task.id) return true;
			const config = this.getCommandConfig(task.commandKey);
			if (!config) return false;
			const sendButton = document.querySelector(`[data-send="${CSS.escape(task.commandKey)}"]`);
			const button = document.querySelector(`.ctrlem-db-auto-send-button[data-command="${CSS.escape(task.commandKey)}"]`);
			if (!sendButton || !button) return false;
			const state = this.createRuntimeState(task, config, sendButton, button);
			if (!state) return false;
			this.states.set(task.commandKey, state);
			this.setButtonState(button, true);
			this.setCommandButtons(task.commandKey, true);
			this.restoreTaskSelection(task, state);
			return true;
		}
		adoptCurrentPageTasks(queue = this.getStoredQueue()) {
			let adopted = false;
			queue.tasks.forEach((task) => {
				if (task.kind === "auto" && this.taskMatchesCurrentPage(task)) adopted = this.adoptTask(task) || adopted;
			});
			return adopted;
		}
		applyStoredQueue(queue) {
			const taskIds = new Set((queue?.tasks || []).map((task) => task.id));
			Array.from(this.states.values()).forEach((state) => {
				if (taskIds.has(state.taskId)) return;
				this.stop(state.commandKey, "removed from queue", { publish: false });
			});
			Array.from(this.manualTasks.keys()).forEach((taskId) => {
				if (!taskIds.has(taskId)) this.manualTasks.delete(taskId);
			});
			this.adoptCurrentPageTasks(queue);
			this.refreshControlButtons();
			this.renderManager();
		}
		requestStopTask(taskId) {
			const state = Array.from(this.states.values()).find((item) => item.taskId === taskId);
			if (state) {
				this.stop(state.commandKey);
				return;
			}
			this.removeTask(taskId);
		}
		requestStopAllTasks() {
			this.stopAll("stopped all", { publish: false });
			this.manualTasks.clear();
			this.tryQueueLock((queue) => {
				queue.tasks = [];
				queue.nextSendAllowedAt = 0;
				queue.rateLimitBackoffUntil = 0;
				this.nextSendAllowedAt = 0;
				this.rateLimitBackoffUntil = 0;
				this.clearActiveSend(queue);
			});
			this.renderManager();
		}
		clickSendButton(button) {
			this.unlockSendButton(button);
			this.bypassSendButton = button;
			this.sendRequestCaptureDepth += 1;
			try {
				button.click();
			} finally {
				this.bypassSendButton = null;
				window.setTimeout(() => {
					this.sendRequestCaptureDepth = Math.max(0, this.sendRequestCaptureDepth - 1);
				}, 0);
				this.scheduleSendButtonUnlock(button);
			}
		}
		getQueueNextSendAllowedAt(queue) {
			return Math.max(Number(queue?.nextSendAllowedAt || 0), Number(queue?.rateLimitBackoffUntil || 0), this.nextSendAllowedAt, this.rateLimitBackoffUntil);
		}
		hasActiveSend(queue) {
			return Boolean(queue?.activeSendTaskId || this.activeSendTaskId || queue?.tasks?.some?.((task) => task.status === TASK_STATUS.SENDING));
		}
		clearActiveSend(queue) {
			if (!queue) return;
			if (!queue.activeSendTaskId || queue.activeSendTaskId === this.activeSendTaskId) this.activeSendTaskId = "";
			queue.activeSendTaskId = "";
			queue.activeSendOwnerId = "";
			queue.activeSendStartedAt = 0;
			queue.activeSendSettleAt = 0;
		}
		beginTaskAttempt(queue, task) {
			const attemptedAt = Date.now();
			const nextAllowedAt = attemptedAt + this.getMinimumRequestIntervalMs();
			const settleAt = Math.max(attemptedAt + SEND_SETTLE_MS, nextAllowedAt);
			task.status = TASK_STATUS.SENDING;
			task.attemptedAt = attemptedAt;
			task.attemptOwnerId = this.instanceId;
			task.dueAt = settleAt;
			queue.lastSentAt = attemptedAt;
			queue.nextSendAllowedAt = Math.max(Number(queue.nextSendAllowedAt || 0), nextAllowedAt);
			queue.activeSendTaskId = task.id;
			queue.activeSendOwnerId = this.instanceId;
			queue.activeSendStartedAt = attemptedAt;
			queue.activeSendSettleAt = settleAt;
			this.nextSendAllowedAt = Math.max(this.nextSendAllowedAt, nextAllowedAt);
			this.activeSendTaskId = task.id;
		}
		clearAttemptFields(task) {
			task.status = TASK_STATUS.PENDING;
			task.attemptedAt = 0;
			task.attemptOwnerId = "";
			task.nextIndexAfterAttempt = -1;
		}
		updateAutoTaskToNextItem(task, state, nextIndex) {
			if (!state || state.config.clickOnly) {
				task.nextIndex = nextIndex;
				return;
			}
			state.items = this.getVisibleItems(state.config);
			if (state.items.length === 0) {
				task.nextIndex = nextIndex;
				return;
			}
			const normalizedNextIndex = nextIndex % state.items.length;
			state.nextIndex = normalizedNextIndex;
			const nextItem = state.items[normalizedNextIndex];
			task.nextIndex = normalizedNextIndex;
			task.itemValue = nextItem?.value || task.itemValue || "";
			task.itemIndex = Number.isFinite(Number(nextItem?.index)) ? Number(nextItem.index) : normalizedNextIndex;
			task.categoryId = nextItem?.categoryId || task.categoryId || "";
			task.category = nextItem?.category || task.category || state.category;
		}
		completeAttemptedTask(queue, task, now = Date.now()) {
			const attemptedAt = Number(task.attemptedAt || queue.activeSendStartedAt || now);
			if (task.kind === "auto") {
				const state = this.states.get(task.commandKey);
				const nextIndex = Number.isFinite(Number(task.nextIndexAfterAttempt)) && Number(task.nextIndexAfterAttempt) >= 0 ? Number(task.nextIndexAfterAttempt) : Number(task.nextIndex || 0);
				this.updateAutoTaskToNextItem(task, state, nextIndex);
				this.clearAttemptFields(task);
				task.dueAt = Math.max(attemptedAt + clampAutoSendInterval(task.taskIntervalSeconds) * 1e3, this.getQueueNextSendAllowedAt(queue));
			} else {
				queue.tasks = queue.tasks.filter((item) => item.id !== task.id);
				this.manualTasks.delete(task.id);
			}
			this.clearActiveSend(queue);
		}
		requeueRateLimitedTask(queue, task, backoffUntil) {
			this.clearAttemptFields(task);
			task.dueAt = Math.max(backoffUntil, Date.now() + this.getMinimumRequestIntervalMs());
			if (task.kind === "auto") {
				const state = this.states.get(task.commandKey);
				if (state && Number.isFinite(Number(task.nextIndex)) && Number(task.nextIndex) >= 0) state.nextIndex = Number(task.nextIndex);
			}
			this.clearActiveSend(queue);
		}
		getRetryableAttemptTask(queue) {
			const activeTaskId = String(queue?.activeSendTaskId || this.activeSendTaskId || "");
			if (activeTaskId) {
				const activeTask = queue.tasks.find((task) => task.id === activeTaskId);
				if (activeTask) return activeTask;
			}
			return [...queue.tasks].filter((task) => task.status === TASK_STATUS.SENDING).sort((a, b) => Number(b.attemptedAt || 0) - Number(a.attemptedAt || 0))[0] || null;
		}
		settleActiveSend(queue, now = Date.now()) {
			if (!this.hasActiveSend(queue)) return false;
			const task = this.getRetryableAttemptTask(queue);
			const activeOwnerId = String(queue.activeSendOwnerId || task?.attemptOwnerId || "");
			if (activeOwnerId && activeOwnerId !== this.instanceId && this.isOwnerAlive(queue, activeOwnerId, now)) return true;
			const settleAt = Math.max(Number(queue.activeSendSettleAt || 0), Number(task?.dueAt || 0), Number(task?.attemptedAt || queue.activeSendStartedAt || 0) + SEND_SETTLE_MS, this.getQueueNextSendAllowedAt(queue));
			if (this.isNativeSendBusy(now) || now < settleAt) return true;
			if (task) this.completeAttemptedTask(queue, task, now);
			else this.clearActiveSend(queue);
			return true;
		}
		runAutoTask(queue, task) {
			const state = this.states.get(task.commandKey);
			if (!state || state.taskId !== task.id || state.stopped) return "skip";
			if (!state.sendButton.isConnected) {
				this.stop(state.commandKey, "send button disconnected", { publish: false });
				return "skip";
			}
			const focusState = this.options.captureFocusState?.();
			if (!state.config.clickOnly) {
				const itemIndex = state.nextIndex % state.items.length;
				const item = state.items[itemIndex];
				if (!this.selectItem(state.config, item, {
					focus: false,
					focusState
				})) {
					this.options.notifySite("Auto-send item is no longer visible", "error");
					this.stop(state.commandKey, "item no longer visible", { publish: false });
					return "remove";
				}
				task.itemValue = item.value || "";
				task.itemIndex = Number.isFinite(Number(item.index)) ? Number(item.index) : itemIndex;
				task.categoryId = item.categoryId || task.categoryId || "";
				task.category = item.category || task.category || state.category;
				task.nextIndex = itemIndex;
				task.nextIndexAfterAttempt = (itemIndex + 1) % state.items.length;
			}
			this.beginTaskAttempt(queue, task);
			this.clickSendButton(state.sendButton);
			this.options.restoreFocusState?.(focusState);
			return "attempted";
		}
		runManualTask(queue, task) {
			const state = this.manualTasks.get(task.id);
			if (!state?.button?.isConnected) return "remove";
			const focusState = this.options.captureFocusState?.();
			this.restoreInputSnapshot(state.snapshot);
			this.beginTaskAttempt(queue, task);
			this.clickSendButton(state.button);
			this.options.restoreFocusState?.(focusState);
			return "attempted";
		}
		isAutoTaskRunnable(task) {
			if (!this.taskMatchesCurrentPage(task)) return false;
			if (!this.adoptTask(task)) return false;
			const state = this.states.get(task.commandKey);
			return Boolean(state && state.taskId === task.id && !state.stopped && state.sendButton?.isConnected);
		}
		getNextRunnableTask(queue, now = Date.now()) {
			const readyTasks = this.sortReadyTasks(queue.tasks.filter((task) => task.status !== TASK_STATUS.SENDING && Number(task.dueAt || 0) <= now));
			for (const task of readyTasks) {
				if (task.kind === "manual") {
					if (task.ownerId === this.instanceId && this.manualTasks.has(task.id)) return task;
					continue;
				}
				if (task.kind !== "auto") continue;
				if (!this.isTaskPageActive(queue, task, now)) continue;
				if (!this.taskMatchesCurrentPage(task)) continue;
				if (this.isAutoTaskRunnable(task)) return task;
			}
			return null;
		}
		processDueQueue() {
			const snapshot = this.getStoredQueue();
			const snapshotNow = Date.now();
			if (this.hasActiveSend(snapshot)) {
				const activeTask = this.getRetryableAttemptTask(snapshot);
				const activeOwnerId = String(snapshot.activeSendOwnerId || activeTask?.attemptOwnerId || "");
				if (activeOwnerId && activeOwnerId !== this.instanceId && this.isOwnerAlive(snapshot, activeOwnerId, snapshotNow)) return;
				this.tryQueueLock((queue) => {
					this.settleActiveSend(queue, Date.now());
				});
				return;
			}
			if (this.isNativeSendBusy(snapshotNow)) return;
			if (snapshotNow < this.getQueueNextSendAllowedAt(snapshot)) return;
			if (!this.getNextRunnableTask(snapshot, snapshotNow)) return;
			this.tryQueueLock((queue) => {
				const now = Date.now();
				if (this.settleActiveSend(queue, now)) return;
				if (this.isNativeSendBusy(now)) return;
				if (now < this.getQueueNextSendAllowedAt(queue)) return;
				const task = this.getNextRunnableTask(queue, now);
				if (!task) return;
				const result = task.kind === "manual" ? this.runManualTask(queue, task) : this.runAutoTask(queue, task);
				if (result === "skip") return;
				if (result === "remove") {
					queue.tasks = queue.tasks.filter((t) => t.id !== task.id);
					if (task.kind === "manual") this.manualTasks.delete(task.id);
					return;
				}
			});
		}
		getInputSnapshot(button, config) {
			const panel = button.closest(".cmd-panel") || button.closest("form") || document;
			const fields = Array.from(panel.querySelectorAll("input, textarea, select"));
			const configuredInputSelector = String(config?.inputSelector || "");
			if (configuredInputSelector) {
				const input = document.querySelector(configuredInputSelector);
				if (input && !fields.includes(input)) fields.push(input);
			}
			return fields.map((field, index) => ({
				selector: field.id ? `#${CSS.escape(field.id)}` : configuredInputSelector && field.matches?.(configuredInputSelector) ? configuredInputSelector : "",
				index,
				value: field.type === "checkbox" || field.type === "radio" ? field.checked : field.value,
				checked: Boolean(field.checked)
			}));
		}
		restoreInputSnapshot(snapshot) {
			snapshot.forEach((item) => {
				const field = item.selector ? document.querySelector(item.selector) : document.querySelectorAll("input, textarea, select")[Number(item.index)];
				if (!field) return;
				if (field.type === "checkbox" || field.type === "radio") field.checked = Boolean(item.checked);
				else if (field.value !== item.value) {
					field.value = item.value;
					field.dispatchEvent(new Event("input", { bubbles: true }));
					field.dispatchEvent(new Event("change", { bubbles: true }));
				}
			});
		}
		getManualValidationConfig(commandKey, config) {
			return INPUT_CAPTURE_COMMANDS[commandKey] || (config?.inputSelector ? config : null);
		}
		isManualSendValid(commandKey, config) {
			const validationConfig = this.getManualValidationConfig(commandKey, config);
			if (!validationConfig?.inputSelector) return true;
			const input = document.querySelector(validationConfig.inputSelector);
			if (!input) return true;
			return isCaptureValueValid(validationConfig, String(input.value || "").trim());
		}
		captureManualSend(event) {
			if (!(event.target instanceof Element)) return;
			const button = event.target.closest("[data-send]");
			if (!button || button !== event.target.closest("[data-send]")) return;
			if (button === this.bypassSendButton) return;
			const commandKey = String(button.dataset.send || "");
			const config = this.getCommandConfig(commandKey);
			if (!commandKey) return;
			if (!this.isManualSendValid(commandKey, config)) {
				this.options.notifySite("Enter a valid value before queueing send", "error");
				event.preventDefault();
				event.stopImmediatePropagation();
				return;
			}
			event.preventDefault();
			event.stopImmediatePropagation();
			const taskId = createId("manualsend");
			const snapshot = this.getInputSnapshot(button, config);
			const pageKey = this.getPageKey();
			const pageUrl = this.getPageUrl();
			const pageCode = this.getPageCode();
			const sendType = this.getSendType(config, "manual");
			const taskKey = this.buildTaskKey(pageKey, commandKey, sendType);
			const now = Date.now();
			const task = {
				id: taskId,
				key: taskKey,
				ownerId: this.instanceId,
				kind: "manual",
				commandKey,
				sendType,
				category: config?.label || commandKey,
				profileTitle: this.getProfileTitle(),
				pageKey,
				pageUrl,
				pageCode,
				createdAt: now,
				dueAt: now,
				sequence: now,
				status: TASK_STATUS.PENDING
			};
			this.manualTasks.set(taskId, {
				button,
				snapshot
			});
			if (!this.tryQueueLock((queue) => {
				task.sequence = this.getNextSequence(queue);
				queue.tasks.unshift(task);
			})) {
				this.manualTasks.delete(taskId);
				this.options.notifySite("Auto-send queue is busy. Try again.", "error");
				return;
			}
			this.processDueQueue();
		}
		getOrCreateControlHost(sendButton) {
			const parent = sendButton.parentElement;
			if (!parent) return null;
			if (!parent.matches(".cmd-panel")) return parent;
			const wrapper = createElement("div", { className: "ctrlem-db-autosend-group" });
			parent.insertBefore(wrapper, sendButton);
			wrapper.appendChild(sendButton);
			return wrapper;
		}
		syncIntervalInputs() {
			const seconds = String(this.getIntervalSeconds());
			document.querySelectorAll(".ctrlem-db-autosend-interval-input").forEach((input) => {
				if (input.value !== seconds) input.value = seconds;
			});
		}
		createIntervalInput() {
			const input = createElement("input", {
				className: "ctrlem-db-interval-input ctrlem-db-autosend-interval-input",
				type: "number",
				value: String(this.getIntervalSeconds()),
				attrs: {
					min: String(AUTO_SEND_INTERVAL_MIN),
					max: String(AUTO_SEND_INTERVAL_MAX),
					step: "1",
					"aria-label": "Auto-send interval in seconds"
				}
			});
			input.addEventListener("change", () => {
				const nextInterval = clampAutoSendInterval(input.value);
				this.options.setAutoSendInterval?.(nextInterval);
				this.syncIntervalInputs();
				this.renderManager();
			});
			return input;
		}
		mountControls() {
			let changed = false;
			AUTO_SEND_COMMAND_KEYS.forEach((commandKey) => {
				const config = this.getCommandConfig(commandKey);
				if (!config) return;
				document.querySelectorAll(`[data-send="${commandKey}"]`).forEach((sendButton) => {
					const parent = this.getOrCreateControlHost(sendButton);
					if (!parent || parent.querySelector(`.ctrlem-db-auto-send-button[data-command="${commandKey}"]`)) return;
					parent.classList.add("ctrlem-db-autosend-group");
					const intervalInput = this.createIntervalInput();
					const button = createElement("button", {
						className: "ctrlem-db-auto-send-button",
						text: "A",
						title: "Start auto-send",
						type: "button",
						attrs: {
							"aria-label": `Auto-send ${commandKey}`,
							"aria-pressed": "false"
						},
						dataset: { command: commandKey }
					});
					button.addEventListener("click", () => this.toggle(config, sendButton, button));
					parent.appendChild(intervalInput);
					parent.appendChild(createElement("span", {
						className: "ctrlem-db-autosend-sec",
						text: "sec"
					}));
					parent.appendChild(button);
					this.adoptCurrentPageTasks();
					this.setButtonState(button, this.states.has(commandKey));
					changed = true;
				});
			});
			this.adoptCurrentPageTasks();
			if (changed) this.options.log("info", "Auto-send controls mounted");
			return changed;
		}
		refreshControlButtons() {
			document.querySelectorAll(".ctrlem-db-auto-send-button").forEach((button) => {
				this.setButtonState(button, this.states.has(button.dataset.command));
			});
		}
		isTaskActiveForDisplay(queue, task, now = Date.now()) {
			if (task.kind === "manual") return this.isOwnerAlive(queue, task.ownerId, now);
			if (task.kind === "auto") return this.isTaskPageActive(queue, task, now);
			return false;
		}
		getReadyDisplayTasks(queue, now = Date.now()) {
			return this.sortReadyTasks(queue.tasks.filter((task) => Number(task.dueAt || 0) <= now && this.isTaskActiveForDisplay(queue, task, now)));
		}
		getEffectiveSendAt(task, queue, readyTasks, now = Date.now()) {
			const dueAt = Number(task?.dueAt || 0);
			if (dueAt > now) return dueAt;
			if (!this.isTaskActiveForDisplay(queue, task, now)) return Number.MAX_SAFE_INTEGER;
			const readyIndex = Math.max(0, readyTasks.findIndex((item) => item.id === task.id));
			return Math.max(now, this.getQueueNextSendAllowedAt(queue)) + readyIndex * this.getMinimumRequestIntervalMs();
		}
		getOrderedTasks(queue, now = Date.now()) {
			const readyTasks = this.getReadyDisplayTasks(queue, now);
			return [...queue.tasks].sort((a, b) => {
				const sendDiff = this.getEffectiveSendAt(a, queue, readyTasks, now) - this.getEffectiveSendAt(b, queue, readyTasks, now);
				if (sendDiff !== 0) return sendDiff;
				const priority = (a.kind === "manual" ? 0 : 1) - (b.kind === "manual" ? 0 : 1);
				if (priority !== 0) return priority;
				return this.taskSortValue(a) - this.taskSortValue(b);
			});
		}
		getCooldownPercent(task, queue = this.getStoredQueue(), readyTasks = this.getReadyDisplayTasks(queue)) {
			const now = Date.now();
			const dueAt = Number(task?.dueAt || 0);
			const taskRemaining = Math.max(0, dueAt - now);
			if (taskRemaining > 0) {
				const intervalMs = Math.max(1, clampAutoSendInterval(task.taskIntervalSeconds) * 1e3);
				const elapsed = intervalMs - Math.min(intervalMs, taskRemaining);
				return Math.max(0, Math.min(100, elapsed / intervalMs * 100));
			}
			if (!this.isTaskActiveForDisplay(queue, task, now)) return 100;
			const minIntervalMs = this.getMinimumRequestIntervalMs();
			const effectiveAt = this.getEffectiveSendAt(task, queue, readyTasks, now);
			const rateRemaining = Math.max(0, effectiveAt - now);
			if (rateRemaining > 0) {
				const elapsed = minIntervalMs - Math.min(minIntervalMs, rateRemaining % minIntervalMs || minIntervalMs);
				return Math.max(0, Math.min(100, elapsed / minIntervalMs * 100));
			}
			return 100;
		}
		getRemainingText(task, queue = this.getStoredQueue(), readyTasks = this.getReadyDisplayTasks(queue)) {
			const now = Date.now();
			const dueAt = Number(task?.dueAt || 0);
			const taskRemaining = Math.max(0, dueAt - now);
			if (taskRemaining > 0) return `${Math.ceil(taskRemaining / 1e3)}s`;
			if (!this.isTaskActiveForDisplay(queue, task, now)) return "waiting tab";
			const effectiveAt = this.getEffectiveSendAt(task, queue, readyTasks, now);
			const rateRemaining = Math.max(0, effectiveAt - now);
			if (rateRemaining > 0) return `rate:${Math.ceil(rateRemaining / 1e3)}s`;
			return "ready";
		}
		getTaskPageUrl(task) {
			return String(task?.pageUrl || task?.pageKey || "").trim();
		}
		openTaskPage(task) {
			const url = this.getTaskPageUrl(task);
			if (!url) return;
			const gmOpenInTab = globalThis.GM_openInTab;
			if (typeof gmOpenInTab === "function") {
				gmOpenInTab(url, {
					active: true,
					insert: true
				});
				return;
			}
			window.open(url, "_blank", "noopener");
		}
		startManagerTimer() {
			if (this.managerTimer) return;
			this.managerTimer = window.setInterval(() => {
				const queue = this.getStoredQueue();
				const readyTasks = this.getReadyDisplayTasks(queue);
				const orderedTasks = this.getOrderedTasks(queue);
				const rows = document.querySelectorAll(`#${MANAGER_ID} .ctrlem-db-autosend-row`);
				if (!queue.tasks.length || rows.length === 0) {
					window.clearInterval(this.managerTimer);
					this.managerTimer = 0;
					return;
				}
				rows.forEach((row) => {
					const task = orderedTasks.find((item) => item.id === row.dataset.taskId);
					if (task) this.updateTaskRow(row, task, queue, readyTasks);
				});
			}, USER_CONFIG.autoSend.managerRefreshMs);
		}
		setElementText(element, text) {
			if (!element) return;
			if (element.textContent !== text) element.textContent = text;
			if (element.title !== void 0 && element.title !== text) element.title = text;
		}
		updateTaskRow(row, task, queue, readyTasks) {
			row.classList.toggle("is-manual", task.kind === "manual");
			row.classList.toggle("is-auto", task.kind === "auto");
			this.setElementText(row.querySelector(".ctrlem-db-autosend-kind"), task.sendType || task.kind);
			this.setElementText(row.querySelector(".ctrlem-db-autosend-profile"), task.profileTitle || "Current profile");
			this.setElementText(row.querySelector(".ctrlem-db-autosend-code"), task.pageCode || "Current page");
			this.setElementText(row.querySelector(".ctrlem-db-autosend-category"), task.category || task.commandKey || "Send");
			const progress = row.querySelector(".ctrlem-db-autosend-progress");
			if (progress) progress.style.width = `${this.getCooldownPercent(task, queue, readyTasks)}%`;
			const wait = row.querySelector(".ctrlem-db-autosend-wait");
			if (wait) {
				const isWaitingTab = !this.isTaskActiveForDisplay(queue, task, Date.now()) && Number(task?.dueAt || 0) <= Date.now();
				this.setElementText(wait, this.getRemainingText(task, queue, readyTasks));
				wait.classList.toggle("is-open-tab", isWaitingTab);
				wait.dataset.openTab = isWaitingTab ? "true" : "false";
				wait.title = isWaitingTab ? "Open this page to resume auto-send" : wait.textContent;
			}
			row.dataset.pageUrl = this.getTaskPageUrl(task);
		}
		syncToastOffset(panel = document.getElementById(MANAGER_ID)) {
			if (!panel) {
				document.documentElement.style.removeProperty(TOAST_BOTTOM_PROPERTY);
				return;
			}
			document.documentElement.style.setProperty(TOAST_BOTTOM_PROPERTY, `${Math.ceil(panel.getBoundingClientRect().height + 16 + 12)}px`);
		}
		createTaskRow(task, queue, readyTasks) {
			const stopButton = createElement("button", {
				className: "ctrlem-db-autosend-mini-button ctrlem-db-autosend-stop",
				text: "Stop",
				title: "Stop this queued send",
				type: "button"
			});
			stopButton.addEventListener("click", () => this.requestStopTask(task.id));
			const row = createElement("div", {
				className: `ctrlem-db-autosend-row is-${task.kind}`,
				dataset: { taskId: task.id }
			}, [
				createElement("span", { className: "ctrlem-db-autosend-kind" }),
				createElement("span", { className: "ctrlem-db-autosend-profile" }),
				createElement("span", { className: "ctrlem-db-autosend-code" }),
				createElement("span", { className: "ctrlem-db-autosend-category" }),
				createElement("span", { className: "ctrlem-db-autosend-cooldown" }, [createElement("span", { className: "ctrlem-db-autosend-progress" })]),
				createElement("span", { className: "ctrlem-db-autosend-wait" }),
				stopButton
			]);
			const wait = row.querySelector(".ctrlem-db-autosend-wait");
			wait?.addEventListener("click", (event) => {
				if (wait.dataset.openTab !== "true") return;
				event.preventDefault();
				event.stopPropagation();
				const currentTask = this.getStoredQueue().tasks.find((item) => item.id === row.dataset.taskId);
				this.openTaskPage(currentTask || { pageUrl: row.dataset.pageUrl });
			});
			this.updateTaskRow(row, task, queue, readyTasks);
			return row;
		}
		renderManager() {
			const queue = this.getStoredQueue();
			const existing = document.getElementById(MANAGER_ID);
			this.refreshControlButtons();
			if (!queue.tasks.length) {
				existing?.remove();
				this.syncToastOffset(null);
				if (this.managerTimer) {
					window.clearInterval(this.managerTimer);
					this.managerTimer = 0;
				}
				return Boolean(existing);
			}
			const readyTasks = this.getReadyDisplayTasks(queue);
			const orderedTasks = this.getOrderedTasks(queue);
			if (existing) {
				existing.classList.toggle("is-collapsed", this.managerCollapsed);
				const toggleButton = existing.querySelector(".ctrlem-db-autosend-toggle");
				if (toggleButton) {
					toggleButton.textContent = this.managerCollapsed ? "Show" : "Hide";
					toggleButton.title = this.managerCollapsed ? "Expand queue" : "Collapse queue";
					toggleButton.setAttribute("aria-expanded", String(!this.managerCollapsed));
				}
				const rowsHost = existing.querySelector(".ctrlem-db-autosend-rows") || existing;
				const rowsById = new Map(Array.from(rowsHost.querySelectorAll(".ctrlem-db-autosend-row")).map((row) => [row.dataset.taskId, row]));
				orderedTasks.forEach((task, index) => {
					let row = rowsById.get(task.id);
					if (row) {
						rowsById.delete(task.id);
						this.updateTaskRow(row, task, queue, readyTasks);
					} else row = this.createTaskRow(task, queue, readyTasks);
					const currentAtIndex = rowsHost.children[index];
					if (row !== currentAtIndex) rowsHost.insertBefore(row, currentAtIndex || null);
				});
				rowsById.forEach((row) => row.remove());
				this.syncToastOffset(existing);
				this.startManagerTimer();
				return false;
			}
			const rows = orderedTasks.map((task) => this.createTaskRow(task, queue, readyTasks));
			const toggleButton = createElement("button", {
				className: "ctrlem-db-autosend-mini-button ctrlem-db-autosend-toggle",
				text: "Hide",
				title: "Collapse queue",
				type: "button",
				attrs: { "aria-expanded": String(!this.managerCollapsed) }
			});
			toggleButton.addEventListener("click", () => {
				this.managerCollapsed = !this.managerCollapsed;
				toggleButton.textContent = this.managerCollapsed ? "Show" : "Hide";
				toggleButton.title = this.managerCollapsed ? "Expand queue" : "Collapse queue";
				toggleButton.setAttribute("aria-expanded", String(!this.managerCollapsed));
				panel.classList.toggle("is-collapsed", this.managerCollapsed);
				this.syncToastOffset(panel);
			});
			const stopAllButton = createElement("button", {
				className: "ctrlem-db-autosend-mini-button ctrlem-db-autosend-stop-all",
				text: "Stop all",
				title: "Stop all queued sends",
				type: "button"
			});
			stopAllButton.addEventListener("click", () => this.requestStopAllTasks());
			const panel = createElement("div", {
				id: MANAGER_ID,
				className: "ctrlem-db-autosend-manager",
				attrs: { role: "status" }
			}, [createElement("div", { className: "ctrlem-db-autosend-head" }, [
				createElement("span", {
					className: "ctrlem-db-autosend-note",
					text: "Keep this tab open for auto-send."
				}),
				toggleButton,
				stopAllButton
			]), createElement("div", { className: "ctrlem-db-autosend-rows" }, rows)]);
			panel.classList.toggle("is-collapsed", this.managerCollapsed);
			document.body.appendChild(panel);
			this.syncToastOffset(panel);
			this.startManagerTimer();
			return true;
		}
	};
	function log(level, message, details) {
		if (!CONFIG.debug && level !== "warn" && level !== "error") return;
		const target = console[level] || console.log;
		if (details === void 0) {
			target.call(console, LOG_PREFIX, message);
			return;
		}
		target.call(console, LOG_PREFIX, message, details);
	}
	var CtrlEmSite = class {
		log;
		uploadApiItemsPromise = null;
		constructor(log) {
			this.log = log;
		}
		readDefaultImagesFromGallery(commandKey) {
			const selectors = uniqueStrings([`#gallery-${commandKey}`, ...Object.values(MEDIA_COMMANDS).filter((config) => config.type === RecordType.IMAGE).map((config) => config.gallerySelector)]);
			const items = [];
			selectors.forEach((selector) => {
				const gallery = document.querySelector(selector);
				if (!gallery) return;
				gallery.querySelectorAll(".gallery-thumb-wrapper").forEach((wrapper) => {
					const uploadId = wrapper.dataset.uploadId;
					const img = wrapper.querySelector(".gallery-thumb");
					const deleteButton = wrapper.querySelector(".gallery-thumb-delete");
					const title = img?.getAttribute("title") || img?.getAttribute("alt") || uploadId || "Uploaded image";
					const url = uploadId ? getUploadImageUrl(uploadId) : img?.src;
					const item = normalizeMediaItem({
						url,
						previewUrl: img?.src || url,
						title,
						uploadId,
						deleteButton,
						canDelete: Boolean(uploadId || deleteButton)
					});
					if (item) items.push(item);
				});
			});
			return uniqueMediaItems(items);
		}
		async fetchDefaultImagesFromApi() {
			if (!this.uploadApiItemsPromise) this.uploadApiItemsPromise = fetch("/api/uploads/my", { credentials: "same-origin" }).then((res) => {
				if (!res.ok) throw new Error(`HTTP ${res.status}`);
				return res.json();
			}).then((uploads) => {
				if (!Array.isArray(uploads)) return [];
				return uniqueMediaItems(uploads.map((upload) => {
					const url = upload?.id ? getUploadImageUrl(upload.id) : upload?.url;
					return {
						url,
						title: upload?.originalName || upload?.name || upload?.id || "Uploaded image",
						previewUrl: upload?.url || url,
						uploadId: upload?.id,
						canDelete: Boolean(upload?.id)
					};
				}));
			}).catch((error) => {
				this.log("warn", "Default uploads API unavailable", { message: error?.message || String(error) });
				return [];
			});
			return this.uploadApiItemsPromise;
		}
		async getDefaultImageItems(commandKey) {
			const domItems = this.readDefaultImagesFromGallery(commandKey);
			const apiItems = await this.fetchDefaultImagesFromApi();
			const items = uniqueMediaItems([...domItems, ...apiItems]);
			if (items.length > 0) this.log("info", "Default loaded", {
				command: commandKey,
				count: items.length,
				dom: domItems.length,
				api: apiItems.length
			});
			else this.log("info", "Default fallback", { command: commandKey });
			return items;
		}
		notify(message, level = "info") {
			const showToast = window.showToast;
			if (typeof showToast === "function") showToast(message, level);
		}
		removeUploadFromSiteGalleries(uploadId) {
			if (!uploadId) return;
			document.querySelectorAll(".gallery-thumb-wrapper").forEach((wrapper) => {
				if (wrapper.dataset.uploadId === uploadId) wrapper.remove();
			});
		}
		async deleteDefaultImageItem(item, tile, refresh) {
			if (!item?.canDelete) return;
			try {
				tile?.classList.add("is-deleting");
				if (item.uploadId) {
					const response = await fetch(`/api/uploads/${encodeURIComponent(item.uploadId)}`, {
						method: "DELETE",
						credentials: "same-origin"
					});
					if (!response.ok) throw new Error(`HTTP ${response.status}`);
					this.removeUploadFromSiteGalleries(item.uploadId);
				} else if (item.deleteButton) item.deleteButton.click();
				this.uploadApiItemsPromise = null;
				this.notify("Image deleted", "success");
				refresh();
			} catch (error) {
				tile?.classList.remove("is-deleting");
				this.notify("Failed to delete image", "error");
				this.log("error", "Failed to delete default image", {
					uploadId: item.uploadId,
					message: error?.message || String(error)
				});
			}
		}
		hideMediaUi(config, panel) {
			let changed = false;
			const elements = [];
			if (config.gallerySelector) {
				const gallery = panel.querySelector(config.gallerySelector);
				if (gallery) elements.push(gallery);
			}
			panel.querySelectorAll(".gallery-thumb-delete").forEach((button) => elements.push(button));
			elements.forEach((element) => {
				if (element.classList.contains("ctrlem-db-hidden-site-ui")) return;
				element.classList.add("ctrlem-db-hidden-site-ui");
				changed = true;
			});
			if (changed) this.log("info", "Site media UI hidden", {
				command: config.key,
				count: elements.length
			});
			return changed;
		}
		getResultsParts() {
			const panel = document.querySelector(CONFIG.selectors.resultsPanel);
			const head = panel?.querySelector(".panel-head");
			const title = head?.querySelector("h2");
			return {
				panel,
				head,
				title,
				description: head?.querySelector("p"),
				actions: head ? Array.from(head.children).find((child) => child !== title?.parentElement) : null,
				responses: document.querySelector(CONFIG.selectors.resultsContainer),
				pagination: document.querySelector(CONFIG.selectors.resultsPagination)
			};
		}
	};
	var IMGBB_MAX_BYTES = USER_CONFIG.upload.imgbbMaxBytes;
	var VIDHOSTING_MAX_BYTES = USER_CONFIG.upload.vidhostingMaxBytes;
	function getErrorMessage$1(error) {
		return error?.message || String(error || "Unknown error");
	}
	function parseJson(text) {
		try {
			return JSON.parse(text);
		} catch {
			return null;
		}
	}
	function createHttpError(service, response, fallback = "Upload failed") {
		const body = response.text.trim();
		return new Error(`${service}: ${body || `${fallback} (HTTP ${response.status})`}`);
	}
	async function postFormData(url, formData) {
		const gmXmlHttpRequest = globalThis.GM_xmlhttpRequest;
		if (typeof gmXmlHttpRequest === "function") {
			console.log("[CtrlEm DB] Using GM_xmlhttpRequest for", url);
			return new Promise((resolve, reject) => {
				gmXmlHttpRequest({
					method: "POST",
					url,
					data: formData,
					timeout: USER_CONFIG.upload.externalRequestTimeoutMs,
					onload: (response) => {
						console.log("[CtrlEm DB] GM_xmlhttpRequest onload", {
							url,
							status: response.status,
							textLength: String(response.responseText || "").length
						});
						resolve({
							status: Number(response.status || 0),
							text: String(response.responseText || "")
						});
					},
					onerror: (error) => {
						console.log("[CtrlEm DB] GM_xmlhttpRequest onerror", {
							url,
							error: getErrorMessage$1(error)
						});
						reject(new Error(getErrorMessage$1(error)));
					},
					ontimeout: () => {
						console.log("[CtrlEm DB] GM_xmlhttpRequest ontimeout", {
							url,
							timeout: USER_CONFIG.upload.externalRequestTimeoutMs
						});
						reject(new Error("Request timed out"));
					}
				});
			});
		}
		console.log("[CtrlEm DB] Using fetch for", url);
		const response = await fetch(url, {
			method: "POST",
			body: formData
		});
		return {
			status: response.status,
			text: await response.text()
		};
	}
	async function uploadImageToImgBB(file, apiKey) {
		const key = String(apiKey || "").trim();
		if (!key) throw new Error("ImgBB API key is required");
		if (file.size > IMGBB_MAX_BYTES) throw new Error(`${file.name}: ImgBB limit is 32 MB`);
		const endpoint = new URL("https://api.imgbb.com/1/upload");
		endpoint.searchParams.set("key", key);
		const formData = new FormData();
		formData.append("image", file, file.name);
		const response = await postFormData(endpoint.toString(), formData);
		const payload = parseJson(response.text);
		if (response.status < 200 || response.status >= 300 || !payload?.success || !payload?.data?.url) {
			const message = payload?.error?.message || payload?.error || payload?.status_txt || response.text;
			throw new Error(`ImgBB: ${message || `Upload failed (HTTP ${response.status})`}`);
		}
		return String(payload.data.url);
	}
	async function uploadFileToCatbox(file, userhash = "") {
		const formData = new FormData();
		formData.append("reqtype", "fileupload");
		if (userhash.trim()) formData.append("userhash", userhash.trim());
		formData.append("fileToUpload", file, file.name);
		console.log("[CtrlEm DB] Catbox upload starting", {
			name: file.name,
			size: file.size,
			type: file.type
		});
		const startTime = Date.now();
		const response = await postFormData("https://catbox.moe/user/api.php", formData);
		const elapsed = Date.now() - startTime;
		const url = response.text.trim();
		console.log("[CtrlEm DB] Catbox upload response", {
			name: file.name,
			status: response.status,
			elapsed,
			url: url.slice(0, 100)
		});
		if (response.status < 200 || response.status >= 300 || !/^https?:\/\//i.test(url)) throw createHttpError("Catbox", response);
		return url;
	}
	async function uploadVideoToVidHosting(file) {
		if (file.size > VIDHOSTING_MAX_BYTES) throw new Error(`${file.name}: VidHosting limit is 100 MB`);
		const formData = new FormData();
		formData.append("file", file, file.name);
		console.log("[CtrlEm DB] VidHosting upload starting", {
			name: file.name,
			size: file.size,
			type: file.type
		});
		const startTime = Date.now();
		const response = await postFormData("https://upload.vidhosting.in/", formData);
		const elapsed = Date.now() - startTime;
		const payload = parseJson(response.text);
		console.log("[CtrlEm DB] VidHosting upload response", {
			name: file.name,
			status: response.status,
			elapsed,
			isJson: !!payload,
			responsePreview: response.text.slice(0, 500)
		});
		if (response.status < 200 || response.status >= 300 || !payload?.success || !payload?.url) {
			const message = payload?.error || payload?.message || response.text;
			throw new Error(`VidHosting: ${message || `Upload failed (HTTP ${response.status})`}`);
		}
		let url = String(payload.url);
		if (url.includes("stream.vidhosting.in") && !url.includes("/videos/")) url = url.replace("stream.vidhosting.in/", "stream.vidhosting.in/videos/");
		return url;
	}
	var ImageCache = class {
		log;
		dbPromise = null;
		inflight = new Map();
		pruneTimer = 0;
		constructor(log) {
			this.log = log;
		}
		getHttpUrl(url) {
			try {
				const parsed = new URL(String(url || "").trim(), window.location.href);
				return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : "";
			} catch {
				return "";
			}
		}
		getCacheKey(url) {
			try {
				const parsed = new URL(String(url || "").trim(), window.location.href);
				if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "";
				parsed.hash = "";
				if (parsed.origin === window.location.origin && /^\/api\/uploads\/[^/]+\/image\/?$/.test(parsed.pathname)) parsed.search = "";
				return parsed.href;
			} catch {
				return "";
			}
		}
		getCacheKeys(sourceUrl, cacheKey = sourceUrl) {
			return Array.from(new Set([this.getCacheKey(cacheKey), this.getCacheKey(sourceUrl)].filter(Boolean)));
		}
		canCacheImagePreview(url) {
			return Boolean(this.getHttpUrl(url));
		}
		openDb() {
			if (!window.indexedDB) return Promise.reject(new Error("IndexedDB unavailable"));
			if (this.dbPromise) return this.dbPromise;
			this.dbPromise = new Promise((resolve, reject) => {
				const request = window.indexedDB.open(IMAGE_CACHE_DB_NAME, 1);
				request.onupgradeneeded = () => {
					const db = request.result;
					const store = db.objectStoreNames.contains("images") ? request.transaction.objectStore(IMAGE_CACHE_STORE) : db.createObjectStore(IMAGE_CACHE_STORE, { keyPath: "url" });
					if (!store.indexNames.contains("lastAccess")) store.createIndex("lastAccess", "lastAccess");
				};
				request.onsuccess = () => resolve(request.result);
				request.onerror = () => reject(request.error || new Error("Failed to open image cache"));
			});
			this.dbPromise.catch(() => {
				this.dbPromise = null;
			});
			return this.dbPromise;
		}
		blobToDataUrl(blob) {
			return new Promise((resolve, reject) => {
				const reader = new FileReader();
				reader.onload = () => resolve(String(reader.result || ""));
				reader.onerror = () => reject(reader.error || new Error("Failed to read image blob"));
				reader.readAsDataURL(blob);
			});
		}
		async readCachedImageDataUrl(url) {
			try {
				const record = await this.readRecord(url);
				if (!record) return null;
				if (record.dataUrl) {
					this.touchRecord(record);
					return record.dataUrl;
				}
				if (record.blob instanceof Blob) {
					const dataUrl = await this.blobToDataUrl(record.blob);
					await this.writeDataUrl(url, dataUrl, Number(record.size || record.blob.size || dataUrl.length || 0));
					return dataUrl;
				}
				return null;
			} catch (error) {
				this.log("debug", "Image cache read skipped", {
					url,
					message: error?.message || String(error)
				});
				return null;
			}
		}
		async readRecord(url) {
			const key = this.getCacheKey(url);
			if (!key) return null;
			const db = await this.openDb();
			return await new Promise((resolve, reject) => {
				const tx = db.transaction(IMAGE_CACHE_STORE, "readonly");
				const request = tx.objectStore(IMAGE_CACHE_STORE).get(key);
				request.onsuccess = () => resolve(request.result || null);
				request.onerror = () => reject(request.error || new Error("Failed to read cached image"));
				tx.onerror = () => reject(tx.error || new Error("Image cache read transaction failed"));
			});
		}
		async touchRecord(record) {
			try {
				const db = await this.openDb();
				await new Promise((resolve, reject) => {
					const tx = db.transaction(IMAGE_CACHE_STORE, "readwrite");
					tx.objectStore(IMAGE_CACHE_STORE).put({
						...record,
						lastAccess: Date.now()
					});
					tx.oncomplete = () => resolve();
					tx.onerror = () => reject(tx.error || new Error("Image cache touch transaction failed"));
				});
			} catch (error) {
				this.log("debug", "Image cache touch skipped", {
					url: record?.url,
					message: error?.message || String(error)
				});
			}
		}
		async writeDataUrl(url, dataUrl, size) {
			const key = this.getCacheKey(url);
			if (!key) return;
			if (!dataUrl || !String(dataUrl).startsWith("data:image/")) return;
			const safeSize = Number(size || dataUrl.length || 0);
			if (safeSize <= 0) return;
			try {
				const db = await this.openDb();
				await new Promise((resolve, reject) => {
					const tx = db.transaction(IMAGE_CACHE_STORE, "readwrite");
					tx.objectStore(IMAGE_CACHE_STORE).put({
						url: key,
						dataUrl,
						size: safeSize,
						cachedAt: Date.now(),
						lastAccess: Date.now()
					});
					tx.oncomplete = () => resolve();
					tx.onerror = () => reject(tx.error || new Error("Image cache write transaction failed"));
				});
				this.schedulePrune();
			} catch (error) {
				this.log("debug", "Image cache write skipped", {
					url,
					message: error?.message || String(error)
				});
			}
		}
		getHeaderValue(headers, name) {
			const pattern = new RegExp(`^${name}:\\s*([^\\r\\n]+)`, "im");
			const match = String(headers || "").match(pattern);
			return match ? match[1].trim() : "";
		}
		getImageMimeFromUrl(url) {
			try {
				const path = new URL(url, window.location.href).pathname.toLowerCase();
				if (path.endsWith(".jpg") || path.endsWith(".jpeg")) return "image/jpeg";
				if (path.endsWith(".png")) return "image/png";
				if (path.endsWith(".gif")) return "image/gif";
				if (path.endsWith(".webp")) return "image/webp";
				if (path.endsWith(".avif")) return "image/avif";
				if (path.endsWith(".bmp")) return "image/bmp";
				if (path.endsWith(".svg")) return "image/svg+xml";
			} catch {}
			return "";
		}
		normalizeImageBlob(blob, mimeType, url) {
			const type = String(mimeType || blob.type || "").split(";")[0].trim().toLowerCase() || this.getImageMimeFromUrl(url);
			if (!type.startsWith("image/")) return null;
			return blob.type === type ? blob : blob.slice(0, blob.size, type);
		}
		canFetchImageWithGm(url) {
			return url.protocol === "http:" || url.protocol === "https:";
		}
		async fetchImageBlobWithGm(url) {
			const gmXmlHttpRequest = globalThis.GM_xmlhttpRequest;
			if (typeof gmXmlHttpRequest !== "function") return null;
			return await new Promise((resolve, reject) => {
				gmXmlHttpRequest({
					method: "GET",
					url,
					responseType: "blob",
					timeout: USER_CONFIG.imageCache.fetchTimeoutMs,
					headers: { Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" },
					onload: (response) => {
						const status = Number(response.status || 0);
						if (status < 200 || status >= 300) {
							reject(new Error(`HTTP ${status}`));
							return;
						}
						const blob = response.response instanceof Blob ? response.response : null;
						if (!blob || blob.size <= 0) {
							resolve(null);
							return;
						}
						resolve(this.normalizeImageBlob(blob, this.getHeaderValue(response.responseHeaders, "content-type"), url));
					},
					onerror: (error) => reject(new Error(error?.message || String(error || "Request failed"))),
					ontimeout: () => reject(new Error("Request timed out"))
				});
			});
		}
		async fetchImageBlob(url) {
			const parsed = new URL(url, window.location.href);
			if (parsed.origin !== window.location.origin) {
				if (!this.canFetchImageWithGm(parsed)) return null;
				try {
					const gmBlob = await this.fetchImageBlobWithGm(parsed.href);
					if (gmBlob) return gmBlob;
				} catch (error) {
					this.log("debug", "GM image fetch fallback", {
						url: parsed.href,
						message: error?.message || String(error)
					});
				}
				return null;
			}
			const response = await fetch(parsed.href, {
				cache: "force-cache",
				credentials: parsed.origin === window.location.origin ? "same-origin" : "omit",
				referrerPolicy: "no-referrer"
			});
			if (!response.ok) throw new Error(`HTTP ${response.status}`);
			const blob = await response.blob();
			if (!(blob instanceof Blob) || blob.size <= 0) return null;
			return this.normalizeImageBlob(blob, response.headers.get("content-type") || "", parsed.href);
		}
		async fetchImageDataUrl(url) {
			const blob = await this.fetchImageBlob(url);
			if (!blob) return null;
			return {
				dataUrl: await this.blobToDataUrl(blob),
				size: blob.size
			};
		}
		async fetchAndCacheImageDataUrl(url, cacheKey) {
			const result = await this.fetchImageDataUrl(url);
			if (!result?.dataUrl) return null;
			await this.writeDataUrl(cacheKey, result.dataUrl, result.size);
			return result.dataUrl;
		}
		async resolveImagePreviewUrl(url, cacheKey = url) {
			const fetchUrl = this.getHttpUrl(url);
			if (!fetchUrl) return url;
			const cacheKeys = this.getCacheKeys(fetchUrl, cacheKey);
			if (cacheKeys.length === 0) return url;
			const primaryKey = cacheKeys[0];
			for (const key of cacheKeys) {
				const cachedDataUrl = await this.readCachedImageDataUrl(key);
				if (!cachedDataUrl) continue;
				if (key !== primaryKey) await this.writeDataUrl(primaryKey, cachedDataUrl, cachedDataUrl.length);
				return cachedDataUrl;
			}
			let pending = this.inflight.get(primaryKey);
			if (!pending) {
				pending = this.fetchAndCacheImageDataUrl(fetchUrl, primaryKey);
				this.inflight.set(primaryKey, pending);
				pending.then(() => this.inflight.delete(primaryKey), () => this.inflight.delete(primaryKey));
			}
			return await pending || url;
		}
		setImagePreviewSource(img, sourceUrl, cacheKey = sourceUrl) {
			const url = String(sourceUrl || "").trim();
			if (!url) return;
			img.dataset.cacheSource = url;
			img.src = IMAGE_PLACEHOLDER_URL;
			this.resolveImagePreviewUrl(url, cacheKey).then((resolvedUrl) => {
				if (img.dataset.cacheSource === url) img.src = resolvedUrl;
			}).catch((error) => {
				this.log("debug", "Image preview cache fallback", {
					url,
					message: error?.message || String(error)
				});
				if (img.dataset.cacheSource === url) img.src = url;
			});
		}
		async getAllRecords() {
			const db = await this.openDb();
			return await new Promise((resolve, reject) => {
				const tx = db.transaction(IMAGE_CACHE_STORE, "readonly");
				const request = tx.objectStore(IMAGE_CACHE_STORE).getAll();
				request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
				request.onerror = () => reject(request.error || new Error("Failed to list cached images"));
				tx.onerror = () => reject(tx.error || new Error("Image cache list transaction failed"));
			});
		}
		async deleteUrls(urls) {
			const keys = Array.from(new Set(urls.map((url) => this.getCacheKey(url)).filter(Boolean)));
			if (!keys.length) return;
			const db = await this.openDb();
			await new Promise((resolve, reject) => {
				const tx = db.transaction(IMAGE_CACHE_STORE, "readwrite");
				const store = tx.objectStore(IMAGE_CACHE_STORE);
				keys.forEach((url) => store.delete(url));
				tx.oncomplete = () => resolve();
				tx.onerror = () => reject(tx.error || new Error("Image cache prune transaction failed"));
			});
		}
		async prune() {
			try {
				const newestFirst = (await this.getAllRecords()).map((record) => ({
					url: record.url,
					size: Number(record.size || record.dataUrl?.length || record.blob?.size || 0),
					lastAccess: Number(record.lastAccess || record.cachedAt || 0)
				})).sort((a, b) => b.lastAccess - a.lastAccess);
				let keptCount = 0;
				let keptBytes = 0;
				const deleteUrls = [];
				newestFirst.forEach((record) => {
					const nextCount = keptCount + 1;
					const nextBytes = keptBytes + record.size;
					if (nextCount <= 3e3 && nextBytes <= 1073741824) {
						keptCount = nextCount;
						keptBytes = nextBytes;
						return;
					}
					if (record.url) deleteUrls.push(record.url);
				});
				await this.deleteUrls(deleteUrls);
				if (deleteUrls.length > 0) this.log("info", "Image cache pruned", { deleted: deleteUrls.length });
			} catch (error) {
				this.log("debug", "Image cache prune skipped", { message: error?.message || String(error) });
			}
		}
		schedulePrune() {
			window.clearTimeout(this.pruneTimer);
			this.pruneTimer = window.setTimeout(() => this.prune(), USER_CONFIG.imageCache.pruneDelayMs);
		}
	};
	var CHECK_TIMEOUT_MS = USER_CONFIG.linkCheck.timeoutMs;
	var CHECK_CONCURRENCY = USER_CONFIG.linkCheck.concurrency;
	function getAcceptHeader(type) {
		if (type === RecordType.IMAGE) return "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8";
		if (type === RecordType.SOUND) return "audio/*,*/*;q=0.8";
		if (type === RecordType.VIDEO) return "video/*,*/*;q=0.8";
		return "*/*";
	}
	function getErrorMessage(error) {
		return error?.message || String(error || "Request failed");
	}
	function isOkStatus(status) {
		return status >= 200 && status < 400;
	}
	function isKnownRangeHost(url) {
		try {
			const host = new URL(url).hostname.toLowerCase();
			return host === "files.catbox.moe" || host === "stream.vidhosting.in" || host === "i.ibb.co";
		} catch {
			return false;
		}
	}
	function getHeaderValue(headers, name) {
		const prefix = `${name.toLowerCase()}:`;
		return String(headers || "").split(/\r?\n/).map((line) => line.trim()).find((line) => line.toLowerCase().startsWith(prefix))?.slice(prefix.length).trim() || "";
	}
	function hasExpectedContentType(type, contentType) {
		const value = String(contentType || "").toLowerCase();
		if (!value) return true;
		if (type === RecordType.IMAGE) return value.startsWith("image/");
		if (type === RecordType.SOUND) return value.startsWith("audio/");
		if (type === RecordType.VIDEO) return value.startsWith("video/");
		return true;
	}
	function shouldRetryWithGet(status) {
		return status === 0 || status === 403 || status === 405 || status === 501;
	}
	function normalizeStatus(status) {
		const value = Number(status || 0);
		return Number.isFinite(value) ? value : 0;
	}
	function getMediaUrlFromLine(type, line) {
		if (type === RecordType.SOUND || type === RecordType.VIDEO) return parseLabeledUrlLine(line)?.url || "";
		return String(line || "").trim();
	}
	async function requestMediaUrl(url, method, accept) {
		const gmXmlHttpRequest = globalThis.GM_xmlhttpRequest;
		const headers = { Accept: accept };
		if (method === "GET") headers.Range = "bytes=0-0";
		if (typeof gmXmlHttpRequest === "function") return await new Promise((resolve, reject) => {
			gmXmlHttpRequest({
				method,
				url,
				headers,
				timeout: CHECK_TIMEOUT_MS,
				responseType: method === "GET" ? "arraybuffer" : void 0,
				onload: (response) => resolve({
					status: normalizeStatus(response.status),
					contentType: getHeaderValue(response.responseHeaders, "content-type")
				}),
				onerror: (error) => reject(new Error(getErrorMessage(error))),
				ontimeout: () => reject(new Error("Request timed out"))
			});
		});
		const response = await fetch(url, {
			method,
			headers,
			cache: "no-store",
			credentials: "omit",
			redirect: "follow",
			referrerPolicy: "no-referrer"
		});
		return {
			status: response.status,
			contentType: response.headers.get("content-type") || ""
		};
	}
	async function probeMediaUrl(url, type) {
		const accept = getAcceptHeader(type);
		try {
			const headResult = await requestMediaUrl(url, "HEAD", accept);
			const headStatus = normalizeStatus(headResult.status);
			if (isOkStatus(headStatus) && !isKnownRangeHost(url)) return hasExpectedContentType(type, headResult.contentType) ? {
				ok: true,
				status: headStatus,
				reason: ""
			} : {
				ok: false,
				status: headStatus,
				reason: `Unexpected content type: ${headResult.contentType}`
			};
			if (!isOkStatus(headStatus) && !shouldRetryWithGet(headStatus)) return {
				ok: false,
				status: headStatus,
				reason: `HTTP ${headStatus}`
			};
		} catch {}
		try {
			const getResult = await requestMediaUrl(url, "GET", accept);
			const getStatus = normalizeStatus(getResult.status);
			if (isOkStatus(getStatus)) return hasExpectedContentType(type, getResult.contentType) ? {
				ok: true,
				status: getStatus,
				reason: ""
			} : {
				ok: false,
				status: getStatus,
				reason: `Unexpected content type: ${getResult.contentType}`
			};
			return {
				ok: false,
				status: getStatus,
				reason: `HTTP ${getStatus}`
			};
		} catch (error) {
			return {
				ok: false,
				status: 0,
				reason: getErrorMessage(error)
			};
		}
	}
	async function findBrokenMediaLinks(type, content, onProgress) {
		const entries = getCategoryDataLines(content).map((line, index) => ({
			index,
			line,
			url: getMediaUrlFromLine(type, line)
		})).filter((entry) => entry.url);
		const broken = [];
		let cursor = 0;
		let checked = 0;
		const markBroken = (entry, reason, status = 0) => {
			broken.push({
				index: entry.index,
				line: entry.line,
				url: entry.url,
				reason,
				status
			});
		};
		entries.forEach((entry) => {
			if (!isHttpUrl(entry.url)) {
				onProgress?.({
					checked,
					total: entries.length,
					brokenCount: broken.length,
					currentUrl: entry.url,
					currentLine: entry.line
				});
				markBroken(entry, "Invalid URL");
				checked += 1;
				onProgress?.({
					checked,
					total: entries.length,
					brokenCount: broken.length,
					currentUrl: entry.url,
					currentLine: entry.line
				});
			}
		});
		const httpEntries = entries.filter((entry) => isHttpUrl(entry.url));
		onProgress?.({
			checked,
			total: entries.length,
			brokenCount: broken.length
		});
		const runWorker = async () => {
			while (cursor < httpEntries.length) {
				const entry = httpEntries[cursor];
				cursor += 1;
				onProgress?.({
					checked,
					total: entries.length,
					brokenCount: broken.length,
					currentUrl: entry.url,
					currentLine: entry.line
				});
				const result = await probeMediaUrl(entry.url, type);
				checked += 1;
				if (!result.ok) markBroken(entry, result.reason, result.status);
				onProgress?.({
					checked,
					total: entries.length,
					brokenCount: broken.length,
					currentUrl: entry.url,
					currentLine: entry.line
				});
			}
		};
		const workers = Array.from({ length: Math.min(CHECK_CONCURRENCY, httpEntries.length) }, runWorker);
		await Promise.all(workers);
		broken.sort((a, b) => a.index - b.index);
		return {
			total: entries.length,
			checked,
			broken
		};
	}
	async function readStoredJson(key, label) {
		try {
			let raw = null;
			const gmGetValue = globalThis.GM_getValue;
			if (typeof gmGetValue === "function") raw = await gmGetValue(key, null);
			else raw = window.localStorage.getItem(key);
			if (!raw) return null;
			return typeof raw === "string" ? JSON.parse(raw) : raw;
		} catch (error) {
			log("warn", `Failed to read ${label}`, { message: error?.message || String(error) });
			return null;
		}
	}
	async function writeStoredJson(key, value) {
		const serialized = JSON.stringify(value);
		const gmSetValue = globalThis.GM_setValue;
		if (typeof gmSetValue === "function") {
			await gmSetValue(key, serialized);
			return;
		}
		window.localStorage.setItem(key, serialized);
	}
	async function readStoredState() {
		return readStoredJson(STORAGE_KEY, "DB state");
	}
	async function writeStoredState(state) {
		await writeStoredJson(STORAGE_KEY, state);
	}
	async function readStoredUiState() {
		return readStoredJson(UI_STORAGE_KEY, "UI state");
	}
	async function writeStoredUiState(state) {
		await writeStoredJson(UI_STORAGE_KEY, state);
	}
	async function readStoredUploaderSettings() {
		return readStoredJson(UPLOADER_SETTINGS_STORAGE_KEY, "uploader settings");
	}
	async function writeStoredUploaderSettings(settings) {
		await writeStoredJson(UPLOADER_SETTINGS_STORAGE_KEY, settings);
	}
	var dragCategoryId = null;
	function renderDbManager(root, viewModel, actions) {
		const activeTab = viewModel.uiState?.activeTab || MANAGER_TABS.EDITOR;
		const layoutChildren = activeTab === MANAGER_TABS.EDITOR ? [createManagerSide(viewModel, actions), createManagerWorkspace(viewModel, actions)] : [createManagerWorkspace(viewModel, actions)];
		root.replaceChildren(createManagerTopBar(viewModel, actions), createElement("div", { className: `ctrlem-db-manager-layout${activeTab !== MANAGER_TABS.EDITOR ? " is-single-panel" : ""}` }, layoutChildren));
	}
	function renderCategoryList(list, viewModel, actions) {
		if (!list) return;
		const selected = viewModel.selectedCategory;
		list.replaceChildren();
		viewModel.categories.forEach((category) => {
			const item = createElement("button", {
				className: `ctrlem-db-category-item${category.id === selected?.id ? " is-selected" : ""}`,
				text: "",
				type: "button",
				title: `${category.name} - drag to reorder`,
				attrs: { draggable: "true" },
				dataset: { id: category.id }
			}, [createElement("span", { className: "ctrlem-db-category-item-main" }, [createElement("span", {
				className: "ctrlem-db-drag-handle",
				text: "::",
				attrs: { "aria-hidden": "true" }
			}), createElement("span", {
				className: "ctrlem-db-category-item-name",
				text: category.name
			})]), createElement("span", {
				className: "ctrlem-db-category-item-count",
				text: String(parseLines(category.content).length)
			})]);
			item.addEventListener("click", () => actions.selectCategory(category.id));
			item.addEventListener("dragstart", (event) => {
				dragCategoryId = category.id;
				item.classList.add("is-dragging");
				event.dataTransfer?.setData("text/plain", category.id);
				event.dataTransfer?.setDragImage(item, 10, 10);
			});
			item.addEventListener("dragend", () => {
				dragCategoryId = null;
				item.classList.remove("is-dragging");
				list.querySelectorAll(".ctrlem-db-category-item.is-drop-target").forEach((target) => {
					target.classList.remove("is-drop-target");
				});
			});
			item.addEventListener("dragover", (event) => {
				event.preventDefault();
				if (dragCategoryId && dragCategoryId !== category.id) item.classList.add("is-drop-target");
			});
			item.addEventListener("dragleave", () => {
				item.classList.remove("is-drop-target");
			});
			item.addEventListener("drop", (event) => {
				event.preventDefault();
				item.classList.remove("is-drop-target");
				const sourceId = event.dataTransfer?.getData("text/plain") || dragCategoryId;
				actions.reorderCategory(sourceId, category.id);
			});
			list.appendChild(item);
		});
		window.setTimeout(() => {
			list.scrollTop = Math.max(0, Number(viewModel.uiState?.categoryListScrollTop) || 0);
		}, 0);
	}
	function setManagerStatus(message, level = "info") {
		const status = document.getElementById(UI_IDS.dbManagerStatus);
		if (!status) return;
		status.textContent = message || "";
		status.dataset.level = level;
	}
	function createManagerTopBar(viewModel, actions) {
		return createElement("div", { className: "ctrlem-db-manager-topbar" }, [createTopTabs(viewModel, actions)]);
	}
	function createTopTabs(viewModel, actions) {
		const activeTab = viewModel.uiState?.activeTab || MANAGER_TABS.EDITOR;
		const row = createElement("div", {
			className: "ctrlem-db-type-row",
			attrs: { role: "tablist" }
		});
		TYPE_ORDER.forEach((type) => {
			const isActive = activeTab === MANAGER_TABS.EDITOR && viewModel.activeType === type;
			const button = createElement("button", {
				className: `ctrlem-db-type-option${isActive ? " is-active" : ""}`,
				text: TYPE_LABELS[type],
				type: "button",
				attrs: {
					role: "tab",
					"aria-selected": String(isActive)
				},
				dataset: { type }
			});
			button.addEventListener("click", () => actions.setActiveType(type));
			row.appendChild(button);
		});
		[{
			key: MANAGER_TABS.SETTINGS,
			label: "Settings"
		}, {
			key: MANAGER_TABS.INFO,
			label: "Info"
		}].forEach((tab) => {
			const isActive = activeTab === tab.key;
			const button = createElement("button", {
				className: `ctrlem-db-type-option${isActive ? " is-active" : ""}`,
				text: tab.label,
				type: "button",
				attrs: {
					role: "tab",
					"aria-selected": String(isActive)
				},
				dataset: { tab: tab.key }
			});
			button.addEventListener("click", () => actions.setActiveTab(tab.key));
			row.appendChild(button);
		});
		return row;
	}
	function createManagerSide(viewModel, actions) {
		const list = createElement("div", {
			id: UI_IDS.dbManagerCategoryList,
			className: "ctrlem-db-category-list"
		});
		let scrollTimer = 0;
		list.addEventListener("scroll", () => {
			window.clearTimeout(scrollTimer);
			scrollTimer = window.setTimeout(() => actions.setCategoryListScroll(list.scrollTop), USER_CONFIG.ui.scrollSaveDelayMs);
		});
		renderCategoryList(list, viewModel, actions);
		return createElement("div", { className: "ctrlem-db-manager-side" }, [
			createElement("div", {
				className: "ctrlem-db-side-title",
				text: "Categories DB:"
			}),
			list,
			createElement("div", {
				id: UI_IDS.dbManagerStatus,
				className: "ctrlem-db-status",
				attrs: { "aria-live": "polite" }
			}),
			createElement("div", { className: "ctrlem-db-side-note" }, [createElement("span", { text: "Categories can be dragged to change order." })])
		]);
	}
	function createManagerWorkspace(viewModel, actions) {
		const activeTab = viewModel.uiState?.activeTab || MANAGER_TABS.EDITOR;
		let panel = createEditorPanel(viewModel, actions);
		if (activeTab === MANAGER_TABS.SETTINGS) panel = createSettingsTools(viewModel, actions);
		if (activeTab === MANAGER_TABS.INFO) panel = createInfoPanel(viewModel);
		return createElement("div", { className: "ctrlem-db-manager-workspace" }, [panel]);
	}
	function createEditorPanel(viewModel, actions) {
		const activeCategory = viewModel.selectedCategory;
		const nameInput = createElement("input", {
			className: "ctrlem-db-category-name-input",
			type: "text",
			value: activeCategory?.name || "",
			attrs: {
				autocomplete: "off",
				spellcheck: "false"
			}
		});
		const addButton = createElement("button", {
			className: "btn btn-sm btn-secondary",
			text: "Add",
			type: "button"
		});
		const deleteButton = createElement("button", {
			className: "btn btn-sm btn-secondary",
			text: "Delete",
			type: "button"
		});
		const textarea = createElement("textarea", {
			id: UI_IDS.dbManagerTextarea,
			className: "ctrlem-db-manager-textarea",
			value: activeCategory?.content || "",
			attrs: {
				wrap: "off",
				spellcheck: "false"
			}
		});
		const commitName = () => {
			nameInput.value = actions.renameCategoryTo(nameInput.value);
		};
		nameInput.addEventListener("change", commitName);
		nameInput.addEventListener("blur", commitName);
		nameInput.addEventListener("keydown", (event) => {
			if (event.key !== "Enter") return;
			event.preventDefault();
			commitName();
			nameInput.blur();
		});
		addButton.addEventListener("click", actions.addCategory);
		deleteButton.addEventListener("click", actions.deleteCategory);
		textarea.addEventListener("input", () => {
			actions.updateEditorContent(textarea.value);
		});
		return createElement("div", {
			className: "ctrlem-db-tab-panel ctrlem-db-editor-panel",
			attrs: { role: "tabpanel" }
		}, [
			createElement("div", { className: "ctrlem-db-editor-head" }, [
				nameInput,
				addButton,
				deleteButton
			]),
			textarea,
			createEditorHelp(viewModel.activeType),
			createEditorSaveRow(viewModel, actions)
		]);
	}
	function createEditorSaveRow(viewModel, actions) {
		const autoSaveToggle = createElement("input", {
			type: "checkbox",
			checked: viewModel.autoSave
		});
		const saveButton = createElement("button", {
			className: "btn btn-sm btn-primary",
			text: "Save",
			type: "button"
		});
		autoSaveToggle.addEventListener("change", () => actions.setAutoSave(autoSaveToggle.checked));
		saveButton.addEventListener("click", () => actions.save());
		return createElement("div", { className: "ctrlem-db-editor-save-row" }, [
			createElement("label", {
				className: "toggle ctrlem-db-editor-autosave-toggle",
				title: "Toggle DB auto-save"
			}, [autoSaveToggle, createElement("span", { className: "toggle-slider" })]),
			createElement("span", {
				className: "text-muted ctrlem-db-autosave-label",
				text: "Auto-save"
			}),
			saveButton
		]);
	}
	function isMediaManagerType(type) {
		return type === RecordType.IMAGE || type === RecordType.SOUND || type === RecordType.VIDEO;
	}
	function createEditorHelp(type) {
		const items = ["One item per line."];
		if (isMediaManagerType(type)) items.push("Media link names can be written after the URL separated by a space.");
		if (type === RecordType.IMAGE) items.push("- (no previews) disables image previews for that category.");
		return createElement("ul", { className: "ctrlem-db-editor-help" }, items.map((text) => createElement("li", { text })));
	}
	function formatBrokenLinkResults(items) {
		return [...new Set(items.map((item) => item.url).filter(Boolean))].join("\n");
	}
	function formatLinkCheckType(type) {
		if (type === RecordType.IMAGE) return "Images";
		if (type === RecordType.SOUND) return "Sounds";
		if (type === RecordType.VIDEO) return "Videos";
		return "-";
	}
	function getLinkCheckScopeType(state) {
		return [
			RecordType.IMAGE,
			RecordType.SOUND,
			RecordType.VIDEO
		].includes(state.scopeType) ? state.scopeType : RecordType.IMAGE;
	}
	function createLinkCheckScopeControls(viewModel, state, actions) {
		const scopeAll = state.scopeAll !== false;
		const scopeType = getLinkCheckScopeType(state);
		const mediaCategories = viewModel.linkCheckCategories || {};
		const categories = Array.isArray(mediaCategories[scopeType]) ? mediaCategories[scopeType] : [];
		const scopeCategoryId = categories.some((category) => category.id === state.scopeCategoryId) ? state.scopeCategoryId : categories[0]?.id || "";
		const allMediaToggle = createElement("input", {
			type: "checkbox",
			checked: scopeAll
		});
		const typeSelect = createElement("select", {
			className: "ctrlem-db-link-check-select",
			value: scopeType
		}, [
			RecordType.IMAGE,
			RecordType.SOUND,
			RecordType.VIDEO
		].map((type) => createElement("option", {
			text: formatLinkCheckType(type),
			value: type,
			attrs: { value: type }
		})));
		const categorySelect = createElement("select", {
			className: "ctrlem-db-link-check-select",
			value: scopeCategoryId
		}, categories.map((category) => createElement("option", {
			text: category.name,
			value: category.id,
			attrs: { value: category.id }
		})));
		typeSelect.value = scopeType;
		categorySelect.value = scopeCategoryId;
		allMediaToggle.disabled = Boolean(state.isBusy);
		typeSelect.disabled = Boolean(state.isBusy);
		categorySelect.disabled = Boolean(state.isBusy);
		allMediaToggle.addEventListener("change", () => actions.setLinkCheckScope({ scopeAll: allMediaToggle.checked }));
		typeSelect.addEventListener("change", () => actions.setLinkCheckScope({ scopeType: typeSelect.value }));
		categorySelect.addEventListener("change", () => actions.setLinkCheckScope({ scopeCategoryId: categorySelect.value }));
		const children = [createElement("label", { className: "ctrlem-db-settings-check ctrlem-db-link-check-all-toggle" }, [allMediaToggle, createElement("span", { text: "All media" })])];
		if (!scopeAll) children.push(createElement("label", { className: "ctrlem-db-link-check-field" }, [createElement("span", { text: "Type" }), typeSelect]), createElement("label", { className: "ctrlem-db-link-check-field" }, [createElement("span", { text: "Category" }), categorySelect]));
		return createElement("div", { className: "ctrlem-db-link-check-scope" }, children);
	}
	function createBrokenLinkTools(viewModel, actions) {
		const state = viewModel.linkCheck || {};
		const broken = Array.isArray(state.broken) ? state.broken : [];
		const status = createElement("div", {
			className: "ctrlem-db-link-check-status",
			text: state.statusMessage || "No check results yet.",
			attrs: { "aria-live": "polite" }
		});
		status.dataset.level = state.statusLevel || "info";
		const checkButton = createElement("button", {
			className: "btn btn-sm btn-secondary",
			text: state.isBusy ? "Checking..." : "Check broken links",
			type: "button"
		});
		const removeButton = createElement("button", {
			className: "btn btn-sm btn-secondary",
			text: "Remove broken",
			type: "button"
		});
		const resultTextarea = createElement("textarea", {
			className: "ctrlem-db-link-check-results",
			value: formatBrokenLinkResults(broken),
			attrs: {
				readonly: "",
				rows: "4",
				placeholder: "Broken links will appear here",
				spellcheck: "false"
			}
		});
		const currentCategory = state.currentCategoryName ? `${formatLinkCheckType(state.currentType)} / ${state.currentCategoryName}` : "-";
		const currentUrl = state.currentUrl || "-";
		checkButton.disabled = Boolean(state.isBusy);
		removeButton.disabled = Boolean(state.isBusy || broken.length === 0);
		checkButton.addEventListener("click", (event) => {
			event.preventDefault();
			event.stopPropagation();
			actions.checkBrokenLinks();
		});
		removeButton.addEventListener("click", (event) => {
			event.preventDefault();
			event.stopPropagation();
			actions.removeBrokenLinks();
		});
		return createElement("div", { className: "ctrlem-db-link-check-tools" }, [
			createElement("div", {
				className: "ctrlem-db-settings-title",
				text: "Check broken links"
			}),
			createElement("div", {
				className: "ctrlem-db-link-check-copy",
				text: "Checking many links can take some time. You can check all media or a specific category."
			}),
			createLinkCheckScopeControls(viewModel, state, actions),
			createElement("div", { className: "ctrlem-db-link-check-actions" }, [
				checkButton,
				removeButton,
				status
			]),
			createElement("div", { className: "ctrlem-db-link-check-progress" }, [
				createElement("div", { className: "ctrlem-db-link-check-row" }, [createElement("span", { text: "Checked links" }), createElement("strong", { text: `${Number(state.checked || 0)}/${Number(state.total || 0)}` })]),
				createElement("div", { className: "ctrlem-db-link-check-row" }, [createElement("span", { text: "Broken" }), createElement("strong", { text: String(Number(state.brokenCount || 0)) })]),
				createElement("div", { className: "ctrlem-db-link-check-row" }, [createElement("span", { text: "Category" }), createElement("strong", { text: currentCategory })]),
				createElement("div", { className: "ctrlem-db-link-check-row ctrlem-db-link-check-row-url" }, [createElement("span", { text: "Current link" }), createElement("strong", {
					text: currentUrl,
					title: currentUrl
				})])
			]),
			resultTextarea
		]);
	}
	function createInfoPanel(viewModel) {
		const profileTitle = String(viewModel.profileTitle || "").trim();
		const children = [createElement("div", {
			className: "ctrlem-db-settings-title",
			text: "CtrlEm DB userscript"
		})];
		if (profileTitle) children.push(createElement("p", {
			className: "ctrlem-db-info-copy",
			text: `Profile: ${profileTitle}`
		}));
		children.push(createElement("p", {
			className: "ctrlem-db-info-copy",
			text: "A local database manager for CtrlEm command inputs, picker rows, media links, uploads, and auto-send workflows."
		}), createElement("ul", { className: "ctrlem-db-info-list" }, [
			createElement("li", { text: "Stores reusable links, text, images, sounds, and videos by category." }),
			createElement("li", { text: "Adds picker controls beside supported CtrlEm inputs." }),
			createElement("li", { text: "Supports import, export, defaults restore, and media upload helpers." }),
			createElement("li", { text: "Can cycle visible category items with auto-send controls." })
		]));
		return createElement("div", {
			className: "ctrlem-db-tab-panel ctrlem-db-info-panel",
			attrs: { role: "tabpanel" }
		}, [createElement("div", { className: "ctrlem-db-settings-section" }, children)]);
	}
	function createSettingsTools(viewModel, actions) {
		const importAllInput = createElement("input", {
			id: UI_IDS.dbManagerImportAll,
			type: "file",
			attrs: {
				accept: ".json,application/json",
				hidden: ""
			}
		});
		importAllInput.addEventListener("change", () => {
			actions.importAllCategories(importAllInput.files?.[0]);
			importAllInput.value = "";
		});
		const exportAllButton = createElement("button", {
			className: "btn btn-sm btn-secondary",
			text: "Export all (json)",
			type: "button"
		});
		const importAllButton = createElement("button", {
			className: "btn btn-sm btn-secondary",
			text: "Import all (json)",
			type: "button"
		});
		const restoreDefaultsButton = createElement("button", {
			className: "btn btn-sm btn-secondary",
			text: "Restore Defaults",
			type: "button"
		});
		exportAllButton.addEventListener("click", actions.exportAll);
		importAllButton.addEventListener("click", () => importAllInput.click());
		restoreDefaultsButton.addEventListener("click", actions.restoreDefaults);
		const imgbbApiKey = createElement("input", {
			className: "ctrlem-db-settings-input",
			type: "password",
			value: viewModel.uploaderSettings?.imgbbApiKey || "",
			attrs: {
				placeholder: "ImgBB API key",
				autocomplete: "off"
			}
		});
		const catboxUserhash = createElement("input", {
			className: "ctrlem-db-settings-input",
			type: "password",
			value: viewModel.uploaderSettings?.catboxUserhash || "",
			attrs: {
				placeholder: "Catbox userhash",
				autocomplete: "off"
			}
		});
		const hideCtrlEmUploader = createElement("input", {
			type: "checkbox",
			checked: viewModel.uploaderSettings?.hideCtrlEmUploader === true
		});
		const autoDownloadSendOrDeleteImages = createElement("input", {
			type: "checkbox",
			checked: viewModel.uploaderSettings?.autoDownloadSendOrDeleteImages !== false
		});
		imgbbApiKey.addEventListener("input", () => actions.setUploaderSetting("imgbbApiKey", imgbbApiKey.value));
		catboxUserhash.addEventListener("input", () => actions.setUploaderSetting("catboxUserhash", catboxUserhash.value));
		hideCtrlEmUploader.addEventListener("change", () => actions.setUploaderSetting("hideCtrlEmUploader", hideCtrlEmUploader.checked));
		autoDownloadSendOrDeleteImages.addEventListener("change", () => {
			actions.setUploaderSetting("autoDownloadSendOrDeleteImages", autoDownloadSendOrDeleteImages.checked);
		});
		return createElement("div", {
			className: "ctrlem-db-tab-panel ctrlem-db-settings-panel",
			attrs: { role: "tabpanel" }
		}, [
			createElement("div", { className: "ctrlem-db-settings-section" }, [
				createElement("div", {
					className: "ctrlem-db-settings-title",
					text: "Uploader settings"
				}),
				createElement("label", { className: "ctrlem-db-settings-field" }, [createElement("span", { text: "ImgBB API key" }), imgbbApiKey]),
				createElement("label", { className: "ctrlem-db-settings-field" }, [createElement("span", { text: "Catbox userhash (optional)" }), catboxUserhash]),
				createElement("label", { className: "ctrlem-db-settings-field ctrlem-db-settings-check" }, [hideCtrlEmUploader, createElement("span", { text: "Hide CtrlEm uploader box" })]),
				createElement("label", { className: "ctrlem-db-settings-field ctrlem-db-settings-check" }, [autoDownloadSendOrDeleteImages, createElement("span", { text: "Auto-download Send or Delete images" })])
			]),
			createElement("div", { className: "ctrlem-db-settings-section" }, [createBrokenLinkTools(viewModel, actions)]),
			createElement("div", { className: "ctrlem-db-settings-section ctrlem-db-database-section" }, [createElement("div", {
				className: "ctrlem-db-settings-title",
				text: "Database"
			}), createElement("div", { className: "ctrlem-db-import-actions" }, [
				exportAllButton,
				importAllButton,
				restoreDefaultsButton,
				importAllInput
			])])
		]);
	}
	var APP_CSS = `
.ctrlem-db-hidden-site-ui {
  display: none !important;
}

.ctrlem-db-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.ctrlem-db-head-copy {
  min-width: 0;
}

.ctrlem-db-button {
  flex: 0 0 auto;
  min-width: 44px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 0.75rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  cursor: pointer;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1;
  transition: var(--transition, all 0.2s ease);
}

.ctrlem-db-button:hover,
.ctrlem-db-button:focus-visible,
.ctrlem-db-button.is-active {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-phrase-list {
  display: grid;
  gap: 8px;
  max-height: 180px;
  margin: 10px 0 0;
  overflow-y: auto;
  padding-right: 2px;
}

.ctrlem-db-text-picker {
  display: grid;
  gap: 8px;
  max-height: none;
}

.ctrlem-db-text-toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
}

.ctrlem-db-text-select {
  width: 100%;
}

.ctrlem-db-category {
  overflow: hidden;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-secondary, #1a1a1a);
}

.ctrlem-db-category-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-bottom: 1px solid var(--border-color, #333);
  background: transparent;
  color: var(--text-secondary, #b0b0b0);
  cursor: pointer;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.3;
  text-align: left;
}

.ctrlem-db-category-title:hover,
.ctrlem-db-category-title:focus-visible {
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  color: var(--text-primary, #fff);
  outline: none;
}

.ctrlem-db-category.is-collapsed .ctrlem-db-category-title {
  border-bottom-color: transparent;
}

.ctrlem-db-category-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ctrlem-db-category-meta {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.ctrlem-db-category-count {
  color: var(--text-muted, #666);
  font-weight: 600;
}

.ctrlem-db-category-chevron {
  display: inline-block;
  color: var(--text-muted, #666);
  transition: transform 0.15s ease;
}

.ctrlem-db-category-title[aria-expanded="false"] .ctrlem-db-category-chevron {
  transform: rotate(-90deg);
}

.ctrlem-db-rows {
  display: grid;
}

.ctrlem-db-text-picker .ctrlem-db-rows {
  max-height: 180px;
  overflow-y: auto;
}

.ctrlem-db-rows[hidden] {
  display: none;
}

.ctrlem-db-row {
  width: 100%;
  min-height: 34px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px;
  border: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  border-left: 2px solid transparent;
  background: transparent;
  color: var(--text-primary, #fff);
  cursor: pointer;
  font: inherit;
  font-size: 0.84rem;
  line-height: 1.35;
  overflow: hidden;
  text-align: left;
  text-overflow: ellipsis;
  transition: var(--transition, all 0.2s ease);
  white-space: nowrap;
}

.ctrlem-db-row:last-child {
  border-bottom: 0;
}

.ctrlem-db-row:hover,
.ctrlem-db-row:focus-visible,
.ctrlem-db-row.is-selected {
  border-left-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-media-picker {
  display: grid;
  gap: 8px;
  margin: 0 0 10px;
}

.ctrlem-db-media-toolbar {
  display: flex;
  align-items: center;
  gap: 6px;
}

.ctrlem-db-media-select {
  width: 100%;
}

.ctrlem-db-category-toolbar select {
  min-width: 0;
  flex: 1 1 auto;
}

.ctrlem-db-category-tool-button {
  flex: 0 0 32px;
  width: 32px;
  height: 32px;
  display: inline-grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--border-color, #333);
  border-radius: 6px;
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  cursor: pointer;
  font: inherit;
  font-size: 0.92rem;
  font-weight: 800;
  line-height: 1;
}

.ctrlem-db-category-tool-button:hover,
.ctrlem-db-category-tool-button:focus-visible {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-category-tool-button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.ctrlem-db-preview-toggle {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 32px;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.74rem;
  font-weight: 700;
  white-space: nowrap;
}

.ctrlem-db-preview-toggle::after {
  content: attr(data-tooltip);
  position: absolute;
  left: 50%;
  bottom: calc(100% + 6px);
  z-index: 4;
  max-width: 180px;
  padding: 0.28rem 0.45rem;
  border: 1px solid var(--border-color, #333);
  border-radius: 6px;
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.32);
  font-size: 0.7rem;
  line-height: 1.2;
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, 2px);
  transition: opacity 0.05s ease, transform 0.05s ease;
  white-space: nowrap;
}

.ctrlem-db-preview-toggle:hover::after,
.ctrlem-db-preview-toggle:focus-within::after {
  opacity: 1;
  transform: translate(-50%, 0);
}

.ctrlem-db-preview-toggle-input {
  accent-color: var(--accent-primary, #5865f2);
}

.ctrlem-db-preview-toggle:has(.ctrlem-db-preview-toggle-input:disabled) {
  cursor: not-allowed;
  opacity: 0.55;
}

.ctrlem-db-media-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  max-height: calc(64px * 4 + 8px * 3);
  overflow-y: auto;
  scrollbar-width: thin;
}

.ctrlem-db-media-grid::-webkit-scrollbar {
  width: 6px;
}

.ctrlem-db-media-tile {
  position: relative;
  width: 64px;
  height: 64px;
  overflow: hidden;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 6px;
  background: var(--bg-secondary, #1a1a1a);
  color: var(--text-primary, #fff);
  cursor: pointer;
  transition: var(--transition, all 0.2s ease);
}

.ctrlem-db-media-tile:hover,
.ctrlem-db-media-tile:focus-visible,
.ctrlem-db-media-tile.is-selected {
  border-color: var(--accent-primary, #5865f2);
  outline: none;
}

.ctrlem-db-media-tile.is-selected {
  box-shadow: 0 0 0 2px rgba(88, 101, 242, 0.3);
}

.ctrlem-db-media-tile.is-deleting {
  opacity: 0.55;
  pointer-events: none;
}

.ctrlem-db-media-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ctrlem-db-media-tile.no-preview {
  display: grid;
  place-items: center;
  padding: 5px;
}

.ctrlem-db-media-url-label {
  display: -webkit-box;
  max-width: 100%;
  overflow: hidden;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.62rem;
  line-height: 1.15;
  overflow-wrap: anywhere;
  text-align: center;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
}

.ctrlem-db-media-delete {
  position: absolute;
  top: 3px;
  right: 3px;
  z-index: 3;
  width: 20px;
  height: 20px;
  display: inline-grid;
  place-items: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  cursor: pointer;
  font: inherit;
  font-size: 0.75rem;
  font-weight: 800;
  line-height: 1;
}

.ctrlem-db-media-delete:hover,
.ctrlem-db-media-delete:focus-visible {
  border-color: var(--danger, #ed4245);
  background: var(--danger, #ed4245);
  outline: none;
}

.ctrlem-db-autosend-group {
  display: flex;
  align-items: center;
  gap: 8px;
}

.ctrlem-db-autosend-group > [data-send] {
  flex: 1 1 auto;
}

.ctrlem-db-auto-send-button {
  flex: 0 0 38px;
  min-width: 38px;
  height: 32px;
  display: inline-grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  cursor: pointer;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 800;
  line-height: 1;
  transition: var(--transition, all 0.2s ease);
}

.ctrlem-db-auto-send-button:hover,
.ctrlem-db-auto-send-button:focus-visible,
.ctrlem-db-auto-send-button.is-active {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-auto-send-button.is-active {
  color: var(--success, #57f287);
}

.ctrlem-db-autosend-sec {
  flex: 0 0 auto;
  color: var(--text-muted, #666);
  font-size: 0.78rem;
  font-weight: 700;
}

#toast-container {
  bottom: var(--ctrlem-db-toast-bottom, 2rem) !important;
}

.ctrlem-db-autosend-manager {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 999998;
  width: min(720px, calc(100vw - 32px));
  display: grid;
  gap: 8px;
  padding: 8px;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-secondary, #1a1a1a);
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.34);
  color: var(--text-primary, #fff);
  font: inherit;
  font-size: 0.78rem;
}

.ctrlem-db-autosend-head {
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px;
}

.ctrlem-db-autosend-note {
  min-width: 0;
  overflow: hidden;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.72rem;
  font-weight: 800;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ctrlem-db-autosend-rows {
  display: grid;
  gap: 6px;
}

.ctrlem-db-autosend-manager.is-collapsed .ctrlem-db-autosend-rows {
  display: none;
}

.ctrlem-db-autosend-row {
  min-width: 0;
  min-height: 38px;
  display: grid;
  grid-template-columns: minmax(68px, 92px) minmax(92px, 1fr) minmax(58px, 78px) minmax(86px, 1fr) minmax(78px, 110px) 38px auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: rgba(255, 255, 255, 0.035);
  animation: ctrlem-db-autosend-row-in 0.16s ease-out;
  transition: transform 0.16s ease, opacity 0.16s ease, border-color 0.16s ease;
}

.ctrlem-db-autosend-row.is-manual {
  border-color: rgba(87, 242, 135, 0.45);
}

.ctrlem-db-autosend-kind,
.ctrlem-db-autosend-profile,
.ctrlem-db-autosend-category,
.ctrlem-db-autosend-code {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ctrlem-db-autosend-kind {
  min-height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.16rem 0.38rem;
  border: 1px solid var(--border-color, #333);
  border-radius: 6px;
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.68rem;
  font-weight: 800;
  text-transform: uppercase;
}

.ctrlem-db-autosend-profile {
  color: var(--text-primary, #fff);
  font-weight: 800;
}

.ctrlem-db-row-label {
  min-width: 0;
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ctrlem-db-row-preview {
  flex: 0 0 auto;
  min-height: 24px;
  padding: 0.18rem 0.45rem;
  border: 1px solid var(--border-color, #333);
  border-radius: 6px;
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-secondary, #b0b0b0);
  cursor: pointer;
  font: inherit;
  font-size: 0.7rem;
  font-weight: 800;
  line-height: 1;
}

.ctrlem-db-row-preview:hover,
.ctrlem-db-row-preview:focus-visible {
  border-color: var(--accent-primary, #5865f2);
  color: var(--text-primary, #fff);
  outline: none;
}

.ctrlem-db-autosend-category {
  font-weight: 800;
}

.ctrlem-db-autosend-code {
  color: var(--text-secondary, #b0b0b0);
  font-weight: 700;
}

.ctrlem-db-autosend-cooldown {
  height: 6px;
  overflow: hidden;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
}

.ctrlem-db-autosend-progress {
  display: block;
  width: 0%;
  height: 100%;
  border-radius: inherit;
  background: var(--accent-primary, #5865f2);
  transition: width 0.18s linear;
}

.ctrlem-db-autosend-row.is-manual .ctrlem-db-autosend-progress {
  background: var(--success, #57f287);
}

.ctrlem-db-autosend-wait {
  color: var(--text-muted, #666);
  font-size: 0.72rem;
  font-weight: 800;
  text-align: right;
}

.ctrlem-db-autosend-wait.is-open-tab {
  color: var(--accent-primary, #5865f2);
  cursor: pointer;
  text-decoration: underline;
}

.ctrlem-db-autosend-mini-button {
  min-width: 54px;
  min-height: 24px;
  padding: 0.22rem 0.45rem;
  border: 1px solid var(--border-color, #333);
  border-radius: 6px;
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  cursor: pointer;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 800;
  line-height: 1;
}

.ctrlem-db-autosend-mini-button:hover,
.ctrlem-db-autosend-mini-button:focus-visible {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-autosend-stop:hover,
.ctrlem-db-autosend-stop:focus-visible {
  border-color: var(--danger, #ed4245);
  color: var(--danger, #ed4245);
}

.ctrlem-db-autosend-stop-all:hover,
.ctrlem-db-autosend-stop-all:focus-visible {
  border-color: var(--danger, #ed4245);
  color: var(--danger, #ed4245);
}

@keyframes ctrlem-db-autosend-row-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.ctrlem-db-interval-field {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-muted, #666);
  font-size: 0.8rem;
}

.ctrlem-db-interval-input {
  width: 64px;
  min-height: 32px;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: inherit;
}

.ctrlem-db-manager {
  padding: 1rem 1.25rem;
  display: grid;
  gap: 12px;
}

.ctrlem-db-manager[hidden] {
  display: none !important;
}

.ctrlem-db-type-row,
.ctrlem-db-manager-actions,
.ctrlem-db-export-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-type-option {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 32px;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-secondary, #b0b0b0);
  cursor: pointer;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 700;
}

.ctrlem-db-type-option.is-active {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  color: var(--text-primary, #fff);
}

.ctrlem-db-manager-layout {
  display: grid;
  grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);
  gap: 12px;
}

.ctrlem-db-manager-layout.is-single-panel {
  grid-template-columns: 1fr;
}

.ctrlem-db-manager-side,
.ctrlem-db-manager-editor,
.ctrlem-db-manager-workspace {
  min-width: 0;
}

.ctrlem-db-manager-side {
  display: grid;
  gap: 8px;
  align-content: start;
}

.ctrlem-db-side-title {
  color: var(--text-primary, #fff);
  font-size: 0.86rem;
  font-weight: 800;
}

.ctrlem-db-category-list {
  display: grid;
  gap: 6px;
  max-height: 380px;
  overflow: auto;
}

.ctrlem-db-side-actions,
.ctrlem-db-import-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-side-note {
  color: var(--text-muted, #666);
  font-size: 0.76rem;
  line-height: 1.35;
}

.ctrlem-db-category-item {
  width: 100%;
  min-height: 34px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  cursor: grab;
  font: inherit;
  font-size: 0.82rem;
  line-height: 1.2;
  text-align: left;
}

.ctrlem-db-category-item:active {
  cursor: grabbing;
}

.ctrlem-db-category-item:hover,
.ctrlem-db-category-item:focus-visible,
.ctrlem-db-category-item.is-selected {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-category-item.is-dragging {
  opacity: 0.55;
}

.ctrlem-db-category-item.is-drop-target {
  border-color: var(--success, #57f287);
}

.ctrlem-db-category-item-main {
  min-width: 0;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.ctrlem-db-drag-handle {
  flex: 0 0 auto;
  color: var(--text-muted, #666);
  font-size: 0.88rem;
  font-weight: 800;
  letter-spacing: 0;
}

.ctrlem-db-category-item-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ctrlem-db-category-item-count {
  flex: 0 0 auto;
  color: var(--text-muted, #666);
  font-size: 0.72rem;
  font-weight: 700;
}

.ctrlem-db-manager-editor {
  display: grid;
  gap: 10px;
}

.ctrlem-db-manager-workspace {
  display: grid;
  align-content: start;
  gap: 10px;
}

.ctrlem-db-tab-panel {
  min-width: 0;
  display: grid;
  gap: 10px;
}

.ctrlem-db-editor-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-category-name-input {
  flex: 1 1 220px;
  min-width: 180px;
  min-height: 34px;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: inherit;
  font-size: 0.82rem;
}

.ctrlem-db-category-name-input:focus {
  border-color: var(--accent-primary, #5865f2);
  outline: none;
}

.ctrlem-db-manager-actions {
  justify-content: space-between;
}

.ctrlem-db-save-group {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.ctrlem-db-manager-topbar {
  display: grid;
  gap: 10px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border-color, #333);
}

.ctrlem-db-editor-save-row {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-editor-autosave-toggle {
  flex: 0 0 auto;
}

.ctrlem-db-topbar-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  min-width: 0;
}

.ctrlem-db-autosave-label {
  font-size: 0.8rem;
}

.ctrlem-db-brand-link {
  position: relative;
  color: rgba(112, 154, 220, 0.82);
  text-decoration: none;
}

.ctrlem-db-brand-link:hover,
.ctrlem-db-brand-link:focus-visible {
  color: rgba(138, 174, 230, 0.92);
  outline: none;
  text-decoration: underline;
}

.ctrlem-db-brand-tooltip {
  position: fixed;
  z-index: 999999;
  width: max-content;
  max-width: min(320px, calc(100vw - 32px));
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border-color, #333);
  border-radius: 6px;
  background: var(--bg-primary, #0d0d0d);
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.32);
  color: var(--text-primary, #fff);
  font-size: 0.72rem;
  font-weight: 600;
  line-height: 1.3;
  pointer-events: none;
  text-align: left;
  white-space: normal;
}

.ctrlem-db-brand-strateg {
  font-weight: 700;
}

.ctrlem-db-brand-tag {
  font-weight: 600;
}

.ctrlem-db-uploaders {
  display: grid;
  gap: 8px;
}

.ctrlem-db-uploader-target {
  color: var(--text-muted, #666);
  font-size: 0.78rem;
}

.ctrlem-db-uploader {
  overflow: hidden;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-secondary, #1a1a1a);
}

.ctrlem-db-uploader-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 34px;
  padding: 0.5rem 0.65rem;
  cursor: pointer;
  color: var(--text-primary, #fff);
  font-size: 0.82rem;
  font-weight: 700;
  list-style: none;
}

.ctrlem-db-uploader-summary::-webkit-details-marker {
  display: none;
}

.ctrlem-db-uploader-summary::after {
  content: 'v';
  flex: 0 0 auto;
  color: var(--text-muted, #666);
  font-size: 0.78rem;
  transition: transform 0.15s ease;
}

.ctrlem-db-uploader[open] > .ctrlem-db-uploader-summary {
  border-bottom: 1px solid var(--border-color, #333);
}

.ctrlem-db-uploader[open] > .ctrlem-db-uploader-summary::after {
  transform: rotate(180deg);
}

.ctrlem-db-uploader-summary:hover,
.ctrlem-db-uploader-summary:focus-visible {
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-uploader-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ctrlem-db-uploader-meta {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--text-muted, #666);
  font-size: 0.72rem;
  font-weight: 700;
}

.ctrlem-db-uploader-body {
  display: grid;
  gap: 8px;
  padding: 0.65rem;
}

.ctrlem-db-uploader-fields,
.ctrlem-db-uploader-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-uploader-input {
  flex: 1 1 220px;
  min-width: 180px;
  min-height: 32px;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: inherit;
  font-size: 0.82rem;
}

.ctrlem-db-uploader-file {
  flex: 2 1 260px;
  min-width: 220px;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.82rem;
}

.ctrlem-db-uploader-note {
  flex: 1 1 220px;
  color: var(--text-muted, #666);
  font-size: 0.78rem;
}

.ctrlem-db-uploader-results {
  width: 100%;
  min-height: 66px;
  max-height: 150px;
  padding: 0.55rem 0.65rem;
  overflow: auto;
  resize: vertical;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: 0.8rem/1.4 Consolas, 'Courier New', monospace;
  white-space: pre;
}

.ctrlem-db-uploader-status {
  min-height: 17px;
  color: var(--text-muted, #666);
  font-size: 0.76rem;
}

.ctrlem-db-uploader-status[data-level="error"] {
  color: var(--danger, #ed4245);
}

.ctrlem-db-uploader-status[data-level="success"] {
  color: var(--success, #57f287);
}

.ctrlem-db-link-check-tools {
  display: grid;
  gap: 8px;
  margin: 0;
}

.ctrlem-db-link-check-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-link-check-copy {
  color: var(--text-muted, #666);
  font-size: 0.78rem;
  line-height: 1.35;
}

.ctrlem-db-link-check-scope {
  display: flex;
  align-items: end;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-link-check-all-toggle {
  min-height: 34px;
}

.ctrlem-db-link-check-field {
  display: grid;
  gap: 4px;
  min-width: 150px;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.76rem;
}

.ctrlem-db-link-check-select {
  width: 100%;
  min-height: 34px;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: inherit;
  font-size: 0.8rem;
}

.ctrlem-db-link-check-select:focus {
  border-color: var(--accent-primary, #5865f2);
  outline: none;
}

.ctrlem-db-link-check-status {
  flex: 1 1 180px;
  min-height: 17px;
  color: var(--text-muted, #666);
  font-size: 0.76rem;
}

.ctrlem-db-link-check-status[data-level="error"] {
  color: var(--danger, #ed4245);
}

.ctrlem-db-link-check-status[data-level="success"] {
  color: var(--success, #57f287);
}

.ctrlem-db-link-check-progress {
  display: grid;
  gap: 6px;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
}

.ctrlem-db-link-check-row {
  display: grid;
  grid-template-columns: 110px minmax(0, 1fr);
  gap: 8px;
  align-items: center;
  color: var(--text-muted, #666);
  font-size: 0.76rem;
}

.ctrlem-db-link-check-row strong {
  min-width: 0;
  overflow: hidden;
  color: var(--text-secondary, #b0b0b0);
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ctrlem-db-link-check-results {
  width: 100%;
  min-height: 84px;
  max-height: 170px;
  padding: 0.55rem 0.65rem;
  overflow: auto;
  resize: vertical;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: 0.8rem/1.4 Consolas, 'Courier New', monospace;
  white-space: pre;
}

.ctrlem-db-settings-panel,
.ctrlem-db-settings-section {
  display: grid;
  gap: 10px;
}

.ctrlem-db-settings-section {
  padding: 0.75rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-secondary, #1a1a1a);
}

.ctrlem-db-settings-title {
  color: var(--text-primary, #fff);
  font-size: 0.84rem;
  font-weight: 700;
}

.ctrlem-db-database-section {
  justify-items: center;
  text-align: center;
}

.ctrlem-db-database-section .ctrlem-db-import-actions {
  justify-content: center;
}

.ctrlem-db-settings-field {
  display: grid;
  gap: 5px;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.78rem;
}

.ctrlem-db-settings-check {
  display: flex;
  align-items: center;
}

.ctrlem-db-info-copy,
.ctrlem-db-info-list,
.ctrlem-db-editor-help {
  margin: 0;
  color: var(--text-muted, #666);
  font-size: 0.78rem;
  line-height: 1.45;
}

.ctrlem-db-info-list,
.ctrlem-db-editor-help {
  padding-left: 1.1rem;
}

.ctrlem-db-settings-input,
.ctrlem-db-imgbb-key-input {
  width: 100%;
  min-height: 34px;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: inherit;
  font-size: 0.82rem;
}

.ctrlem-db-command-upload {
  display: grid;
  gap: 8px;
  margin: 10px 0;
}

.ctrlem-db-command-upload-head {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 34px;
  padding: 0.42rem 0.55rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-secondary, #1a1a1a);
  cursor: pointer;
  font: inherit;
  text-align: left;
  transition: var(--transition, all 0.2s ease);
}

.ctrlem-db-command-upload-head:hover,
.ctrlem-db-command-upload-head:focus-visible {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
}

.ctrlem-db-command-upload-head:focus-visible {
  outline: 2px solid rgba(88, 101, 242, 0.35);
  outline-offset: 1px;
}

.ctrlem-db-command-upload-title {
  color: var(--text-primary, #fff);
  font-size: 0.82rem;
  font-weight: 700;
}

.ctrlem-db-command-upload-target {
  margin-left: auto;
  color: var(--text-muted, #666);
  font-size: 0.74rem;
}

.ctrlem-db-command-upload-chevron {
  flex: 0 0 auto;
  color: var(--text-muted, #666);
  font-size: 0.74rem;
  font-weight: 800;
  transition: transform 0.15s ease;
}

.ctrlem-db-command-upload.is-collapsed .ctrlem-db-command-upload-chevron {
  transform: rotate(-90deg);
}

.ctrlem-db-command-upload-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}

.ctrlem-db-command-upload-recommendation {
  margin: 0;
  color: var(--text-muted, #666);
  font-size: 0.76rem;
  line-height: 1.35;
}

.ctrlem-db-command-upload.is-collapsed .ctrlem-db-command-upload-recommendation {
  display: none;
}

.ctrlem-db-command-upload-grid[hidden] {
  display: none;
}

.ctrlem-db-upload-card {
  min-width: 0;
  display: grid;
  gap: 8px;
  align-content: start;
  padding: 0.65rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-secondary, #1a1a1a);
}

.ctrlem-db-upload-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}

.ctrlem-db-upload-card-title {
  color: var(--text-primary, #fff);
  font-size: 0.82rem;
  font-weight: 700;
}

.ctrlem-db-upload-card-note {
  color: var(--text-muted, #666);
  font-size: 0.72rem;
  line-height: 1.3;
  text-align: right;
}

.ctrlem-db-command-upload-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.ctrlem-db-command-upload-file {
  flex: 1 1 180px;
  min-width: 0;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.78rem;
}

.ctrlem-db-external-dropzone {
  min-height: 92px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0.65rem;
  border: 2px dashed var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-secondary, #b0b0b0);
  cursor: pointer;
  text-align: center;
  transition: var(--transition, all 0.2s ease);
}

.ctrlem-db-external-dropzone:hover,
.ctrlem-db-external-dropzone:focus-visible,
.ctrlem-db-external-dropzone.drag-active {
  border-color: var(--accent-primary, #5865f2);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-external-dropzone.is-busy {
  cursor: wait;
  opacity: 0.72;
}

.ctrlem-db-external-dropzone-icon {
  display: inline-grid;
  place-items: center;
  min-width: 42px;
  min-height: 22px;
  color: var(--text-muted, #666);
  font-size: 0.68rem;
  font-weight: 800;
  text-transform: uppercase;
}

.ctrlem-db-external-dropzone-main {
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.82rem;
  line-height: 1.3;
}

.ctrlem-db-external-browse {
  color: var(--accent-primary, #5865f2);
  font-weight: 700;
  text-decoration: underline;
}

.ctrlem-db-external-dropzone-hint {
  color: var(--text-muted, #666);
  font-size: 0.7rem;
  line-height: 1.25;
}

.ctrlem-db-imgbb-key-status {
  min-height: 17px;
  color: var(--text-muted, #666);
  font-size: 0.74rem;
}

.ctrlem-db-imgbb-key-status[data-level="error"] {
  color: var(--danger, #ed4245);
}

.ctrlem-db-imgbb-key-status[data-level="success"] {
  color: var(--success, #57f287);
}

.ctrlem-db-upload-native-card .upload-dropzone {
  min-height: 92px;
  margin: 0;
  padding: 0.65rem;
}

.ctrlem-db-upload-dropzone {
  --ctrlem-upload-accent: var(--accent-primary, #5865f2);
  --ctrlem-upload-border: var(--border-color, #333);
  border-color: var(--ctrlem-upload-border);
  cursor: pointer;
  transition: var(--transition, all 0.2s ease);
}

.ctrlem-db-upload-dropzone[data-tool="ctrlem"] {
  --ctrlem-upload-accent: #57f287;
  --ctrlem-upload-border: rgba(87, 242, 135, 0.45);
}

.ctrlem-db-upload-dropzone[data-tool="imgbb"] {
  --ctrlem-upload-accent: #4ea1ff;
  --ctrlem-upload-border: rgba(78, 161, 255, 0.48);
}

.ctrlem-db-upload-dropzone[data-tool="catbox"] {
  --ctrlem-upload-accent: #ffb347;
  --ctrlem-upload-border: rgba(255, 179, 71, 0.5);
}

.ctrlem-db-upload-dropzone[data-tool="vidhosting"] {
  --ctrlem-upload-accent: #d685ff;
  --ctrlem-upload-border: rgba(214, 133, 255, 0.5);
}

.ctrlem-db-upload-dropzone:hover,
.ctrlem-db-upload-dropzone:focus-visible,
.ctrlem-db-upload-dropzone.drag-active {
  border-color: var(--ctrlem-upload-accent);
  background: var(--accent-light, rgba(88, 101, 242, 0.1));
  outline: none;
}

.ctrlem-db-upload-dropzone.is-busy {
  cursor: wait;
  opacity: 0.72;
  position: relative;
  overflow: hidden;
  animation: ctrlem-db-pulse-border 1.2s ease-in-out infinite;
}

.ctrlem-db-upload-dropzone.is-busy::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.06) 50%,
    transparent 100%
  );
  background-size: 200% 100%;
  animation: ctrlem-db-shimmer 1.5s ease-in-out infinite;
  pointer-events: none;
}

.ctrlem-db-upload-dropzone.is-busy .ctrlem-db-upload-dropzone-main,
.ctrlem-db-upload-dropzone.is-busy .upload-hint,
.ctrlem-db-upload-dropzone.is-busy svg {
  position: relative;
  z-index: 2;
}

.ctrlem-db-upload-dropzone.is-busy svg {
  animation: ctrlem-db-spin 1.2s linear infinite;
}

.ctrlem-db-upload-dropzone.flash-success {
  border-color: var(--success, #57f287) !important;
  background: rgba(87, 242, 135, 0.08) !important;
  transition: border-color 0.3s ease, background 0.3s ease;
}

.ctrlem-db-upload-dropzone.flash-error {
  border-color: var(--danger, #ed4245) !important;
  background: rgba(237, 66, 69, 0.08) !important;
  transition: border-color 0.3s ease, background 0.3s ease;
}

.ctrlem-db-upload-dropzone.is-disabled {
  cursor: pointer;
  opacity: 0.72;
}

.ctrlem-db-upload-dropzone.is-disabled:hover,
.ctrlem-db-upload-dropzone.is-disabled:focus-visible {
  opacity: 1;
}

.ctrlem-db-upload-native-card .upload-dropzone svg {
  width: 22px;
  height: 22px;
  color: var(--ctrlem-upload-accent, currentColor);
}

.ctrlem-db-upload-native-card .upload-hint {
  font-size: 0.7rem;
  line-height: 1.25;
}

.ctrlem-db-upload-dropzone-main {
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.82rem;
  line-height: 1.3;
}

.ctrlem-db-upload-tool-label {
  color: var(--ctrlem-upload-accent, var(--accent-primary, #5865f2));
  font-weight: 800;
}

.ctrlem-db-upload-dropzone .upload-browse-label {
  color: var(--ctrlem-upload-accent, var(--accent-primary, #5865f2));
  cursor: pointer;
  font-weight: 700;
  text-decoration: underline;
}

.ctrlem-db-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 999999;
  display: grid;
  place-items: center;
  padding: 16px;
  background: rgba(0, 0, 0, 0.62);
}

.ctrlem-db-modal {
  width: min(420px, 100%);
  display: grid;
  gap: 10px;
  padding: 1rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-secondary, #1a1a1a);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}

.ctrlem-db-modal-title {
  color: var(--text-primary, #fff);
  font-size: 0.95rem;
  font-weight: 800;
}

.ctrlem-db-modal-copy {
  margin: 0;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.8rem;
  line-height: 1.45;
}

.ctrlem-db-media-preview-modal {
  width: min(560px, 100%);
}

.ctrlem-db-media-preview-url {
  min-width: 0;
  display: block;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--border-color, #333);
  border-radius: 6px;
  background: var(--bg-primary, #0d0d0d);
  overflow-wrap: anywhere;
  color: var(--accent-primary, #5865f2);
  font-size: 0.76rem;
}

.ctrlem-db-media-preview-name {
  width: 100%;
  min-height: 34px;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: inherit;
  font-size: 0.82rem;
}

.ctrlem-db-media-preview-status {
  min-height: 17px;
  color: var(--text-muted, #666);
  font-size: 0.74rem;
}

.ctrlem-db-media-preview-status[data-level="error"] {
  color: var(--danger, #ed4245);
}

.ctrlem-db-media-preview-status[data-level="success"] {
  color: var(--success, #57f287);
}

.ctrlem-db-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.ctrlem-db-upload-session {
  width: min(620px, 100%);
  max-height: min(680px, calc(100vh - 32px));
  overflow: auto;
}

.ctrlem-db-upload-session-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(180px, 240px);
  gap: 12px;
  align-items: end;
}

.ctrlem-db-upload-session-category {
  display: grid;
  gap: 5px;
  color: var(--text-secondary, #b0b0b0);
  font-size: 0.74rem;
}

.ctrlem-db-upload-session-select {
  width: 100%;
  min-height: 34px;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: inherit;
  font-size: 0.82rem;
}

.ctrlem-db-upload-session-list {
  display: grid;
  gap: 6px;
}

.ctrlem-db-upload-session-progress {
  color: var(--text-muted, #666);
  font-size: 0.74rem;
  font-weight: 700;
  min-height: 18px;
}

.ctrlem-db-upload-session-row {
  display: grid;
  grid-template-columns: minmax(110px, 1fr) 1fr minmax(0, 1.35fr);
  gap: 8px;
  align-items: center;
  min-height: 52px;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  transition: border-color 0.25s ease, background 0.25s ease;
}

.ctrlem-db-upload-session-row.is-uploading {
  border-color: var(--accent-primary, #5865f2);
  background: rgba(88, 101, 242, 0.06);
  animation: ctrlem-db-row-pulse 1.2s ease-in-out infinite;
}

.ctrlem-db-upload-session-row.is-uploaded {
  border-color: var(--success, #57f287);
  background: rgba(87, 242, 135, 0.06);
}

.ctrlem-db-upload-session-row.is-failed {
  border-color: var(--danger, #ed4245);
  background: rgba(237, 66, 69, 0.06);
  animation: ctrlem-db-shake 0.4s ease-in-out;
}

.ctrlem-db-upload-progress-bar {
  width: 100%;
  height: 6px;
  overflow: hidden;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
}

.ctrlem-db-upload-progress-fill {
  display: block;
  width: 0%;
  height: 100%;
  border-radius: inherit;
  background: var(--accent-primary, #5865f2);
  transition: width 0.3s ease;
}

.ctrlem-db-upload-session-row.is-uploading .ctrlem-db-upload-progress-fill {
  width: 60%;
  animation: ctrlem-db-indeterminate 1.4s ease-in-out infinite;
}

.ctrlem-db-upload-session-row.is-uploaded .ctrlem-db-upload-progress-fill {
  width: 100%;
  background: var(--success, #57f287);
}

.ctrlem-db-upload-session-row.is-failed .ctrlem-db-upload-progress-fill {
  width: 100%;
  background: var(--danger, #ed4245);
}

.ctrlem-db-upload-session-state-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
  border-radius: 50%;
  font-size: 0.65rem;
  font-weight: 800;
  line-height: 1;
}

.ctrlem-db-upload-session-row.is-uploaded .ctrlem-db-upload-session-state-icon {
  background: var(--success, #57f287);
  color: #fff;
  animation: ctrlem-db-check-in 0.3s ease-out;
}

.ctrlem-db-upload-session-row.is-failed .ctrlem-db-upload-session-state-icon {
  background: var(--danger, #ed4245);
  color: #fff;
}

.ctrlem-db-upload-speed {
  color: var(--text-muted, #666);
  font-size: 0.65rem;
  white-space: nowrap;
}

.ctrlem-db-upload-session-file,
.ctrlem-db-upload-session-detail {
  min-width: 0;
  overflow-wrap: anywhere;
}

.ctrlem-db-upload-session-file {
  color: var(--text-primary, #fff);
  font-size: 0.8rem;
  font-weight: 700;
}

.ctrlem-db-upload-session-state {
  color: var(--text-muted, #666);
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
}

.ctrlem-db-upload-session-row[data-status="uploaded"] .ctrlem-db-upload-session-state,
.ctrlem-db-upload-session-note {
  color: var(--success, #57f287);
}

.ctrlem-db-upload-session-note.is-duplicate {
  color: var(--text-muted, #666);
}

.ctrlem-db-upload-session-row[data-status="failed"] .ctrlem-db-upload-session-state,
.ctrlem-db-upload-session-error {
  color: var(--danger, #ed4245);
}

.ctrlem-db-upload-session-detail {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  color: var(--text-muted, #666);
  font-size: 0.74rem;
}

.ctrlem-db-upload-session-detail a {
  max-width: 100%;
  color: var(--accent-primary, #5865f2);
}

.ctrlem-db-upload-session-status {
  min-height: 18px;
  color: var(--text-muted, #666);
  font-size: 0.76rem;
}

.ctrlem-db-upload-session-status[data-level="error"] {
  color: var(--danger, #ed4245);
}

.ctrlem-db-upload-session-status[data-level="success"] {
  color: var(--success, #57f287);
}

.ctrlem-db-upload-session-cancel {
  opacity: 0.78;
}

.ctrlem-db-manager-textarea {
  width: 100%;
  min-height: 280px;
  max-height: 520px;
  padding: 0.75rem 0.85rem;
  overflow: auto;
  resize: vertical;
  border: 1px solid var(--border-color, #333);
  border-radius: var(--border-radius, 8px);
  background: var(--bg-primary, #0d0d0d);
  color: var(--text-primary, #fff);
  font: 0.85rem/1.45 Consolas, 'Courier New', monospace;
  transition: var(--transition, all 0.2s ease);
  white-space: pre;
  word-wrap: normal;
}

.ctrlem-db-manager-textarea:focus {
  outline: none;
  border-color: var(--accent-primary, #5865f2);
}

.ctrlem-db-status {
  min-height: 18px;
  color: var(--text-muted, #666);
  font-size: 0.78rem;
  line-height: 1.35;
}

.ctrlem-db-empty {
  margin: 10px 0 0;
  color: var(--text-muted, #666);
  font-size: 0.8rem;
}

@media (max-width: 980px) {
  .ctrlem-db-manager-layout {
    grid-template-columns: 1fr;
  }

  .ctrlem-db-manager-actions,
  .ctrlem-db-topbar-controls {
    align-items: flex-start;
    flex-direction: column;
  }

  .ctrlem-db-status {
    width: 100%;
    text-align: left;
  }

  .ctrlem-db-command-upload-grid {
    grid-template-columns: 1fr;
  }

  .ctrlem-db-autosend-row {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .ctrlem-db-autosend-kind,
  .ctrlem-db-autosend-code,
  .ctrlem-db-autosend-category,
  .ctrlem-db-autosend-wait {
    grid-column: auto;
  }

  .ctrlem-db-autosend-profile {
    grid-column: 1 / 2;
  }

  .ctrlem-db-autosend-cooldown {
    grid-column: 1 / -1;
    width: 100%;
  }
  
  .ctrlem-db-upload-session-head,
  .ctrlem-db-upload-session-row {
    grid-template-columns: 1fr;
  }
  }
  
  /* ── Upload feedback animations ── */
  
  @keyframes ctrlem-db-shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
  }
  
  @keyframes ctrlem-db-pulse-border {
    0%, 100% { border-color: var(--ctrlem-upload-border, var(--border-color, #333)); }
    50% { border-color: var(--ctrlem-upload-accent, var(--accent-primary, #5865f2)); }
  }
  
  @keyframes ctrlem-db-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
  
  @keyframes ctrlem-db-row-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(88, 101, 242, 0); }
    50% { box-shadow: 0 0 0 4px rgba(88, 101, 242, 0.15); }
  }
  
  @keyframes ctrlem-db-indeterminate {
    0% { transform: translateX(-100%); }
    50% { transform: translateX(0%); }
    100% { transform: translateX(100%); }
  }
  
  @keyframes ctrlem-db-check-in {
    0% { transform: scale(0); opacity: 0; }
    60% { transform: scale(1.2); }
    100% { transform: scale(1); opacity: 1; }
  }
  
  @keyframes ctrlem-db-shake {
    0%, 100% { transform: translateX(0); }
    20% { transform: translateX(-4px); }
    40% { transform: translateX(4px); }
    60% { transform: translateX(-3px); }
    80% { transform: translateX(3px); }
  }
  
  /* ── Upload toast notifications ── */
  
  .ctrlem-db-toast-container {
    position: fixed;
    bottom: 24px;
    right: 24px;
    z-index: 999999;
    display: grid;
    gap: 8px;
    pointer-events: none;
  }
  
  .ctrlem-db-toast {
    min-width: 200px;
    max-width: 340px;
    padding: 0.65rem 0.85rem;
    border: 1px solid var(--border-color, #333);
    border-radius: var(--border-radius, 8px);
    background: var(--bg-secondary, #1a1a1a);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
    color: var(--text-primary, #fff);
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.35;
    pointer-events: auto;
    animation: ctrlem-db-toast-in 0.25s ease-out;
    transition: opacity 0.2s ease, transform 0.2s ease;
  }
  
  .ctrlem-db-toast.is-leaving {
    opacity: 0;
    transform: translateX(20px);
  }
  
  .ctrlem-db-toast.is-success {
    border-color: var(--success, #57f287);
  }
  
  .ctrlem-db-toast.is-error {
    border-color: var(--danger, #ed4245);
  }
  
  .ctrlem-db-toast.is-warning {
    border-color: var(--accent-primary, #5865f2);
  }
  
  @keyframes ctrlem-db-toast-in {
    from {
      opacity: 0;
      transform: translateX(20px);
    }
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }
  `;
	function addStyles() {
		const gmAddStyle = globalThis.GM_addStyle;
		if (typeof gmAddStyle === "function") {
			gmAddStyle(APP_CSS);
			return;
		}
		const style = createElement("style", { attrs: { "data-ctrlem-db": "styles" } });
		style.textContent = APP_CSS;
		document.head.appendChild(style);
	}
	function getUploadPanelId(commandKey) {
		return `${UI_IDS.uploadPrefix}-${commandKey}`;
	}
	var STATUS = Object.freeze({
		QUEUED: "Queued",
		UPLOADING: "Uploading",
		UPLOADED: "Uploaded",
		FAILED: "Failed"
	});
	var UPLOAD_DELAY_MS = USER_CONFIG.upload.delayMs;
	var CTRLEM_IMAGE_MAX_BYTES = USER_CONFIG.upload.ctrlemImageMaxBytes;
	var CTRLEM_SOUND_MAX_BYTES = USER_CONFIG.upload.ctrlemSoundMaxBytes;
	var CTRLEM_IMAGE_TYPES = new Set([
		"image/jpeg",
		"image/png",
		"image/gif",
		"image/webp"
	]);
	var CTRLEM_SOUND_TYPES = new Set([
		"audio/mpeg",
		"audio/wav",
		"audio/ogg",
		"audio/mp4",
		"audio/x-m4a",
		"audio/flac",
		"audio/webm",
		"audio/opus"
	]);
	function getToolLabel(tool) {
		if (tool === "ctrlem") return "CtrlEm";
		if (tool === "imgbb") return "ImgBB";
		if (tool === "catbox") return "Catbox";
		if (tool === "vidhosting") return "VidHosting";
		return tool;
	}
	function getMediaLabel(config) {
		if (config.type === RecordType.IMAGE) return "image";
		if (config.type === RecordType.SOUND) return "audio";
		if (config.type === RecordType.VIDEO) return "video";
		return "file";
	}
	function getAccept(config, tool) {
		if (tool === "imgbb") return "image/jpeg,image/png,image/gif,image/webp";
		if (tool === "vidhosting") return "video/*";
		if (tool === "catbox" && config.type === RecordType.SOUND) return "audio/*";
		if (tool === "catbox" && config.type === RecordType.VIDEO) return "video/*";
		return "";
	}
	function isAcceptedFile(config, tool, file) {
		if (tool === "imgbb") return file.type.startsWith("image/");
		if (tool === "vidhosting") return file.type.startsWith("video/");
		if (tool === "catbox" && config.type === RecordType.SOUND) return file.type.startsWith("audio/");
		if (tool === "catbox" && config.type === RecordType.VIDEO) return file.type.startsWith("video/");
		return true;
	}
	function getToolNote(config, tool) {
		if (tool === "ctrlem" && config.type === RecordType.IMAGE) return "Max 5MB - JPG, PNG, GIF, WebP";
		if (tool === "ctrlem" && config.type === RecordType.SOUND) return "Max 15MB - MP3, WAV, OGG, M4A";
		if (tool === "imgbb") return "Max 32MB - JPG, PNG, GIF, WebP - Requires key";
		if (tool === "catbox") return config.type === RecordType.SOUND ? "Audio files - Optional userhash in Settings" : "Video files - Optional userhash in Settings";
		if (tool === "vidhosting") return "Video files";
		return "";
	}
	function getUploadTargetText(config) {
		if (config.type === RecordType.IMAGE) return "Upload images and gifs - expand/collapse";
		if (config.type === RecordType.SOUND) return "Upload sounds - expand/collapse";
		if (config.type === RecordType.VIDEO) return "Upload videos - expand/collapse";
		return "Upload files - expand/collapse";
	}
	function delay(ms) {
		return new Promise((resolve) => window.setTimeout(resolve, ms));
	}
	function getCtrlEmValidationError(config, file) {
		if (config.type === RecordType.IMAGE) {
			if (file.size > CTRLEM_IMAGE_MAX_BYTES) return "Image must be under 5MB.";
			if (!CTRLEM_IMAGE_TYPES.has(file.type)) return "Only JPG, PNG, GIF, and WebP images are allowed.";
		}
		if (config.type === RecordType.SOUND) {
			if (file.size > CTRLEM_SOUND_MAX_BYTES) return "Audio must be under 15MB.";
			if (!CTRLEM_SOUND_TYPES.has(file.type)) return "Only MP3, WAV, OGG, M4A, FLAC audio allowed.";
		}
		return "";
	}
	async function uploadCtrlEmFile(config, file) {
		const validationError = getCtrlEmValidationError(config, file);
		if (validationError) throw new Error(validationError);
		const formData = new FormData();
		const isSound = config.type === RecordType.SOUND;
		formData.append(isSound ? "sound" : "image", file);
		const response = await fetch(isSound ? "/api/upload/sound" : "/api/upload/image", {
			method: "POST",
			body: formData,
			credentials: "same-origin"
		});
		if (!response.ok) {
			const error = await response.json().catch(() => ({}));
			throw new Error(error?.error || "Upload failed");
		}
		const data = await response.json();
		const url = String(data?.url || "");
		if (!url) throw new Error("CtrlEm upload did not return a URL.");
		return url.startsWith("http") ? url : `${window.location.origin}${url}`;
	}
	function createUploadIcon() {
		const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
		svg.setAttribute("width", "24");
		svg.setAttribute("height", "24");
		svg.setAttribute("viewBox", "0 0 24 24");
		svg.setAttribute("fill", "none");
		svg.setAttribute("stroke", "currentColor");
		svg.setAttribute("stroke-width", "2");
		[
			["path", { d: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" }],
			["polyline", { points: "17 8 12 3 7 8" }],
			["line", {
				x1: "12",
				y1: "3",
				x2: "12",
				y2: "15"
			}]
		].forEach(([tagName, attrs]) => {
			const child = document.createElementNS("http://www.w3.org/2000/svg", tagName);
			Object.entries(attrs).forEach(([name, value]) => child.setAttribute(name, String(value)));
			svg.appendChild(child);
		});
		return svg;
	}
	function getCategorySelect(config, actions) {
		const categories = actions.getUploadCategories(config);
		const target = actions.getUploadTarget(config);
		const select = createElement("select", {
			className: "ctrlem-db-upload-session-select",
			attrs: { "aria-label": "Upload category" }
		});
		categories.forEach((category) => {
			const option = createElement("option", {
				value: category.id,
				text: category.name
			});
			option.selected = category.id === target.id;
			select.appendChild(option);
		});
		if (!select.value && categories[0]) select.value = categories[0].id;
		return select;
	}
	function createUploadSession(options) {
		const { config, source, files, actions } = options;
		const totalFiles = files.length;
		const items = files.map((file, index) => ({
			file,
			fileName: file?.name || `${getMediaLabel(config)} ${index + 1}`,
			status: STATUS.QUEUED,
			url: "",
			error: "",
			appended: false,
			duplicate: false,
			row: null,
			statusEl: null,
			detailEl: null,
			progressBarEl: null,
			progressFillEl: null,
			stateIconEl: null,
			speedEl: null
		}));
		const categorySelect = getCategorySelect(config, actions);
		const list = createElement("div", { className: "ctrlem-db-upload-session-list" });
		const progressEl = createElement("div", {
			className: "ctrlem-db-upload-session-progress",
			attrs: { "aria-live": "polite" }
		});
		const status = createElement("div", {
			className: "ctrlem-db-upload-session-status",
			attrs: { "aria-live": "polite" }
		});
		const addButton = createElement("button", {
			className: "btn btn-primary",
			text: "Add links",
			type: "button",
			attrs: { disabled: "" }
		});
		const cancelButton = createElement("button", {
			className: "btn btn-sm btn-secondary ctrlem-db-upload-session-cancel",
			text: "Cancel",
			type: "button"
		});
		const updateProgressCounter = () => {
			progressEl.textContent = `${items.filter((item) => item.status === STATUS.UPLOADED || item.status === STATUS.FAILED).length} of ${totalFiles} files processed`;
		};
		const modal = createElement("div", {
			className: "ctrlem-db-modal-backdrop ctrlem-db-upload-session-backdrop",
			attrs: {
				role: "dialog",
				"aria-modal": "true",
				"aria-label": `${config.label} upload status`
			}
		}, [createElement("div", { className: "ctrlem-db-modal ctrlem-db-upload-session" }, [
			createElement("div", { className: "ctrlem-db-upload-session-head" }, [createElement("div", {
				className: "ctrlem-db-modal-title",
				text: `${source} upload`
			}), createElement("label", { className: "ctrlem-db-upload-session-category" }, [createElement("span", { text: "Category" }), categorySelect])]),
			progressEl,
			list,
			status,
			createElement("div", { className: "ctrlem-db-modal-actions" }, [cancelButton, addButton])
		])]);
		const renderItem = (item) => {
			item.row.dataset.status = String(item.status).toLowerCase();
			item.row.classList.remove("is-uploading", "is-uploaded", "is-failed");
			if (item.status === STATUS.UPLOADING) item.row.classList.add("is-uploading");
			else if (item.status === STATUS.UPLOADED) item.row.classList.add("is-uploaded");
			else if (item.status === STATUS.FAILED) item.row.classList.add("is-failed");
			item.statusEl.replaceChildren();
			if (item.status === STATUS.UPLOADED) {
				item.stateIconEl = createElement("span", {
					className: "ctrlem-db-upload-session-state-icon",
					text: "✓"
				});
				item.statusEl.appendChild(item.stateIconEl);
				item.statusEl.appendChild(document.createTextNode(" Uploaded"));
			} else if (item.status === STATUS.FAILED) {
				item.stateIconEl = createElement("span", {
					className: "ctrlem-db-upload-session-state-icon",
					text: "✕"
				});
				item.statusEl.appendChild(item.stateIconEl);
				item.statusEl.appendChild(document.createTextNode(" Failed"));
			} else if (item.status === STATUS.UPLOADING) item.statusEl.textContent = "Uploading...";
			else item.statusEl.textContent = item.status;
			item.detailEl.replaceChildren();
			if (item.url) item.detailEl.appendChild(createElement("a", {
				text: item.url,
				attrs: {
					href: item.url,
					target: "_blank",
					rel: "noopener noreferrer"
				}
			}));
			if (item.error) item.detailEl.appendChild(createElement("span", {
				className: "ctrlem-db-upload-session-error",
				text: item.error
			}));
			if (item.duplicate) item.detailEl.appendChild(createElement("span", {
				className: "ctrlem-db-upload-session-note is-duplicate",
				text: "already exists"
			}));
			if (item.appended) item.detailEl.appendChild(createElement("span", {
				className: "ctrlem-db-upload-session-note",
				text: "added"
			}));
			if (item.speedEl && item.speedEl.textContent) item.detailEl.appendChild(item.speedEl);
			updateProgressCounter();
		};
		const updateActions = () => {
			addButton.disabled = !items.some((item) => item.status === STATUS.UPLOADED && item.url);
		};
		items.forEach((item) => {
			const progressFill = createElement("span", { className: "ctrlem-db-upload-progress-fill" });
			const progressBar = createElement("div", { className: "ctrlem-db-upload-progress-bar" }, [progressFill]);
			item.progressBarEl = progressBar;
			item.progressFillEl = progressFill;
			const stateCol = createElement("div", { className: "ctrlem-db-upload-session-state" });
			item.speedEl = createElement("span", { className: "ctrlem-db-upload-speed" });
			item.row = createElement("div", { className: "ctrlem-db-upload-session-row" }, [
				createElement("div", {
					className: "ctrlem-db-upload-session-file",
					text: item.fileName
				}),
				progressBar,
				stateCol
			]);
			item.statusEl = stateCol;
			item.detailEl = createElement("div", { className: "ctrlem-db-upload-session-detail" });
			item.row.appendChild(item.detailEl);
			list.appendChild(item.row);
			renderItem(item);
		});
		const close = () => modal.remove();
		const setItemStatus = (item, nextStatus, patch = {}) => {
			Object.assign(item, patch, { status: nextStatus });
			renderItem(item);
			updateActions();
		};
		const setItemProgress = (item, progress = {}) => {
			if (progress.statusText !== void 0 && item.statusEl) item.statusEl.textContent = progress.statusText;
			if (progress.speed !== void 0 && item.speedEl) item.speedEl.textContent = progress.speed;
		};
		const addLinks = async () => {
			const uploadedItems = items.filter((item) => item.status === STATUS.UPLOADED && item.url);
			if (uploadedItems.length === 0) return;
			addButton.disabled = true;
			cancelButton.disabled = true;
			status.textContent = "Adding links...";
			status.dataset.level = "info";
			let appended = 0;
			for (const item of uploadedItems) {
				const result = await actions.appendUploadedUrl({
					config,
					url: item.url,
					fileName: item.fileName,
					targetCategoryId: categorySelect.value,
					source
				});
				item.appended = Boolean(result.appended);
				item.duplicate = !result.appended;
				if (result.appended) appended += 1;
				renderItem(item);
			}
			status.textContent = appended > 0 ? `Added ${appended} link${appended === 1 ? "" : "s"}.` : "No new links added.";
			status.dataset.level = appended > 0 ? "success" : "info";
			cancelButton.disabled = false;
			if (appended > 0) window.setTimeout(close, USER_CONFIG.ui.uploadSessionCloseDelayMs);
			else updateActions();
		};
		addButton.addEventListener("click", () => {
			addLinks().catch((error) => {
				status.textContent = error?.message || String(error);
				status.dataset.level = "error";
				cancelButton.disabled = false;
				updateActions();
			});
		});
		cancelButton.addEventListener("click", close);
		modal.addEventListener("click", (event) => {
			if (event.target === modal) close();
		});
		document.body.appendChild(modal);
		return {
			items,
			modal,
			setItemStatus,
			setItemProgress,
			setStatus(message, level = "info") {
				status.textContent = message;
				status.dataset.level = level;
			}
		};
	}
	function decorateNativeDropzone(dropzone) {
		dropzone.classList.remove("ctrlem-db-hidden-site-ui");
		dropzone.classList.add("ctrlem-db-upload-dropzone");
		dropzone.dataset.tool = "ctrlem";
		dropzone.querySelector("input[type=\"file\"]")?.setAttribute("multiple", "");
		const main = Array.from(dropzone.querySelectorAll("span")).find((span) => !span.classList.contains("upload-hint"));
		if (main && !main.querySelector(".ctrlem-db-upload-tool-label")) main.prepend(createElement("span", {
			className: "ctrlem-db-upload-tool-label",
			text: `[${getToolLabel("ctrlem")}] Upload `
		}));
	}
	function createNativeTool(config, input, dropzone, actions) {
		decorateNativeDropzone(dropzone);
		const fileInput = dropzone.querySelector("input[type=\"file\"]");
		let isBusy = false;
		const uploadFiles = async (files) => {
			if (isBusy || files.length === 0) return;
			const session = createUploadSession({
				config,
				source: getToolLabel("ctrlem"),
				files,
				actions
			});
			isBusy = true;
			dropzone.classList.add("is-busy");
			session.setStatus("Uploading files...");
			for (let index = 0; index < session.items.length; index += 1) {
				const item = session.items[index];
				if (index > 0) {
					session.setStatus(`Waiting ${UPLOAD_DELAY_MS / 1e3}s before next upload...`);
					await delay(UPLOAD_DELAY_MS);
				}
				session.setItemStatus(item, STATUS.UPLOADING);
				const startTime = Date.now();
				const fileSizeMB = item.file.size / (1024 * 1024);
				let speedInterval = null;
				if (item.file.size > 1024 * 1024) speedInterval = window.setInterval(() => {
					const elapsed = (Date.now() - startTime) / 1e3;
					if (elapsed < 1) return;
					const speed = fileSizeMB / elapsed;
					session.setItemProgress(item, {
						statusText: `Uploading... (${elapsed.toFixed(0)}s)`,
						speed: speed > .1 ? `${speed.toFixed(1)} MB/s` : ""
					});
				}, 500);
				try {
					const url = await uploadCtrlEmFile(config, item.file);
					if (speedInterval) window.clearInterval(speedInterval);
					input.value = url;
					session.setItemStatus(item, STATUS.UPLOADED, { url });
				} catch (error) {
					if (speedInterval) window.clearInterval(speedInterval);
					session.setItemStatus(item, STATUS.FAILED, { error: error?.message || String(error) });
				}
			}
			isBusy = false;
			dropzone.classList.remove("is-busy");
			if (fileInput) fileInput.value = "";
			const successful = session.items.filter((item) => item.status === STATUS.UPLOADED).length;
			const failed = session.items.filter((item) => item.status === STATUS.FAILED).length;
			if (successful > 0 && failed === 0) {
				dropzone.classList.add("flash-success");
				window.setTimeout(() => dropzone.classList.remove("flash-success"), 1200);
			} else if (failed > 0 && successful === 0) {
				dropzone.classList.add("flash-error");
				window.setTimeout(() => dropzone.classList.remove("flash-error"), 1200);
			}
			session.setStatus(successful > 0 ? "Upload finished. Review links, choose a category, then add them." : "No files uploaded.", successful > 0 ? "success" : "error");
			showUploadToast(successful, failed);
		};
		dropzone.addEventListener("drop", (event) => {
			event.preventDefault();
			event.stopImmediatePropagation();
			dropzone.classList.remove("drag-active");
			uploadFiles(Array.from(event.dataTransfer?.files || []));
		}, true);
		fileInput?.addEventListener("change", (event) => {
			event.stopImmediatePropagation();
			uploadFiles(Array.from(fileInput.files || []));
		}, true);
		return [dropzone];
	}
	function createImgBBDisabledTool(actions) {
		const openPrompt = () => actions.openImgBBKeyPrompt();
		const dropzone = createElement("div", {
			className: "upload-dropzone ctrlem-db-upload-dropzone is-disabled",
			attrs: {
				role: "button",
				tabindex: "0",
				"aria-label": "Set ImgBB API key before uploading"
			},
			dataset: { tool: "imgbb" }
		}, [
			createUploadIcon(),
			createElement("span", { className: "ctrlem-db-upload-dropzone-main" }, [createElement("span", {
				className: "ctrlem-db-upload-tool-label",
				text: "[ImgBB] "
			}), createElement("span", { text: "Set API key before uploading" })]),
			createElement("span", {
				className: "upload-hint",
				text: "Click to open ImgBB key settings"
			})
		]);
		dropzone.addEventListener("click", openPrompt);
		dropzone.addEventListener("keydown", (event) => {
			if (event.key !== "Enter" && event.key !== " ") return;
			event.preventDefault();
			openPrompt();
		});
		dropzone.addEventListener("dragover", (event) => event.preventDefault());
		dropzone.addEventListener("drop", (event) => {
			event.preventDefault();
			openPrompt();
		});
		return [dropzone];
	}
	function createExternalTool(config, tool, input, actions) {
		if (tool === "imgbb" && !String(actions.getUploaderSettings().imgbbApiKey || "").trim()) return createImgBBDisabledTool(actions);
		const label = getToolLabel(tool);
		const fileInputId = `file-${config.key}-${tool}`;
		const fileInput = createElement("input", {
			className: "ctrlem-db-command-upload-file",
			type: "file",
			attrs: {
				accept: getAccept(config, tool),
				multiple: "",
				id: fileInputId,
				style: "display:none"
			}
		});
		const browseLabel = createElement("label", {
			className: "upload-browse-label",
			text: "browse",
			attrs: { for: fileInputId }
		});
		browseLabel.addEventListener("click", (event) => event.stopPropagation());
		const dropzone = createElement("div", {
			className: "upload-dropzone ctrlem-db-upload-dropzone",
			attrs: {
				role: "button",
				tabindex: "0"
			},
			dataset: {
				uploadFor: config.key,
				tool
			}
		}, [
			createUploadIcon(),
			createElement("span", { className: "ctrlem-db-upload-dropzone-main" }, [
				createElement("span", {
					className: "ctrlem-db-upload-tool-label",
					text: `[${label}] Upload `
				}),
				createElement("span", { text: `Drag & drop ${getMediaLabel(config)} or ` }),
				browseLabel
			]),
			createElement("span", {
				className: "upload-hint",
				text: getToolNote(config, tool)
			}),
			fileInput,
			createElement("div", {
				className: "upload-spinner",
				attrs: { style: "display:none" },
				text: "Uploading..."
			})
		]);
		let isBusy = false;
		const setBusy = (busy) => {
			isBusy = busy;
			fileInput.disabled = busy;
			dropzone.classList.toggle("is-busy", busy);
			dropzone.setAttribute("aria-disabled", String(busy));
		};
		const uploadFiles = async (files) => {
			if (isBusy || files.length === 0) return;
			const session = createUploadSession({
				config,
				source: label,
				files,
				actions
			});
			setBusy(true);
			session.setStatus("Uploading files...");
			for (const item of session.items) {
				if (!isAcceptedFile(config, tool, item.file)) {
					session.setItemStatus(item, STATUS.FAILED, { error: `${item.fileName}: not accepted by ${label}.` });
					continue;
				}
				session.setItemStatus(item, STATUS.UPLOADING);
				const startTime = Date.now();
				const fileSizeMB = item.file.size / (1024 * 1024);
				let speedInterval = null;
				if (item.file.size > 1024 * 1024) speedInterval = window.setInterval(() => {
					const elapsed = (Date.now() - startTime) / 1e3;
					if (elapsed < 1) return;
					const speed = fileSizeMB / elapsed;
					session.setItemProgress(item, {
						statusText: `Uploading... (${elapsed.toFixed(0)}s)`,
						speed: speed > .1 ? `${speed.toFixed(1)} MB/s` : ""
					});
				}, 500);
				try {
					const url = await actions.uploadContentFile({
						config,
						tool,
						file: item.file
					});
					if (speedInterval) window.clearInterval(speedInterval);
					session.setItemStatus(item, STATUS.UPLOADED, { url });
					input.value = url;
					input.dispatchEvent(new Event("input", { bubbles: true }));
					input.dispatchEvent(new Event("change", { bubbles: true }));
				} catch (error) {
					if (speedInterval) window.clearInterval(speedInterval);
					session.setItemStatus(item, STATUS.FAILED, { error: error?.message || String(error) });
				}
			}
			setBusy(false);
			fileInput.value = "";
			const successful = session.items.filter((item) => item.status === STATUS.UPLOADED).length;
			const failed = session.items.filter((item) => item.status === STATUS.FAILED).length;
			if (successful > 0 && failed === 0) {
				dropzone.classList.add("flash-success");
				window.setTimeout(() => dropzone.classList.remove("flash-success"), 1200);
			} else if (failed > 0 && successful === 0) {
				dropzone.classList.add("flash-error");
				window.setTimeout(() => dropzone.classList.remove("flash-error"), 1200);
			}
			session.setStatus(successful > 0 ? "Upload finished. Review links, choose a category, then add them." : "No files uploaded.", successful > 0 ? "success" : "error");
			showUploadToast(successful, failed);
		};
		dropzone.addEventListener("click", () => {
			if (!isBusy) fileInput.click();
		});
		dropzone.addEventListener("keydown", (event) => {
			if (event.key !== "Enter" && event.key !== " ") return;
			event.preventDefault();
			if (!isBusy) fileInput.click();
		});
		dropzone.addEventListener("dragover", (event) => {
			event.preventDefault();
			if (!isBusy) dropzone.classList.add("drag-active");
		});
		dropzone.addEventListener("dragenter", (event) => {
			event.preventDefault();
			if (!isBusy) dropzone.classList.add("drag-active");
		});
		dropzone.addEventListener("dragleave", () => {
			dropzone.classList.remove("drag-active");
		});
		dropzone.addEventListener("drop", (event) => {
			event.preventDefault();
			dropzone.classList.remove("drag-active");
			uploadFiles(Array.from(event.dataTransfer?.files || []));
		});
		fileInput.addEventListener("change", () => {
			uploadFiles(Array.from(fileInput.files || []));
		});
		return [dropzone];
	}
	function showUploadToast(successful, failed) {
		if (successful === 0 && failed === 0) return;
		let container = document.querySelector(".ctrlem-db-toast-container");
		if (!container) {
			container = createElement("div", { className: "ctrlem-db-toast-container" });
			document.body.appendChild(container);
		}
		let text;
		let level;
		if (failed === 0) {
			text = `✓ All ${successful} file${successful === 1 ? "" : "s"} uploaded successfully`;
			level = "is-success";
		} else if (successful === 0) {
			text = `✕ All ${failed} file${failed === 1 ? "" : "s"} failed to upload`;
			level = "is-error";
		} else {
			text = `⚠ ${successful} uploaded, ${failed} failed`;
			level = "is-warning";
		}
		const toast = createElement("div", {
			className: `ctrlem-db-toast ${level}`,
			text
		});
		container.appendChild(toast);
		window.setTimeout(() => {
			toast.classList.add("is-leaving");
			window.setTimeout(() => toast.remove(), 250);
		}, 3500);
	}
	function createUploadPanel(options) {
		const { config, input, nativeDropzone, uiState = {}, onUiStateChange, actions } = options;
		const grid = createElement("div", { className: "ctrlem-db-command-upload-grid" });
		const cardChildren = [];
		const isCollapsed = uiState.collapsed !== false;
		const tools = config.tools || [];
		const hideCtrlEmUploader = actions.getUploaderSettings?.().hideCtrlEmUploader === true;
		if (nativeDropzone && (!tools.includes("ctrlem") || hideCtrlEmUploader)) nativeDropzone.classList.add("ctrlem-db-hidden-site-ui");
		tools.forEach((tool) => {
			if (tool === "ctrlem") {
				if (nativeDropzone && !hideCtrlEmUploader) cardChildren.push(...createNativeTool(config, input, nativeDropzone, actions));
				return;
			}
			cardChildren.push(...createExternalTool(config, tool, input, actions));
		});
		grid.appendChild(createElement("div", { className: "ctrlem-db-upload-card ctrlem-db-upload-native-card" }, cardChildren));
		const bodyId = `${getUploadPanelId(config.key)}-body`;
		grid.id = bodyId;
		grid.hidden = isCollapsed;
		const panel = createElement("div", {
			id: getUploadPanelId(config.key),
			className: `ctrlem-db-command-upload${isCollapsed ? " is-collapsed" : ""}`,
			attrs: { "aria-label": `${config.label} upload tools` },
			dataset: {
				command: config.key,
				type: config.type
			}
		}, [
			createElement("button", {
				className: "ctrlem-db-command-upload-head",
				type: "button",
				attrs: {
					"aria-expanded": String(!isCollapsed),
					"aria-controls": bodyId
				}
			}, [
				createElement("span", {
					className: "ctrlem-db-command-upload-title",
					text: "Upload"
				}),
				createElement("span", {
					className: "ctrlem-db-command-upload-target",
					text: getUploadTargetText(config)
				}),
				createElement("span", {
					className: "ctrlem-db-command-upload-chevron",
					text: "v"
				})
			]),
			createElement("div", {
				className: "ctrlem-db-command-upload-recommendation",
				text: "Recommended: use ImgBB/Catbox first for faster uploads and longer-lived links."
			}),
			grid
		]);
		const button = panel.querySelector(".ctrlem-db-command-upload-head");
		button.addEventListener("click", () => {
			const nextCollapsed = !grid.hidden;
			grid.hidden = nextCollapsed;
			panel.classList.toggle("is-collapsed", nextCollapsed);
			button.setAttribute("aria-expanded", String(!nextCollapsed));
			onUiStateChange?.({ collapsed: nextCollapsed });
		});
		return panel;
	}
	var LINK_CHECK_MEDIA_TYPES = [
		RecordType.IMAGE,
		RecordType.SOUND,
		RecordType.VIDEO
	];
	function createDbManagerBrandLink() {
		const tooltipText = "Test on me, and I will always be glad to have your attention. Thank you for using it. Have a great mood!";
		const link = createElement("a", {
			className: "ctrlem-db-brand-link",
			attrs: {
				href: "https://ctrlem.com/u/KPD0M",
				target: "_blank",
				rel: "noopener noreferrer",
				"data-tooltip": tooltipText
			}
		}, [
			createElement("span", {
				className: "ctrlem-db-brand-strateg",
				text: "Strateg"
			}),
			createElement("span", { text: " " }),
			createElement("span", {
				className: "ctrlem-db-brand-tag",
				text: ""
			})
		]);
		let tooltip = null;
		const hideTooltip = () => {
			tooltip?.remove();
			tooltip = null;
		};
		const showTooltip = () => {
			hideTooltip();
			tooltip = createElement("div", {
				className: "ctrlem-db-brand-tooltip",
				text: tooltipText
			});
			document.body.appendChild(tooltip);
			const linkRect = link.getBoundingClientRect();
			const tooltipRect = tooltip.getBoundingClientRect();
			const top = Math.max(8, linkRect.top - tooltipRect.height - 8);
			const left = Math.min(window.innerWidth - tooltipRect.width - 8, Math.max(8, linkRect.left + linkRect.width / 2 - tooltipRect.width / 2));
			tooltip.style.top = `${top}px`;
			tooltip.style.left = `${left}px`;
		};
		link.addEventListener("mouseenter", showTooltip);
		link.addEventListener("mouseleave", hideTooltip);
		link.addEventListener("focus", showTooltip);
		link.addEventListener("blur", hideTooltip);
		link.addEventListener("click", hideTooltip);
		return createElement("span", { className: "ctrlem-db-brand" }, [createElement("span", { text: "DB Userscript by " }), link]);
	}
	var CtrlEmDbApp = class {
		dbState = createSeedState();
		uiState = createUiState();
		saveTimer = 0;
		uiSaveTimer = 0;
		uploaderSettingsSaveTimer = 0;
		mediaRenderToken = 0;
		sendOrDeleteDownloadArms = [];
		pendingMediaMounts = new Set();
		linkCheckState = {
			categoryId: "",
			scopeAll: true,
			scopeType: RecordType.IMAGE,
			scopeCategoryId: "",
			isBusy: false,
			checked: 0,
			total: 0,
			brokenCount: 0,
			currentType: "",
			currentCategoryName: "",
			currentUrl: "",
			broken: [],
			statusMessage: "",
			statusLevel: "info"
		};
		uploaderSettings = {
			imgbbApiKey: "",
			catboxUserhash: "",
			hideCtrlEmUploader: false,
			autoDownloadSendOrDeleteImages: true
		};
		managerSelection = {
			isOpen: false,
			selectedCategoryByType: {},
			originalTitle: "",
			originalDescription: "",
			editorCommandKey: ""
		};
		site = new CtrlEmSite(log);
		imageCache = new ImageCache(log);
		autoSend = new AutoSendController({
			getState: () => this.dbState,
			selectTextItem: (input, list, button, options) => this.selectTextItem(input, list, button, options),
			selectMediaItem: (input, picker, button, options) => this.selectMediaItem(input, picker, button, options),
			getPickerUiState: (commandKey) => this.getCommandPickerUiState(commandKey),
			captureFocusState: () => this.captureFocusState(),
			restoreFocusState: (state) => this.restoreFocusState(state),
			setAutoSendInterval: (seconds) => this.setAutoSendInterval(seconds, { status: false }),
			setMinimumRequestInterval: (seconds) => this.setMinimumRequestInterval(seconds, { status: false }),
			notifySite: (message, level = "info") => this.site.notify(message, level),
			log
		});
		managerActions = {
			save: () => this.saveDbState("manual save"),
			setAutoSave: (enabled) => this.setAutoSave(enabled),
			setAutoSendInterval: (seconds) => this.setAutoSendInterval(seconds),
			setMinimumRequestInterval: (seconds) => this.setMinimumRequestInterval(seconds),
			updateEditorContent: (content) => this.updateEditorContent(content),
			importCategories: (files) => this.importCategoriesFromFiles(files),
			importAllCategories: (file) => this.importAllCategoriesFromFile(file),
			renameCategory: () => this.renameSelectedCategory(),
			deleteCategory: () => this.deleteSelectedCategory(),
			exportCategory: () => this.exportSelectedCategory(),
			exportAll: () => this.exportAllCategories(),
			restoreDefaults: () => this.restoreDefaultCategories(),
			addCategory: () => this.addCategory(),
			selectCategory: (categoryId) => this.selectManagerCategory(categoryId),
			reorderCategory: (sourceId, targetId) => this.reorderCategory(sourceId, targetId),
			renameCategoryTo: (name) => this.renameSelectedCategoryTo(name),
			setActiveType: (type) => this.setActiveType(type),
			setActiveTab: (tab) => this.setActiveTab(tab),
			setCategoryListScroll: (scrollTop) => this.setCategoryListScroll(scrollTop),
			setUploaderSetting: (name, value) => this.setUploaderSetting(name, value),
			getUploaderSettings: () => this.uploaderSettings,
			getUploadTarget: (config) => this.getUploadTarget(config),
			getUploadCategories: (config) => this.getUploadCategories(config),
			uploadContentFile: (request) => this.uploadContentFile(request),
			uploadCommandFile: (request) => this.uploadCommandFile(request),
			appendUploadedUrl: (request) => this.appendUploadedUrl(request),
			openImgBBKeyPrompt: () => this.openImgBBKeyPrompt(),
			setLinkCheckScope: (patch) => this.setLinkCheckScope(patch),
			checkBrokenLinks: () => this.checkBrokenLinks(),
			removeBrokenLinks: () => this.removeBrokenLinks()
		};
		async start() {
			await this.loadDbState();
			log("info", "Script started", {
				href: window.location.href,
				summary: getDatabaseSummary(this.dbState)
			});
			addStyles();
			this.mountUi("initial");
			this.startObserver();
		}
		async loadDbState() {
			const [stored, storedUi, storedUploaderSettings] = await Promise.all([
				readStoredState(),
				readStoredUiState(),
				readStoredUploaderSettings()
			]);
			this.dbState = normalizeDbState(stored || createSeedState());
			this.uiState = normalizeUiState(storedUi || createUiState());
			this.restoreUploaderSettings(storedUploaderSettings);
			resetManagerSelections(this.dbState, this.managerSelection);
			this.restoreManagerSelectionsFromUiState();
			log("info", "DB state loaded", getDatabaseSummary(this.dbState));
		}
		restoreUploaderSettings(settings) {
			const source = settings && typeof settings === "object" ? settings : {};
			Object.assign(this.uploaderSettings, {
				imgbbApiKey: String(source.imgbbApiKey || ""),
				catboxUserhash: String(source.catboxUserhash || ""),
				hideCtrlEmUploader: source.hideCtrlEmUploader === true,
				autoDownloadSendOrDeleteImages: source.autoDownloadSendOrDeleteImages !== false
			});
		}
		getSelectedCategory(type) {
			return getSelectedCategory(this.dbState, this.managerSelection, type);
		}
		restoreManagerSelectionsFromUiState() {
			TYPE_ORDER.forEach((type) => {
				const storedId = this.uiState.manager.selectedCategoryByType[type];
				if (storedId) this.managerSelection.selectedCategoryByType[type] = storedId;
				const selected = this.getSelectedCategory(type);
				this.uiState.manager.selectedCategoryByType[type] = selected?.id || "";
			});
		}
		scheduleUiStateSave(reason = "ui changed") {
			window.clearTimeout(this.uiSaveTimer);
			this.uiSaveTimer = window.setTimeout(() => {
				writeStoredUiState(clonePlain(this.uiState)).catch((error) => {
					log("warn", "Failed to save UI state", {
						reason,
						message: error?.message || String(error)
					});
				});
			}, USER_CONFIG.ui.saveDelayMs);
		}
		scheduleUploaderSettingsSave(reason = "uploader settings changed") {
			window.clearTimeout(this.uploaderSettingsSaveTimer);
			this.uploaderSettingsSaveTimer = window.setTimeout(() => {
				writeStoredUploaderSettings(clonePlain(this.uploaderSettings)).catch((error) => {
					log("warn", "Failed to save uploader settings", {
						reason,
						message: error?.message || String(error)
					});
				});
			}, USER_CONFIG.ui.saveDelayMs);
		}
		setManagerSelectedCategory(type, categoryId) {
			this.managerSelection.selectedCategoryByType[type] = categoryId;
			this.uiState.manager.selectedCategoryByType[type] = categoryId;
			this.scheduleUiStateSave("manager category selected");
		}
		getCommandPickerUiState(commandKey) {
			return getPickerUiState(this.uiState, commandKey);
		}
		updateCommandPickerUiState(commandKey, patch) {
			if (!commandKey) return;
			Object.assign(this.getCommandPickerUiState(commandKey), patch);
			this.scheduleUiStateSave("picker state changed");
		}
		getCommandUploadUiState(commandKey) {
			return getUploadUiState(this.uiState, commandKey);
		}
		updateCommandUploadUiState(commandKey, patch) {
			if (!commandKey) return;
			Object.assign(this.getCommandUploadUiState(commandKey), patch);
			this.scheduleUiStateSave("upload panel state changed");
		}
		captureFocusState() {
			const element = document.activeElement;
			if (!element || element === document.body || element === document.documentElement) return null;
			return {
				element,
				selectionStart: typeof element.selectionStart === "number" ? element.selectionStart : null,
				selectionEnd: typeof element.selectionEnd === "number" ? element.selectionEnd : null,
				selectionDirection: typeof element.selectionDirection === "string" ? element.selectionDirection : void 0
			};
		}
		restoreFocusState(state) {
			const element = state?.element;
			if (!element?.isConnected) return;
			try {
				if (document.activeElement !== element) element.focus({ preventScroll: true });
				if (state.selectionStart !== null && typeof element.setSelectionRange === "function") element.setSelectionRange(state.selectionStart, state.selectionEnd, state.selectionDirection);
			} catch {}
		}
		getProfileTitle() {
			const heading = document.querySelector(".profile-details h1");
			if (!heading) return "";
			const clone = heading.cloneNode(true);
			clone.querySelectorAll(".verified-badge, [title=\"Verified\"]").forEach((element) => element.remove());
			return String(clone.textContent || "").replace(/\s+/g, " ").trim();
		}
		getManagerViewModel() {
			const selectedCategory = this.getSelectedCategory(this.dbState.activeType);
			this.normalizeLinkCheckScope();
			return {
				activeType: this.dbState.activeType,
				autoSave: this.dbState.autoSave,
				autoSendIntervalSeconds: this.dbState.autoSendIntervalSeconds,
				minimumRequestIntervalSeconds: this.dbState.minimumRequestIntervalSeconds,
				categories: getUserCategories(this.dbState, this.dbState.activeType),
				selectedCategory,
				profileTitle: this.getProfileTitle(),
				uploaderSettings: this.uploaderSettings,
				uiState: this.uiState.manager,
				linkCheck: this.linkCheckState,
				linkCheckCategories: LINK_CHECK_MEDIA_TYPES.reduce((result, type) => {
					result[type] = getUserCategories(this.dbState, type).map((category) => ({
						id: category.id,
						name: category.name
					}));
					return result;
				}, {})
			};
		}
		flushEditorToState() {
			const textarea = document.getElementById(UI_IDS.dbManagerTextarea);
			if (!textarea || !this.managerSelection.isOpen) return;
			const category = this.getSelectedCategory(this.dbState.activeType);
			if (category) category.content = textarea.value;
		}
		syncEditorTextarea() {
			if (!this.managerSelection.isOpen) return;
			const textarea = document.getElementById(UI_IDS.dbManagerTextarea);
			const category = this.getSelectedCategory(this.dbState.activeType);
			if (textarea && category && textarea.value !== category.content) textarea.value = category.content;
		}
		async saveDbState(reason = "manual", options = {}) {
			const { flushEditor = true, refreshConsumers = true, syncTextarea = true } = options;
			try {
				if (flushEditor) this.flushEditorToState();
				this.dbState.autoSendIntervalSeconds = clampAutoSendInterval(this.dbState.autoSendIntervalSeconds);
				this.dbState.minimumRequestIntervalSeconds = clampMinimumRequestInterval(this.dbState.minimumRequestIntervalSeconds);
				normalizeAllCategoryContent(this.dbState);
				if (syncTextarea) this.syncEditorTextarea();
				await writeStoredState(clonePlain(this.dbState));
				log("info", "DB state saved", {
					reason,
					summary: getDatabaseSummary(this.dbState)
				});
				setManagerStatus(`Saved: ${reason}`, "success");
				this.renderCategoryList();
				if (refreshConsumers) this.refreshDbConsumers(reason);
				return true;
			} catch (error) {
				log("error", "Failed to save DB state", {
					reason,
					message: error?.message || String(error)
				});
				setManagerStatus("Save failed. Check console logs.", "error");
				return false;
			}
		}
		scheduleDataCommit(reason) {
			window.clearTimeout(this.saveTimer);
			this.saveTimer = window.setTimeout(async () => {
				this.flushEditorToState();
				if (this.dbState?.autoSave) {
					await this.saveDbState(reason, { syncTextarea: false });
					return;
				}
				this.renderCategoryList();
				this.refreshDbConsumers(reason);
			}, AUTOSAVE_DELAY);
		}
		refreshDbConsumers(reason = "refresh") {
			this.mediaRenderToken += 1;
			Object.values(TEXT_COMMANDS).forEach((config) => {
				document.getElementById(getTextPickerId(config.key))?.remove();
			});
			this.mountTextPickers();
			Object.values(MEDIA_COMMANDS).forEach((config) => {
				this.refreshMediaPicker(config);
			});
			this.mountUploadPanels();
			log("debug", "DB consumers refreshed", { reason });
		}
		async captureInputValue(config) {
			const input = document.querySelector(config.inputSelector);
			const value = String(input?.value || "").trim();
			if (!input || !isCaptureValueValid(config, value)) return false;
			if (input.dataset.ctrlemDbSelectedSource === "default" && input.dataset.ctrlemDbSelectedValue === value) return false;
			if (hasStoredValue(this.dbState, config.type, value)) return false;
			const category = appendInputValue(this.dbState, this.managerSelection, config.type, value);
			this.uiState.manager.selectedCategoryByType[config.type] = this.managerSelection.selectedCategoryByType[config.type] || category.id;
			this.scheduleUiStateSave("input category captured");
			if (this.managerSelection.isOpen && this.dbState.activeType === config.type && this.managerSelection.selectedCategoryByType[config.type] === category.id) {
				const textarea = document.getElementById(UI_IDS.dbManagerTextarea);
				if (textarea) textarea.value = category.content;
			}
			await this.saveDbState(`input captured: ${config.key}`);
			log("info", "Input captured", {
				command: config.key,
				type: config.type
			});
			return true;
		}
		mountDatabaseButton() {
			if (document.getElementById(UI_IDS.dbButton)) return false;
			const head = document.querySelector(CONFIG.selectors.commandsHead);
			if (!head) {
				log("debug", "Commands panel header was not found yet");
				return false;
			}
			let textBlock = head.querySelector(":scope > .ctrlem-db-head-copy");
			if (!textBlock) {
				const title = head.querySelector(":scope > h2");
				const description = head.querySelector(":scope > p");
				textBlock = createElement("div", { className: "ctrlem-db-head-copy" });
				if (title) textBlock.appendChild(title);
				if (description) textBlock.appendChild(description);
				head.prepend(textBlock);
			}
			const button = createElement("button", {
				id: UI_IDS.dbButton,
				className: "ctrlem-db-button",
				text: "DB",
				title: "Open DB manager",
				type: "button",
				attrs: {
					"aria-label": "Open DB manager",
					"aria-pressed": "false"
				}
			});
			button.addEventListener("click", () => this.toggleDbManager());
			head.classList.add("ctrlem-db-head");
			head.appendChild(button);
			log("info", "DB button mounted", { selector: CONFIG.selectors.commandsHead });
			return true;
		}
		setDbButtonActive(isActive) {
			const button = document.getElementById(UI_IDS.dbButton);
			if (!button) return;
			button.classList.toggle("is-active", isActive);
			button.setAttribute("aria-pressed", String(isActive));
		}
		markInputSelection(input, options) {
			input.dataset.ctrlemDbSelectedValue = options.value || "";
			input.dataset.ctrlemDbSelectedType = options.type || "";
			input.dataset.ctrlemDbSelectedSource = options.source || "saved";
		}
		selectTextItem(input, list, button, options = {}) {
			const value = button.dataset.value || "";
			const rows = list.querySelector(".ctrlem-db-rows");
			this.markInputSelection(input, {
				value,
				type: button.dataset.type,
				source: "saved"
			});
			input.value = value;
			input.dispatchEvent(new Event("input", { bubbles: true }));
			input.dispatchEvent(new Event("change", { bubbles: true }));
			if (options.focus !== false) this.focusWithoutScroll(input);
			else this.restoreFocusState(options.focusState);
			list.querySelectorAll(".ctrlem-db-row.is-selected").forEach((row) => {
				row.classList.remove("is-selected");
			});
			button.classList.add("is-selected");
			this.updateCommandPickerUiState(button.dataset.command || "", {
				categoryId: button.dataset.categoryId || list.dataset.categoryId || "",
				categoryName: button.dataset.category || list.dataset.category || "",
				value,
				itemIndex: Number(button.dataset.index || -1),
				scrollTop: rows?.scrollTop || 0
			});
			log("info", "Text item selected", {
				command: button.dataset.command,
				type: button.dataset.type,
				category: button.dataset.category,
				length: value.length
			});
		}
		getCommandPickerElement(commandKey) {
			return document.getElementById(getMediaPickerId(commandKey)) || document.getElementById(getTextPickerId(commandKey));
		}
		focusWithoutScroll(element) {
			try {
				element.focus({ preventScroll: true });
			} catch {
				element.focus();
			}
		}
		ensurePasteUrlSeparator(panel, input) {
			let separator = panel.querySelector(".upload-separator");
			if (!separator) separator = createElement("div", { className: "upload-separator" });
			separator.replaceChildren(createElement("span", { text: "or paste a URL" }));
			if (separator.nextElementSibling !== input) input.insertAdjacentElement("beforebegin", separator);
			return separator;
		}
		insertTextPicker(panel, config, input, picker) {
			if (UPLOAD_COMMANDS[config.key]) {
				const separator = this.ensurePasteUrlSeparator(panel, input);
				const uploadPanel = document.getElementById(getUploadPanelId(config.key));
				if (uploadPanel && panel.contains(uploadPanel)) {
					uploadPanel.insertAdjacentElement("beforebegin", picker);
					return;
				}
				separator.insertAdjacentElement("beforebegin", picker);
				return;
			}
			input.insertAdjacentElement("afterend", picker);
		}
		mountTextPicker(config) {
			const pickerId = getTextPickerId(config.key);
			if (document.getElementById(pickerId)) return false;
			const panel = document.querySelector(config.panelSelector);
			const input = document.querySelector(config.inputSelector);
			if (!panel || !input || !panel.contains(input)) {
				log("debug", "Text command input was not found yet", {
					command: config.key,
					panel: Boolean(panel),
					input: Boolean(input)
				});
				return false;
			}
			const picker = createTextPicker({
				config,
				input,
				categories: getCategories(this.dbState, config.type),
				uiState: this.getCommandPickerUiState(config.key),
				onSelect: ({ input: sourceInput, list, button }) => this.selectTextItem(sourceInput, list, button),
				onPreview: ({ button }) => this.openMediaPreview(button),
				onCategoryChange: (details) => log("info", "Text category changed", details),
				onUiStateChange: (patch) => this.updateCommandPickerUiState(config.key, patch),
				onAddCategory: () => this.addCategory(config.type, config.key),
				onRenameCategory: (categoryId) => this.openCategoryEditor(config.type, categoryId, config.key)
			});
			this.insertTextPicker(panel, config, input, picker);
			log("info", "Text picker mounted", {
				command: config.key,
				type: config.type,
				selector: config.inputSelector,
				summary: getDatabaseSummary(this.dbState)[config.type]
			});
			return true;
		}
		mountTextPickers() {
			return Object.values(TEXT_COMMANDS).map((config) => this.mountTextPicker(config)).some(Boolean);
		}
		selectMediaItem(input, picker, button, options = {}) {
			const url = button.dataset.url || "";
			const grid = picker.querySelector(".ctrlem-db-media-grid");
			this.markInputSelection(input, {
				value: url,
				type: button.dataset.type,
				source: button.dataset.source || "saved"
			});
			input.value = url;
			input.dispatchEvent(new Event("input", { bubbles: true }));
			input.dispatchEvent(new Event("change", { bubbles: true }));
			if (options.focus !== false) this.focusWithoutScroll(input);
			else this.restoreFocusState(options.focusState);
			picker.querySelectorAll(".ctrlem-db-media-tile.is-selected").forEach((tile) => {
				tile.classList.remove("is-selected");
			});
			button.classList.add("is-selected");
			this.updateCommandPickerUiState(button.dataset.command || "", {
				categoryId: button.dataset.categoryId || picker.dataset.categoryId || "",
				categoryName: button.dataset.category || picker.dataset.category || "",
				value: url,
				itemIndex: Number(button.dataset.index || -1),
				scrollTop: grid?.scrollTop || 0
			});
			log("info", "Media selected", {
				command: button.dataset.command,
				type: button.dataset.type,
				category: button.dataset.category,
				url
			});
		}
		async getMediaCategories(config) {
			const categories = getStaticMediaCategories(this.dbState, config.type);
			if (config.type !== RecordType.IMAGE) return categories;
			const defaultItems = await this.site.getDefaultImageItems(config.key);
			if (defaultItems.length === 0) return categories;
			return [...categories, {
				id: "default",
				name: "Default",
				items: defaultItems,
				isDefault: true
			}];
		}
		insertMediaPicker(panel, input, picker) {
			const separator = this.ensurePasteUrlSeparator(panel, input);
			const uploadPanel = document.getElementById(getUploadPanelId(picker.dataset.command || ""));
			if (uploadPanel && panel.contains(uploadPanel)) {
				uploadPanel.insertAdjacentElement("beforebegin", picker);
				return;
			}
			separator.insertAdjacentElement("beforebegin", picker);
		}
		async refreshMediaPicker(config) {
			const pickerId = getMediaPickerId(config.key);
			const picker = document.getElementById(pickerId);
			const panel = document.querySelector(config.panelSelector);
			const input = document.querySelector(config.inputSelector);
			const canMount = panel && input && panel.contains(input);
			if (panel) this.site.hideMediaUi(config, panel);
			if (!picker) {
				if (canMount && !this.pendingMediaMounts.has(config.key)) this.mountMediaPicker(config);
				return Boolean(canMount);
			}
			if (!canMount || !panel.contains(picker) || picker.ctrlemDbInput !== input) {
				picker.remove();
				if (canMount && !this.pendingMediaMounts.has(config.key)) this.mountMediaPicker(config);
				return true;
			}
			const renderToken = this.mediaRenderToken;
			const categories = await this.getMediaCategories(config);
			if (renderToken !== this.mediaRenderToken || !picker.isConnected) return false;
			if (updateMediaPicker(picker, {
				categories,
				input,
				uiState: this.getCommandPickerUiState(config.key)
			})) return true;
			picker.remove();
			if (!this.pendingMediaMounts.has(config.key)) this.mountMediaPicker(config);
			return true;
		}
		async mountMediaPicker(config) {
			const pickerId = getMediaPickerId(config.key);
			if (document.getElementById(pickerId) || this.pendingMediaMounts.has(config.key)) return false;
			const renderToken = this.mediaRenderToken;
			const panel = document.querySelector(config.panelSelector);
			const input = document.querySelector(config.inputSelector);
			if (!panel || !input || !panel.contains(input)) {
				log("debug", "Media command input was not found yet", {
					command: config.key,
					panel: Boolean(panel),
					input: Boolean(input)
				});
				return false;
			}
			this.pendingMediaMounts.add(config.key);
			try {
				const categories = await this.getMediaCategories(config);
				if (renderToken !== this.mediaRenderToken) return false;
				if (!panel.isConnected || !input.isConnected || document.getElementById(pickerId)) return false;
				const picker = createMediaPicker({
					config,
					categories,
					input,
					uiState: this.getCommandPickerUiState(config.key),
					onSelect: ({ input: sourceInput, picker: sourcePicker, button }) => this.selectMediaItem(sourceInput, sourcePicker, button),
					onDelete: ({ item, tile }) => this.site.deleteDefaultImageItem(item, tile, () => this.refreshDbConsumers("default image deleted")),
					onCategoryChange: (details) => log("info", "Media category changed", details),
					onUiStateChange: (patch) => this.updateCommandPickerUiState(config.key, patch),
					onAddCategory: () => this.addCategory(config.type, config.key),
					onRenameCategory: (categoryId) => this.openCategoryEditor(config.type, categoryId, config.key),
					onPreviewToggle: (categoryId, enabled) => this.setImagePreviewsEnabled(categoryId, enabled),
					setImagePreviewSource: (image, sourceUrl, cacheKey) => this.imageCache.setImagePreviewSource(image, sourceUrl, cacheKey)
				});
				picker.ctrlemDbInput = input;
				this.insertMediaPicker(panel, input, picker);
				log("info", "Media picker mounted", {
					command: config.key,
					type: config.type,
					categories: categories.length,
					items: categories.reduce((count, category) => count + category.items.length, 0)
				});
				return true;
			} catch (error) {
				log("error", "Failed to mount media picker", {
					command: config.key,
					message: error?.message || String(error)
				});
				return false;
			} finally {
				this.pendingMediaMounts.delete(config.key);
				if (renderToken !== this.mediaRenderToken && !document.getElementById(pickerId)) window.setTimeout(() => this.mountMediaPicker(config), 0);
			}
		}
		mountMediaPickers() {
			let changed = false;
			Object.values(MEDIA_COMMANDS).forEach((config) => {
				const panel = document.querySelector(config.panelSelector);
				if (panel) changed = this.site.hideMediaUi(config, panel) || changed;
				const input = document.querySelector(config.inputSelector);
				const canMount = panel && input && panel.contains(input);
				const isMissing = !document.getElementById(getMediaPickerId(config.key));
				const isPending = this.pendingMediaMounts.has(config.key);
				if (canMount && isMissing && !isPending) {
					changed = true;
					this.mountMediaPicker(config);
				}
			});
			return changed;
		}
		insertUploadPanel(panel, config, uploadPanel, input) {
			const picker = this.getCommandPickerElement(config.key);
			if (picker && panel.contains(picker)) {
				picker.insertAdjacentElement("afterend", uploadPanel);
				return;
			}
			this.ensurePasteUrlSeparator(panel, input).insertAdjacentElement("beforebegin", uploadPanel);
		}
		mountUploadPanel(config) {
			if (document.getElementById(getUploadPanelId(config.key))) return false;
			const panel = document.querySelector(config.panelSelector);
			const input = document.querySelector(config.inputSelector);
			if (!panel || !input || !panel.contains(input)) return false;
			const nativeDropzone = config.nativeDropzoneSelector ? panel.querySelector(config.nativeDropzoneSelector) : null;
			const uploadPanel = createUploadPanel({
				config,
				input,
				nativeDropzone,
				uiState: this.getCommandUploadUiState(config.key),
				onUiStateChange: (patch) => this.updateCommandUploadUiState(config.key, patch),
				actions: this.managerActions
			});
			this.insertUploadPanel(panel, config, uploadPanel, input);
			log("info", "Upload panel mounted", {
				command: config.key,
				tools: config.tools,
				native: Boolean(nativeDropzone)
			});
			return true;
		}
		mountUploadPanels() {
			return Object.values(UPLOAD_COMMANDS).map((config) => this.mountUploadPanel(config)).some(Boolean);
		}
		getSendOrDeleteDownloadFilename(src) {
			const timestamp = new Date().toISOString().slice(0, 19).replace("T", "-").replace(/:/g, "-");
			let extension = "png";
			try {
				const match = new URL(src, window.location.href).pathname.match(/\.([a-z0-9]{2,5})$/i);
				if (match) extension = match[1].toLowerCase();
			} catch {
				const match = src.split("?")[0].match(/\.([a-z0-9]{2,5})$/i);
				if (match) extension = match[1].toLowerCase();
			}
			return `sendOrDelete-${timestamp}.${extension}`;
		}
		downloadSendOrDeleteImage(src) {
			const url = new URL(src, window.location.href).href;
			const name = this.getSendOrDeleteDownloadFilename(url);
			const gmDownload = globalThis.GM_download;
			if (typeof gmDownload === "function") {
				gmDownload({
					url,
					name,
					saveAs: false
				});
				return;
			}
			const link = createElement("a", { attrs: {
				href: url,
				download: name
			} });
			document.body.appendChild(link);
			link.click();
			link.remove();
		}
		getSendOrDeleteResponseSignature(item) {
			const image = item.querySelector(".response-image[src]");
			if (image?.src) return `image:${new URL(image.src, window.location.href).href}`;
			return `text:${String(item.querySelector(".response-body")?.textContent || item.textContent || "").trim()}`;
		}
		getCurrentSendOrDeleteResponseCounts() {
			const container = document.querySelector(CONFIG.selectors.resultsContainer);
			const counts = new Map();
			if (!container) return counts;
			container.querySelectorAll(".response-item").forEach((item) => {
				if (String(item.querySelector(".response-badge")?.textContent || "").trim().toLowerCase() !== "sendordelete") return;
				const signature = this.getSendOrDeleteResponseSignature(item);
				counts.set(signature, (counts.get(signature) || 0) + 1);
			});
			return counts;
		}
		armSendOrDeleteDownload() {
			if (this.uploaderSettings.autoDownloadSendOrDeleteImages === false) return;
			this.sendOrDeleteDownloadArms.push({
				baseline: this.getCurrentSendOrDeleteResponseCounts(),
				armedAt: Date.now()
			});
			log("debug", "Send or Delete image download armed", { pending: this.sendOrDeleteDownloadArms.length });
		}
		mountSendOrDeleteDownloadTrigger() {
			if (document.documentElement.dataset.ctrlemDbSendOrDeleteDownloadTrigger === "true") return false;
			document.documentElement.dataset.ctrlemDbSendOrDeleteDownloadTrigger = "true";
			document.addEventListener("click", (event) => {
				if (!(event.target instanceof Element)) return;
				const button = event.target.closest("[data-send=\"sendOrDelete\"]");
				if (!button || button.disabled) return;
				this.armSendOrDeleteDownload();
			}, true);
			return true;
		}
		scanSendOrDeleteResponses() {
			const container = document.querySelector(CONFIG.selectors.resultsContainer);
			if (!container || this.sendOrDeleteDownloadArms.length === 0) return false;
			let downloaded = false;
			const shouldDownload = this.uploaderSettings.autoDownloadSendOrDeleteImages !== false;
			const seen = new Map();
			container.querySelectorAll(".response-item").forEach((item) => {
				if (this.sendOrDeleteDownloadArms.length === 0) return;
				if (String(item.querySelector(".response-badge")?.textContent || "").trim().toLowerCase() !== "sendordelete") return;
				const signature = this.getSendOrDeleteResponseSignature(item);
				const seenCount = (seen.get(signature) || 0) + 1;
				seen.set(signature, seenCount);
				if (item.dataset.ctrlemDbSendOrDeleteDownloaded === "true") return;
				const armIndex = this.sendOrDeleteDownloadArms.findIndex((arm) => seenCount > (arm.baseline.get(signature) || 0));
				if (armIndex < 0) return;
				item.dataset.ctrlemDbSendOrDeleteDownloaded = "true";
				this.sendOrDeleteDownloadArms.splice(armIndex, 1);
				this.sendOrDeleteDownloadArms.forEach((arm) => {
					arm.baseline.set(signature, (arm.baseline.get(signature) || 0) + 1);
				});
				const image = item.querySelector(".response-image[src]");
				if (!shouldDownload || !image?.src) return;
				this.downloadSendOrDeleteImage(image.src);
				downloaded = true;
				log("info", "Send or Delete image download started", { src: image.src });
			});
			return downloaded;
		}
		toggleDbManager() {
			if (this.managerSelection.isOpen) {
				this.closeDbManager();
				return;
			}
			this.openDbManager();
		}
		openDbManager() {
			const parts = this.site.getResultsParts();
			if (!parts.panel || !parts.head || !parts.title || !parts.description) {
				log("warn", "Results panel was not found for DB manager");
				return;
			}
			if (!this.managerSelection.originalTitle) {
				this.managerSelection.originalTitle = parts.title.textContent || "Results";
				this.managerSelection.originalDescription = parts.description.textContent || "";
			}
			this.ensureDbManagerRoot(parts.head);
			this.renderDbManager();
			parts.title.replaceChildren(createDbManagerBrandLink());
			parts.description.textContent = "Edit links, phrases, images, sounds, and videos.";
			parts.actions?.classList.add("ctrlem-db-hidden-site-ui");
			parts.pagination?.classList.add("ctrlem-db-hidden-site-ui");
			const root = document.getElementById(UI_IDS.dbManager);
			if (root) root.hidden = false;
			this.managerSelection.isOpen = true;
			this.setDbButtonActive(true);
			log("info", "DB manager opened");
		}
		closeDbManager() {
			const parts = this.site.getResultsParts();
			const root = document.getElementById(UI_IDS.dbManager);
			this.flushEditorToState();
			if (this.dbState?.autoSave) this.saveDbState("manager closed");
			else this.refreshDbConsumers("manager closed");
			if (root) root.hidden = true;
			if (parts.title) parts.title.textContent = this.managerSelection.originalTitle || "Results";
			if (parts.description) parts.description.textContent = this.managerSelection.originalDescription || "";
			parts.actions?.classList.remove("ctrlem-db-hidden-site-ui");
			parts.pagination?.classList.remove("ctrlem-db-hidden-site-ui");
			this.managerSelection.isOpen = false;
			this.managerSelection.editorCommandKey = "";
			this.setDbButtonActive(false);
			log("info", "DB manager closed");
		}
		ensureDbManagerRoot(head) {
			let root = document.getElementById(UI_IDS.dbManager);
			if (root) return root;
			root = createElement("div", {
				id: UI_IDS.dbManager,
				className: "ctrlem-db-manager",
				attrs: { hidden: "" }
			});
			head.insertAdjacentElement("afterend", root);
			return root;
		}
		renderDbManager() {
			const root = document.getElementById(UI_IDS.dbManager);
			if (!root) return;
			renderDbManager(root, this.getManagerViewModel(), this.managerActions);
		}
		renderCategoryList() {
			const list = document.getElementById(UI_IDS.dbManagerCategoryList);
			if (!list) return;
			renderCategoryList(list, this.getManagerViewModel(), this.managerActions);
		}
		setActiveType(type) {
			this.flushEditorToState();
			this.dbState.activeType = normalizeType(type);
			this.uiState.manager.activeTab = "editor";
			const selected = this.getSelectedCategory(this.dbState.activeType);
			if (selected) this.setManagerSelectedCategory(this.dbState.activeType, selected.id);
			this.renderDbManager();
			this.scheduleUiStateSave("manager type changed");
			this.saveAndRefreshAfterStructuralChange("type changed");
			log("info", "Manager type changed", { type: this.dbState.activeType });
		}
		setActiveTab(tab) {
			if (![
				"editor",
				"settings",
				"info"
			].includes(tab)) return;
			this.flushEditorToState();
			this.uiState.manager.activeTab = tab;
			this.renderDbManager();
			this.scheduleUiStateSave("manager tab changed");
			log("info", "Manager tab changed", { tab });
		}
		setCategoryListScroll(scrollTop) {
			this.uiState.manager.categoryListScrollTop = Math.max(0, Number(scrollTop) || 0);
			this.scheduleUiStateSave("category list scrolled");
		}
		selectManagerCategory(categoryId) {
			this.flushEditorToState();
			this.setManagerSelectedCategory(this.dbState.activeType, categoryId);
			this.renderDbManager();
			if (this.dbState.autoSave) this.saveDbState("category selected");
			log("info", "Category selected", {
				type: this.dbState.activeType,
				categoryId
			});
		}
		setAutoSave(enabled) {
			this.dbState.autoSave = enabled;
			log("info", "Auto-save toggled", { enabled: this.dbState.autoSave });
			this.saveDbState("auto-save toggled");
			setManagerStatus(this.dbState.autoSave ? "Auto-save enabled" : "Auto-save disabled", "info");
		}
		setAutoSendInterval(seconds, options = {}) {
			const nextInterval = clampAutoSendInterval(seconds);
			this.dbState.autoSendIntervalSeconds = nextInterval;
			log("info", "Auto-send interval changed", { seconds: nextInterval });
			this.saveDbState("auto-send interval changed");
			this.autoSend.syncIntervalInputs();
			if (options.status !== false) setManagerStatus(`Auto-send interval: ${nextInterval}s`, "info");
		}
		setMinimumRequestInterval(seconds, options = {}) {
			const nextInterval = clampMinimumRequestInterval(seconds);
			this.dbState.minimumRequestIntervalSeconds = nextInterval;
			log("info", "Auto-send minimum request interval changed", { seconds: nextInterval });
			this.saveDbState("auto-send minimum request interval changed");
			if (options.status !== false) setManagerStatus(`Auto-send min request interval: ${nextInterval}s`, "info");
		}
		setUploaderSetting(name, value) {
			if (!Object.prototype.hasOwnProperty.call(this.uploaderSettings, name)) return;
			this.uploaderSettings[name] = typeof this.uploaderSettings[name] === "boolean" ? value === true || value === "true" : String(value || "");
			if (name === "autoDownloadSendOrDeleteImages" && this.uploaderSettings[name] === false) this.sendOrDeleteDownloadArms = [];
			this.scheduleUploaderSettingsSave();
		}
		normalizeLinkCheckScope() {
			const scopeAll = this.linkCheckState.scopeAll !== false;
			const scopeType = LINK_CHECK_MEDIA_TYPES.includes(this.linkCheckState.scopeType) ? this.linkCheckState.scopeType : RecordType.IMAGE;
			const categories = getUserCategories(this.dbState, scopeType);
			const selectedCategory = categories.find((category) => category.id === this.linkCheckState.scopeCategoryId) || categories[0] || null;
			Object.assign(this.linkCheckState, {
				scopeAll,
				scopeType,
				scopeCategoryId: selectedCategory?.id || ""
			});
			return {
				scopeAll,
				scopeType,
				scopeCategoryId: selectedCategory?.id || "",
				category: selectedCategory
			};
		}
		setLinkCheckScope(patch) {
			if (this.linkCheckState.isBusy) return;
			if (typeof patch?.scopeAll === "boolean") this.linkCheckState.scopeAll = patch.scopeAll;
			if (LINK_CHECK_MEDIA_TYPES.includes(patch?.scopeType)) {
				this.linkCheckState.scopeType = patch.scopeType;
				if (patch.scopeCategoryId === void 0) this.linkCheckState.scopeCategoryId = "";
			}
			if (patch?.scopeCategoryId !== void 0) this.linkCheckState.scopeCategoryId = String(patch.scopeCategoryId || "");
			this.normalizeLinkCheckScope();
			this.renderDbManager();
		}
		async renameMediaItem(details, name) {
			const type = String(details?.type || "");
			if (type !== RecordType.SOUND && type !== RecordType.VIDEO) return false;
			const url = String(details?.value || "").trim();
			if (!url) return false;
			const categories = getUserCategories(this.dbState, type);
			const category = categories.find((item) => item.id === details?.categoryId) || categories.find((item) => item.name === details?.category);
			if (!category) return false;
			const lines = parseLines(category.content);
			const requestedIndex = Number(details?.index);
			let lineIndex = Number.isFinite(requestedIndex) && requestedIndex >= 0 && getRecordKey(type, lines[requestedIndex]) === url ? requestedIndex : -1;
			if (lineIndex < 0) lineIndex = lines.findIndex((line) => getRecordKey(type, line) === url);
			if (lineIndex < 0) return false;
			const cleanName = String(name || "").trim();
			lines[lineIndex] = cleanName ? `${url} ${cleanName}` : url;
			category.content = formatCategoryContent(lines.join("\n"));
			this.updateCommandPickerUiState(details?.command || "", {
				categoryId: category.id,
				categoryName: category.name,
				value: url,
				itemIndex: lineIndex
			});
			if (this.managerSelection.isOpen && this.dbState.activeType === type && this.managerSelection.selectedCategoryByType[type] === category.id) {
				const textarea = document.getElementById(UI_IDS.dbManagerTextarea);
				if (textarea) textarea.value = category.content;
			}
			await this.saveDbState("media renamed", { flushEditor: false });
			this.site.notify("Media renamed", "success");
			return true;
		}
		openMediaPreview(button) {
			const type = String(button?.dataset?.type || "");
			const url = String(button?.dataset?.value || "").trim();
			if (!url || type !== RecordType.SOUND && type !== RecordType.VIDEO) return;
			const opened = window.open(url, "_blank", "noopener,noreferrer");
			const title = type === RecordType.SOUND ? "Sound preview" : "Video preview";
			const nameInput = createElement("input", {
				className: "ctrlem-db-media-preview-name",
				value: button.dataset.label || "",
				attrs: {
					placeholder: "Display name",
					autocomplete: "off"
				}
			});
			const status = createElement("div", {
				className: "ctrlem-db-media-preview-status",
				attrs: { "aria-live": "polite" }
			});
			const closeButton = createElement("button", {
				className: "btn btn-sm btn-secondary",
				text: "Cancel",
				type: "button"
			});
			const saveButton = createElement("button", {
				className: "btn btn-sm btn-primary",
				text: "Save name",
				type: "button"
			});
			const modal = createElement("div", {
				className: "ctrlem-db-modal-backdrop ctrlem-db-media-preview-backdrop",
				attrs: {
					role: "dialog",
					"aria-modal": "true",
					"aria-label": title
				}
			}, [createElement("div", { className: "ctrlem-db-modal ctrlem-db-media-preview-modal" }, [
				createElement("div", {
					className: "ctrlem-db-modal-title",
					text: title
				}),
				createElement("a", {
					className: "ctrlem-db-media-preview-url",
					text: url,
					attrs: {
						href: url,
						target: "_blank",
						rel: "noopener noreferrer"
					}
				}),
				nameInput,
				status,
				createElement("div", { className: "ctrlem-db-modal-actions" }, [closeButton, saveButton])
			])]);
			const close = () => {
				modal.remove();
			};
			status.textContent = opened ? "Your preview was opened in a new tab. You can rename the link or close this window." : "Your preview can be opened from the link above. You can rename the link or close this window.";
			const save = async () => {
				saveButton.disabled = true;
				status.textContent = "Saving...";
				status.dataset.level = "info";
				if (!await this.renameMediaItem(button.dataset, nameInput.value)) {
					saveButton.disabled = false;
					status.textContent = "Media item was not found.";
					status.dataset.level = "error";
					return;
				}
				button.dataset.label = nameInput.value.trim();
				close();
			};
			closeButton.addEventListener("click", close);
			saveButton.addEventListener("click", () => {
				save().catch((error) => {
					saveButton.disabled = false;
					status.textContent = error?.message || String(error);
					status.dataset.level = "error";
				});
			});
			nameInput.addEventListener("keydown", (event) => {
				if (event.key === "Enter") saveButton.click();
				if (event.key === "Escape") close();
			});
			modal.addEventListener("click", (event) => {
				if (event.target === modal) close();
			});
			document.body.appendChild(modal);
			window.setTimeout(() => this.focusWithoutScroll(nameInput), 0);
		}
		openImgBBKeyPrompt() {
			document.getElementById(UI_IDS.imgbbKeyModal)?.remove();
			const input = createElement("input", {
				className: "ctrlem-db-imgbb-key-input",
				type: "password",
				value: this.uploaderSettings.imgbbApiKey || "",
				attrs: {
					placeholder: "ImgBB API key",
					autocomplete: "off"
				}
			});
			const status = createElement("div", {
				className: "ctrlem-db-imgbb-key-status",
				attrs: { "aria-live": "polite" }
			});
			const closeButton = createElement("button", {
				className: "btn btn-sm btn-secondary",
				text: "Cancel",
				type: "button"
			});
			const saveButton = createElement("button", {
				className: "btn btn-sm btn-primary",
				text: "Save key",
				type: "button"
			});
			const modal = createElement("div", {
				id: UI_IDS.imgbbKeyModal,
				className: "ctrlem-db-modal-backdrop",
				attrs: {
					role: "dialog",
					"aria-modal": "true",
					"aria-label": "ImgBB API key"
				}
			}, [createElement("div", { className: "ctrlem-db-modal" }, [
				createElement("div", {
					className: "ctrlem-db-modal-title",
					text: "ImgBB API key"
				}),
				createElement("p", {
					className: "ctrlem-db-modal-copy",
					text: "ImgBB needs an API key for full media uploading above CtrlEm small-file limits."
				}),
				input,
				status,
				createElement("div", { className: "ctrlem-db-modal-actions" }, [closeButton, saveButton])
			])]);
			const close = () => modal.remove();
			const save = () => {
				const value = input.value.trim();
				if (!value) {
					status.textContent = "Enter an ImgBB API key.";
					status.dataset.level = "error";
					return;
				}
				this.setUploaderSetting("imgbbApiKey", value);
				status.textContent = "Saved.";
				status.dataset.level = "success";
				window.setTimeout(close, USER_CONFIG.ui.modalCloseDelayMs);
			};
			closeButton.addEventListener("click", close);
			saveButton.addEventListener("click", save);
			input.addEventListener("keydown", (event) => {
				if (event.key === "Enter") save();
				if (event.key === "Escape") close();
			});
			modal.addEventListener("click", (event) => {
				if (event.target === modal) close();
			});
			document.body.appendChild(modal);
			window.setTimeout(() => input.focus(), 0);
		}
		async uploadContentFile(request) {
			const tool = String(request?.tool || "");
			const file = request?.file;
			if (!file) throw new Error("File is required");
			try {
				if (tool === "imgbb") {
					const url = await uploadImageToImgBB(file, request?.apiKey || this.uploaderSettings.imgbbApiKey);
					log("info", "ImgBB upload completed", {
						name: file.name,
						url
					});
					return url;
				}
				if (tool === "catbox") {
					const url = await uploadFileToCatbox(file, request?.userhash || this.uploaderSettings.catboxUserhash);
					log("info", "Catbox upload completed", {
						name: file.name,
						url
					});
					return url;
				}
				if (tool === "vidhosting") {
					const url = await uploadVideoToVidHosting(file);
					log("info", "VidHosting upload completed", {
						name: file.name,
						url
					});
					return url;
				}
				throw new Error(`Unknown upload tool: ${tool}`);
			} catch (error) {
				log("error", "Content upload failed", {
					tool,
					name: file.name,
					message: error?.message || String(error)
				});
				throw error;
			}
		}
		getUploadTarget(config) {
			const categories = getUserCategories(this.dbState, config.type);
			const selectedId = this.getCommandPickerUiState(config.key).categoryId || this.managerSelection.selectedCategoryByType[config.type];
			const category = categories.find((item) => item.id === selectedId) || categories[0];
			return {
				id: category?.id || "",
				name: category?.name || "Current category",
				type: config.type
			};
		}
		getUploadCategories(config) {
			return getUserCategories(this.dbState, config.type).map((category) => ({
				id: category.id,
				name: category.name,
				type: config.type
			}));
		}
		async appendUploadedUrl(request) {
			const config = request?.config || {};
			const url = String(request?.url || "").trim();
			const fileName = String(request?.fileName || "").trim();
			const value = String(request?.value || request?.line || ((config.type === RecordType.SOUND || config.type === RecordType.VIDEO) && fileName ? `${url} ${fileName}` : url)).trim();
			const categories = getUserCategories(this.dbState, config.type);
			const category = categories.find((item) => item.id === request?.targetCategoryId) || categories.find((item) => item.id === this.getCommandPickerUiState(config.key).categoryId) || categories[0];
			if (!category || !url || !value) return {
				appended: false,
				categoryName: category?.name || "Current category"
			};
			const existing = parseLines(category.content);
			const existingKeys = new Set(existing.map((line) => getRecordKey(config.type, line)));
			const key = getRecordKey(config.type, value);
			const appended = Boolean(key && !existingKeys.has(key));
			if (appended) {
				category.content = formatCategoryContent([...existing, value].join("\n"));
				this.updateCommandPickerUiState(config.key, {
					categoryId: category.id,
					categoryName: category.name,
					value: url,
					itemIndex: existing.length
				});
				if (this.managerSelection.isOpen && this.dbState.activeType === config.type && this.managerSelection.selectedCategoryByType[config.type] === category.id) {
					const textarea = document.getElementById(UI_IDS.dbManagerTextarea);
					if (textarea) textarea.value = category.content;
				}
				if (this.dbState.autoSave) await this.saveDbState(`${config.label || config.key} upload appended`, { flushEditor: false });
				else {
					this.renderCategoryList();
					this.refreshDbConsumers(`${config.label || config.key} upload appended`);
				}
			}
			log("info", "Uploaded URL handled", {
				command: config.key,
				source: request?.source,
				category: category.name,
				appended,
				url
			});
			return {
				appended,
				categoryName: category.name,
				url,
				value
			};
		}
		async uploadCommandFile(request) {
			const url = await this.uploadContentFile(request);
			return this.appendUploadedUrl({
				config: request.config,
				url,
				targetCategoryId: request.targetCategoryId,
				source: request.tool
			});
		}
		resetLinkCheckState(categoryId = "", patch = {}) {
			const scopeAll = this.linkCheckState.scopeAll !== false;
			const scopeType = LINK_CHECK_MEDIA_TYPES.includes(this.linkCheckState.scopeType) ? this.linkCheckState.scopeType : RecordType.IMAGE;
			const scopeCategoryId = String(this.linkCheckState.scopeCategoryId || "");
			Object.assign(this.linkCheckState, {
				categoryId,
				scopeAll,
				scopeType,
				scopeCategoryId,
				isBusy: false,
				checked: 0,
				total: 0,
				brokenCount: 0,
				currentType: "",
				currentCategoryName: "",
				currentUrl: "",
				broken: [],
				statusMessage: "",
				statusLevel: "info",
				token: "",
				...patch
			});
		}
		async checkBrokenLinks() {
			this.flushEditorToState();
			const scope = this.normalizeLinkCheckScope();
			const targets = scope.scopeAll ? LINK_CHECK_MEDIA_TYPES.flatMap((type) => getUserCategories(this.dbState, type).map((category) => ({
				type,
				category
			}))) : scope.category ? [{
				type: scope.scopeType,
				category: scope.category
			}] : [];
			const total = targets.reduce((count, target) => count + getCategoryDataLines(target.category.content).length, 0);
			const token = createId("link-check");
			this.resetLinkCheckState("", {
				isBusy: true,
				total,
				statusMessage: "Checking media links...",
				token
			});
			this.renderDbManager();
			setManagerStatus("Checking media links...", "info");
			try {
				const broken = [];
				let checked = 0;
				for (const target of targets) {
					const result = await findBrokenMediaLinks(target.type, target.category.content, (progress) => {
						if (this.linkCheckState.token !== token) return;
						Object.assign(this.linkCheckState, {
							checked: checked + progress.checked,
							total,
							brokenCount: broken.length + progress.brokenCount,
							currentType: target.type,
							currentCategoryName: target.category.name,
							currentUrl: progress.currentUrl || "",
							statusMessage: `Checked links: ${checked + progress.checked}/${total}. Broken: ${broken.length + progress.brokenCount}`,
							statusLevel: "info"
						});
						setManagerStatus(`Checked links: ${checked + progress.checked}/${total}. Broken: ${broken.length + progress.brokenCount}`, "info");
						this.renderDbManager();
					});
					if (this.linkCheckState.token !== token) return;
					checked += result.checked;
					broken.push(...result.broken.map((item) => ({
						...item,
						type: target.type,
						categoryId: target.category.id,
						categoryName: target.category.name
					})));
				}
				if (this.linkCheckState.token !== token) return;
				Object.assign(this.linkCheckState, {
					isBusy: false,
					checked,
					total,
					brokenCount: broken.length,
					currentType: "",
					currentCategoryName: "",
					currentUrl: "",
					broken,
					statusMessage: broken.length > 0 ? `Broken links found: ${broken.length}/${total}` : `No broken links found: ${total}`,
					statusLevel: broken.length > 0 ? "error" : "success"
				});
				this.renderDbManager();
				setManagerStatus(this.linkCheckState.statusMessage, this.linkCheckState.statusLevel);
				log("info", "Broken media link check completed", {
					scope: scope.scopeAll ? "all" : `${scope.scopeType}:${scope.scopeCategoryId}`,
					total,
					broken: broken.length
				});
			} catch (error) {
				if (this.linkCheckState.token !== token) return;
				Object.assign(this.linkCheckState, {
					isBusy: false,
					currentType: "",
					currentCategoryName: "",
					currentUrl: "",
					statusMessage: `Link check failed: ${error?.message || String(error)}`,
					statusLevel: "error"
				});
				this.renderDbManager();
				setManagerStatus("Link check failed. Check console logs.", "error");
				log("error", "Broken media link check failed", { message: error?.message || String(error) });
			}
		}
		async removeBrokenLinks() {
			this.flushEditorToState();
			const broken = Array.isArray(this.linkCheckState.broken) ? this.linkCheckState.broken : [];
			if (broken.length === 0) {
				setManagerStatus("No broken links to remove", "info");
				return;
			}
			if (!window.confirm(`Remove ${broken.length} broken link(s) from media database?`)) return;
			const brokenByCategory = new Map();
			broken.forEach((item) => {
				if (!item?.type || !item?.categoryId || !item?.line) return;
				const key = `${item.type}:${item.categoryId}`;
				const group = brokenByCategory.get(key) || {
					type: item.type,
					categoryId: item.categoryId,
					lines: new Set()
				};
				group.lines.add(item.line);
				brokenByCategory.set(key, group);
			});
			let removedCount = 0;
			let shouldSyncEditor = false;
			brokenByCategory.forEach((group) => {
				const category = getUserCategories(this.dbState, group.type).find((item) => item.id === group.categoryId);
				if (!category) return;
				const beforeLines = parseLines(category.content);
				const afterLines = beforeLines.filter((line) => !group.lines.has(line));
				const categoryRemovedCount = beforeLines.length - afterLines.length;
				if (categoryRemovedCount === 0) return;
				removedCount += categoryRemovedCount;
				category.content = formatCategoryContent(afterLines.join("\n"));
				shouldSyncEditor = shouldSyncEditor || this.managerSelection.isOpen && this.dbState.activeType === group.type && this.managerSelection.selectedCategoryByType[group.type] === category.id;
			});
			if (shouldSyncEditor) this.syncEditorTextarea();
			const brokenUrls = [...new Set(broken.map((item) => String(item.url || "")).filter(Boolean))];
			this.imageCache.deleteUrls(brokenUrls).catch((error) => {
				log("debug", "Broken link cache cleanup skipped", { message: error?.message || String(error) });
			});
			await this.saveDbState("broken links removed", { flushEditor: false });
			this.resetLinkCheckState("", {
				statusMessage: `Removed broken links: ${removedCount}`,
				statusLevel: removedCount > 0 ? "success" : "info"
			});
			this.renderDbManager();
			setManagerStatus(`Removed broken links: ${removedCount}`, removedCount > 0 ? "success" : "info");
			log("info", "Broken media links removed", { removed: removedCount });
		}
		updateEditorContent(content) {
			const category = this.getSelectedCategory(this.dbState.activeType);
			if (category) category.content = content;
			if (category?.id && [
				RecordType.IMAGE,
				RecordType.SOUND,
				RecordType.VIDEO
			].includes(this.dbState.activeType) && (this.linkCheckState.broken.length > 0 || this.linkCheckState.statusMessage)) this.resetLinkCheckState("", { statusMessage: "Category changed. Run the check again." });
			this.scheduleDataCommit("editor input");
		}
		addCategory(type = this.dbState.activeType, commandKey = "") {
			this.flushEditorToState();
			const normalizedType = normalizeType(type);
			const nextName = window.prompt("Category name", "");
			if (nextName === null) return;
			const cleanName = nextName.trim();
			if (!cleanName) {
				log("warn", "Category add cancelled: empty name");
				setManagerStatus("Add cancelled: empty category name.", "error");
				return;
			}
			const category = {
				id: createId(normalizedType),
				name: makeUniqueCategoryName(this.dbState, normalizedType, cleanName),
				content: ""
			};
			getUserCategories(this.dbState, normalizedType).push(category);
			this.setManagerSelectedCategory(normalizedType, category.id);
			if (commandKey) {
				this.updateCommandPickerUiState(commandKey, {
					categoryId: category.id,
					categoryName: category.name,
					itemIndex: -1,
					scrollTop: 0
				});
				if (this.managerSelection.isOpen && this.dbState.activeType === normalizedType) this.renderDbManager();
			} else {
				this.uiState.manager.activeTab = "editor";
				this.renderDbManager();
				this.focusManagerTextarea();
			}
			this.saveAndRefreshAfterStructuralChange("category added");
			log("info", "Category added", {
				type: normalizedType,
				category: category.name
			});
		}
		renameSelectedCategory() {
			this.renameCategory(this.dbState.activeType);
		}
		renameSelectedCategoryTo(name) {
			this.flushEditorToState();
			const type = this.dbState.activeType;
			const category = this.getSelectedCategory(type);
			if (!category) return "";
			const cleanName = String(name || "").trim();
			if (!cleanName) {
				log("warn", "Category rename cancelled: empty name");
				setManagerStatus("Rename cancelled: empty category name.", "error");
				return category.name;
			}
			const nextName = makeUniqueCategoryName(this.dbState, type, cleanName, category.id);
			if (nextName === category.name) return category.name;
			category.name = nextName;
			this.setManagerSelectedCategory(type, category.id);
			this.renderDbManager();
			this.saveAndRefreshAfterStructuralChange("category renamed");
			log("info", "Category renamed", {
				type,
				category: category.name
			});
			return category.name;
		}
		renameCategory(type = this.dbState.activeType, categoryId = "", commandKey = "") {
			this.flushEditorToState();
			const normalizedType = normalizeType(type);
			const category = categoryId ? getUserCategories(this.dbState, normalizedType).find((item) => item.id === categoryId) : this.getSelectedCategory(normalizedType);
			if (!category) return;
			const nextName = window.prompt("Category name", category.name);
			if (nextName === null) return;
			const cleanName = nextName.trim();
			if (!cleanName) {
				log("warn", "Category rename cancelled: empty name");
				setManagerStatus("Rename cancelled: empty category name.", "error");
				return;
			}
			category.name = makeUniqueCategoryName(this.dbState, normalizedType, cleanName, category.id);
			this.setManagerSelectedCategory(normalizedType, category.id);
			if (commandKey) this.updateCommandPickerUiState(commandKey, {
				categoryId: category.id,
				categoryName: category.name
			});
			this.renderDbManager();
			this.saveAndRefreshAfterStructuralChange("category renamed");
			log("info", "Category renamed", {
				type: normalizedType,
				category: category.name
			});
		}
		openCategoryEditor(type = this.dbState.activeType, categoryId = "", commandKey = "") {
			this.flushEditorToState();
			const normalizedType = normalizeType(type);
			const category = categoryId ? getUserCategories(this.dbState, normalizedType).find((item) => item.id === categoryId) : this.getSelectedCategory(normalizedType);
			if (!category) return;
			if (this.managerSelection.isOpen && this.dbState.activeType === normalizedType && this.uiState.manager.activeTab === "editor" && this.managerSelection.selectedCategoryByType[normalizedType] === category.id && this.managerSelection.editorCommandKey === commandKey) {
				this.closeDbManager();
				return;
			}
			this.dbState.activeType = normalizedType;
			this.uiState.manager.activeTab = "editor";
			this.setManagerSelectedCategory(normalizedType, category.id);
			if (commandKey) this.updateCommandPickerUiState(commandKey, {
				categoryId: category.id,
				categoryName: category.name
			});
			if (this.managerSelection.isOpen) this.renderDbManager();
			else this.openDbManager();
			this.managerSelection.editorCommandKey = commandKey;
			this.focusManagerCategoryName();
			log("info", "Category editor opened", {
				type: normalizedType,
				category: category.name
			});
		}
		setImagePreviewsEnabled(categoryId, enabled) {
			this.flushEditorToState();
			const category = getUserCategories(this.dbState, RecordType.IMAGE).find((item) => item.id === categoryId);
			if (!category) return;
			const dataLines = parseLines(category.content).filter((line) => !isNoPreviewsMarker(line));
			const shouldEnable = enabled && getCategoryDataLines(category.content).length <= IMAGE_PREVIEW_MAX_ITEMS;
			category.content = formatCategoryContent(shouldEnable ? dataLines.join("\n") : [NO_PREVIEWS_MARKER, ...dataLines].join("\n"));
			this.setManagerSelectedCategory(RecordType.IMAGE, category.id);
			if (this.managerSelection.isOpen && this.dbState.activeType === RecordType.IMAGE && this.managerSelection.selectedCategoryByType[RecordType.IMAGE] === category.id) this.syncEditorTextarea();
			this.saveAndRefreshAfterStructuralChange("image previews toggled");
			log("info", "Image previews toggled", {
				category: category.name,
				enabled: shouldEnable
			});
		}
		deleteSelectedCategory() {
			const type = this.dbState.activeType;
			const categories = getUserCategories(this.dbState, type);
			const category = this.getSelectedCategory(type);
			if (!category) return;
			if (!window.confirm(`Delete category "${category.name}"?`)) return;
			const oldIndex = categories.findIndex((item) => item.id === category.id);
			if (oldIndex >= 0) categories.splice(oldIndex, 1);
			if (categories.length === 0) categories.push(createEmptyCategory(type));
			const nextIndex = Math.max(0, Math.min(oldIndex, categories.length - 1));
			this.setManagerSelectedCategory(type, categories[nextIndex].id);
			this.renderDbManager();
			this.saveAndRefreshAfterStructuralChange("category deleted");
			log("info", "Category deleted", {
				type,
				category: category.name
			});
		}
		reorderCategory(sourceId, targetId) {
			const type = this.dbState.activeType;
			if (!sourceId || !targetId || sourceId === targetId) return;
			const categories = getUserCategories(this.dbState, type);
			const sourceIndex = categories.findIndex((category) => category.id === sourceId);
			const targetIndex = categories.findIndex((category) => category.id === targetId);
			if (sourceIndex < 0 || targetIndex < 0) return;
			const [moved] = categories.splice(sourceIndex, 1);
			categories.splice(targetIndex, 0, moved);
			this.setManagerSelectedCategory(type, moved.id);
			this.renderCategoryList();
			this.saveAndRefreshAfterStructuralChange("category reordered");
			log("info", "Category reordered", {
				type,
				category: moved.name,
				targetId
			});
		}
		saveAndRefreshAfterStructuralChange(reason) {
			this.flushEditorToState();
			if (this.dbState.autoSave) {
				this.saveDbState(reason);
				return;
			}
			this.renderCategoryList();
			this.refreshDbConsumers(reason);
		}
		focusManagerTextarea() {
			window.setTimeout(() => {
				const textarea = document.getElementById(UI_IDS.dbManagerTextarea);
				if (textarea) textarea.focus();
			}, 0);
		}
		focusManagerCategoryName() {
			window.setTimeout(() => {
				const input = document.querySelector(".ctrlem-db-category-name-input");
				if (!input) return;
				input.focus();
				input.select?.();
			}, 0);
		}
		exportSelectedCategory() {
			this.flushEditorToState();
			const type = this.dbState.activeType;
			const category = this.getSelectedCategory(type);
			if (!category) return;
			const filename = `${getSafeFileName(type, "type")}-${getSafeFileName(category.name, "category")}.txt`;
			downloadTextFile(filename, formatCategoryContent(category.content), "text/plain;charset=utf-8");
			setManagerStatus(`Exported category: ${category.name}`, "success");
			log("info", "Category exported", {
				type,
				category: category.name,
				filename
			});
		}
		exportAllCategories() {
			this.flushEditorToState();
			const filename = `ctrlem-db-${new Date().toISOString().slice(0, 10)}.json`;
			downloadTextFile(filename, JSON.stringify(createExportPayload(this.dbState), null, 2), "application/json;charset=utf-8");
			setManagerStatus("Exported all categories", "success");
			log("info", "All categories exported", { filename });
		}
		async replaceAllCategories(rawState, reason, successMessage) {
			const keepAutoSave = this.dbState?.autoSave !== false;
			const keepAutoSendInterval = clampAutoSendInterval(this.dbState?.autoSendIntervalSeconds);
			const keepMinimumRequestInterval = clampMinimumRequestInterval(this.dbState?.minimumRequestIntervalSeconds);
			this.dbState = normalizeDbState(rawState);
			this.dbState.autoSave = keepAutoSave;
			if (rawState?.autoSendIntervalSeconds === void 0) this.dbState.autoSendIntervalSeconds = keepAutoSendInterval;
			if (rawState?.minimumRequestIntervalSeconds === void 0) this.dbState.minimumRequestIntervalSeconds = keepMinimumRequestInterval;
			resetManagerSelections(this.dbState, this.managerSelection);
			this.restoreManagerSelectionsFromUiState();
			this.scheduleUiStateSave("manager selections restored");
			this.renderDbManager();
			await this.saveDbState(reason, { flushEditor: false });
			setManagerStatus(successMessage, "success");
			log("info", successMessage, getDatabaseSummary(this.dbState));
		}
		async importAllCategoriesFromFile(file) {
			if (!file) return;
			try {
				const text = await file.text();
				const parsed = JSON.parse(text);
				if (!parsed?.types || typeof parsed.types !== "object") throw new Error("JSON must contain a \"types\" object");
				await this.replaceAllCategories(parsed, "all categories imported", "Imported all categories from JSON");
			} catch (error) {
				setManagerStatus("Import all failed. Select a valid DB JSON export.", "error");
				log("error", "Failed to import all categories", {
					name: file.name,
					message: error?.message || String(error)
				});
			}
		}
		async restoreDefaultCategories() {
			await this.replaceAllCategories(createSeedState(), "defaults restored", "Restored default categories");
		}
		async importCategoriesFromFiles(files) {
			if (!files.length) return;
			this.flushEditorToState();
			let importedCount = 0;
			let firstImported = null;
			for (const file of files) try {
				parseImportFile(file, await file.text(), this.dbState.activeType, log).forEach((addition) => {
					const type = normalizeType(addition.type || this.dbState.activeType);
					const category = {
						id: createId(type),
						name: makeUniqueCategoryName(this.dbState, type, addition.name),
						content: formatCategoryContent(addition.content)
					};
					getUserCategories(this.dbState, type).push(category);
					importedCount += 1;
					if (!firstImported) firstImported = {
						type,
						id: category.id
					};
				});
			} catch (error) {
				log("error", "Failed to import file", {
					name: file.name,
					message: error?.message || String(error)
				});
			}
			if (firstImported) {
				this.dbState.activeType = firstImported.type;
				this.setManagerSelectedCategory(firstImported.type, firstImported.id);
			}
			this.renderDbManager();
			this.saveAndRefreshAfterStructuralChange("categories imported");
			setManagerStatus(`Imported categories: ${importedCount}`, importedCount ? "success" : "error");
			log("info", "Categories imported", {
				count: importedCount,
				files: files.length
			});
		}
		mountUi(reason) {
			if ([
				this.mountDatabaseButton(),
				mountInputCapture({
					captureInputValue: (config) => this.captureInputValue(config),
					log
				}),
				this.mountTextPickers(),
				this.mountMediaPickers(),
				this.mountUploadPanels(),
				this.mountSendOrDeleteDownloadTrigger(),
				this.autoSend.mountControls(),
				this.autoSend.renderManager(),
				this.scanSendOrDeleteResponses()
			].some(Boolean)) log("debug", "UI mount pass completed", { reason });
		}
		debounce(callback, delay) {
			let timer = 0;
			return () => {
				window.clearTimeout(timer);
				timer = window.setTimeout(callback, delay);
			};
		}
		startObserver() {
			if (!document.body) return;
			new MutationObserver(this.debounce(() => {
				this.mountUi("dom-mutation");
			}, USER_CONFIG.ui.domObserverDelayMs)).observe(document.body, {
				childList: true,
				subtree: true
			});
			log("debug", "DOM observer started");
		}
	};
	async function bootCtrlEmDb() {
		await new CtrlEmDbApp().start();
	}
	if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => {
		bootCtrlEmDb();
	}, { once: true });
	else bootCtrlEmDb();
})();