SimpCity Thread Gallery

Browse SimpCity thread media in a full-screen gallery with filtering, lightbox viewing and verified downloads.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         SimpCity Thread Gallery
// @namespace    local.simpcity.gallery.hybrid
// @version      0.9.7
// @description  Browse SimpCity thread media in a full-screen gallery with filtering, lightbox viewing and verified downloads.
// @license      MIT
// @match        https://simpcity.cr/threads/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @connect      *
// @run-at       document-idle
// ==/UserScript==

(() => {
  'use strict';

  // DESIGN DIRECTION: the media canvas is the page; a single
  // floating instrument plate carries every frequent control; the dock at the
  // foot of the canvas changes identity between browse, select and download.
  // v0.9.7 keeps the established architecture while reducing rendering work
  // and adding safer scanning plus reply-level grouping and selection controls.

  const APP_VERSION = '0.9.7';
  const APP_ID = 'sc-thread-gallery';
  const SETTINGS_KEY = 'sc-thread-gallery-settings-v1';
  const DOWNLOAD_HISTORY_KEY = 'sc-thread-gallery-download-history-v1';
  const STORAGE_MIGRATION_KEY = 'sc-thread-gallery-storage-migrated-v072';
  const LARGE_THREAD_WARNING_PAGES = 50;
  const SCAN_WARNING_TEXT = 'Scanning a very large thread can use substantial RAM and may temporarily slow or freeze the browser. Media files remain unloaded until viewed.';
  if (document.getElementById(APP_ID)) return;

  const DEFAULT_SETTINGS = Object.freeze({
    filter: 'all',
    compact: false,
    sort: 'thread-asc',
    groupBy: 'none',
    perPage: 60,
    theme: 'dark',
    filtersCollapsed: false,
    layoutMode: 'masonry',
    cardScale: 100,
    downloadConcurrency: 2,
    zipPartSizeMb: 300,
    zipPartMaxFiles: 24,
    archiveLayout: 'page-author',
    includeManifest: true,
    dedupeContent: true,
    warnLargeThreadScan: true,
  });
  const THEME_MODES = Object.freeze(['dark', 'light', 'midnight', 'graphite']);
  const THEME_LABELS = Object.freeze({ dark: 'Darkroom', light: 'Daylight', midnight: 'Indigo', graphite: 'Graphite' });
  const VIEW_PAGE_SIZES = Object.freeze([40, 60, 100, 200, 300, 500]);

  const storage = {
    mode: typeof GM_getValue === 'function' && typeof GM_setValue === 'function' ? 'Tampermonkey isolated storage' : 'page localStorage fallback',
    get(key, fallback) {
      try {
        const raw = typeof GM_getValue === 'function' ? GM_getValue(key, undefined) : localStorage.getItem(key);
        if (raw === undefined || raw === null) return fallback;
        if (typeof raw === 'string') return JSON.parse(raw);
        return raw;
      } catch { return fallback; }
    },
    set(key, value) {
      const encoded = JSON.stringify(value);
      if (typeof GM_setValue === 'function') GM_setValue(key, encoded);
      else localStorage.setItem(key, encoded);
    },
    remove(key) {
      try {
        if (typeof GM_deleteValue === 'function') GM_deleteValue(key);
        else localStorage.removeItem(key);
      } catch { /* Reset should continue even if storage is restricted. */ }
    },
  };

  function migrateLegacyStorage() {
    if (typeof GM_getValue !== 'function' || typeof GM_setValue !== 'function') return false;
    try {
      if (GM_getValue(STORAGE_MIGRATION_KEY, false)) return false;
      let migrated = false;
      [SETTINGS_KEY, DOWNLOAD_HISTORY_KEY].forEach(key => {
        const isolatedValue = GM_getValue(key, undefined);
        const legacyValue = localStorage.getItem(key);
        if (isolatedValue === undefined && legacyValue !== null) {
          GM_setValue(key, legacyValue);
          migrated = true;
        }
        if (legacyValue !== null) localStorage.removeItem(key);
      });
      GM_setValue(STORAGE_MIGRATION_KEY, true);
      return migrated;
    } catch { return false; }
  }

  const migratedLegacyStorage = migrateLegacyStorage();
  const storedSettings = storage.get(SETTINGS_KEY, {});
  const storedDownloadHistory = storage.get(DOWNLOAD_HISTORY_KEY, {});
  const savedSettings = storedSettings && typeof storedSettings === 'object' && !Array.isArray(storedSettings) ? storedSettings : {};
  const savedDownloadHistory = storedDownloadHistory && typeof storedDownloadHistory === 'object' && !Array.isArray(storedDownloadHistory) ? storedDownloadHistory : {};

  function normalizeDownloadHistory(history) {
    return Object.fromEntries(Object.entries(history).map(([key, rawEntry]) => {
      const entry = rawEntry && typeof rawEntry === 'object' && !Array.isArray(rawEntry) ? rawEntry : {};
      return [key, {
        filename: String(entry.filename || ''),
        size: Math.max(0, Number(entry.size || 0)),
        time: Math.max(0, Number(entry.time || 0)),
        verified: entry.verified === true,
        legacy: entry.verified !== true,
        mime: String(entry.mime || ''),
        method: String(entry.method || (entry.verified === true ? 'validated-media' : 'pre-v0.7.3')),
        container: String(entry.container || ''),
      }];
    }));
  }

  const state = {
    items: [],
    filter: ['all', 'image', 'video', 'link', 'text'].includes(savedSettings.filter) ? savedSettings.filter : 'all',
    query: '',
    compact: Boolean(savedSettings.compact),
    sort: ['thread-asc', 'thread-desc', 'author', 'host', 'type'].includes(savedSettings.sort) ? savedSettings.sort : 'thread-asc',
    groupBy: savedSettings.groupBy === 'reply' ? 'reply' : 'none',
    perPage: VIEW_PAGE_SIZES.includes(Number(savedSettings.perPage)) ? Number(savedSettings.perPage) : DEFAULT_SETTINGS.perPage,
    theme: THEME_MODES.includes(savedSettings.theme) ? savedSettings.theme : DEFAULT_SETTINGS.theme,
    filtersCollapsed: Boolean(savedSettings.filtersCollapsed),
    layoutMode: ['masonry', 'grid', 'feed'].includes(savedSettings.layoutMode) ? savedSettings.layoutMode : 'masonry',
    cardScale: Number.isFinite(Number(savedSettings.cardScale)) ? Math.min(160, Math.max(70, Math.round(Number(savedSettings.cardScale) / 5) * 5)) : 100,
    downloadConcurrency: [1, 2, 3, 4].includes(Number(savedSettings.downloadConcurrency)) ? Number(savedSettings.downloadConcurrency) : 2,
    zipPartSizeMb: [100, 300, 600, 1000].includes(Number(savedSettings.zipPartSizeMb)) ? Number(savedSettings.zipPartSizeMb) : 300,
    zipPartMaxFiles: [24, 50, 100].includes(Number(savedSettings.zipPartMaxFiles)) ? Number(savedSettings.zipPartMaxFiles) : 24,
    archiveLayout: ['flat', 'page', 'page-author', 'reply'].includes(savedSettings.archiveLayout) ? savedSettings.archiveLayout : 'page-author',
    includeManifest: savedSettings.includeManifest !== false,
    dedupeContent: savedSettings.dedupeContent !== false,
    warnLargeThreadScan: savedSettings.warnLargeThreadScan !== false,
    authorFilter: 'all',
    hostFilter: 'all',
    selectedOnly: false,
    viewPage: 1,
    viewPages: 1,
    renderedItems: [],
    threadPageCount: 1,
    sourcePage: 1,
    selectionMode: false,
    selected: new Set(),
    collapsedReplies: new Set(),
    scanning: false,
    cancelScan: false,
    scannedPages: 0,
    scanCurrentPage: 0,
    scanTotalPages: 0,
    scanFailedPages: 0,
    scanCanceling: false,
    scanController: null,
    sourceLabel: 'Current page',
    lightboxIndex: 0,
    viewerItems: [],
    viewerZoom: 1,
    viewerPanX: 0,
    viewerPanY: 0,
    viewerInfoOpen: true,
    viewerRenderToken: 0,
    viewerReturnFocus: null,
    downloading: false,
    cancelDownload: false,
    downloadProgress: { current: 0, total: 0, ok: 0, failed: 0, verification: 0, skipped: 0, duplicates: 0, zipPercent: 0, label: '' },
    downloadJobs: [],
    activeDownloadRequests: new Set(),
    downloadDetailsOpen: false,
    downloadHistory: normalizeDownloadHistory(savedDownloadHistory),
    diagnostics: {
      startedAt: Date.now(),
      migratedLegacyStorage,
      resolverFailures: [],
      downloadFailures: [],
      scanFailures: [],
      events: [],
    },
  };

  let datasetRevision = 0;
  let selectionRevision = 0;
  let visibleItemsCache = { signature: '', items: [] };

  function invalidateVisibleItems({ dataset = false, selection = false } = {}) {
    if (dataset) datasetRevision++;
    if (selection) selectionRevision++;
    visibleItemsCache = { signature: '', items: [] };
  }

  const IMAGE_EXT = /\.(?:jpe?g|png|gif|webp|avif|bmp)(?:$|[?#./])/i;
  const VIDEO_EXT = /\.(?:mp4|webm|mov|m4v)(?:$|[?#./])/i;
  const IGNORE_IMG = /(?:avatar|smilie|emoji|reaction|sprite|logo)/i;
  const TYPE_ORDER = { image: 0, video: 1, embed: 1, link: 2, text: 3 };
  const LAYOUT_MODES = Object.freeze(['masonry', 'grid', 'feed']);
  const LAYOUT_LABELS = Object.freeze({ masonry: 'Masonry', grid: 'Uniform grid', feed: 'Feed' });
  const ICON_PATHS = {
    gallery: '<rect x="3.25" y="6.75" width="14" height="12.5" rx="2.5"/><path d="M7.25 6.75V5.5a2.25 2.25 0 0 1 2.25-2.25h8A2.25 2.25 0 0 1 19.75 5.5v8.25a2.25 2.25 0 0 1-1.5 2.12"/><circle cx="7.9" cy="11" r="1.15"/><path d="m3.6 17.9 3.9-3.6a1.4 1.4 0 0 1 1.9 0l2 1.9 2.2-2a1.4 1.4 0 0 1 1.9 0l1.75 1.6"/>',
    settings: '<circle cx="12" cy="12" r="3.1"/><path d="M19.35 13.4a1.55 1.55 0 0 0 .32 1.72l.05.06a1.85 1.85 0 1 1-2.62 2.62l-.06-.06a1.55 1.55 0 0 0-1.72-.31 1.55 1.55 0 0 0-.94 1.42v.17a1.85 1.85 0 1 1-3.7 0v-.09a1.55 1.55 0 0 0-1-1.42 1.55 1.55 0 0 0-1.72.31l-.06.06a1.85 1.85 0 1 1-2.62-2.62l.06-.06a1.55 1.55 0 0 0 .31-1.72 1.55 1.55 0 0 0-1.42-.94h-.17a1.85 1.85 0 1 1 0-3.7h.09a1.55 1.55 0 0 0 1.42-1 1.55 1.55 0 0 0-.31-1.72l-.06-.06a1.85 1.85 0 1 1 2.62-2.62l.06.06a1.55 1.55 0 0 0 1.72.31h.07a1.55 1.55 0 0 0 .94-1.42v-.17a1.85 1.85 0 1 1 3.7 0v.09a1.55 1.55 0 0 0 .94 1.42 1.55 1.55 0 0 0 1.72-.31l.06-.06a1.85 1.85 0 1 1 2.62 2.62l-.06.06a1.55 1.55 0 0 0-.31 1.72v.07a1.55 1.55 0 0 0 1.42.94h.17a1.85 1.85 0 1 1 0 3.7h-.09a1.55 1.55 0 0 0-1.42.94Z"/>',
    layout: '<rect x="3.25" y="4.25" width="7.25" height="7.25" rx="1.6"/><rect x="13.5" y="4.25" width="7.25" height="4.5" rx="1.6"/><rect x="3.25" y="14.5" width="7.25" height="5.25" rx="1.6"/><rect x="13.5" y="11.75" width="7.25" height="8" rx="1.6"/>',
    keyboard: '<rect x="2.75" y="6" width="18.5" height="12" rx="2.5"/><path d="M6.5 9.75h.01M10 9.75h.01M13.5 9.75h.01M17 9.75h.01M6.5 13.9h.01M17 13.9h.01M9.5 13.9h5" stroke-width="2.1"/>',
    close: '<path d="m6.75 6.75 10.5 10.5M17.25 6.75 6.75 17.25"/>',
    download: '<path d="M12 3.75v10.5m0 0 3.75-3.75M12 14.25 8.25 10.5"/><path d="M4.25 16.25v2a2 2 0 0 0 2 2h11.5a2 2 0 0 0 2-2v-2"/>',
    select: '<rect x="3.5" y="3.5" width="17" height="17" rx="4"/><path d="m8 12.2 2.7 2.7L16.2 9.4"/>',
    image: '<rect x="3" y="4.25" width="18" height="15.5" rx="2.5"/><circle cx="8.4" cy="9.4" r="1.5"/><path d="m3.4 17.6 4.4-4.1a1.5 1.5 0 0 1 2.05 0l2.15 2 2.4-2.2a1.5 1.5 0 0 1 2.05 0l4.15 3.8"/>',
    video: '<rect x="2.75" y="5" width="18.5" height="14" rx="2.75"/><path d="M10.4 9.35a.4.4 0 0 1 .61-.34l3.9 2.65a.4.4 0 0 1 0 .68l-3.9 2.65a.4.4 0 0 1-.61-.34Z"/>',
    copy: '<rect x="8.5" y="8.5" width="11" height="11" rx="2.5"/><path d="M15.5 8.5v-2a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h2"/>',
    reset: '<path d="M20.25 12a8.25 8.25 0 1 1-2.42-5.83"/><path d="M20.25 4.5V10h-5.5"/>',
    upload: '<path d="M12 15V4.5m0 0L8.25 8.25M12 4.5l3.75 3.75"/><path d="M4.25 16.25v2a2 2 0 0 0 2 2h11.5a2 2 0 0 0 2-2v-2"/>',
    info: '<circle cx="12" cy="12" r="8.5"/><path d="M12 11.25v5"/><path d="M12 7.9h.01" stroke-width="2.2"/>',
    filter: '<path d="M4 5.25h16l-6.15 7v5.35l-3.7 1.65V12.25Z"/>',
    sliders: '<path d="M4.5 7.5h9M17.5 7.5h2M4.5 16.5h2M10.5 16.5h9"/><circle cx="15.5" cy="7.5" r="2.25"/><circle cx="8.5" cy="16.5" r="2.25"/>',
    sun: '<circle cx="12" cy="12" r="3.9"/><path d="M12 2.75v1.9M12 19.35v1.9M4.72 4.72l1.35 1.35M17.93 17.93l1.35 1.35M2.75 12h1.9M19.35 12h1.9M4.72 19.28l1.35-1.35M17.93 6.07l1.35-1.35"/>',
    moon: '<path d="M20.25 14.6A8.75 8.75 0 0 1 9.4 3.75a8.75 8.75 0 1 0 10.85 10.85Z"/>',
    palette: '<path d="M12 3.25a8.75 8.75 0 0 0 0 17.5h1.15a1.9 1.9 0 0 0 1.35-3.25l-.3-.3a1.35 1.35 0 0 1 .95-2.3h1.6a4.5 4.5 0 0 0 4.5-4.5c0-3.95-4.15-7.15-9.25-7.15Z"/><circle cx="7.9" cy="10.4" r="1.05"/><circle cx="10.4" cy="7.1" r="1.05"/><circle cx="14" cy="7.1" r="1.05"/><circle cx="16.6" cy="10.4" r="1.05"/>',
    chevron: '<path d="m6.75 9.75 5.25 5 5.25-5"/>',
    more: '<path d="M6 12h.01M12 12h.01M18 12h.01" stroke-width="2.6"/>',
    scan: '<path d="M3.75 8V5.75a2 2 0 0 1 2-2H8M16 3.75h2.25a2 2 0 0 1 2 2V8M20.25 16v2.25a2 2 0 0 1-2 2H16M8 20.25H5.75a2 2 0 0 1-2-2V16"/><path d="M7.5 12h9"/>',
    masonry: '<rect x="3.5" y="3.5" width="7" height="10.25" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="6" rx="1.6"/><rect x="3.5" y="16" width="7" height="4.5" rx="1.6"/><rect x="13.5" y="11.75" width="7" height="8.75" rx="1.6"/>',
    grid: '<rect x="3.5" y="3.5" width="7.25" height="7.25" rx="1.6"/><rect x="13.25" y="3.5" width="7.25" height="7.25" rx="1.6"/><rect x="3.5" y="13.25" width="7.25" height="7.25" rx="1.6"/><rect x="13.25" y="13.25" width="7.25" height="7.25" rx="1.6"/>',
    feed: '<rect x="3.5" y="4.25" width="6.75" height="6" rx="1.5"/><path d="M13 6h7.5M13 8.9h5.25"/><rect x="3.5" y="13.75" width="6.75" height="6" rx="1.5"/><path d="M13 15.5h7.5M13 18.4h5.25"/>',
    zoomIn: '<circle cx="10.75" cy="10.75" r="6.5"/><path d="m15.6 15.6 4.65 4.65M10.75 7.9v5.7M7.9 10.75h5.7"/>',
    zoomOut: '<circle cx="10.75" cy="10.75" r="6.5"/><path d="m15.6 15.6 4.65 4.65M7.9 10.75h5.7"/>',
    fit: '<path d="M9.75 4.25H4.25V9.75M14.25 4.25h5.5V9.75M9.75 19.75H4.25V14.25M14.25 19.75h5.5V14.25"/><rect x="8.75" y="8.75" width="6.5" height="6.5" rx="1.4"/>',
    fullscreen: '<path d="M9 3.75H5.75a2 2 0 0 0-2 2V9M15 3.75h3.25a2 2 0 0 1 2 2V9M9 20.25H5.75a2 2 0 0 1-2-2V15M15 20.25h3.25a2 2 0 0 0 2-2V15"/>',
    external: '<path d="M14.25 3.75h6v6M20.25 3.75 12 12"/><path d="M18.5 13.5v4.75a2 2 0 0 1-2 2h-11a2 2 0 0 1-2-2v-11a2 2 0 0 1 2-2h4.75"/>',
    play: '<circle cx="12" cy="12" r="8.5"/><path d="M10.4 9.15a.4.4 0 0 1 .61-.34l4.35 2.85a.4.4 0 0 1 0 .68l-4.35 2.85a.4.4 0 0 1-.61-.34Z"/>',
    resolution: '<rect x="2.75" y="4.5" width="18.5" height="12.5" rx="2.5"/><path d="M8.5 20.25h7M12 17v3.25M6.75 8.25h2.75V11M17.25 8.25H14.5V11"/>',
    search: '<circle cx="10.75" cy="10.75" r="6.75"/><path d="m15.75 15.75 4.5 4.5"/>',
    link: '<path d="M10.1 13.4a4.6 4.6 0 0 0 6.85.5l2.05-2.05a4.6 4.6 0 0 0-6.5-6.5l-1.2 1.2"/><path d="M13.9 10.6a4.6 4.6 0 0 0-6.85-.5L5 12.15a4.6 4.6 0 0 0 6.5 6.5l1.2-1.2"/>',
    text: '<path d="M4.75 6.25V4.75h14.5v1.5M12 4.75v14.5M9 19.25h6"/>',
    page: '<path d="M6 3.75h7.5l4.5 4.5v12a1.5 1.5 0 0 1-1.5 1.5H6a1.5 1.5 0 0 1-1.5-1.5V5.25A1.5 1.5 0 0 1 6 3.75Z"/><path d="M13.5 3.75v4.5H18M8.25 13h7.5M8.25 16.5h5.25"/>',
    archive: '<rect x="3.25" y="7.75" width="17.5" height="12.5" rx="2.25"/><path d="M3.25 11.5h17.5"/><path d="M5.75 7.75V5.5a1.5 1.5 0 0 1 1.5-1.5h9.5a1.5 1.5 0 0 1 1.5 1.5v2.25"/><path d="M10 15.25h4"/>',
    history: '<path d="M3.75 12a8.25 8.25 0 1 0 2.42-5.83L3.75 8.5"/><path d="M3.75 3.75V8.5h4.75"/><path d="M12 7.75V12l2.9 1.75"/>',
    clear: '<rect x="3.75" y="3.75" width="16.5" height="16.5" rx="4" stroke-dasharray="3.2 2.4"/><path d="m9.25 9.25 5.5 5.5m0-5.5-5.5 5.5"/>',
    check: '<circle cx="12" cy="12" r="8.5"/><path d="m8.25 12.2 2.7 2.7 4.8-5.8"/>',
    trash: '<path d="M4.25 6.75h15.5"/><path d="M9.25 6.75V4.9a1.15 1.15 0 0 1 1.15-1.15h3.2A1.15 1.15 0 0 1 14.75 4.9v1.85"/><path d="M6.75 6.75 7.7 19.1a1.5 1.5 0 0 0 1.5 1.4h5.6a1.5 1.5 0 0 0 1.5-1.4l.95-12.35"/><path d="M10.5 10.5v6M13.5 10.5v6"/>',
    previous: '<path d="m14.5 5.75-6.25 6.25 6.25 6.25"/>',
    next: '<path d="m9.5 5.75 6.25 6.25-6.25 6.25"/>',
    up: '<path d="m6.75 11 5.25-5.25L17.25 11"/><path d="M12 6v12.25"/>',
  };

  function icon(name, className = '') {
    const paths = ICON_PATHS[name] || ICON_PATHS.info;
    return `<svg class="scg-icon ${escapeHtml(className)}" data-scg-icon="${escapeHtml(name)}" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
  }

  function iconWell(name, className = '') {
    return `<i class="scg-icon-well ${escapeHtml(className)}" aria-hidden="true">${icon(name)}</i>`;
  }

  function setIconButton(button, iconName, label) {
    if (!button) return;
    const wellClass = ['download', 'archive'].includes(iconName) ? 'scg-icon-well-primary' : (iconName === 'close' ? 'scg-icon-well-danger' : '');
    button.innerHTML = `${iconWell(iconName, wellClass)}<span>${escapeHtml(label)}</span>`;
    button.setAttribute('aria-label', label);
  }

  function setIconOnlyButton(button, iconName, label, wellClass = '') {
    if (!button) return;
    button.innerHTML = iconWell(iconName, wellClass);
    button.setAttribute('aria-label', label);
    if (button.hasAttribute('data-tooltip')) button.dataset.tooltip = label;
  }

  function recordDiagnostic(category, stage, error, item = null) {
    const bucket = state.diagnostics[category];
    if (!Array.isArray(bucket)) return;
    const entry = {
      time: new Date().toISOString(),
      stage,
      message: String(error?.message || error || 'Unknown error').slice(0, 300),
      host: item ? hostOf(item.originalLink || item.url || '') : '',
      type: item?.type || '',
      page: Number(item?.pageNumber || 0),
    };
    bucket.push(entry);
    if (bucket.length > 100) bucket.splice(0, bucket.length - 100);
    if (document.querySelector(`#${APP_ID} .scg-settings-panel.open`)) refreshSettingsPanel();
  }

  const absoluteUrl = (value, base = location.href) => {
    if (!value) return '';
    try {
      const url = new URL(value, base);
      return /^(?:https?):$/.test(url.protocol) ? url.href : '';
    } catch { return ''; }
  };

  const threadBaseUrl = (rawUrl = location.href) => {
    try {
      const url = new URL(rawUrl, location.href);
      url.pathname = url.pathname.replace(/\/page-\d+\/?$/i, '').replace(/\/$/, '');
      url.search = '';
      url.hash = '';
      return url.href.replace(/\/$/, '');
    } catch { return String(rawUrl).replace(/\/page-\d+(?:[/?#].*)?$/i, '').replace(/[?#].*$/, '').replace(/\/$/, ''); }
  };

  const pageNumberFromUrl = (rawUrl = location.href) => {
    try { return Number(new URL(rawUrl, location.href).pathname.match(/\/page-(\d+)\/?$/i)?.[1] || 1); }
    catch { return Number(String(rawUrl).match(/\/page-(\d+)/i)?.[1] || 1); }
  };

  const threadPageUrl = page => page > 1 ? `${threadBaseUrl()}/page-${page}` : threadBaseUrl();

  function slugify(value, maxLength = 44) {
    return String(value || '')
      .normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '')
      .replace(/-+/g, '-').slice(0, maxLength).replace(/-+$/g, '')
      .toLowerCase();
  }

  function threadIdentity(rawUrl = location.href) {
    try {
      const url = new URL(rawUrl, location.href);
      const segment = decodeURIComponent(url.pathname.match(/\/threads\/([^/]+)/i)?.[1] || 'thread');
      const match = segment.match(/^(.*?)(?:\.(\d+))?$/);
      return { slug: slugify(match?.[1] || segment) || 'thread', id: match?.[2] || '' };
    } catch { return { slug: 'thread', id: '' }; }
  }

  function threadTitle() {
    const heading = document.querySelector('h1.p-title-value, .p-title-value, h1');
    if (heading) {
      const clone = heading.cloneNode(true);
      clone.querySelectorAll('.label, .prefix, .js-prefixTitle, script, style').forEach(node => node.remove());
      const title = (clone.textContent || '').replace(/\s+/g, ' ').trim();
      if (title) return title;
    }
    const identity = threadIdentity();
    return identity.slug.split('-').filter(Boolean).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') || 'Thread gallery';
  }

  function threadFilePrefix(rawUrl = location.href) {
    const thread = threadIdentity(rawUrl);
    return [thread.slug, thread.id].filter(Boolean).join('-').slice(0, 58) || 'simpcity-thread';
  }

  function detectThreadPageCount(doc, pageUrl = location.href) {
    const values = [pageNumberFromUrl(pageUrl)];
    doc.querySelectorAll('.pageNav-page a, .pageNav-main a, .pageNavSimple a, a.pageNav-jump').forEach(anchor => {
      const textNumber = Number((anchor.textContent || '').trim());
      const hrefNumber = Number(anchor.getAttribute('href')?.match(/\/page-(\d+)/i)?.[1] || 0);
      if (Number.isFinite(textNumber) && textNumber > 0) values.push(textNumber);
      if (hrefNumber > 0) values.push(hrefNumber);
    });
    const lastText = doc.querySelector('.pageNavSimple-el--last, .pageNav-page--last')?.textContent || '';
    const lastNumber = Number(lastText.match(/\d+/g)?.pop() || 0);
    if (lastNumber > 0) values.push(lastNumber);
    return Math.max(1, ...values);
  }

  function updateThreadPageInfo(doc = document, pageUrl = location.href) {
    state.threadPageCount = Math.max(state.threadPageCount, detectThreadPageCount(doc, pageUrl));
  }

  const bestImageUrl = (img, base) => {
    const srcset = img.getAttribute('srcset');
    const largestSrcset = srcset?.split(',').map(part => {
      const match = part.trim().match(/^(.*?)\s+(\d+)(?:w|x)$/);
      return match ? { url: match[1], size: Number(match[2]) } : null;
    }).filter(Boolean).sort((a, b) => b.size - a.size)[0]?.url;
    const raw = img.getAttribute('data-full-url') || img.getAttribute('data-original') ||
      img.getAttribute('data-url') || largestSrcset || img.getAttribute('data-src') ||
      img.getAttribute('data-lazy-src') ||
      img.currentSrc || img.getAttribute('src') || '';
    return absoluteUrl(raw, base);
  };

  const unwrapSimpcityRedirect = (rawUrl) => {
    try {
      const url = new URL(rawUrl, location.href);
      if (url.origin === location.origin && url.pathname === '/redirect/') {
        const target = url.searchParams.get('to');
        if (target) return absoluteUrl(atob(target), location.href);
      }
      return absoluteUrl(url.href);
    } catch { return ''; }
  };

  const normalizeSourceUrl = (rawUrl, base = location.href) => unwrapSimpcityRedirect(absoluteUrl(rawUrl, base));

  const HOST_REGISTRY = Object.freeze([
    Object.freeze({
      id: 'goonbox', media: 'image', imagePage: true,
      matches: host => /(?:^|\.)goonbox\.cr$/i.test(host),
      imageId: url => url.pathname.match(/^\/(?:img|image|view)\/([\w-]+)/i)?.[1] || '',
      thumbnailToCandidate: thumb => thumb.replace(/\.md(?=\.[a-z0-9]+(?:$|[?#]))/i, ''),
    }),
    Object.freeze({
      id: 'pixhost', media: 'image', imagePage: true,
      matches: host => /^(?:t\d+|img\d+|www)\.pixhost\.to$|^pixhost\.to$/i.test(host),
      thumbnailToCandidate: thumb => thumb
        .replace(/:\/\/t(\d+)\.pixhost\.to\/thumbs\//i, '://img$1.pixhost.to/images/')
        .replace(/\/thumbs\/(\d+)\//i, '/images/$1/'),
    }),
    Object.freeze({
      id: 'imgbox', media: 'image', imagePage: true,
      matches: host => /(?:^|\.)imgbox\.com$/i.test(host),
    }),
    Object.freeze({
      id: 'simp-image-cdn', media: 'image',
      matches: host => /^(?:jpg6\.su|jpg7\.cr|simp[46]\.selti-delivery\.ru)$/i.test(host),
      thumbnailToCandidate: thumb => thumb.replace(/\.md(?=\.[a-z0-9]+(?:$|[?#]))/i, ''),
    }),
    Object.freeze({
      id: 'turbo', media: 'video',
      matches: host => /(?:^|\.)(?:turbo\.cr|turbovid\.cr)$/i.test(host),
      mediaId: url => url.pathname.match(/^\/(?:v|embed|d)\/([^/?]+)/i)?.[1] || '',
      embed: url => {
        const id = url.pathname.match(/^\/(?:v|embed|d)\/([^/?]+)/i)?.[1];
        return id ? `https://turbo.cr/embed/${encodeURIComponent(id)}` : url.href;
      },
    }),
    Object.freeze({
      id: 'youtube', media: 'video',
      matches: host => /(?:^|\.)(?:youtube\.com|youtu\.be)$/i.test(host),
      embed: url => {
        const id = url.hostname.replace(/^www\./, '') === 'youtu.be'
          ? url.pathname.split('/').filter(Boolean)[0]
          : url.searchParams.get('v') || url.pathname.match(/\/(?:shorts|embed)\/([^/?]+)/)?.[1];
        return id ? `https://www.youtube.com/embed/${encodeURIComponent(id)}` : url.href;
      },
    }),
    Object.freeze({
      id: 'vimeo', media: 'video',
      matches: host => /(?:^|\.)vimeo\.com$/i.test(host),
      embed: url => {
        const id = url.pathname.match(/\/(\d+)/)?.[1];
        return id ? `https://player.vimeo.com/video/${id}` : url.href;
      },
    }),
    Object.freeze({
      id: 'streamable', media: 'video',
      matches: host => /(?:^|\.)streamable\.com$/i.test(host),
      embed: url => {
        const id = url.pathname.split('/').filter(Boolean).pop();
        return id ? `https://streamable.com/e/${encodeURIComponent(id)}` : url.href;
      },
    }),
    Object.freeze({
      id: 'redgifs', media: 'video',
      matches: host => /(?:^|\.)redgifs\.com$/i.test(host),
      embed: url => {
        const id = url.pathname.split('/').filter(Boolean).pop();
        return id ? `https://www.redgifs.com/ifr/${encodeURIComponent(id)}` : url.href;
      },
    }),
    Object.freeze({
      id: 'dood', media: 'video',
      matches: host => /(?:^|\.)dood[^.]*\./i.test(host),
      embed: url => { url.pathname = url.pathname.replace(/^\/d\//, '/e/'); return url.href; },
    }),
    Object.freeze({
      id: 'generic-video-host', media: 'video',
      matches: host => /(?:^|\.)(?:voe\.|gofile\.io$|cyberfile\.|pixeldrain\.com$|bunkr\.|saint2\.|saint\.to$|imgur\.com$|mega\.nz$)/i.test(host),
    }),
  ]);

  function hostDefinition(rawUrl) {
    try {
      const url = new URL(normalizeSourceUrl(rawUrl));
      return HOST_REGISTRY.find(definition => definition.matches(url.hostname.replace(/^www\./, ''))) || null;
    } catch { return null; }
  }

  function hostMediaId(rawUrl) {
    try {
      const normalized = normalizeSourceUrl(rawUrl);
      const url = new URL(normalized);
      return hostDefinition(normalized)?.mediaId?.(url) || '';
    } catch { return ''; }
  }

  function isKnownVideoHost(rawUrl) {
    return hostDefinition(rawUrl)?.media === 'video';
  }

  function isImagePageHost(rawUrl) {
    return hostDefinition(rawUrl)?.imagePage === true;
  }

  function imagePreviewCandidate(thumb, originalLink = '') {
    if (!thumb) return { status: 'failed', url: '', method: 'missing-thumbnail' };
    const definition = hostDefinition(originalLink) || hostDefinition(thumb);
    const transformed = definition?.thumbnailToCandidate?.(thumb) || thumb.replace(/([/_-])thumb(?:nail)?([/_-])/i, '$1$2');
    if (transformed && transformed !== thumb) return { status: 'heuristic', url: transformed, method: `${definition?.id || 'generic'}-thumbnail-rewrite` };
    return { status: 'heuristic', url: thumb, method: 'thumbnail' };
  }

  const isMediaItem = item => item?.type === 'image' || item?.type === 'video' || item?.type === 'embed';

  function isDownloadableMedia(item) {
    if (!isMediaItem(item)) return false;
    const candidate = item.resolution?.candidate;
    if (candidate?.status === 'failed' || (item.resolution?.attempted && !candidate?.url)) return false;
    if (item.type === 'image') return Boolean(candidate?.url || item.url || item.thumb);
    if (item.type === 'video') return Boolean((candidate?.url && VIDEO_EXT.test(candidate.url)) || VIDEO_EXT.test(item.url || ''));
    return Boolean(candidate?.status === 'confirmed' && candidate.url) || hostDefinition(item.url)?.id === 'turbo';
  }

  function downloadHistoryKey(item) {
    const base = threadBaseUrl(item?.pageUrl || item?.postUrl || location.href);
    return `${base}|${item?.selectionKey || itemDedupeKey(item)}`;
  }

  function downloadHistoryEntry(item) {
    return item ? state.downloadHistory[downloadHistoryKey(item)] || null : null;
  }

  function wasDownloaded(item) {
    return downloadHistoryEntry(item)?.verified === true;
  }

  function hasLegacyDownload(item) {
    const entry = downloadHistoryEntry(item);
    return Boolean(entry && entry.verified !== true);
  }

  function downloadHistoryCounts() {
    return Object.values(state.downloadHistory).reduce((counts, entry) => {
      if (entry?.verified === true) counts.verified++;
      else counts.legacy++;
      return counts;
    }, { verified: 0, legacy: 0 });
  }

  function persistDownloadHistory() {
    try {
      const entries = Object.entries(state.downloadHistory)
        .sort((a, b) => Number(b[1]?.time || 0) - Number(a[1]?.time || 0))
        .slice(0, 5000);
      state.downloadHistory = Object.fromEntries(entries);
      storage.set(DOWNLOAD_HISTORY_KEY, state.downloadHistory);
    } catch { /* Downloading must still work if local storage is unavailable. */ }
  }

  function rememberDownloaded(item, filename, validation, deferPersist = false, container = '') {
    if (!validation?.verified) return false;
    state.downloadHistory[downloadHistoryKey(item)] = {
      filename,
      size: Math.max(0, Number(validation.size || validation.blob?.size || 0)),
      time: Date.now(),
      verified: true,
      legacy: false,
      mime: String(validation.mime || ''),
      method: String(validation.method || 'validated-media'),
      container: String(container || ''),
    };
    if (!deferPersist) {
      persistDownloadHistory();
      updateDownloadedIndicators();
    }
    return true;
  }

  function persistSettings() {
    try {
      storage.set(SETTINGS_KEY, {
        filter: state.filter,
        compact: state.compact,
        sort: state.sort,
        groupBy: state.groupBy,
        perPage: state.perPage,
        theme: state.theme,
        filtersCollapsed: state.filtersCollapsed,
        layoutMode: state.layoutMode,
        cardScale: state.cardScale,
        downloadConcurrency: state.downloadConcurrency,
        zipPartSizeMb: state.zipPartSizeMb,
        zipPartMaxFiles: state.zipPartMaxFiles,
        archiveLayout: state.archiveLayout,
        includeManifest: state.includeManifest,
        dedupeContent: state.dedupeContent,
        warnLargeThreadScan: state.warnLargeThreadScan,
      });
    }
    catch { /* Private browsing or storage restrictions should not break the gallery. */ }
  }

  function refreshThreadHeader() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    const title = app.querySelector('[data-thread-title]');
    const meta = app.querySelector('[data-thread-meta]');
    const source = app.querySelector('[data-source-summary]');
    if (title) {
      title.textContent = threadTitle();
      title.title = threadTitle();
    }
    if (meta) {
      const threadWide = /thread scan|entire thread/i.test(state.sourceLabel);
      meta.textContent = threadWide
        ? `${state.scanning ? 'Scanning' : 'Indexed'} ${state.scannedPages || 0} of ${state.threadPageCount} pages`
        : `Page ${state.sourcePage || pageNumberFromUrl()} of ${state.threadPageCount}`;
    }
    if (source) source.textContent = `${state.sourceLabel} · ${state.items.length} indexed`;
  }

  function applyShellState() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    app.dataset.theme = state.theme;
    app.dataset.layout = state.layoutMode;
    const cardScale = Math.min(160, Math.max(70, Number(state.cardScale) || 100));
    const scale = cardScale / 100;
    app.style.setProperty('--scg-masonry-width', `${Math.round(280 * scale)}px`);
    app.style.setProperty('--scg-masonry-compact-width', `${Math.round(195 * scale)}px`);
    app.style.setProperty('--scg-group-width', `${Math.round(250 * scale)}px`);
    app.style.setProperty('--scg-group-compact-width', `${Math.round(180 * scale)}px`);
    app.style.setProperty('--scg-grid-min', `${Math.round(250 * scale)}px`);
    app.style.setProperty('--scg-grid-compact-min', `${Math.round(180 * scale)}px`);
    app.style.setProperty('--scg-grid-height', `${Math.round(386 * scale)}px`);
    app.style.setProperty('--scg-grid-compact-height', `${Math.round(312 * scale)}px`);
    app.classList.toggle('compact', state.compact);
    app.classList.toggle('filters-collapsed', state.filtersCollapsed);
    const toast = document.getElementById('scg-gallery-toast');
    if (toast) toast.dataset.theme = state.theme;
    const launch = document.getElementById('scg-launch');
    if (launch) launch.dataset.theme = state.theme;
    const filterToggle = app.querySelector('[data-action="filters-toggle"]');
    if (filterToggle) {
      setIconOnlyButton(filterToggle, 'chevron', state.filtersCollapsed ? 'Show filters and view options' : 'Hide filters and view options');
      filterToggle.setAttribute('aria-expanded', String(!state.filtersCollapsed));
      filterToggle.classList.toggle('active', !state.filtersCollapsed);
    }
    setIconButton(app.querySelector('[data-action="density"]'), 'layout', state.compact ? 'Comfortable cards' : 'Compact cards');
    const themeIndex = Math.max(0, THEME_MODES.indexOf(state.theme));
    const nextTheme = THEME_MODES[(themeIndex + 1) % THEME_MODES.length];
    const themeButton = app.querySelector('[data-action="theme"]');
    setIconButton(themeButton, 'palette', `Theme: ${THEME_LABELS[state.theme]}`);
    if (themeButton) themeButton.title = `Current theme: ${THEME_LABELS[state.theme]}. Activate for ${THEME_LABELS[nextTheme]}.`;
    app.querySelectorAll('[data-layout-mode]').forEach(button => {
      const active = button.dataset.layoutMode === state.layoutMode;
      button.classList.toggle('active', active);
      button.setAttribute('aria-pressed', String(active));
    });
    refreshRefineTrigger();
    const themeSelect = app.querySelector('[data-setting-theme]');
    const densitySelect = app.querySelector('[data-setting-density]');
    const filterCheck = app.querySelector('[data-setting-filters-collapsed]');
    const layoutSelect = app.querySelector('[data-setting-layout]');
    const sizeSliders = app.querySelectorAll('[data-card-scale]');
    const sizeLabels = app.querySelectorAll('[data-card-scale-value]');
    if (themeSelect) themeSelect.value = state.theme;
    if (densitySelect) densitySelect.value = state.compact ? 'compact' : 'comfortable';
    sizeSliders.forEach(slider => {
      slider.value = String(cardScale);
      slider.disabled = state.layoutMode === 'feed';
      slider.setAttribute('aria-valuetext', `${cardScale}%${state.layoutMode === 'feed' ? ', unavailable in feed layout' : ''}`);
    });
    sizeLabels.forEach(label => { label.textContent = state.layoutMode === 'feed' ? 'Feed' : `${cardScale}%`; });
    if (filterCheck) filterCheck.checked = state.filtersCollapsed;
    if (layoutSelect) layoutSelect.value = state.layoutMode;
    refreshThreadHeader();
  }

  // Progressive disclosure: the Refine popover advertises how much it is hiding.
  function activeRefinements() {
    return [
      state.sort !== DEFAULT_SETTINGS.sort,
      state.authorFilter && state.authorFilter !== 'all',
      state.hostFilter && state.hostFilter !== 'all',
      state.groupBy !== DEFAULT_SETTINGS.groupBy,
      Boolean(state.selectedOnly),
    ].filter(Boolean).length;
  }

  function refreshRefineTrigger() {
    const app = document.getElementById(APP_ID);
    const trigger = app?.querySelector('[data-action="refine-toggle"]');
    if (!trigger) return;
    const count = activeRefinements();
    const badge = trigger.querySelector('[data-refine-count]');
    trigger.classList.toggle('active', count > 0);
    trigger.setAttribute('aria-label', count ? `Refine results, ${count} active` : 'Refine results');
    if (badge) {
      badge.textContent = String(count);
      badge.toggleAttribute('hidden', count === 0);
    }
  }

  function closeDisclosures() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    app.querySelectorAll('.scg-menu.open, .scg-refine-popover.open').forEach(panel => panel.classList.remove('open'));
    app.querySelectorAll('[data-action="overflow"], [data-action="refine-toggle"]').forEach(button => {
      button.setAttribute('aria-expanded', 'false');
    });
  }

  function toggleDisclosure(trigger, panel) {
    if (!trigger || !panel) return;
    const shouldOpen = !panel.classList.contains('open');
    closeDisclosures();
    if (!shouldOpen) return;
    panel.classList.add('open');
    trigger.setAttribute('aria-expanded', 'true');
    panel.querySelector('select:not(:disabled), button:not(:disabled), input:not(:disabled)')?.focus({ preventScroll: true });
  }

  function focusableWithin(container) {
    return [...container.querySelectorAll('button:not(:disabled),a[href],select:not(:disabled),input:not(:disabled),textarea:not(:disabled),video[controls],[tabindex]:not([tabindex="-1"])')]
      .filter(node => node.offsetParent !== null);
  }

  // Boundary-only containment: native tabbing is left intact inside the dialog.
  function containFocus(container, event) {
    const focusable = focusableWithin(container);
    if (!focusable.length) return;
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    const inside = container.contains(document.activeElement);
    if (event.shiftKey && (!inside || document.activeElement === first)) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && (!inside || document.activeElement === last)) {
      event.preventDefault();
      first.focus();
    }
  }

  const prefersReducedMotion = () => {
    try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    catch { return false; }
  };

  function cycleLayoutMode(direction = 1, announce = true) {
    const current = Math.max(0, LAYOUT_MODES.indexOf(state.layoutMode));
    const next = LAYOUT_MODES[(current + direction + LAYOUT_MODES.length) % LAYOUT_MODES.length];
    commitState({ layoutMode: next }, { persist: true, shellOnly: true });
    if (announce) notify(`${LAYOUT_LABELS[next]} layout`);
  }

  function commitState(patch, options = {}) {
    const changes = typeof patch === 'function' ? patch(state) : patch;
    if (changes && typeof changes === 'object') Object.assign(state, changes);
    if (options.persist) persistSettings();
    if (options.shellOnly) {
      applyShellState();
      if (document.querySelector(`#${APP_ID} .scg-settings-panel.open`)) refreshSettingsPanel();
    } else if (options.render !== false) render();
    else {
      updateSelectionUi();
      updateDownloadUi();
    }
  }

  const cleanText = (body) => {
    const clone = body.cloneNode(true);
    clone.querySelectorAll('img, video, audio, iframe, svg, script, style, .fa, [class*="fa-"], .bbCodeBlock--quote, .message-signature, .js-unfurl-fauxBlockLink').forEach(el => el.remove());
    return (clone.innerText || clone.textContent || '').replace(/\s+/g, ' ').trim();
  };

  const postParts = (root) => {
    const article = root.matches?.('article.message, .message') ? root : root.closest?.('article.message, .message');
    const body = root.querySelector?.('.message-body .bbWrapper, .message-content .bbWrapper, .bbWrapper');
    const author = (root.querySelector?.('.message-name, [itemprop="name"], .username')?.textContent || 'Unknown').trim();
    const postAnchor = root.querySelector?.('.message-attribution-main a[href*="/post-"], a[href*="#post-"], a.message-attribution-main');
    const postUrl = absoluteUrl(postAnchor?.getAttribute('href') || location.href);
    const postId = article?.getAttribute('data-content') || article?.id || postUrl.split('#').pop() || '';
    const numberAnchor = root.querySelector?.('.message-attribution-opposite a, a[href*="#post-"]');
    const postNumber = (numberAnchor?.textContent || '').match(/#(\d+)/)?.[1] || '';
    return { article, body, author, postUrl, postId, postNumber };
  };

  function extractFromDocument(doc, pageUrl) {
    const results = [];
    const posts = [...doc.querySelectorAll('article.message, .message--post')];
    const roots = posts.length ? posts : [...doc.querySelectorAll('.message')];

    roots.forEach((root, postIndex) => {
      const { body, author, postUrl, postId, postNumber } = postParts(root);
      if (!body) return;
      const content = body.cloneNode(true);
      content.querySelectorAll('.bbCodeBlock--quote, blockquote.bbCodeBlock, [data-quote], .message-signature').forEach(node => node.remove());
      const base = pageUrl || doc.baseURI;
      const common = {
        author,
        postUrl: absoluteUrl(postUrl, base),
        postId,
        postNumber,
        pageUrl: base,
        pageNumber: pageNumberFromUrl(base),
        postIndex,
      };

      content.querySelectorAll('img').forEach((img, mediaIndex) => {
        const linked = img.closest('a[href]');
        const classes = `${img.className || ''} ${linked?.className || ''}`;
        if (IGNORE_IMG.test(classes)) return;
        const thumb = bestImageUrl(img, base);
        const originalLink = normalizeSourceUrl(linked?.getAttribute('href'), base);
        const lightboxHint = /(?:js-lbImage|lbContainer|attachment)/i.test(classes) ||
          /lightbox/i.test(linked?.getAttribute('data-xf-init') || '');
        const linkedDirect = Boolean(originalLink && !isImagePageHost(originalLink) &&
          (IMAGE_EXT.test(originalLink) || /\/attachments\//i.test(originalLink)));
        const preview = imagePreviewCandidate(thumb, originalLink);
        const seed = linkedDirect
          ? { status: 'confirmed', url: originalLink, method: 'direct-image-link', mediaType: 'image' }
          : { ...preview, mediaType: 'image' };
        const src = seed.url;
        if (!src || /(?:avatar|smilies|emoji|reactions)\//i.test(src)) return;
        results.push({
          type: 'image', url: src, thumb, originalLink, lightboxHint,
          mediaKey: `${String(postId).replace(/[^a-z0-9_-]/gi, '')}-${postIndex}-${mediaIndex}`,
          resolution: { candidate: seed },
          caption: img.getAttribute('alt') || img.getAttribute('title') || '', mediaIndex, ...common,
        });
      });

      content.querySelectorAll('video').forEach((video, mediaIndex) => {
        const source = video.querySelector('source');
        const src = absoluteUrl(video.currentSrc || video.getAttribute('data-src') || video.getAttribute('src') ||
          source?.getAttribute('data-src') || source?.getAttribute('src'), base);
        if (!src) return;
        results.push({
          type: 'video', url: src, poster: absoluteUrl(video.getAttribute('data-poster') || video.getAttribute('poster'), base),
          resolution: { candidate: { status: 'confirmed', url: src, method: 'video-element', mediaType: 'video' } },
          mediaIndex, ...common,
        });
      });

      content.querySelectorAll('iframe[src], iframe[data-src]').forEach((frame, mediaIndex) => {
        const src = normalizeSourceUrl(frame.getAttribute('data-src') || frame.getAttribute('src'), base);
        if (!src) return;
        results.push({
          type: 'embed', url: src, title: frame.getAttribute('title') || 'Embedded video',
          wasFrame: true, mediaIndex, ...common,
        });
      });

      content.querySelectorAll('a[href]').forEach((anchor, mediaIndex) => {
        const href = normalizeSourceUrl(anchor.getAttribute('href'), base);
        if (!href || anchor.querySelector('img')) return;
        if (/\/threads\/|\/posts\/|#post-|\/members\//i.test(href)) return;
        const label = (anchor.textContent || href).replace(/\s+/g, ' ').trim();
        if (VIDEO_EXT.test(href) || isKnownVideoHost(href)) {
          const directVideo = VIDEO_EXT.test(href);
          results.push({
            type: directVideo ? 'video' : 'embed', url: href, title: label,
            resolution: directVideo ? { candidate: { status: 'confirmed', url: href, method: 'direct-video-link', mediaType: 'video' } } : {},
            mediaIndex, ...common,
          });
        } else {
          results.push({ type: 'link', url: href, title: label, mediaIndex, ...common });
        }
      });

      const text = cleanText(content);
      if (text) results.push({ type: 'text', text, ...common });
    });
    return results;
  }

  function itemDedupeKey(item) {
    const rawUrl = item.type === 'embed' ? embedUrl(item.url) : item.url;
    const normalizedUrl = rawUrl ? rawUrl.replace(/([?&])width=\d+|([?&])height=\d+/g, '$1').replace(/[?&]$/, '') : '';
    return item.type === 'text'
      ? `text|${item.postId}|${item.text.slice(0, 300)}`
      : `${item.type === 'embed' ? 'videoish' : item.type}|${normalizedUrl}`;
  }

  function appendUniqueItems(target, additions, seen = new Set(target.map(itemDedupeKey))) {
    additions.forEach(item => {
      const key = itemDedupeKey(item);
      if (seen.has(key)) return;
      seen.add(key);
      item.selectionKey = key;
      item.searchText = `${item.author || ''} ${item.title || ''} ${item.caption || ''} ${item.text || ''} ${item.url || ''} ${item.originalLink || ''} ${item.postNumber || ''} ${item.pageNumber || ''}`.toLowerCase();
      target.push(item);
    });
    return target;
  }

  function dedupe(items) {
    return appendUniqueItems([], items, new Set());
  }

  function escapeHtml(value = '') {
    return String(value).replace(/[&<>'"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[c]));
  }

  function hostOf(url) {
    try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return 'link'; }
  }

  function requestText(url, options = {}) {
    const headers = options.headers || {};
    const timeout = options.timeout || 18000;
    return new Promise((resolve, reject) => {
      if (typeof GM_xmlhttpRequest !== 'function') {
        const sameOrigin = (() => { try { return new URL(url).origin === location.origin; } catch { return false; } })();
        fetch(url, {
          credentials: sameOrigin ? 'include' : 'omit',
          headers,
          referrer: headers.Referer || headers.referer || undefined,
        })
          .then(response => response.ok ? response.text() : Promise.reject(new Error(`HTTP ${response.status}`)))
          .then(resolve, reject);
        return;
      }
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        headers,
        timeout,
        onload: response => response.status >= 200 && response.status < 400
          ? resolve(response.responseText)
          : reject(new Error(`HTTP ${response.status}`)),
        onerror: () => reject(new Error('Media host request failed')),
        ontimeout: () => reject(new Error('Media host request timed out')),
      });
    });
  }

  function embedUrl(rawUrl) {
    try {
      const normalized = normalizeSourceUrl(rawUrl);
      const url = new URL(normalized);
      const embedded = hostDefinition(normalized)?.embed?.(url) || url.href;
      return absoluteUrl(embedded);
    } catch { return ''; }
  }

  function iframeHtml(item) {
    const src = embedUrl(item.url);
    if (!src) return '<div class="scg-loading">This embedded media URL was blocked because it was not HTTP(S).</div>';
    return `<iframe loading="lazy" src="${escapeHtml(src)}" title="${escapeHtml(item.title || 'Embedded video')}" allow="autoplay; fullscreen; picture-in-picture" sandbox="allow-scripts allow-same-origin allow-forms allow-presentation allow-popups" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>`;
  }

  function mediaFromHtml(html, base, kind) {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    const selectors = kind === 'image'
      ? ['#image[src]', '#img[src]', "meta[property='og:image']", "meta[name='twitter:image']", "link[rel='image_src']"]
      : ["meta[property='og:video:secure_url']", "meta[property='og:video:url']", "meta[property='og:video']", 'video[src]', 'video source[src]'];
    for (const selector of selectors) {
      const node = doc.querySelector(selector);
      const value = node?.getAttribute('content') || node?.getAttribute('href') || node?.getAttribute('src');
      if (value) return absoluteUrl(value.replace(/&amp;/g, '&'), base);
    }
    if (kind === 'video') {
      const scriptText = [...doc.scripts].map(script => script.textContent || '').join('\n');
      const matches = scriptText.match(/https?:\\?\/\\?\/[^"'<>\s]+/gi) || [];
      for (const match of matches) {
        const candidate = absoluteUrl(match.replace(/\\\//g, '/').replace(/\\u0026/gi, '&'), base);
        if (VIDEO_EXT.test(candidate)) return candidate;
      }
    }
    return '';
  }

  function directUrlFromResponse(rawText, base) {
    const candidates = [];
    const collect = (value, key = '') => {
      if (typeof value === 'string') {
        const cleaned = value.replace(/\\\//g, '/').replace(/\\u0026/gi, '&').replace(/&amp;/gi, '&').trim();
        if (/^(?:https?:)?\/\//i.test(cleaned) || (/^\//.test(cleaned) && /(?:url|file|src|source|video|media|download)/i.test(key))) {
          candidates.push({ url: absoluteUrl(cleaned, base), key });
        }
        return;
      }
      if (Array.isArray(value)) return value.forEach(entry => collect(entry, key));
      if (value && typeof value === 'object') Object.entries(value).forEach(([childKey, child]) => collect(child, childKey));
    };
    try { collect(JSON.parse(rawText)); }
    catch { collect(rawText); }
    const rawMatches = String(rawText).match(/https?:\\?\/\\?\/[^"'<>\s]+/gi) || [];
    rawMatches.forEach(url => candidates.push({ url: absoluteUrl(url.replace(/\\\//g, '/').replace(/\\u0026/gi, '&'), base), key: '' }));
    const priority = /^(?:url|file|src|source|video|media|download|download_url|direct_url|signed_url)$/i;
    return candidates
      .filter(candidate => {
        try {
          const url = new URL(candidate.url);
          const isTurboPage = hostDefinition(candidate.url)?.id === 'turbo' && /^\/(?:embed|v|d)\//i.test(url.pathname);
          return !isTurboPage && !/\/api\/sign/i.test(url.pathname);
        } catch { return false; }
      })
      .sort((a, b) => Number(priority.test(b.key)) - Number(priority.test(a.key)) || Number(VIDEO_EXT.test(b.url)) - Number(VIDEO_EXT.test(a.url)))[0]?.url || '';
  }

  function resolutionResult(status, url = '', method = '', mediaType = '', extras = {}) {
    const normalizedUrl = url ? absoluteUrl(url) : '';
    if (status !== 'failed' && !normalizedUrl) return { status: 'failed', url: '', method: method || 'invalid-url', mediaType, reason: 'Resolver returned a non-HTTP(S) URL', ...extras };
    return { status, url: normalizedUrl, method, mediaType, ...extras };
  }

  const failedResolution = (reason, mediaType, extras = {}) => ({
    status: 'failed', url: '', method: extras.method || 'resolver', mediaType, reason: String(reason || 'No downloadable media found'), ...extras,
  });

  async function resolveTurboVideo(item) {
    const id = hostMediaId(item.url);
    if (!id) return failedResolution('Turbo video ID was not found', 'video', { method: 'turbo' });
    const embed = `https://turbo.cr/embed/${id}`;
    const api = `https://turbo.cr/api/sign?v=${encodeURIComponent(id)}`;
    const headers = { Accept: '*/*', Referer: embed };
    const fallbackUrl = `https://turbo.cr/d/${id}`;

    const trySignedUrl = async () => {
      const direct = directUrlFromResponse(await requestText(api, { headers }), api);
      return direct ? resolutionResult('confirmed', direct, 'turbo-signed-api', 'video', { headers, fallbackUrl }) : null;
    };

    try {
      const result = await trySignedUrl();
      if (result) return result;
    } catch (error) {
      console.debug('[SimpCity Gallery] Turbo signed URL request failed', error);
      recordDiagnostic('resolverFailures', 'Turbo signed URL', error, item);
    }

    try {
      const html = await requestText(embed, { headers: { Accept: 'text/html,*/*', Referer: item.postUrl || location.href } });
      const embeddedRaw = mediaFromHtml(html, embed, 'video');
      const embedded = embeddedRaw ? directUrlFromResponse(JSON.stringify({ url: embeddedRaw }), embed) : '';
      if (embedded) return resolutionResult('confirmed', embedded, 'turbo-embed-page', 'video', { headers, fallbackUrl });
      const result = await trySignedUrl();
      if (result) return result;
    } catch (error) {
      console.debug('[SimpCity Gallery] Turbo embed resolution failed', error);
      recordDiagnostic('resolverFailures', 'Turbo embed', error, item);
    }
    return failedResolution('Turbo did not expose a direct video file', 'video', { method: 'turbo', fallbackUrl });
  }

  async function resolveCandidate(item) {
    if (!item) return failedResolution('Media item is missing', '');
    item.resolution ||= {};
    if (item.resolution.attempted && item.resolution.candidate) return item.resolution.candidate;
    if (item.resolution.promise) return item.resolution.promise;
    if (item.resolution.failedAt && Date.now() - item.resolution.failedAt < 60000) {
      return item.resolution.candidate || failedResolution('Resolver is cooling down after a failure', item.type === 'image' ? 'image' : 'video');
    }
    item.resolution.promise = resolveCandidateUncached(item);
    try {
      const result = await item.resolution.promise;
      item.resolution.candidate = result;
      item.resolution.attempted = true;
      if (result.status === 'failed') item.resolution.failedAt = Date.now();
      return result;
    } finally {
      delete item.resolution.promise;
    }
  }

  async function resolveCandidateUncached(item) {
    const mediaType = item.type === 'image' ? 'image' : 'video';
    const seed = item.resolution?.candidate;
    if (seed?.status === 'confirmed') return seed;
    if (mediaType === 'image') {
      const sourceDefinition = hostDefinition(item.originalLink);
      let sourceUrl;
      try { sourceUrl = new URL(normalizeSourceUrl(item.originalLink)); } catch { sourceUrl = null; }
      const imageId = sourceUrl ? sourceDefinition?.imageId?.(sourceUrl) : '';
      if (sourceDefinition?.id === 'goonbox' && imageId) {
        try {
          const payload = JSON.parse(await requestText(`https://goonbox.cr/api/images/${imageId}`));
          const original = payload?.image?.original_url;
          if (original) return resolutionResult('confirmed', absoluteUrl(original, 'https://goonbox.cr/'), 'goonbox-api', 'image');
        } catch (error) {
          console.debug('[SimpCity Gallery] Goonbox API resolution failed', error);
          recordDiagnostic('resolverFailures', 'Goonbox original image', error, item);
        }
      }
      if (item.originalLink && item.originalLink !== seed?.url && (sourceDefinition?.imagePage || item.lightboxHint)) {
        try {
          const found = mediaFromHtml(await requestText(item.originalLink), item.originalLink, 'image');
          if (found) return resolutionResult('confirmed', found, `${sourceDefinition?.id || 'generic'}-image-page`, 'image');
        } catch (error) {
          console.debug('[SimpCity Gallery] Image resolution failed', error);
          recordDiagnostic('resolverFailures', 'Image page resolution', error, item);
        }
      }
      if (seed?.url) return resolutionResult('heuristic', seed.url, seed.method || 'image-preview', 'image');
      const preview = imagePreviewCandidate(item.thumb, item.originalLink);
      return preview.url ? resolutionResult('heuristic', preview.url, preview.method, 'image') : failedResolution('No image candidate was found', 'image');
    }
    if (seed?.url && VIDEO_EXT.test(seed.url)) return resolutionResult('confirmed', seed.url, seed.method || 'direct-video', 'video');
    if (hostDefinition(item.url)?.id === 'turbo') return resolveTurboVideo(item);
    try {
      const found = mediaFromHtml(await requestText(item.url), item.url, 'video');
      if (found && VIDEO_EXT.test(found)) return resolutionResult('confirmed', found, `${hostDefinition(item.url)?.id || 'generic'}-video-page`, 'video');
    } catch (error) {
      console.debug('[SimpCity Gallery] Video resolution failed', error);
      recordDiagnostic('resolverFailures', 'Generic video resolution', error, item);
    }
    return failedResolution('The video host did not expose a direct media file', 'video', { fallbackUrl: normalizeSourceUrl(item.url) });
  }

  async function resolveForDisplay(item) {
    const result = await resolveCandidate(item);
    if (result.status !== 'failed') return result;
    if (item?.type === 'image' && item.thumb) return resolutionResult('heuristic', item.thumb, 'thumbnail-fallback', 'image', { reason: result.reason });
    return result;
  }

  function qualifyDownloadResolution(item, result) {
    if (!result || result.status === 'failed' || !result.url) return result || failedResolution('No media resolution was returned', item?.type === 'image' ? 'image' : 'video');
    if (result.status === 'heuristic' && /^(?:thumbnail|thumbnail-fallback)$/.test(result.method)) {
      return failedResolution('Only a forum thumbnail was available; it was not treated as the original file', result.mediaType, {
        method: result.method,
        fallbackUrl: item.originalLink && item.originalLink !== result.url ? item.originalLink : '',
      });
    }
    return { ...result, requiresByteValidation: true };
  }

  async function resolveForDownload(item) {
    const result = await resolveCandidate(item);
    return qualifyDownloadResolution(item, result);
  }

  function safeFilename(item, url, verifiedExtension = '') {
    let ext = verifiedExtension || url?.match(/\.(jpe?g|png|gif|webp|avif|bmp|mp4|webm|mov|m4v)(?:$|[?#./])/i)?.[1] || (item.type === 'image' ? 'jpg' : 'mp4');
    if (ext.toLowerCase() === 'jpeg') ext = 'jpg';
    const page = Math.max(1, Number(item.pageNumber || pageNumberFromUrl(item.pageUrl || item.postUrl)) || 1);
    const postDigits = String(item.postNumber || item.postId || '').match(/\d+/g)?.pop();
    const reply = postDigits || String(Number(item.postIndex || 0) + 1);
    const type = item.type === 'image' ? 'img' : 'vid';
    const order = String(Number(item.mediaIndex || 0) + 1).padStart(2, '0');
    const base = `${threadFilePrefix(item.pageUrl || item.postUrl)}-p${page}-r${reply}-${type}${order}`;
    return `${base}.${ext.toLowerCase()}`;
  }

  function formatBytes(bytes) {
    const value = Number(bytes || 0);
    if (!value) return '0 B';
    const units = ['B', 'KB', 'MB', 'GB'];
    const index = Math.min(units.length - 1, Math.floor(Math.log(value) / Math.log(1024)));
    const amount = value / (1024 ** index);
    return `${amount >= 100 || index === 0 ? amount.toFixed(0) : amount.toFixed(1)} ${units[index]}`;
  }

  function blankDownloadProgress() {
    return { current: 0, total: 0, ok: 0, failed: 0, verification: 0, skipped: 0, duplicates: 0, zipPercent: 0, label: '' };
  }

  let downloadJobSequence = 0;
  function createDownloadJob(item, status = 'queued') {
    const job = {
      id: ++downloadJobSequence,
      item,
      filename: safeFilename(item, item.resolution?.candidate?.url || item.url || item.thumb),
      status,
      loaded: 0,
      total: 0,
      packProgress: 0,
      error: '',
    };
    state.downloadJobs.push(job);
    scheduleDownloadUi.dirtyJobs.add(job.id);
    return job;
  }

  function setDownloadJob(job, changes) {
    if (!job) return;
    Object.assign(job, changes);
    scheduleDownloadUi.dirtyJobs.add(job.id);
    scheduleDownloadUi();
  }

  function scheduleDownloadUi() {
    if (scheduleDownloadUi.timer) return;
    scheduleDownloadUi.timer = setTimeout(() => {
      scheduleDownloadUi.timer = 0;
      updateDownloadUi();
    }, 250);
  }
  scheduleDownloadUi.dirtyJobs = new Set();

  function updateDownloadedIndicators() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    app.querySelectorAll('.scg-card[data-index]').forEach(cardNode => {
      const item = state.renderedItems[Number(cardNode.dataset.index)];
      const saved = wasDownloaded(item);
      const legacy = hasLegacyDownload(item);
      cardNode.classList.toggle('downloaded', saved);
      cardNode.classList.toggle('download-legacy', legacy);
      const marker = cardNode.querySelector('.scg-saved-status');
      if (marker) {
        marker.classList.toggle('visible', saved || legacy);
        marker.classList.toggle('legacy', legacy);
        marker.textContent = saved ? 'VERIFIED' : (legacy ? 'LEGACY' : '');
        marker.title = saved ? 'Validated media recorded in download history' : (legacy ? 'Older unverified history entry; this item will not be skipped' : '');
      }
      const button = cardNode.querySelector('[data-download]');
      setIconButton(button, 'download', saved ? 'Download again' : (legacy ? 'Download verified copy' : 'Download'));
    });
    const lightbox = app.querySelector('.scg-lightbox.open');
    const lightboxButton = lightbox?.querySelector('[data-download-current]');
    const lightboxItem = currentViewerItem();
    if (lightboxButton && lightboxItem) setIconOnlyButton(lightboxButton, 'download', wasDownloaded(lightboxItem) ? 'Download again (D)' : (hasLegacyDownload(lightboxItem) ? 'Download verified copy (D)' : 'Download (D)'), 'scg-icon-well-primary');
  }

  function notify(message, duration = 3500) {
    const toast = document.getElementById('scg-gallery-toast');
    if (!toast) return;
    toast.textContent = message;
    toast.classList.add('show');
    clearTimeout(notify.timer);
    notify.timer = setTimeout(() => toast.classList.remove('show'), duration);
  }

  function confirmAction({ title = 'Confirm action', message, confirmLabel = 'Continue', danger = false, preferenceLabel = '' } = {}) {
    const panel = document.querySelector(`#${APP_ID} .scg-confirm-panel`);
    if (!panel) return Promise.resolve(false);
    const previousFocus = document.activeElement;
    panel.querySelector('[data-confirm-title]').textContent = title;
    panel.querySelector('[data-confirm-message]').textContent = message || '';
    const accept = panel.querySelector('[data-confirm-accept]');
    setIconButton(accept, danger ? 'trash' : 'check', confirmLabel);
    accept.classList.toggle('danger', danger);
    const preference = panel.querySelector('[data-confirm-preference]');
    const preferenceInput = preference?.querySelector('input');
    if (preference) preference.hidden = !preferenceLabel;
    if (preferenceInput) {
      preferenceInput.checked = false;
      preference.querySelector('span').textContent = preferenceLabel;
    }
    panel.classList.add('open');
    accept.focus();
    return new Promise(resolve => {
      const finish = value => {
        panel.classList.remove('open');
        accept.onclick = null;
        panel.querySelector('[data-confirm-cancel]').onclick = null;
        panel.onclick = null;
        previousFocus?.focus?.();
        resolve(preferenceLabel ? { confirmed: value, preferenceChecked: Boolean(preferenceInput?.checked) } : value);
      };
      accept.onclick = () => finish(true);
      panel.querySelector('[data-confirm-cancel]').onclick = () => finish(false);
      panel.onclick = event => { if (event.target === panel) finish(false); };
    });
  }

  async function saveValidatedDownload(item, resolution, job) {
    const validation = await fetchValidatedMedia(item, resolution, (loaded, total) => setDownloadJob(job, {
      status: 'downloading', loaded, total,
    }));
    const filename = safeFilename(item, resolution.url, validation.extension);
    saveBlob(validation.blob, filename);
    setDownloadJob(job, { status: 'saved', loaded: validation.size, total: validation.size, filename });
    return { filename, validation };
  }

  async function downloadMedia(item, options = {}) {
    const quiet = Boolean(options.quiet);
    const allowFallback = options.allowFallback !== false;
    if (state.downloading && !options.queueOwned) {
      if (!quiet) notify('Wait for the ZIP queue to finish or cancel it first.');
      return { ok: false, busy: true };
    }
    if (wasDownloaded(item) && !options.force && !quiet) {
      const confirmed = await confirmAction({
        title: 'Download this file again?',
        message: 'The gallery already has a verified download record for this media.',
        confirmLabel: 'Download again',
      });
      if (!confirmed) return { ok: false, skipped: true };
    }
    if (!state.downloading) {
      const activeJobs = state.downloadJobs.filter(job => ['queued', 'preparing', 'downloading', 'packing'].includes(job.status));
      state.downloadJobs = activeJobs.length ? state.downloadJobs.slice(-20) : [];
      state.downloadProgress = blankDownloadProgress();
      state.downloadProgress.total = 1;
      if (!activeJobs.length) state.downloadDetailsOpen = false;
    }
    const job = options.job || createDownloadJob(item, 'preparing');
    state.downloadProgress.label = `Preparing ${job.filename}`;
    updateDownloadUi();
    if (!quiet) notify('Preparing original file...');
    let resolution = failedResolution('Download resolution did not run', item.type === 'image' ? 'image' : 'video');
    try { resolution = await resolveForDownload(item); }
    catch (error) {
      console.debug('[SimpCity Gallery] Download resolution failed', error);
      recordDiagnostic('resolverFailures', 'Download preparation', error, item);
      resolution = failedResolution(error.message || 'Download resolution failed', item.type === 'image' ? 'image' : 'video');
    }

    if (resolution.status === 'failed' || !resolution.url) {
      if (allowFallback && resolution.fallbackUrl) {
        window.open(resolution.fallbackUrl, '_blank', 'noopener');
        if (!quiet) notify('No direct file was exposed. The host page was opened for manual verification.', 5200);
      } else if (!quiet) {
        notify('No direct file was exposed by this video host.', 4500);
      }
      const verification = Boolean(resolution.fallbackUrl);
      setDownloadJob(job, { status: verification ? 'verification' : 'failed', error: resolution.reason || 'No direct file available' });
      state.downloadProgress[verification ? 'verification' : 'failed']++;
      state.downloadProgress.current = 1;
      state.downloadProgress.label = verification ? 'Media host verification required' : 'Download unavailable';
      updateDownloadUi();
      return { ok: false, verification };
    }

    try {
      setDownloadJob(job, { status: 'downloading', filename: safeFilename(item, resolution.url) });
      const saved = await saveValidatedDownload(item, resolution, job);
      rememberDownloaded(item, saved.filename, saved.validation);
      state.downloadProgress.ok = 1;
      state.downloadProgress.current = 1;
      state.downloadProgress.label = `Validated and saved ${saved.filename}`;
      updateDownloadUi();
      if (!quiet) notify('Media validated and sent to Downloads.');
      return { ok: true };
    } catch (error) {
      console.debug('[SimpCity Gallery] Browser download failed', error);
      recordDiagnostic('downloadFailures', 'Individual download', error, item);
      setDownloadJob(job, { status: error?.name === 'AbortError' ? 'canceled' : 'failed', error: error.message || 'Download failed' });
      state.downloadProgress.failed = error?.name === 'AbortError' ? 0 : 1;
      state.downloadProgress.current = 1;
      state.downloadProgress.label = error?.name === 'AbortError' ? 'Download canceled' : 'Download failed';
      updateDownloadUi();
      if (!quiet) {
        notify(`Download rejected: ${error.message || 'the response was not valid media'}`, 6000);
      }
      return { ok: false, verification: false };
    }
  }

  function selectedMediaItems() {
    return state.items.filter(item => isDownloadableMedia(item) && state.selected.has(item.selectionKey));
  }

  function setSelected(item, selected) {
    if (!isDownloadableMedia(item)) return;
    const changed = selected ? !state.selected.has(item.selectionKey) : state.selected.has(item.selectionKey);
    if (selected) state.selected.add(item.selectionKey);
    else state.selected.delete(item.selectionKey);
    if (changed) invalidateVisibleItems({ selection: true });
  }

  function clearSelection() {
    if (!state.selected.size) return false;
    state.selected.clear();
    invalidateVisibleItems({ selection: true });
    return true;
  }

  function exitSelectionMode() {
    state.selectionMode = false;
    clearSelection();
    state.selectedOnly = false;
  }

  function toggleSelected(item) {
    const selected = !state.selected.has(item.selectionKey);
    setSelected(item, selected);
    updateSelectionUi();
    return selected;
  }

  async function copyText(value, successMessage = 'Link copied.') {
    try {
      await navigator.clipboard.writeText(value);
      notify(successMessage);
    } catch {
      const input = document.createElement('textarea');
      input.value = value;
      input.style.position = 'fixed';
      input.style.opacity = '0';
      document.body.appendChild(input);
      input.select();
      document.execCommand('copy');
      input.remove();
      notify(successMessage);
    }
  }

  function normalizedPreferences(input = {}) {
    return {
      filter: ['all', 'image', 'video', 'link', 'text'].includes(input.filter) ? input.filter : DEFAULT_SETTINGS.filter,
      compact: Boolean(input.compact),
      sort: ['thread-asc', 'thread-desc', 'author', 'host', 'type'].includes(input.sort) ? input.sort : DEFAULT_SETTINGS.sort,
      groupBy: input.groupBy === 'reply' ? 'reply' : DEFAULT_SETTINGS.groupBy,
      perPage: VIEW_PAGE_SIZES.includes(Number(input.perPage)) ? Number(input.perPage) : DEFAULT_SETTINGS.perPage,
      theme: THEME_MODES.includes(input.theme) ? input.theme : DEFAULT_SETTINGS.theme,
      filtersCollapsed: Boolean(input.filtersCollapsed),
      layoutMode: LAYOUT_MODES.includes(input.layoutMode) ? input.layoutMode : DEFAULT_SETTINGS.layoutMode,
      cardScale: Number.isFinite(Number(input.cardScale)) ? Math.min(160, Math.max(70, Math.round(Number(input.cardScale) / 5) * 5)) : DEFAULT_SETTINGS.cardScale,
      downloadConcurrency: [1, 2, 3, 4].includes(Number(input.downloadConcurrency)) ? Number(input.downloadConcurrency) : DEFAULT_SETTINGS.downloadConcurrency,
      zipPartSizeMb: [100, 300, 600, 1000].includes(Number(input.zipPartSizeMb)) ? Number(input.zipPartSizeMb) : DEFAULT_SETTINGS.zipPartSizeMb,
      zipPartMaxFiles: [24, 50, 100].includes(Number(input.zipPartMaxFiles)) ? Number(input.zipPartMaxFiles) : DEFAULT_SETTINGS.zipPartMaxFiles,
      archiveLayout: ['flat', 'page', 'page-author', 'reply'].includes(input.archiveLayout) ? input.archiveLayout : DEFAULT_SETTINGS.archiveLayout,
      includeManifest: input.includeManifest !== false,
      dedupeContent: input.dedupeContent !== false,
      warnLargeThreadScan: input.warnLargeThreadScan !== false,
    };
  }

  function currentPreferences() {
    return normalizedPreferences({
      filter: state.filter,
      compact: state.compact,
      sort: state.sort,
      groupBy: state.groupBy,
      perPage: state.perPage,
      theme: state.theme,
      filtersCollapsed: state.filtersCollapsed,
      layoutMode: state.layoutMode,
      cardScale: state.cardScale,
      downloadConcurrency: state.downloadConcurrency,
      zipPartSizeMb: state.zipPartSizeMb,
      zipPartMaxFiles: state.zipPartMaxFiles,
      archiveLayout: state.archiveLayout,
      includeManifest: state.includeManifest,
      dedupeContent: state.dedupeContent,
      warnLargeThreadScan: state.warnLargeThreadScan,
    });
  }

  function applyPreferences(input, shouldPersist = true) {
    const preferences = normalizedPreferences(input);
    Object.assign(state, preferences, {
      query: '',
      authorFilter: 'all',
      hostFilter: 'all',
      selectedOnly: false,
      viewPage: 1,
    });
    const app = document.getElementById(APP_ID);
    const search = app?.querySelector('[data-search]');
    if (search) search.value = '';
    if (shouldPersist) persistSettings();
    render();
    refreshSettingsPanel();
  }

  function buildDebugReport() {
    const count = counts();
    const thread = threadIdentity();
    const history = downloadHistoryCounts();
    const jobStates = state.downloadJobs.reduce((summary, job) => {
      summary[job.status] = (summary[job.status] || 0) + 1;
      return summary;
    }, {});
    return {
      app: 'SimpCity Thread Gallery',
      version: APP_VERSION,
      generatedAt: new Date().toISOString(),
      storage: storage.mode,
      legacyStorageMigrated: state.diagnostics.migratedLegacyStorage,
      thread: { id: thread.id || 'unknown', page: pageNumberFromUrl(), knownPages: state.threadPageCount },
      scan: { source: state.sourceLabel, scannedPages: state.scannedPages, scanning: state.scanning },
      items: count,
      view: { ...currentPreferences(), matchedPage: state.viewPage, viewPages: state.viewPages },
      downloads: { active: state.downloading, states: jobStates, history, archive: archiveDownloadOptions(), validation: 'supported media byte signatures' },
      failures: {
        resolver: state.diagnostics.resolverFailures,
        download: state.diagnostics.downloadFailures,
        scan: state.diagnostics.scanFailures,
      },
    };
  }

  function refreshSettingsPanel() {
    const panel = document.querySelector(`#${APP_ID} .scg-settings-panel`);
    if (!panel) return;
    const count = counts();
    const history = downloadHistoryCounts();
    const set = (selector, value) => {
      panel.querySelectorAll(selector).forEach(node => { node.textContent = String(value); });
    };
    set('[data-diag-version]', APP_VERSION);
    set('[data-diag-storage]', storage.mode);
    set('[data-diag-pages]', `${state.scannedPages} scanned / ${state.threadPageCount} known`);
    set('[data-diag-items]', `${count.all} total (${count.image} images, ${count.video} videos)`);
    set('[data-diag-resolvers]', state.diagnostics.resolverFailures.length);
    set('[data-diag-downloads]', state.diagnostics.downloadFailures.length);
    set('[data-diag-scans]', state.diagnostics.scanFailures.length);
    set('[data-diag-history]', `${history.verified} verified / ${history.legacy} legacy`);
    const migration = panel.querySelector('[data-diag-migration]');
    if (migration) migration.textContent = state.diagnostics.migratedLegacyStorage ? 'Previous settings migrated successfully' : 'No legacy migration was required';
    const themeSelect = panel.querySelector('[data-setting-theme]');
    const densitySelect = panel.querySelector('[data-setting-density]');
    const filtersCollapsed = panel.querySelector('[data-setting-filters-collapsed]');
    const layoutSelect = panel.querySelector('[data-setting-layout]');
    const cardScale = panel.querySelector('[data-card-scale]');
    const archiveLayout = panel.querySelector('[data-setting-archive-layout]');
    const concurrency = panel.querySelector('[data-setting-download-concurrency]');
    const partSize = panel.querySelector('[data-setting-zip-part-size]');
    const partFiles = panel.querySelector('[data-setting-zip-part-files]');
    const manifest = panel.querySelector('[data-setting-include-manifest]');
    const dedupeContent = panel.querySelector('[data-setting-dedupe-content]');
    const warnLargeScan = panel.querySelector('[data-setting-warn-large-scan]');
    if (themeSelect) themeSelect.value = state.theme;
    if (densitySelect) densitySelect.value = state.compact ? 'compact' : 'comfortable';
    if (filtersCollapsed) filtersCollapsed.checked = state.filtersCollapsed;
    if (layoutSelect) layoutSelect.value = state.layoutMode;
    if (cardScale) cardScale.value = String(state.cardScale);
    if (archiveLayout) archiveLayout.value = state.archiveLayout;
    if (concurrency) concurrency.value = String(state.downloadConcurrency);
    if (partSize) partSize.value = String(state.zipPartSizeMb);
    if (partFiles) partFiles.value = String(state.zipPartMaxFiles);
    if (manifest) manifest.checked = state.includeManifest;
    if (dedupeContent) dedupeContent.checked = state.dedupeContent;
    if (warnLargeScan) warnLargeScan.checked = state.warnLargeThreadScan;
  }

  function openSettingsPanel() {
    const panel = document.querySelector(`#${APP_ID} .scg-settings-panel`);
    if (!panel) return;
    refreshSettingsPanel();
    panel.classList.add('open');
    panel.querySelector('.scg-settings-dialog')?.focus();
  }

  function closeSettingsPanel() {
    document.querySelector(`#${APP_ID} .scg-settings-panel`)?.classList.remove('open');
  }

  function exportSettings(includeHistory = false) {
    const payload = {
      app: 'SimpCity Thread Gallery',
      schema: 2,
      historySchema: 2,
      version: APP_VERSION,
      exportedAt: new Date().toISOString(),
      settings: currentPreferences(),
    };
    if (includeHistory) payload.downloadHistory = state.downloadHistory;
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    saveBlob(blob, `simpcity-gallery-settings-${timestampForFilename()}.json`);
    notify(includeHistory ? 'Settings and download history exported.' : 'Settings exported.');
  }

  async function importSettings(file) {
    if (!file) return;
    try {
      const payload = JSON.parse(await file.text());
      if (!payload || typeof payload !== 'object' || !payload.settings || typeof payload.settings !== 'object') throw new Error('This is not a valid gallery settings file');
      if (payload.downloadHistory && typeof payload.downloadHistory === 'object' && !Array.isArray(payload.downloadHistory)) {
        state.downloadHistory = normalizeDownloadHistory(payload.downloadHistory);
        persistDownloadHistory();
      }
      applyPreferences(payload.settings, true);
      updateDownloadedIndicators();
      notify('Settings imported successfully.');
    } catch (error) {
      notify(`Import failed: ${error.message}`, 6000);
    }
  }

  function resetPreferences(clearHistory = false) {
    storage.remove(SETTINGS_KEY);
    if (clearHistory) {
      storage.remove(DOWNLOAD_HISTORY_KEY);
      state.downloadHistory = {};
      updateDownloadedIndicators();
    }
    applyPreferences(DEFAULT_SETTINGS, true);
    notify(clearHistory ? 'Preferences and download history reset.' : 'Preferences reset.');
  }

  function clearDownloadHistory() {
    if (state.downloading) return false;
    state.downloadHistory = {};
    storage.remove(DOWNLOAD_HISTORY_KEY);
    state.downloadJobs = [];
    state.downloadProgress = blankDownloadProgress();
    scheduleDownloadUi.dirtyJobs.clear();
    updateDownloadedIndicators();
    updateDownloadUi();
    refreshSettingsPanel();
    return true;
  }

  function clearDiagnostics() {
    state.diagnostics.resolverFailures = [];
    state.diagnostics.downloadFailures = [];
    state.diagnostics.scanFailures = [];
    state.diagnostics.events = [];
    refreshSettingsPanel();
    notify('Diagnostic history cleared.');
  }

  function selectItems(items, replace = false) {
    let changed = false;
    if (replace && state.selected.size) {
      state.selected.clear();
      changed = true;
    }
    items.filter(isDownloadableMedia).forEach(item => {
      if (!state.selected.has(item.selectionKey)) changed = true;
      state.selected.add(item.selectionKey);
    });
    if (changed) invalidateVisibleItems({ selection: true });
    updateSelectionUi();
  }

  function updateSelectionUi() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    app.classList.toggle('selecting', state.selectionMode);
    const selected = selectedMediaItems();
    const count = selected.length;
    const countNode = app.querySelector('.scg-selected-count');
    const downloadButton = app.querySelector('[data-action="download-selected"]');
    const clearButton = app.querySelector('[data-action="clear-selection"]');
    if (countNode) countNode.textContent = `${count} selected`;
    if (downloadButton) {
      setIconButton(downloadButton, state.downloading ? 'close' : 'archive', state.downloading ? 'Cancel ZIP queue' : `Download ZIP (${count})`);
      downloadButton.disabled = !count && !state.downloading;
      downloadButton.classList.toggle('cancel-download', state.downloading);
    }
    if (clearButton) clearButton.disabled = !count;
    const modeButton = app.querySelector('[data-action="selection-mode"]');
    if (modeButton) setIconButton(modeButton, state.selectionMode ? 'close' : 'select', state.selectionMode ? 'Exit selection' : `Select media${count ? ` (${count})` : ''}`);
    const visible = state.renderedItems;
    app.querySelectorAll('.scg-card[data-index]').forEach(cardNode => {
      const item = visible[Number(cardNode.dataset.index)];
      const checked = Boolean(item && isDownloadableMedia(item) && state.selected.has(item.selectionKey));
      cardNode.classList.toggle('selected', state.selectionMode && checked);
      const input = cardNode.querySelector('[data-select]');
      if (input) input.checked = checked;
    });
    updateReplySelectionUi();
  }

  function updateReplySelectionUi() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    app.querySelectorAll('.scg-reply-group[data-reply-key]').forEach(group => {
      const replySelection = replySelectionState(group.dataset.replyKey);
      const input = group.querySelector('[data-reply-select]');
      if (input) {
        input.checked = replySelection.checked;
        input.indeterminate = replySelection.indeterminate;
        input.setAttribute('aria-checked', replySelection.indeterminate ? 'mixed' : String(replySelection.checked));
      }
      const count = group.querySelector('[data-reply-selection-count]');
      if (count) count.textContent = `${replySelection.selected} of ${replySelection.total} downloadable selected`;
    });
  }

  function downloadJobDetail(job, terminal) {
    const indeterminate = job.status === 'downloading' && !job.total;
    const jobPercent = job.total ? Math.min(100, Math.round((job.loaded / job.total) * 100)) : (terminal.has(job.status) ? 100 : (indeterminate ? 34 : 0));
    const detail = job.status === 'downloading'
      ? `${formatBytes(job.loaded)}${job.total ? ` / ${formatBytes(job.total)}` : ''}`
      : job.status === 'skipped' ? 'Already downloaded - skipped'
        : job.status === 'duplicate' ? (job.error || 'Duplicate content - merged')
          : job.status === 'verification' ? 'Host verification required'
            : job.status === 'failed' ? (job.error || 'Failed')
              : job.status === 'packing' ? `Preparing ZIP - ${Math.round(job.packProgress || 0)}%`
                : job.status === 'saved' ? 'Saved in ZIP or Downloads'
                  : job.status.charAt(0).toUpperCase() + job.status.slice(1);
    return { indeterminate, jobPercent, detail };
  }

  function updateDownloadJobRow(list, job, terminal) {
    let row = list.querySelector(`[data-job-id="${job.id}"]`);
    if (!row) {
      row = document.createElement('div');
      row.dataset.jobId = String(job.id);
      row.innerHTML = '<div><b></b><span></span></div><div class="scg-job-track"><i></i></div>';
      list.appendChild(row);
    }
    const { indeterminate, jobPercent, detail } = downloadJobDetail(job, terminal);
    row.className = `scg-download-job scg-job-${job.status}`;
    const filename = row.querySelector('b');
    filename.textContent = job.filename;
    filename.title = job.filename;
    row.querySelector('span').textContent = detail;
    const fill = row.querySelector('i');
    fill.classList.toggle('indeterminate', indeterminate);
    fill.style.width = `${jobPercent}%`;
  }

  function updateDownloadUi() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    const progress = app.querySelector('.scg-progress');
    if (!progress) return;
    const p = state.downloadProgress;
    const jobs = state.downloadJobs;
    const terminal = new Set(['saved', 'duplicate', 'failed', 'verification', 'skipped', 'canceled']);
    const finished = jobs.filter(job => terminal.has(job.status)).length;
    const average = jobs.length ? jobs.reduce((sum, job) => {
      if (terminal.has(job.status)) return sum + 1;
      if (job.status === 'packing') return sum + .92 + (.07 * Number(job.packProgress || 0) / 100);
      return sum + (job.total ? Math.min(.92, job.loaded / job.total) : 0);
    }, 0) / jobs.length : 0;
    const percent = p.zipPercent > 0 ? Math.round(p.zipPercent) : Math.round(average * 100);
    const loaded = jobs.reduce((sum, job) => sum + Number(job.loaded || 0), 0);
    const total = jobs.reduce((sum, job) => sum + Number(job.total || 0), 0);
    const totalsKnown = jobs.filter(job => !['skipped', 'duplicate', 'verification', 'failed', 'canceled'].includes(job.status)).every(job => job.total > 0 || job.status === 'queued' || job.status === 'preparing');
    app.classList.toggle('has-downloads', jobs.length > 0);
    app.classList.toggle('downloading', state.downloading);
    progress.classList.add('visible');
    progress.classList.toggle('expanded', state.downloadDetailsOpen);
    progress.querySelector('[data-action="download-details"]')?.setAttribute('aria-expanded', String(state.downloadDetailsOpen));
    progress.querySelector('.scg-progress-fill').style.width = `${percent}%`;
    progress.querySelector('.scg-progress-text').textContent = p.label || (jobs.length ? `${finished} / ${jobs.length}` : 'Downloads idle');
    const overall = progress.querySelector('.scg-download-overall');
    if (overall) overall.textContent = jobs.length ? `${finished} of ${jobs.length} complete${loaded ? ` - ${formatBytes(loaded)}${total && totalsKnown ? ` / ${formatBytes(total)}` : ''}` : ''}` : 'No downloads yet.';
    const list = progress.querySelector('.scg-download-jobs');
    if (list) {
      const recentJobs = jobs.slice(-40);
      const recentIds = new Set(recentJobs.map(job => String(job.id)));
      list.querySelectorAll('[data-job-id]').forEach(row => {
        if (!recentIds.has(row.dataset.jobId)) row.remove();
      });
      list.querySelector('.scg-download-empty')?.remove();
      recentJobs.forEach(job => {
        if (scheduleDownloadUi.dirtyJobs.has(job.id) || !list.querySelector(`[data-job-id="${job.id}"]`)) {
          updateDownloadJobRow(list, job, terminal);
        }
      });
      if (!recentJobs.length && !list.querySelector('.scg-download-empty')) {
        const empty = document.createElement('div');
        empty.className = 'scg-download-empty';
        empty.textContent = 'No downloads yet.';
        list.appendChild(empty);
      }
      scheduleDownloadUi.dirtyJobs.clear();
    }
    app.querySelectorAll('[data-action="page"], [data-action="load-source-page"], [data-source-page]').forEach(control => {
      control.disabled = state.downloading || state.scanning;
    });
    const threadScanButton = app.querySelector('[data-action="thread"]');
    if (threadScanButton) threadScanButton.disabled = state.downloading;
    updateSelectionUi();
  }

  function fetchMediaBlob(url, headers, onProgress) {
    onProgress ||= () => {};
    return new Promise((resolve, reject) => {
      if (typeof GM_xmlhttpRequest !== 'function') {
        const sameOrigin = (() => { try { return new URL(url).origin === location.origin; } catch { return false; } })();
        fetch(url, { credentials: sameOrigin ? 'include' : 'omit', headers }).then(async response => {
          if (!response.ok) throw new Error(`HTTP ${response.status}`);
          const total = Number(response.headers.get('content-length') || 0);
          if (!response.body?.getReader) {
            const blob = await response.blob();
            onProgress(blob.size, total || blob.size);
            return blob;
          }
          const reader = response.body.getReader();
          const chunks = [];
          let loaded = 0;
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            chunks.push(value);
            loaded += value.byteLength;
            onProgress(loaded, total);
          }
          return new Blob(chunks, { type: response.headers.get('content-type') || '' });
        }).then(resolve, reject);
        return;
      }

      let control;
      const cleanup = () => { if (control) state.activeDownloadRequests.delete(control); };
      control = GM_xmlhttpRequest({
        method: 'GET',
        url,
        headers: headers || undefined,
        responseType: 'blob',
        timeout: 600000,
        onprogress: event => onProgress(Number(event?.loaded || 0), Number(event?.total || 0)),
        onload: response => {
          cleanup();
          if (response.status < 200 || response.status >= 400) return reject(new Error(`HTTP ${response.status}`));
          const contentType = response.responseHeaders?.match(/^content-type:\s*([^;\r\n]+)/im)?.[1] || '';
          const blob = response.response instanceof Blob ? response.response : new Blob([response.response], { type: contentType });
          if (!blob.size) return reject(new Error('The media host returned an empty file'));
          onProgress(blob.size, blob.size);
          resolve(blob);
        },
        onerror: () => { cleanup(); reject(new Error('Media download failed')); },
        ontimeout: () => { cleanup(); reject(new Error('Media download timed out')); },
        onabort: () => { cleanup(); reject(new DOMException('Download canceled', 'AbortError')); },
      });
      if (control?.abort) state.activeDownloadRequests.add(control);
    });
  }

  function bytesMatch(bytes, signature, offset = 0) {
    return signature.every((value, index) => bytes[offset + index] === value);
  }

  function asciiBytes(bytes, offset, length) {
    return String.fromCharCode(...bytes.slice(offset, offset + length));
  }

  function sniffMediaSignature(bytes) {
    if (bytesMatch(bytes, [0xff, 0xd8, 0xff])) return { mediaType: 'image', extension: 'jpg', mime: 'image/jpeg', format: 'jpeg' };
    if (bytesMatch(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return { mediaType: 'image', extension: 'png', mime: 'image/png', format: 'png' };
    if (asciiBytes(bytes, 0, 6) === 'GIF87a' || asciiBytes(bytes, 0, 6) === 'GIF89a') return { mediaType: 'image', extension: 'gif', mime: 'image/gif', format: 'gif' };
    if (asciiBytes(bytes, 0, 4) === 'RIFF' && asciiBytes(bytes, 8, 4) === 'WEBP') return { mediaType: 'image', extension: 'webp', mime: 'image/webp', format: 'webp' };
    if (asciiBytes(bytes, 0, 2) === 'BM') return { mediaType: 'image', extension: 'bmp', mime: 'image/bmp', format: 'bmp' };
    if (bytesMatch(bytes, [0x1a, 0x45, 0xdf, 0xa3])) return { mediaType: 'video', extension: 'webm', mime: 'video/webm', format: 'webm' };
    if (asciiBytes(bytes, 4, 4) === 'ftyp') {
      const brand = asciiBytes(bytes, 8, 4).toLowerCase();
      const compatible = asciiBytes(bytes, 8, Math.min(32, Math.max(0, bytes.length - 8))).toLowerCase();
      if (/(?:avif|avis)/.test(brand) || /(?:avif|avis)/.test(compatible)) return { mediaType: 'image', extension: 'avif', mime: 'image/avif', format: 'avif' };
      if (/^qt\s*$/.test(brand)) return { mediaType: 'video', extension: 'mov', mime: 'video/quicktime', format: 'quicktime' };
      return { mediaType: 'video', extension: 'mp4', mime: 'video/mp4', format: 'mp4' };
    }
    return null;
  }

  async function validateMediaBlob(blob, expectedMediaType) {
    if (!(blob instanceof Blob) || !blob.size) throw new Error('The media host returned an empty file');
    const bytes = new Uint8Array(await blob.slice(0, 64).arrayBuffer());
    const prefix = new TextDecoder('utf-8', { fatal: false }).decode(bytes).trimStart().toLowerCase();
    const declaredMime = String(blob.type || '').split(';')[0].trim().toLowerCase();
    if (/^(?:<!doctype\s+html|<html|<head|<body|<\?xml|\{|\[)/.test(prefix) || /^(?:text\/|application\/(?:json|xml|xhtml\+xml))/.test(declaredMime)) {
      throw new Error('The host returned an HTML, text, or JSON response instead of media');
    }
    const signature = sniffMediaSignature(bytes);
    if (!signature) throw new Error('The downloaded bytes do not match a supported image or video format');
    if (signature.mediaType !== expectedMediaType) throw new Error(`Expected ${expectedMediaType}, but the response contains ${signature.mediaType}`);
    return {
      verified: true,
      blob,
      size: blob.size,
      mime: signature.mime,
      extension: signature.extension,
      format: signature.format,
      method: `byte-signature:${signature.format}`,
    };
  }

  async function fetchValidatedMedia(item, resolution, onProgress) {
    if (!resolution?.url) throw new Error('No media URL is available for validation');
    const expectedMediaType = item.type === 'image' ? 'image' : 'video';
    const blob = await fetchMediaBlob(resolution.url, resolution.headers, onProgress);
    const validation = await validateMediaBlob(blob, expectedMediaType);
    return {
      ...validation,
      method: `${resolution.method || resolution.status}+${validation.method}`,
      resolutionStatus: resolution.status,
      resolutionMethod: resolution.method,
    };
  }

  async function fetchZipEntry(item, job) {
    try {
      setDownloadJob(job, { status: 'preparing' });
      const resolution = await resolveForDownload(item);
      if (resolution.status === 'failed' || !resolution.url) {
        const verification = Boolean(resolution.fallbackUrl);
        setDownloadJob(job, { status: verification ? 'verification' : 'failed', error: resolution.reason || 'No direct file available' });
        return { item, job, verification, failed: !verification };
      }
      job.filename = safeFilename(item, resolution.url);
      setDownloadJob(job, { status: 'downloading', filename: job.filename });
      const validation = await fetchValidatedMedia(item, resolution, (loaded, total) => {
        setDownloadJob(job, { status: 'downloading', loaded, total });
      });
      job.filename = safeFilename(item, resolution.url, validation.extension);
      setDownloadJob(job, { status: 'packing', loaded: validation.size, total: validation.size, filename: job.filename });
      return { item, job, blob: validation.blob, validation, resolution };
    } catch (error) {
      const canceled = error?.name === 'AbortError' || state.cancelDownload;
      setDownloadJob(job, { status: canceled ? 'canceled' : 'failed', error: error.message || 'Download failed' });
      if (!canceled) recordDiagnostic('downloadFailures', 'ZIP media fetch', error, item);
      return { item, job, canceled, failed: !canceled };
    }
  }

  function archiveDownloadOptions() {
    return {
      concurrency: Math.min(4, Math.max(1, Number(state.downloadConcurrency) || DEFAULT_SETTINGS.downloadConcurrency)),
      partSizeBytes: Math.min(1000, Math.max(100, Number(state.zipPartSizeMb) || DEFAULT_SETTINGS.zipPartSizeMb)) * 1024 * 1024,
      partMaxFiles: [24, 50, 100].includes(Number(state.zipPartMaxFiles)) ? Number(state.zipPartMaxFiles) : DEFAULT_SETTINGS.zipPartMaxFiles,
      archiveLayout: ['flat', 'page', 'page-author', 'reply'].includes(state.archiveLayout) ? state.archiveLayout : DEFAULT_SETTINGS.archiveLayout,
      includeManifest: state.includeManifest !== false,
      dedupeContent: state.dedupeContent !== false,
    };
  }

  function sanitizeArchiveSegment(value, fallback = 'unknown') {
    return slugify(value, 52) || fallback;
  }

  function archivePathForEntry(entry, layout = 'page-author') {
    const item = entry?.item || {};
    const filename = String(entry?.job?.filename || 'media.bin').split('/').pop();
    if (layout === 'flat') return filename;
    const page = `page-${String(Math.max(1, Number(item.pageNumber || 1))).padStart(3, '0')}`;
    if (layout === 'page') return `${page}/${filename}`;
    if (layout === 'reply') {
      const replyDigits = String(item.postNumber || item.postId || '').match(/\d+/g)?.pop() || String(Number(item.postIndex || 0) + 1);
      return `${page}/reply-${String(replyDigits).padStart(3, '0')}/${filename}`;
    }
    return `${page}/${sanitizeArchiveSegment(item.author, 'unknown-author')}/${filename}`;
  }

  function csvCell(value) {
    const text = String(value ?? '');
    return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  }

  function buildManifestCsv(entries, archiveName) {
    const columns = ['archive_path', 'stored', 'duplicate_of', 'duplicate_archive', 'media_type', 'page', 'reply', 'author', 'host', 'source_url', 'post_url', 'bytes', 'mime', 'resolution_method', 'crc32', 'archive'];
    const rows = entries.map(entry => {
      const item = entry.item || {};
      const sourceUrl = normalizeSourceUrl(item.originalLink || item.url || '');
      const values = [
        entry.archivePath || entry.duplicateOf || '',
        entry.archivePath ? 'yes' : 'no',
        entry.duplicateOf || '',
        entry.duplicateContainer || '',
        item.type === 'embed' ? 'video' : item.type,
        Number(item.pageNumber || 1),
        String(item.postNumber || item.postId || Number(item.postIndex || 0) + 1),
        item.author || 'Unknown',
        hostOf(sourceUrl),
        sourceUrl,
        normalizeSourceUrl(item.postUrl || ''),
        Number(entry.validation?.size || entry.blob?.size || 0),
        entry.validation?.mime || '',
        entry.validation?.method || entry.resolution?.method || '',
        Number(entry.crc >>> 0).toString(16).padStart(8, '0'),
        archiveName,
      ];
      return values.map(csvCell).join(',');
    });
    return `\uFEFF${[columns.join(','), ...rows].join('\r\n')}\r\n`;
  }

  function timestampForFilename() {
    const date = new Date();
    const two = value => String(value).padStart(2, '0');
    return `${date.getFullYear()}${two(date.getMonth() + 1)}${two(date.getDate())}-${two(date.getHours())}${two(date.getMinutes())}`;
  }

  const CRC32_TABLE = (() => {
    const table = new Uint32Array(256);
    for (let index = 0; index < 256; index++) {
      let value = index;
      for (let bit = 0; bit < 8; bit++) value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
      table[index] = value >>> 0;
    }
    return table;
  })();

  function zipCanceledError() {
    return new DOMException('ZIP creation canceled', 'AbortError');
  }

  function updateCrc32(crc, bytes) {
    let value = crc >>> 0;
    for (let index = 0; index < bytes.length; index++) value = CRC32_TABLE[(value ^ bytes[index]) & 0xff] ^ (value >>> 8);
    return value >>> 0;
  }

  function crc32Bytes(bytes) {
    return (updateCrc32(0xffffffff, bytes) ^ 0xffffffff) >>> 0;
  }

  async function crc32Blob(blob, job, completedBytes, archiveBytes) {
    let crc = 0xffffffff;
    const chunkSize = 4 * 1024 * 1024;
    for (let offset = 0; offset < blob.size; offset += chunkSize) {
      if (state.cancelDownload) throw zipCanceledError();
      const bytes = new Uint8Array(await blob.slice(offset, Math.min(blob.size, offset + chunkSize)).arrayBuffer());
      crc = updateCrc32(crc, bytes);
      const processed = Math.min(blob.size, offset + bytes.length);
      const packProgress = blob.size ? (processed / blob.size) * 100 : 100;
      setDownloadJob(job, { status: 'packing', packProgress });
      state.downloadProgress.zipPercent = archiveBytes ? ((completedBytes + processed) / archiveBytes) * 100 : packProgress;
      await new Promise(resolve => setTimeout(resolve, 0));
    }
    if (state.cancelDownload) throw zipCanceledError();
    return (crc ^ 0xffffffff) >>> 0;
  }

  function dosDateTime(date = new Date()) {
    const year = Math.max(1980, date.getFullYear());
    return {
      time: ((date.getHours() & 31) << 11) | ((date.getMinutes() & 63) << 5) | ((Math.floor(date.getSeconds() / 2)) & 31),
      date: (((year - 1980) & 127) << 9) | (((date.getMonth() + 1) & 15) << 5) | (date.getDate() & 31),
    };
  }

  function localZipHeader(filenameBytes, crc, size, modified) {
    const bytes = new Uint8Array(30);
    const view = new DataView(bytes.buffer);
    view.setUint32(0, 0x04034b50, true);
    view.setUint16(4, 20, true);
    view.setUint16(6, 0x0800, true);
    view.setUint16(8, 0, true);
    view.setUint16(10, modified.time, true);
    view.setUint16(12, modified.date, true);
    view.setUint32(14, crc, true);
    view.setUint32(18, size, true);
    view.setUint32(22, size, true);
    view.setUint16(26, filenameBytes.length, true);
    view.setUint16(28, 0, true);
    return bytes;
  }

  function centralZipHeader(filenameBytes, crc, size, offset, modified) {
    const bytes = new Uint8Array(46);
    const view = new DataView(bytes.buffer);
    view.setUint32(0, 0x02014b50, true);
    view.setUint16(4, 20, true);
    view.setUint16(6, 20, true);
    view.setUint16(8, 0x0800, true);
    view.setUint16(10, 0, true);
    view.setUint16(12, modified.time, true);
    view.setUint16(14, modified.date, true);
    view.setUint32(16, crc, true);
    view.setUint32(20, size, true);
    view.setUint32(24, size, true);
    view.setUint16(28, filenameBytes.length, true);
    view.setUint16(30, 0, true);
    view.setUint16(32, 0, true);
    view.setUint16(34, 0, true);
    view.setUint16(36, 0, true);
    view.setUint32(38, 0, true);
    view.setUint32(42, offset, true);
    return bytes;
  }

  function endOfCentralDirectory(entryCount, centralSize, centralOffset) {
    const bytes = new Uint8Array(22);
    const view = new DataView(bytes.buffer);
    view.setUint32(0, 0x06054b50, true);
    view.setUint16(4, 0, true);
    view.setUint16(6, 0, true);
    view.setUint16(8, entryCount, true);
    view.setUint16(10, entryCount, true);
    view.setUint32(12, centralSize, true);
    view.setUint32(16, centralOffset, true);
    view.setUint16(20, 0, true);
    return bytes;
  }

  async function buildStoredZip(entries, partNumber, archiveName, options = archiveDownloadOptions(), session = { fingerprints: new Map() }) {
    const encoder = new TextEncoder();
    const archiveBytes = entries.reduce((sum, entry) => sum + entry.blob.size, 0);
    if (entries.some(entry => entry.blob.size > 0xffffffff) || archiveBytes > 0xffffffff) {
      throw new Error('A ZIP part would exceed the 4 GB browser limit');
    }
    const localParts = [];
    const centralParts = [];
    const includedEntries = [];
    const duplicateEntries = [];
    const persistedFingerprints = session.fingerprints instanceof Map ? session.fingerprints : new Map();
    const pendingFingerprints = new Map();
    let localOffset = 0;
    let completedBytes = 0;
    let centralSize = 0;
    let storedEntryCount = 0;
    const usedNames = new Set();

    const uniqueArchiveName = rawName => {
      const normalized = String(rawName || 'media.bin').replace(/\\/g, '/').split('/').filter(segment => segment && segment !== '.' && segment !== '..').join('/') || 'media.bin';
      const slash = normalized.lastIndexOf('/');
      const directory = slash >= 0 ? normalized.slice(0, slash + 1) : '';
      const basename = slash >= 0 ? normalized.slice(slash + 1) : normalized;
      const match = basename.match(/^(.*?)(\.[^.]+)?$/);
      const stem = match?.[1] || 'media';
      const extension = match?.[2] || '';
      let candidate = `${directory}${stem}${extension}`;
      let suffix = 2;
      while (usedNames.has(candidate.toLowerCase())) candidate = `${directory}${stem}-${suffix++}${extension}`;
      usedNames.add(candidate.toLowerCase());
      return candidate;
    };

    const appendStoredFile = (archivePath, blob, crc) => {
      const filenameBytes = encoder.encode(archivePath);
      const size = blob.size;
      if (size > 0xffffffff || localOffset > 0xffffffff) throw new Error('A ZIP part exceeded the 4 GB browser limit');
      const modified = dosDateTime();
      const localHeader = localZipHeader(filenameBytes, crc, size, modified);
      const centralHeader = centralZipHeader(filenameBytes, crc, size, localOffset, modified);
      localParts.push(localHeader, filenameBytes, blob);
      centralParts.push(centralHeader, filenameBytes);
      localOffset += localHeader.byteLength + filenameBytes.byteLength + size;
      centralSize += centralHeader.byteLength + filenameBytes.byteLength;
      storedEntryCount++;
    };

    for (const entry of entries) {
      if (state.cancelDownload) throw zipCanceledError();
      const size = entry.blob.size;
      const crc = await crc32Blob(entry.blob, entry.job, completedBytes, archiveBytes);
      const fingerprint = `${size}:${crc.toString(16).padStart(8, '0')}`;
      const existing = options.dedupeContent ? (pendingFingerprints.get(fingerprint) || persistedFingerprints.get(fingerprint)) : null;
      entry.crc = crc;
      entry.fingerprint = fingerprint;
      completedBytes += size;
      if (existing) {
        entry.duplicateOf = existing.archivePath;
        entry.duplicateContainer = existing.archiveName;
        duplicateEntries.push(entry);
        setDownloadJob(entry.job, { status: 'packing', packProgress: 100 });
        continue;
      }
      entry.archivePath = uniqueArchiveName(archivePathForEntry(entry, options.archiveLayout));
      entry.job.filename = entry.archivePath;
      appendStoredFile(entry.archivePath, entry.blob, crc);
      includedEntries.push(entry);
      if (options.dedupeContent) pendingFingerprints.set(fingerprint, { archivePath: entry.archivePath, archiveName });
      setDownloadJob(entry.job, { status: 'packing', packProgress: 100 });
      state.downloadProgress.label = `Preparing ZIP part ${partNumber} - ${Math.round((completedBytes / Math.max(1, archiveBytes)) * 100)}%`;
      scheduleDownloadUi();
    }

    if (state.cancelDownload) throw zipCanceledError();
    let manifestAdded = false;
    if (options.includeManifest) {
      const manifestBytes = encoder.encode(buildManifestCsv([...includedEntries, ...duplicateEntries], archiveName));
      const manifestBlob = new Blob([manifestBytes], { type: 'text/csv;charset=utf-8' });
      appendStoredFile(uniqueArchiveName('manifest.csv'), manifestBlob, crc32Bytes(manifestBytes));
      manifestAdded = true;
    }
    if (!storedEntryCount) return { blob: null, includedEntries, duplicateEntries, manifestAdded: false };
    if (localOffset > 0xffffffff || centralSize > 0xffffffff) throw new Error('A ZIP part exceeded the 4 GB browser limit');
    const blob = new Blob([...localParts, ...centralParts, endOfCentralDirectory(storedEntryCount, centralSize, localOffset)], { type: 'application/zip' });
    pendingFingerprints.forEach((value, key) => persistedFingerprints.set(key, value));
    session.fingerprints = persistedFingerprints;
    return { blob, includedEntries, duplicateEntries, manifestAdded };
  }

  function saveBlob(blob, filename) {
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = filename;
    anchor.style.display = 'none';
    document.body.appendChild(anchor);
    anchor.click();
    anchor.remove();
    setTimeout(() => URL.revokeObjectURL(url), 10000);
  }

  async function saveZipPart(entries, partNumber, stamp, options, session) {
    if (!entries.length) return { ok: true, archiveSaved: false };
    const archiveName = `${threadFilePrefix()}-selected-${stamp}-part${String(partNumber).padStart(2, '0')}.zip`;
    state.downloadProgress.label = `Preparing ${archiveName}`;
    state.downloadProgress.zipPercent = 0;
    updateDownloadUi();
    try {
      const result = await buildStoredZip(entries, partNumber, archiveName, options, session);
      if (state.cancelDownload) throw zipCanceledError();
      if (result.blob) saveBlob(result.blob, archiveName);
      result.includedEntries.forEach(entry => {
        setDownloadJob(entry.job, { status: 'saved' });
        rememberDownloaded(entry.item, entry.archivePath, entry.validation, true, archiveName);
        state.downloadProgress.ok++;
      });
      result.duplicateEntries.forEach(entry => {
        setDownloadJob(entry.job, { status: 'duplicate', packProgress: 100, error: `Same bytes as ${entry.duplicateOf}` });
        rememberDownloaded(entry.item, entry.duplicateOf, entry.validation, true, entry.duplicateContainer || archiveName);
        state.downloadProgress.duplicates++;
      });
      persistDownloadHistory();
      updateDownloadedIndicators();
      state.downloadProgress.zipPercent = 0;
      return { ok: true, archiveSaved: Boolean(result.blob), included: result.includedEntries.length, duplicates: result.duplicateEntries.length };
    } catch (error) {
      const canceled = error?.name === 'AbortError' || state.cancelDownload;
      entries.forEach(entry => setDownloadJob(entry.job, { status: canceled ? 'canceled' : 'failed', error: canceled ? 'Canceled before ZIP was saved' : 'ZIP creation failed' }));
      if (!canceled) state.downloadProgress.failed += entries.length;
      state.downloadProgress.zipPercent = 0;
      if (!canceled) console.error('[SimpCity Gallery] ZIP creation failed', error);
      if (!canceled) recordDiagnostic('downloadFailures', 'ZIP creation', error);
      return { ok: false, archiveSaved: false, included: 0, duplicates: 0 };
    }
  }

  async function bulkDownload() {
    if (state.downloading) {
      state.cancelDownload = true;
      state.downloadProgress.label = 'Canceling active downloads...';
      [...state.activeDownloadRequests].forEach(request => {
        try { request.abort(); } catch { /* The request may have just completed. */ }
      });
      updateDownloadUi();
      return;
    }
    const selected = selectedMediaItems();
    if (!selected.length) return notify('Select images or videos first.');

    state.downloadJobs = [];
    state.downloadProgress = blankDownloadProgress();
    state.downloadProgress.total = selected.length;
    const queue = [];
    selected.forEach(item => {
      const alreadySaved = wasDownloaded(item);
      const job = createDownloadJob(item, alreadySaved ? 'skipped' : 'queued');
      if (alreadySaved) state.downloadProgress.skipped++;
      else queue.push({ item, job });
    });
    state.downloadDetailsOpen = true;
    if (!queue.length) {
      state.downloadProgress.current = selected.length;
      state.downloadProgress.label = `All ${selected.length} selected files were already downloaded`;
      updateDownloadUi();
      return notify('Everything selected is already in the gallery download history.', 5200);
    }

    state.downloading = true;
    state.cancelDownload = false;
    const options = archiveDownloadOptions();
    const archiveSession = { fingerprints: new Map() };
    state.downloadProgress.label = `Starting ${queue.length} files with ${options.concurrency} simultaneous downloads`;
    updateDownloadUi();

    let partEntries = [];
    let partBytes = 0;
    let partNumber = 1;
    const stamp = timestampForFilename();
    const flushPart = async () => {
      if (!partEntries.length) return;
      const entries = partEntries;
      partEntries = [];
      partBytes = 0;
      const result = await saveZipPart(entries, partNumber, stamp, options, archiveSession);
      if (result.archiveSaved) partNumber++;
      entries.forEach(entry => { entry.blob = null; });
    };

    const processResult = async result => {
      if (result.verification) state.downloadProgress.verification++;
      else if (result.failed) state.downloadProgress.failed++;
      if (result.blob) {
        if (state.cancelDownload) {
          setDownloadJob(result.job, { status: 'canceled', error: 'Canceled before ZIP was saved' });
          result.blob = null;
        } else {
          if (partEntries.length && (partEntries.length >= options.partMaxFiles || partBytes + result.blob.size > options.partSizeBytes)) await flushPart();
          if (state.cancelDownload) {
            setDownloadJob(result.job, { status: 'canceled', error: 'Canceled before ZIP was saved' });
            result.blob = null;
          } else {
            partEntries.push(result);
            partBytes += result.blob.size;
            if (partEntries.length >= options.partMaxFiles || partBytes >= options.partSizeBytes) await flushPart();
          }
        }
      }
      state.downloadProgress.current = state.downloadJobs.filter(job => ['saved', 'duplicate', 'failed', 'verification', 'skipped', 'canceled'].includes(job.status)).length;
      updateDownloadUi();
    };

    let cursor = 0;
    let processing = Promise.resolve();
    const enqueueResult = result => {
      const next = processing.then(() => processResult(result));
      processing = next.catch(() => {});
      return next;
    };
    const worker = async () => {
      while (!state.cancelDownload) {
        const index = cursor++;
        if (index >= queue.length) return;
        const entry = queue[index];
        const result = await fetchZipEntry(entry.item, entry.job);
        await enqueueResult(result);
      }
    };
    await Promise.all(Array.from({ length: Math.min(options.concurrency, queue.length) }, () => worker()));
    await processing;
    if (!state.cancelDownload) await flushPart();
    else {
      partEntries.forEach(entry => {
        setDownloadJob(entry.job, { status: 'canceled', error: 'Canceled before ZIP was saved' });
        entry.blob = null;
      });
      partEntries = [];
      partBytes = 0;
    }

    const p = state.downloadProgress;
    const stopped = state.cancelDownload;
    if (stopped) {
      state.downloadJobs.forEach(job => {
        if (['queued', 'preparing', 'downloading', 'packing'].includes(job.status)) setDownloadJob(job, { status: 'canceled' });
      });
    }
    state.downloading = false;
    state.cancelDownload = false;
    state.activeDownloadRequests.clear();
    p.current = state.downloadJobs.filter(job => ['saved', 'duplicate', 'failed', 'verification', 'skipped', 'canceled'].includes(job.status)).length;
    p.label = stopped
      ? `Canceled - unfinished ZIP discarded${p.ok ? `, ${p.ok} files were already saved in earlier parts` : ''}`
      : `Finished - ${p.ok} zipped${p.duplicates ? `, ${p.duplicates} duplicates merged` : ''}${p.skipped ? `, ${p.skipped} already downloaded` : ''}${p.verification ? `, ${p.verification} need verification` : ''}${p.failed ? `, ${p.failed} failed` : ''}`;
    updateDownloadUi();
    notify(p.label, 6000);
  }

  function counts() {
    const c = { all: state.items.length, image: 0, video: 0, link: 0, text: 0 };
    state.items.forEach(i => {
      if (i.type === 'image') c.image++;
      else if (i.type === 'video' || i.type === 'embed') c.video++;
      else if (i.type === 'link') c.link++;
      else if (i.type === 'text') c.text++;
    });
    return c;
  }

  function itemSourceHost(item) {
    if (item.type === 'text') return '';
    return hostOf(item.originalLink || item.url || item.postUrl);
  }

  function compareThreadOrder(a, b) {
    return (Number(a.pageNumber || 1) - Number(b.pageNumber || 1)) ||
      (Number(a.postIndex || 0) - Number(b.postIndex || 0)) ||
      (Number(a.mediaIndex || 0) - Number(b.mediaIndex || 0)) ||
      (Number(TYPE_ORDER[a.type] ?? 9) - Number(TYPE_ORDER[b.type] ?? 9));
  }

  function visibleItems() {
    const signature = JSON.stringify([
      datasetRevision, selectionRevision, state.filter, state.query, state.sort, state.groupBy,
      state.authorFilter, state.hostFilter, state.selectedOnly, state.perPage, state.viewPage,
    ]);
    if (visibleItemsCache.signature === signature) return visibleItemsCache.items;
    const q = state.query.trim().toLowerCase();
    const filtered = state.items.filter(item => {
      const typeMatch = state.filter === 'all' || item.type === state.filter ||
        (state.filter === 'video' && item.type === 'embed');
      const host = itemSourceHost(item);
      const authorMatch = state.authorFilter === 'all' || item.author === state.authorFilter;
      const hostMatch = state.hostFilter === 'all' || host === state.hostFilter;
      const selectedMatch = !state.selectedOnly || (isDownloadableMedia(item) && state.selected.has(item.selectionKey));
      return typeMatch && authorMatch && hostMatch && selectedMatch && (!q || item.searchText.includes(q));
    });

    const thread = (a, b) => compareThreadOrder(a, b);
    const alpha = (left, right) => String(left || '').localeCompare(String(right || ''), undefined, { sensitivity: 'base' });
    if (state.sort === 'thread-desc') filtered.sort((a, b) => thread(b, a));
    else if (state.sort === 'author') filtered.sort((a, b) => alpha(a.author, b.author) || thread(a, b));
    else if (state.sort === 'host') filtered.sort((a, b) => alpha(itemSourceHost(a), itemSourceHost(b)) || thread(a, b));
    else if (state.sort === 'type') filtered.sort((a, b) => (TYPE_ORDER[a.type] ?? 9) - (TYPE_ORDER[b.type] ?? 9) || thread(a, b));
    else filtered.sort(thread);
    visibleItemsCache = { signature, items: filtered };
    return filtered;
  }

  function replyKey(item) {
    return `${item.pageNumber || 1}|${item.postId || item.postUrl || item.postIndex || 0}`;
  }

  function setReplyCollapsed(key, collapsed) {
    const normalizedKey = String(key || '');
    if (!normalizedKey) return false;
    const changed = collapsed ? !state.collapsedReplies.has(normalizedKey) : state.collapsedReplies.has(normalizedKey);
    if (collapsed) state.collapsedReplies.add(normalizedKey);
    else state.collapsedReplies.delete(normalizedKey);
    return changed;
  }

  function setReplyGroupsCollapsed(items, collapsed) {
    const keys = new Set(items.map(replyKey));
    let changed = false;
    keys.forEach(key => { if (setReplyCollapsed(key, collapsed)) changed = true; });
    return changed;
  }

  function matchesActiveSelectionType(item) {
    if (state.filter === 'image') return item.type === 'image';
    if (state.filter === 'video') return item.type === 'video' || item.type === 'embed';
    return !state.filter || state.filter === 'all';
  }

  function replyDownloadableItems(key) {
    return state.items.filter(item => replyKey(item) === key && isDownloadableMedia(item) && matchesActiveSelectionType(item));
  }

  function replySelectionState(key) {
    const eligible = replyDownloadableItems(key);
    const selected = eligible.reduce((total, item) => total + (state.selected.has(item.selectionKey) ? 1 : 0), 0);
    return {
      eligible,
      total: eligible.length,
      selected,
      checked: eligible.length > 0 && selected === eligible.length,
      indeterminate: selected > 0 && selected < eligible.length,
    };
  }

  function setReplySelected(key, selected) {
    const { eligible } = replySelectionState(key);
    let changed = false;
    eligible.forEach(item => {
      const hasItem = state.selected.has(item.selectionKey);
      if (selected && !hasItem) {
        state.selected.add(item.selectionKey);
        changed = true;
      } else if (!selected && hasItem) {
        state.selected.delete(item.selectionKey);
        changed = true;
      }
    });
    if (changed) invalidateVisibleItems({ selection: true });
    return changed;
  }

  function buildViewPages(items) {
    const limit = state.perPage;
    if (!items.length) return [[]];
    if (state.groupBy !== 'reply') {
      const pages = [];
      for (let index = 0; index < items.length; index += limit) pages.push(items.slice(index, index + limit));
      return pages;
    }

    const grouped = new Map();
    items.forEach(item => {
      const key = replyKey(item);
      if (!grouped.has(key)) grouped.set(key, []);
      grouped.get(key).push(item);
    });
    const units = [];
    grouped.forEach(groupItems => {
      for (let index = 0; index < groupItems.length; index += limit) units.push(groupItems.slice(index, index + limit));
    });
    const pages = [];
    let current = [];
    units.forEach(unit => {
      if (current.length && current.length + unit.length > limit) {
        pages.push(current);
        current = [];
      }
      current.push(...unit);
    });
    if (current.length) pages.push(current);
    return pages.length ? pages : [[]];
  }

  function mediaItems() {
    return state.renderedItems.filter(isMediaItem);
  }

  function viewerMediaItems() {
    return state.viewerItems.length ? state.viewerItems : mediaItems();
  }

  function currentViewerItem() {
    return viewerMediaItems()[state.lightboxIndex] || null;
  }

  function observeFullImages(items) {
    state.imageObserver?.disconnect();
    const app = document.getElementById(APP_ID);
    if (!app || typeof IntersectionObserver !== 'function') return;
    state.imageObserver = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) return;
        state.imageObserver.unobserve(entry.target);
        const item = items[Number(entry.target.dataset.resolveIndex)];
        if (!item || item.type !== 'image') return;
        resolveForDisplay(item).then(result => {
          if (result.url && entry.target.isConnected && entry.target.src !== result.url) entry.target.src = result.url;
        });
      });
    }, { root: app.querySelector('.scg-scroll'), rootMargin: '700px 0px', threshold: 0.01 });
    app.querySelectorAll('img[data-resolve-index]').forEach(img => state.imageObserver.observe(img));
  }

  function releaseRenderedMedia() {
    state.imageObserver?.disconnect();
    const grid = document.querySelector(`#${APP_ID} .scg-grid`);
    if (!grid) return;
    grid.querySelectorAll('video').forEach(video => {
      try { video.pause(); } catch { /* Ignore detached media errors. */ }
      video.removeAttribute('src');
      video.querySelectorAll('source').forEach(source => source.removeAttribute('src'));
      try { video.load(); } catch { /* Ignore detached media errors. */ }
    });
    grid.querySelectorAll('iframe').forEach(frame => frame.setAttribute('src', 'about:blank'));
    grid.querySelectorAll('img').forEach(img => img.removeAttribute('src'));
    grid.replaceChildren();
  }

  function card(item, index) {
    const sourceHost = hostOf(item.originalLink || item.url || item.postUrl);
    const meta = `<div class="scg-meta"><span>${escapeHtml(item.author)}</span><a href="${escapeHtml(item.postUrl)}" target="_blank" rel="noopener">${icon('external')}<span>View post</span></a></div>`;
    const isSelected = state.selectionMode && state.selected.has(item.selectionKey);
    const downloaded = isMediaItem(item) && wasDownloaded(item);
    const legacyDownload = isMediaItem(item) && hasLegacyDownload(item);
    const historyClass = downloaded ? 'downloaded' : (legacyDownload ? 'download-legacy' : '');
    const selection = isDownloadableMedia(item)
      ? `<label class="scg-select" title="Select for bulk download"><input type="checkbox" data-select="${index}" ${state.selected.has(item.selectionKey) ? 'checked' : ''}><span></span></label>`
      : '';
    const typeLabel = item.type === 'image' ? 'IMAGE' : (item.type === 'video' || item.type === 'embed' ? 'VIDEO' : item.type.toUpperCase());
    const historyLabel = downloaded ? 'VERIFIED' : (legacyDownload ? 'LEGACY' : '');
    const historyTitle = downloaded ? 'Validated media recorded in download history' : (legacyDownload ? 'Older unverified history entry; this item will not be skipped' : '');
    const badge = `<div class="scg-badge"><b>${typeLabel}</b><span>${escapeHtml(sourceHost)}</span><em class="scg-saved-status ${historyLabel ? 'visible' : ''} ${legacyDownload ? 'legacy' : ''}" title="${escapeHtml(historyTitle)}">${historyLabel}</em></div>`;
    const actions = `<div class="scg-actions"><button data-open="${index}">${iconWell('info')}<span>View</span></button><button data-download="${index}" class="scg-download-action">${iconWell('download', 'scg-icon-well-primary')}<span>${downloaded ? 'Download again' : (legacyDownload ? 'Download verified copy' : 'Download')}</span></button></div>`;
    if (item.type === 'image') {
      const displayUrl = item.resolution?.candidate?.url || item.url || item.thumb;
      return `<article class="scg-card scg-media ${isSelected ? 'selected' : ''} ${historyClass}" data-index="${index}">${selection}${badge}<button class="scg-preview" data-open="${index}" title="View original image"><img loading="lazy" data-resolve-index="${index}" src="${escapeHtml(displayUrl)}" alt="${escapeHtml(item.caption)}"></button>${actions}${meta}</article>`;
    }
    if (item.type === 'video' && VIDEO_EXT.test(item.url)) {
      return `<article class="scg-card scg-media ${isSelected ? 'selected' : ''} ${historyClass}" data-index="${index}">${selection}${badge}<video controls preload="none" poster="${escapeHtml(item.poster || '')}" src="${escapeHtml(item.url)}"></video>${actions}${meta}</article>`;
    }
    if (item.type === 'video' || item.type === 'embed') {
      const transformed = embedUrl(item.url);
      if (item.wasFrame || transformed !== unwrapSimpcityRedirect(item.url)) {
        return `<article class="scg-card scg-media ${isSelected ? 'selected' : ''} ${historyClass}" data-index="${index}">${selection}${badge}<div class="scg-embed scg-embed-placeholder"><button data-load-player="${index}">${iconWell('play', 'scg-icon-well-primary')}<b>Play video</b><span>Load ${escapeHtml(sourceHost)} player</span></button></div>${actions}${meta}</article>`;
      }
      return `<article class="scg-card scg-linkcard ${isSelected ? 'selected' : ''} ${historyClass}" data-index="${index}">${selection}${badge}<a class="scg-host" href="${escapeHtml(item.url)}" target="_blank" rel="noopener"><strong>${escapeHtml(item.title || 'Open video host')}</strong><small>${escapeHtml(hostOf(item.url))}</small></a>${actions}${meta}</article>`;
    }
    if (item.type === 'link') {
      return `<article class="scg-card scg-linkcard" data-index="${index}">${badge}<a class="scg-host" href="${escapeHtml(item.url)}" target="_blank" rel="noopener"><strong>${escapeHtml(item.title || item.url)}</strong><small>${escapeHtml(hostOf(item.url))}</small></a>${meta}</article>`;
    }
    const expand = item.text.length > 360 ? `<button class="scg-expand" data-expand>${iconWell('chevron')}<span>Show more</span></button>` : '';
    return `<article class="scg-card scg-textcard" data-index="${index}">${badge}<p>${escapeHtml(item.text)}</p>${expand}${meta}</article>`;
  }

  function setSelectOptions(select, options, selectedValue) {
    if (!select) return selectedValue;
    const signature = JSON.stringify(options);
    if (select.dataset.signature !== signature) {
      select.replaceChildren(...options.map(option => {
        const node = document.createElement('option');
        node.value = option.value;
        node.textContent = option.label;
        return node;
      }));
      select.dataset.signature = signature;
    }
    const valid = options.some(option => option.value === String(selectedValue));
    select.value = valid ? String(selectedValue) : options[0]?.value || '';
    return select.value;
  }

  function refreshFilterControls() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    const authors = [...new Set(state.items.map(item => item.author).filter(Boolean))]
      .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
    const hosts = [...new Set(state.items.map(itemSourceHost).filter(Boolean))]
      .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }));
    state.authorFilter = setSelectOptions(app.querySelector('[data-author-filter]'), [
      { value: 'all', label: `All authors (${authors.length})` },
      ...authors.map(author => ({ value: author, label: author })),
    ], state.authorFilter);
    state.hostFilter = setSelectOptions(app.querySelector('[data-host-filter]'), [
      { value: 'all', label: `All hosts (${hosts.length})` },
      ...hosts.map(host => ({ value: host, label: host })),
    ], state.hostFilter);

    const pageOptions = Array.from({ length: state.threadPageCount }, (_, index) => ({
      value: String(index + 1),
      label: `Page ${index + 1}`,
    }));
    state.sourcePage = Number(setSelectOptions(app.querySelector('[data-source-page]'), pageOptions, state.sourcePage) || 1);
    const sourceSelect = app.querySelector('[data-source-page]');
    if (sourceSelect) sourceSelect.disabled = state.scanning;
  }

  function replyGroupsMarkup(items) {
    const groups = new Map();
    items.forEach((item, index) => {
      const key = replyKey(item);
      if (!groups.has(key)) groups.set(key, { first: item, entries: [] });
      groups.get(key).entries.push({ item, index });
    });
    return [...groups.entries()].map(([key, group]) => {
      const first = group.first;
      const replyLabel = first.postNumber ? `Reply #${first.postNumber}` : `Reply ${Number(first.postIndex || 0) + 1}`;
      const collapsed = state.collapsedReplies.has(key);
      const toggleLabel = collapsed ? `Expand ${replyLabel}` : `Collapse ${replyLabel}`;
      const replySelection = replySelectionState(key);
      const replySelect = state.selectionMode && replySelection.total
        ? `<label class="scg-reply-select">${iconWell('select')}<input type="checkbox" data-reply-select="${escapeHtml(key)}" ${replySelection.checked ? 'checked' : ''} aria-label="Select all downloadable media in ${escapeHtml(replyLabel)}"><span>Select reply</span></label>`
        : '';
      return `<section class="scg-reply-group ${collapsed ? 'collapsed' : ''}" data-reply-key="${escapeHtml(key)}"><header>${replySelect}<button class="scg-reply-toggle" data-reply-toggle="${escapeHtml(key)}" aria-expanded="${String(!collapsed)}" aria-label="${escapeHtml(toggleLabel)}">${iconWell('chevron')}<span>${collapsed ? 'Expand' : 'Collapse'}</span></button><div><b>${escapeHtml(replyLabel)}</b><span>${escapeHtml(first.author || 'Unknown')}</span><span>Page ${Number(first.pageNumber || 1)}</span><span>${group.entries.length} item${group.entries.length === 1 ? '' : 's'}</span><span data-reply-selection-count>${replySelection.selected} of ${replySelection.total} downloadable selected</span><em class="scg-collapsed-status">${collapsed ? 'Collapsed' : ''}</em></div><a href="${escapeHtml(first.postUrl)}" target="_blank" rel="noopener">${icon('external')}<span>View reply</span></a></header><div class="scg-group-grid">${collapsed ? '' : group.entries.map(entry => card(entry.item, entry.index)).join('')}</div></section>`;
    }).join('');
  }

  function updateViewPager(matchedCount) {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    const pageSelect = app.querySelector('[data-view-page]');
    const options = Array.from({ length: state.viewPages }, (_, index) => ({ value: String(index + 1), label: `View ${index + 1}` }));
    setSelectOptions(pageSelect, options, state.viewPage);
    app.querySelector('[data-action="view-prev"]').disabled = state.scanning || state.viewPage <= 1;
    app.querySelector('[data-action="view-next"]').disabled = state.scanning || state.viewPage >= state.viewPages;
    pageSelect.disabled = state.scanning;
    app.querySelector('[data-page-size]').disabled = state.scanning;
    app.querySelector('[data-page-size]').value = String(state.perPage);
    app.querySelector('.scg-view-summary').textContent = `${matchedCount} matched - showing ${state.renderedItems.length}`;
  }

  function emptyStateMarkup() {
    const indexed = state.items.length;
    const heading = indexed ? 'No media matches these filters' : 'Nothing indexed yet';
    const hint = indexed
      ? 'Try clearing the search box, switching the type filter back to All, or opening Refine and resetting the author, host and grouping facets.'
      : 'Use "This page" to index the reply page you are on, or "Scan thread" to walk every page. Media stays unloaded until you scroll it into view.';
    return `<div class="scg-empty" role="status"><span class="scg-empty-art">${icon(indexed ? 'search' : 'scan')}</span><b>${escapeHtml(heading)}</b><span>${escapeHtml(hint)}</span></div>`;
  }

  function skeletonMarkup(count = 12) {
    return Array.from({ length: count }, (_, index) => `<div class="scg-skeleton" aria-hidden="true"><div class="scg-skeleton-media" style="--scg-skeleton-height:${160 + ((index % 4) * 36)}px"></div><div class="scg-skeleton-line"></div><div class="scg-skeleton-line short"></div></div>`).join('');
  }

  function render() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    refreshLaunchButton();
    if (!app.classList.contains('open')) {
      releaseRenderedMedia();
      state.renderedItems = [];
      return;
    }
    const c = counts();
    app.classList.toggle('scanning', state.scanning);
    app.classList.toggle('selecting', state.selectionMode);
    app.classList.toggle('grouped-replies', state.groupBy === 'reply');
    applyShellState();
    app.querySelectorAll('[data-filter], [data-search], [data-card-scale], .scg-refinebar button, .scg-refinebar select, .scg-viewbar button, .scg-viewbar select, [data-action="selection-mode"], [data-action="page"], [data-action="load-source-page"]').forEach(control => {
      control.disabled = state.scanning;
    });
    app.querySelectorAll('[data-filter]').forEach(btn => {
      const active = btn.dataset.filter === state.filter;
      btn.classList.toggle('active', active);
      btn.setAttribute('aria-pressed', String(active));
      const key = btn.dataset.filter;
      const total = c[key] ?? 0;
      btn.querySelector('span').textContent = total;
      const name = btn.querySelector('b')?.textContent || key;
      btn.setAttribute('aria-label', `${name}, ${total} item${total === 1 ? '' : 's'}`);
    });
    refreshFilterControls();
    const matched = state.scanning ? [] : visibleItems();
    const pages = buildViewPages(matched);
    state.viewPages = pages.length;
    state.viewPage = Math.min(Math.max(1, state.viewPage), state.viewPages);
    state.renderedItems = pages[state.viewPage - 1] || [];
    const grid = app.querySelector('.scg-grid');
    releaseRenderedMedia();
    grid.classList.toggle('grouped', state.groupBy === 'reply');
    grid.innerHTML = state.scanning
      ? skeletonMarkup(state.compact ? 16 : 12)
      : state.renderedItems.length
        ? (state.groupBy === 'reply' ? replyGroupsMarkup(state.renderedItems) : state.renderedItems.map((item, index) => card(item, index)).join(''))
        : emptyStateMarkup();
    if (!state.scanning) observeFullImages(state.renderedItems);
    app.querySelector('.scg-status').textContent = state.scanning
      ? `Scanning ${state.scannedPages || 1} of ${state.threadPageCount}`
      : state.sourceLabel;
    const activityDetail = app.querySelector('[data-activity-detail]');
    if (activityDetail) activityDetail.textContent = state.scanning
      ? `${state.items.length} items indexed · media stays unloaded`
      : `${LAYOUT_LABELS[state.layoutMode]} · ${matched.length} matched · ${state.items.length} indexed · view ${state.viewPage} of ${state.viewPages}`;
    setIconButton(app.querySelector('[data-action="thread"]'), state.scanning ? 'close' : 'scan', state.scanning ? 'Cancel scan' : 'Scan entire thread');
    setIconButton(app.querySelector('[data-action="selection-mode"]'), state.selectionMode ? 'close' : 'select', state.selectionMode
      ? 'Exit selection'
      : `Select media${state.selected.size ? ` (${selectedMediaItems().length})` : ''}`);
    const selectedOnly = app.querySelector('[data-selected-only]');
    selectedOnly.classList.toggle('active', state.selectedOnly);
    selectedOnly.setAttribute('aria-pressed', String(Boolean(state.selectedOnly)));
    refreshRefineTrigger();
    app.querySelector('[data-sort]').value = state.sort;
    app.querySelector('[data-group-by]').value = state.groupBy;
    updateViewPager(matched.length);
    updateSelectionUi();
    updateDownloadUi();
    refreshThreadHeader();
    if (app.querySelector('.scg-settings-panel.open')) refreshSettingsPanel();
  }

  function normalizeMediaDimensions(width, height) {
    const normalizedWidth = Math.max(0, Math.round(Number(width) || 0));
    const normalizedHeight = Math.max(0, Math.round(Number(height) || 0));
    return normalizedWidth && normalizedHeight ? { width: normalizedWidth, height: normalizedHeight } : null;
  }

  function mediaResolutionText(dimensions, qualifier = '') {
    const normalized = normalizeMediaDimensions(dimensions?.width, dimensions?.height);
    if (!normalized) return 'Unavailable';
    const value = `${normalized.width.toLocaleString()} × ${normalized.height.toLocaleString()}`;
    return qualifier ? `${qualifier} · ${value}` : value;
  }

  function cachedMediaResolution(item) {
    const dimensions = normalizeMediaDimensions(item?.mediaDimensions?.width, item?.mediaDimensions?.height);
    if (!dimensions) return 'Detecting…';
    return mediaResolutionText(dimensions, item.mediaDimensions.qualifier || '');
  }

  function updateViewerResolution(box, item, width, height, qualifier = '', mediaUrl = '') {
    const dimensions = normalizeMediaDimensions(width, height);
    if (!box || !item || !dimensions) return false;
    item.mediaDimensions = { ...dimensions, qualifier, mediaUrl: absoluteUrl(mediaUrl), detectedAt: Date.now() };
    const value = mediaResolutionText(dimensions, qualifier);
    box.querySelectorAll('[data-viewer-resolution]').forEach(node => {
      node.textContent = value;
      node.setAttribute('title', `${dimensions.width} by ${dimensions.height} pixels`);
    });
    return true;
  }

  function setViewerResolutionStatus(box, value) {
    if (!box) return;
    box.querySelectorAll('[data-viewer-resolution]').forEach(node => {
      node.textContent = value;
      node.removeAttribute('title');
    });
  }

  function bindViewerVideoResolution(video, box, item, stillCurrent) {
    if (!video) {
      if (stillCurrent()) setViewerResolutionStatus(box, 'Unavailable');
      return;
    }
    const update = () => {
      if (!stillCurrent()) return;
      if (!updateViewerResolution(box, item, video.videoWidth, video.videoHeight, '', video.currentSrc || video.src)) {
        setViewerResolutionStatus(box, 'Unavailable');
      }
    };
    video.addEventListener('loadedmetadata', update);
    video.addEventListener('error', () => {
      if (stillCurrent()) setViewerResolutionStatus(box, 'Unavailable');
    }, { once: true });
    if (video.readyState >= 1) update();
  }

  function probeVideoDimensions(url, timeoutMs = 12000) {
    const mediaUrl = absoluteUrl(url);
    if (!mediaUrl) return Promise.resolve(null);
    return new Promise(resolve => {
      const video = document.createElement('video');
      let settled = false;
      const finish = dimensions => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        video.onloadedmetadata = null;
        video.onerror = null;
        video.removeAttribute('src');
        try { video.load(); } catch { /* Detached metadata probe. */ }
        resolve(dimensions);
      };
      const timer = setTimeout(() => finish(null), timeoutMs);
      video.preload = 'metadata';
      video.muted = true;
      video.playsInline = true;
      video.onloadedmetadata = () => finish(normalizeMediaDimensions(video.videoWidth, video.videoHeight));
      video.onerror = () => finish(null);
      video.src = mediaUrl;
    });
  }

  function viewerItemTitle(item) {
    return String(item.caption || item.title || (item.type === 'image' ? 'Image' : 'Video')).replace(/\s+/g, ' ').trim();
  }

  function viewerReplyLabel(item) {
    return item.postNumber ? `Reply #${item.postNumber}` : `Reply ${Number(item.postIndex || 0) + 1}`;
  }

  function viewerStripMarkup(items, activeIndex) {
    const radius = 7;
    const start = Math.max(0, Math.min(activeIndex - radius, Math.max(0, items.length - ((radius * 2) + 1))));
    const end = Math.min(items.length, start + ((radius * 2) + 1));
    return items.slice(start, end).map((item, offset) => {
      const index = start + offset;
      const preview = absoluteUrl(item.type === 'image' ? item.thumb : item.poster);
      const label = `${index + 1}. ${viewerItemTitle(item)}`;
      return `<button class="scg-viewer-thumb ${index === activeIndex ? 'active' : ''}" data-viewer-index="${index}" title="${escapeHtml(label)}" aria-label="${escapeHtml(label)}" aria-current="${index === activeIndex ? 'true' : 'false'}">${preview ? `<img loading="lazy" src="${escapeHtml(preview)}" alt="">` : `<span>${icon(item.type === 'image' ? 'image' : 'play')}</span>`}<em>${index + 1}</em></button>`;
    }).join('');
  }

  function resetViewerTransform(update = true) {
    state.viewerZoom = 1;
    state.viewerPanX = 0;
    state.viewerPanY = 0;
    if (update) updateViewerTransform();
  }

  function updateViewerTransform() {
    const box = document.querySelector(`#${APP_ID} .scg-lightbox.open`);
    const image = box?.querySelector('.scg-viewer-image');
    if (!box || !image) return;
    const transform = `translate3d(${state.viewerPanX}px,${state.viewerPanY}px,0) scale(${state.viewerZoom})`;
    image.style.setProperty('--scg-viewer-transform', transform);
    image.style.setProperty('transform', transform, 'important');
    box.classList.toggle('viewer-zoomed', state.viewerZoom > 1.01);
    const value = box.querySelector('[data-viewer-zoom-value]');
    if (value) value.textContent = `${Math.round(state.viewerZoom * 100)}%`;
    box.querySelector('[data-viewer-zoom-out]')?.toggleAttribute('disabled', state.viewerZoom <= 1);
  }

  function setViewerZoom(value) {
    const next = Math.min(6, Math.max(1, Number(value) || 1));
    state.viewerZoom = Math.round(next * 100) / 100;
    if (state.viewerZoom === 1) {
      state.viewerPanX = 0;
      state.viewerPanY = 0;
    }
    updateViewerTransform();
    requestAnimationFrame(updateViewerTransform);
  }

  function toggleViewerInfo(force) {
    const box = document.querySelector(`#${APP_ID} .scg-lightbox.open`);
    if (!box) return;
    state.viewerInfoOpen = typeof force === 'boolean' ? force : !state.viewerInfoOpen;
    box.classList.toggle('details-hidden', !state.viewerInfoOpen);
    const button = box.querySelector('[data-viewer-info]');
    if (button) button.setAttribute('aria-pressed', String(state.viewerInfoOpen));
    wakeViewerChrome();
  }

  async function toggleViewerFullscreen() {
    const box = document.querySelector(`#${APP_ID} .scg-lightbox.open`);
    if (!box) return;
    try {
      if (document.fullscreenElement) await document.exitFullscreen();
      else await box.requestFullscreen();
    } catch (error) {
      notify(`Fullscreen is unavailable: ${error.message}`);
    }
  }

  function bindViewerImageInteractions(image, stage) {
    image.draggable = false;
    image.ondblclick = event => {
      event.preventDefault();
      setViewerZoom(state.viewerZoom > 1 ? 1 : 2);
    };
    stage.addEventListener('wheel', event => {
      if (!image.isConnected) return;
      event.preventDefault();
      setViewerZoom(state.viewerZoom + (event.deltaY < 0 ? .25 : -.25));
    }, { passive: false });
    let gesture = null;
    stage.onpointerdown = event => {
      if (event.button !== 0) return;
      gesture = { x: event.clientX, y: event.clientY, panX: state.viewerPanX, panY: state.viewerPanY, pointerType: event.pointerType };
      stage.setPointerCapture?.(event.pointerId);
      if (state.viewerZoom > 1) image.classList.add('dragging');
    };
    stage.onpointermove = event => {
      if (!gesture || state.viewerZoom <= 1) return;
      state.viewerPanX = gesture.panX + event.clientX - gesture.x;
      state.viewerPanY = gesture.panY + event.clientY - gesture.y;
      updateViewerTransform();
    };
    const finish = event => {
      if (!gesture) return;
      const deltaX = event.clientX - gesture.x;
      const deltaY = event.clientY - gesture.y;
      const swipe = state.viewerZoom <= 1 && gesture.pointerType === 'touch' && Math.abs(deltaX) > 65 && Math.abs(deltaX) > Math.abs(deltaY) * 1.25;
      gesture = null;
      image.classList.remove('dragging');
      if (swipe) navigateLightbox(deltaX > 0 ? -1 : 1);
    };
    stage.onpointerup = finish;
    stage.onpointercancel = finish;
  }

  function bindViewerVideoLifecycle(video, stage) {
    if (!video) return;
    const stopBuffering = () => stage.classList.remove('viewer-buffering');
    video.addEventListener('loadstart', () => stage.classList.add('viewer-buffering'));
    video.addEventListener('waiting', () => stage.classList.add('viewer-buffering'));
    video.addEventListener('canplay', stopBuffering);
    video.addEventListener('playing', stopBuffering);
    video.addEventListener('error', () => {
      stopBuffering();
      stage.innerHTML = '<div class="scg-viewer-error">This video could not be played here. Use Open source to watch it on the host.</div>';
    }, { once: true });
  }

  let viewerChromeTimer = 0;

  function viewerChromeEngaged(box) {
    const active = document.activeElement;
    const keyboardFocused = active instanceof Element && active.matches(':focus-visible');
    return [...box.querySelectorAll('.scg-viewer-topbar, .scg-viewer-footer')].some(panel =>
      panel.matches(':hover') || (keyboardFocused && panel.contains(active)));
  }

  function armViewerChromeIdle(box, delay = 2600) {
    clearTimeout(viewerChromeTimer);
    viewerChromeTimer = window.setTimeout(() => {
      if (!box.classList.contains('open')) return;
      if (viewerChromeEngaged(box)) {
        armViewerChromeIdle(box, 800);
        return;
      }
      box.classList.add('viewer-idle');
    }, delay);
  }

  function wakeViewerChrome() {
    const box = document.querySelector(`#${APP_ID} .scg-lightbox.open`);
    if (!box) return;
    box.classList.remove('viewer-idle');
    armViewerChromeIdle(box);
  }

  function closeLightbox() {
    const box = document.querySelector(`#${APP_ID} .scg-lightbox`);
    if (!box) return;
    clearTimeout(viewerChromeTimer);
    state.viewerRenderToken++;
    box.querySelectorAll('video').forEach(video => { try { video.pause(); } catch { /* Detached player. */ } });
    box.querySelectorAll('iframe').forEach(frame => frame.setAttribute('src', 'about:blank'));
    if (document.fullscreenElement === box) document.exitFullscreen?.().catch?.(() => {});
    box.classList.remove('open', 'viewer-zoomed', 'details-hidden', 'viewer-idle');
    box.innerHTML = '';
    state.viewerItems = [];
    resetViewerTransform(false);
    const returnFocus = state.viewerReturnFocus;
    state.viewerReturnFocus = null;
    returnFocus?.focus?.({ preventScroll: true });
  }

  async function renderLightbox() {
    const items = viewerMediaItems();
    if (!items.length) return closeLightbox();
    state.lightboxIndex = (state.lightboxIndex + items.length) % items.length;
    const item = items[state.lightboxIndex];
    const box = document.querySelector(`#${APP_ID} .scg-lightbox`);
    const sourceUrl = item.originalLink || item.url;
    const sourceHost = hostOf(sourceUrl);
    const title = viewerItemTitle(item);
    const typeLabel = item.type === 'image' ? 'Image' : 'Video';
    const savedLabel = wasDownloaded(item) ? 'Verified download saved' : (hasLegacyDownload(item) ? 'Legacy download marker' : 'Not downloaded');
    const resolutionLabel = cachedMediaResolution(item);
    const token = ++state.viewerRenderToken;
    const wasOpen = box.classList.contains('open');
    resetViewerTransform(false);
    box.innerHTML = `
      <div class="scg-viewer-shell">
        <header class="scg-viewer-topbar">
          <div class="scg-viewer-identity"><b>${state.lightboxIndex + 1}<span>/ ${items.length}</span></b><div><div class="scg-viewer-titleline"><strong title="${escapeHtml(title)}">${escapeHtml(title)}</strong><span class="scg-viewer-resolution" data-viewer-resolution>${escapeHtml(resolutionLabel)}</span></div><small>${escapeHtml(item.author || 'Unknown')} · ${escapeHtml(viewerReplyLabel(item))} · Page ${Number(item.pageNumber || 1)} · ${escapeHtml(sourceHost)}</small></div></div>
          <div class="scg-viewer-tools" role="toolbar" aria-label="Viewer tools">
            <div class="scg-viewer-zoom ${item.type === 'image' ? '' : 'unavailable'}" role="group" aria-label="Zoom"><button data-viewer-zoom-out data-tooltip="Zoom out" aria-label="Zoom out">${iconWell('zoomOut')}</button><button data-viewer-fit data-tooltip="Fit image" aria-label="Fit image">${iconWell('fit')}<span data-viewer-zoom-value>100%</span></button><button data-viewer-zoom-in data-tooltip="Zoom in" aria-label="Zoom in">${iconWell('zoomIn')}</button></div>
            <button class="scg-viewer-download" data-download-current data-tooltip="${wasDownloaded(item) ? 'Download again (D)' : 'Download (D)'}" aria-label="${wasDownloaded(item) ? 'Download again' : 'Download media'}">${iconWell('download', 'scg-icon-well-primary')}</button>
            <button data-viewer-info data-tooltip="Toggle details (I)" aria-label="Toggle details" aria-pressed="${state.viewerInfoOpen}">${iconWell('info')}</button>
            <button data-viewer-fullscreen data-tooltip="Fullscreen (F)" aria-label="Toggle fullscreen">${iconWell('fullscreen')}</button>
            <button class="scg-lightbox-close scg-tip-end" data-viewer-close data-tooltip="Close viewer (Esc)" aria-label="Close viewer">${iconWell('close', 'scg-icon-well-danger')}</button>
          </div>
        </header>
        <div class="scg-viewer-body">
          <button class="scg-nav scg-prev" data-viewer-prev aria-label="Previous media">${icon('previous')}</button>
          <section class="scg-lightbox-stage" data-viewer-stage aria-live="polite"><div class="scg-viewer-loading"><i></i><span>Resolving original media…</span></div></section>
          <aside class="scg-viewer-details">
            <div class="scg-viewer-details-head"><span>${icon(item.type === 'image' ? 'image' : 'video')}</span><div><b>${escapeHtml(typeLabel)}</b><small>${escapeHtml(sourceHost)}</small></div></div>
            ${item.caption ? `<p class="scg-viewer-caption">${escapeHtml(item.caption)}</p>` : ''}
            <dl><div><dt>Resolution</dt><dd data-viewer-resolution>${escapeHtml(resolutionLabel)}</dd></div><div><dt>Posted by</dt><dd>${escapeHtml(item.author || 'Unknown')}</dd></div><div><dt>Location</dt><dd>${escapeHtml(viewerReplyLabel(item))}, page ${Number(item.pageNumber || 1)}</dd></div><div><dt>Download</dt><dd>${escapeHtml(savedLabel)}</dd></div><div><dt>Collection</dt><dd>${items.length} filtered media items</dd></div></dl>
            <div class="scg-viewer-details-links"><a href="${escapeHtml(sourceUrl)}" target="_blank" rel="noopener">${icon('external')}<span>Open source</span></a><a href="${escapeHtml(item.postUrl)}" target="_blank" rel="noopener">${icon('external')}<span>View reply</span></a></div>
            <p class="scg-viewer-shortcuts"><b>Keys</b><br>← → previous / next · wheel or + − zoom · 0 fit<br>I details · F fullscreen · Space play or pause<br>D download · S select · Esc close</p>
          </aside>
          <button class="scg-nav scg-next" data-viewer-next aria-label="Next media">${icon('next')}</button>
        </div>
        <footer class="scg-viewer-footer">
          <div class="scg-viewer-strip" role="group" aria-label="Nearby media">${viewerStripMarkup(items, state.lightboxIndex)}</div>
          <div class="scg-lightbox-actions"><button class="scg-tip-up scg-tip-end" data-select-current data-tooltip="Add or remove this item from the selection">${iconWell('select')}<span>${state.selected.has(item.selectionKey) ? 'Unselect' : 'Select'} (S)</span></button><button class="scg-tip-up scg-tip-end" data-copy-current data-tooltip="Copy the source link">${iconWell('copy')}<span>Copy link</span></button></div>
        </footer>
      </div>`;
    box.classList.add('open');
    box.classList.toggle('details-hidden', !state.viewerInfoOpen);
    box.setAttribute('aria-label', `${typeLabel} ${state.lightboxIndex + 1} of ${items.length}: ${title}`);
    box.querySelector('[data-viewer-close]').onclick = closeLightbox;
    box.querySelector('[data-viewer-prev]').onclick = () => navigateLightbox(-1);
    box.querySelector('[data-viewer-next]').onclick = () => navigateLightbox(1);
    box.querySelectorAll('[data-viewer-index]').forEach(button => { button.onclick = () => goToViewerIndex(Number(button.dataset.viewerIndex)); });
    box.querySelector('[data-download-current]').onclick = () => downloadMedia(item);
    box.querySelector('[data-select-current]').onclick = event => {
      setIconButton(event.currentTarget, 'select', `${toggleSelected(item) ? 'Unselect' : 'Select'} (S)`);
    };
    box.querySelector('[data-copy-current]').onclick = () => copyText(sourceUrl);
    box.querySelector('[data-viewer-info]').onclick = () => toggleViewerInfo();
    box.querySelector('[data-viewer-fullscreen]').onclick = toggleViewerFullscreen;
    box.querySelector('[data-viewer-zoom-in]').onclick = () => setViewerZoom(state.viewerZoom + .25);
    box.querySelector('[data-viewer-zoom-out]').onclick = () => setViewerZoom(state.viewerZoom - .25);
    box.querySelector('[data-viewer-fit]').onclick = () => setViewerZoom(1);
    const viewerTools = box.querySelector('.scg-viewer-tools');
    viewerTools.onpointerdown = event => event.stopPropagation();
    viewerTools.onclick = event => event.stopPropagation();
    box.onpointermove = wakeViewerChrome;
    box.onpointerdown = wakeViewerChrome;
    box.onfocusin = wakeViewerChrome;
    box.onpointerleave = () => armViewerChromeIdle(box, 700);
    wakeViewerChrome();
    box.querySelector('.scg-viewer-thumb.active')?.scrollIntoView({ block: 'nearest', inline: 'center' });
    if (!wasOpen) box.focus({ preventScroll: true });

    const stage = box.querySelector('[data-viewer-stage]');
    const stillCurrent = () => box.classList.contains('open') && token === state.viewerRenderToken && currentViewerItem() === item;
    if (item.type === 'image') {
      const display = await resolveForDisplay(item);
      if (!stillCurrent()) return;
      const imageUrl = display.url || item.thumb;
      stage.innerHTML = `<img class="scg-viewer-image" src="${escapeHtml(imageUrl)}" alt="${escapeHtml(item.caption || title)}">`;
      const image = stage.querySelector('img');
      bindViewerImageInteractions(image, stage);
      let resolutionQualifier = display.status === 'confirmed' ? '' : 'Preview';
      const captureImageResolution = () => {
        if (!stillCurrent()) return;
        if (!updateViewerResolution(box, item, image.naturalWidth, image.naturalHeight, resolutionQualifier, image.currentSrc || image.src)) {
          setViewerResolutionStatus(box, 'Unavailable');
        }
      };
      image.onload = captureImageResolution;
      image.onerror = () => {
        const fallback = absoluteUrl(item.thumb);
        if (fallback && image.src !== fallback) {
          resolutionQualifier = 'Preview';
          image.src = fallback;
          notify('The host blocked the original image; showing its thumbnail.');
        } else {
          setViewerResolutionStatus(box, 'Unavailable');
          stage.innerHTML = '<div class="scg-viewer-error">This image could not be displayed. Use Open source to view it on the host.</div>';
        }
      };
      if (image.complete && image.naturalWidth) requestAnimationFrame(captureImageResolution);
      updateViewerTransform();
      const next = items[(state.lightboxIndex + 1) % items.length];
      if (next?.type === 'image') resolveForDisplay(next).catch(() => {});
    } else if (item.type === 'video' && VIDEO_EXT.test(item.url)) {
      if (!stillCurrent()) return;
      stage.innerHTML = `<video controls autoplay playsinline preload="metadata" poster="${escapeHtml(item.poster || '')}" src="${escapeHtml(item.url)}"></video>`;
      const video = stage.querySelector('video');
      bindViewerVideoLifecycle(video, stage);
      bindViewerVideoResolution(video, box, item, stillCurrent);
    } else if (item.wasFrame || embedUrl(item.url) !== unwrapSimpcityRedirect(item.url)) {
      if (!stillCurrent()) return;
      stage.innerHTML = iframeHtml(item) || '<div class="scg-viewer-error">This host does not expose an embeddable player. Use Open source to watch it on the host.</div>';
      if (!stage.querySelector('iframe')) {
        setViewerResolutionStatus(box, 'Unavailable');
      } else {
        setViewerResolutionStatus(box, 'Detecting…');
        resolveForDisplay(item).then(async direct => {
          if (!stillCurrent()) return;
          const dimensions = direct?.url ? await probeVideoDimensions(direct.url) : null;
          if (!stillCurrent()) return;
          if (dimensions) updateViewerResolution(box, item, dimensions.width, dimensions.height, '', direct.url);
          else setViewerResolutionStatus(box, 'Host-controlled embed');
        }).catch(() => {
          if (stillCurrent()) setViewerResolutionStatus(box, 'Host-controlled embed');
        });
      }
    } else {
      const direct = await resolveForDisplay(item);
      if (!stillCurrent()) return;
      stage.innerHTML = direct.url
        ? `<video controls autoplay playsinline preload="metadata" poster="${escapeHtml(item.poster || '')}" src="${escapeHtml(direct.url)}"></video>`
        : '<div class="scg-viewer-error">This host does not expose an embeddable player. Use Open source to watch it on the host.</div>';
      const video = stage.querySelector('video');
      bindViewerVideoLifecycle(video, stage);
      bindViewerVideoResolution(video, box, item, stillCurrent);
    }
  }

  function openLightbox(item) {
    const filteredMedia = visibleItems().filter(isMediaItem);
    const items = filteredMedia.includes(item) ? filteredMedia : mediaItems();
    const index = items.indexOf(item);
    if (index < 0) return;
    state.viewerItems = items;
    state.viewerReturnFocus = document.activeElement;
    state.lightboxIndex = index;
    state.viewerInfoOpen = false;
    renderLightbox();
  }

  function goToViewerIndex(index) {
    const items = viewerMediaItems();
    if (!items.length) return;
    state.lightboxIndex = Math.min(Math.max(0, Number(index) || 0), items.length - 1);
    resetViewerTransform(false);
    renderLightbox();
  }

  function navigateLightbox(direction) {
    state.lightboxIndex += Number(direction) || 0;
    resetViewerTransform(false);
    renderLightbox();
  }

  function resetDatasetState() {
    invalidateVisibleItems({ dataset: true, selection: true });
    state.selected.clear();
    state.collapsedReplies.clear();
    state.selectionMode = false;
    state.selectedOnly = false;
    state.authorFilter = 'all';
    state.hostFilter = 'all';
    state.viewPage = 1;
    state.viewPages = 1;
    state.renderedItems = [];
    if (!state.downloading) {
      state.downloadProgress = blankDownloadProgress();
      state.downloadJobs = [];
      state.downloadDetailsOpen = false;
    }
  }

  function updateScanStatus() {
    const app = document.getElementById(APP_ID);
    if (!app) return;
    const status = state.scanCanceling
      ? `Canceling scan after ${state.scannedPages} completed page${state.scannedPages === 1 ? '' : 's'}`
      : `Page ${state.scanCurrentPage} - ${state.scannedPages} of ${state.scanTotalPages} completed`;
    app.querySelector('.scg-status').textContent = status;
    const detail = app.querySelector('[data-activity-detail]');
    refreshThreadHeader();
    if (detail) detail.textContent = `${state.items.length} items found - ${state.scanFailedPages} failed page${state.scanFailedPages === 1 ? '' : 's'} - media stays unloaded`;
    setIconButton(app.querySelector('[data-action="thread"]'), 'close', state.scanCanceling ? 'Canceling scan' : 'Cancel scan');
  }

  async function confirmLargeThreadScan(pageCount) {
    const result = await confirmAction({
      title: 'Scan this large thread?',
      message: `This thread has ${pageCount} detected pages. ${SCAN_WARNING_TEXT}`,
      confirmLabel: 'Continue',
      danger: true,
      preferenceLabel: "Don't warn me again",
    });
    if (result?.confirmed && result.preferenceChecked) {
      state.warnLargeThreadScan = false;
      persistSettings();
      if (document.querySelector(`#${APP_ID} .scg-settings-panel.open`)) refreshSettingsPanel();
    }
    return Boolean(result?.confirmed);
  }

  async function scanThread() {
    if (state.downloading) return notify('Finish or cancel the ZIP queue before scanning another page.');
    if (state.scanning) {
      state.cancelScan = true;
      state.scanCanceling = true;
      state.scanController?.abort();
      updateScanStatus();
      return;
    }
    updateThreadPageInfo(document, location.href);
    const detectedPages = Math.max(1, state.threadPageCount);
    if (state.warnLargeThreadScan && detectedPages >= LARGE_THREAD_WARNING_PAGES) {
      if (!await confirmLargeThreadScan(detectedPages)) return;
    }
    state.scanning = true;
    state.cancelScan = false;
    state.scanCanceling = false;
    state.scannedPages = 0;
    state.scanCurrentPage = 0;
    state.scanFailedPages = 0;
    state.scanTotalPages = Math.min(detectedPages, 250);
    state.items = [];
    resetDatasetState();
    state.sourceLabel = 'Entire thread';
    render();

    const seenItems = new Set();
    const controller = new AbortController();
    state.scanController = controller;
    try {
      for (let page = 1; page <= state.scanTotalPages && !state.cancelScan; page++) {
        state.scanCurrentPage = page;
        updateScanStatus();
        const pageUrl = threadPageUrl(page);
        let doc;
        try {
          if (new URL(pageUrl).pathname === location.pathname) {
            doc = document;
          } else {
            const response = await fetch(pageUrl, { credentials: 'include', signal: controller.signal });
            if (!response.ok) throw new Error(`Page request failed (${response.status})`);
            const html = await response.text();
            if (state.cancelScan || controller.signal.aborted) break;
            doc = new DOMParser().parseFromString(html, 'text/html');
          }
          if (state.cancelScan || controller.signal.aborted) break;
          updateThreadPageInfo(doc, pageUrl);
          appendUniqueItems(state.items, extractFromDocument(doc, pageUrl), seenItems);
          state.scannedPages++;
        } catch (error) {
          if (error?.name === 'AbortError' || state.cancelScan) break;
          state.scanFailedPages++;
          recordDiagnostic('scanFailures', `Whole-thread page ${page}`, error);
        } finally {
          doc = null;
        }
        updateScanStatus();
        if (page < state.scanTotalPages && !state.cancelScan) await new Promise(resolve => setTimeout(resolve, 250));
      }
      if (detectedPages > 250 && !state.cancelScan) {
        state.sourceLabel = 'Partial thread scan (250-page safety limit)';
        notify('Stopped at the 250-page safety limit. Use the page picker for later pages.', 6500);
      } else if (state.scanFailedPages) {
        state.sourceLabel = `Thread scan (${state.scanFailedPages} failed page${state.scanFailedPages === 1 ? '' : 's'})`;
      }
    } catch (error) {
      if (error?.name !== 'AbortError') {
        console.error('[SimpCity Gallery]', error);
        recordDiagnostic('scanFailures', 'Whole-thread scan', error);
        notify(`Gallery scan stopped: ${error.message}`, 6500);
      }
    } finally {
      if (state.cancelScan && state.scannedPages) state.sourceLabel = `Partial thread scan (${state.scannedPages} pages)`;
      else if (state.cancelScan) state.sourceLabel = 'Thread scan canceled';
      state.scanController = null;
      state.scanning = false;
      state.scanCanceling = false;
      state.cancelScan = false;
      render();
    }
  }

  function scanCurrentPage() {
    if (state.downloading) return notify('Finish or cancel the ZIP queue before scanning another page.');
    updateThreadPageInfo(document, location.href);
    state.items = dedupe(extractFromDocument(document, location.href));
    resetDatasetState();
    state.sourcePage = pageNumberFromUrl(location.href);
    state.sourceLabel = `Thread page ${state.sourcePage}`;
    state.scannedPages = 1;
    render();
  }

  async function scanSpecificPage(page) {
    if (state.downloading) return notify('Finish or cancel the ZIP queue before scanning another page.');
    if (state.scanning) return;
    const targetPage = Math.min(Math.max(1, Number(page) || 1), state.threadPageCount);
    state.scanning = true;
    state.cancelScan = false;
    state.items = [];
    resetDatasetState();
    state.sourcePage = targetPage;
    state.sourceLabel = `Thread page ${targetPage}`;
    render();
    try {
      let doc;
      const targetUrl = threadPageUrl(targetPage);
      if (new URL(targetUrl).pathname === location.pathname) doc = document;
      else {
        const response = await fetch(targetUrl, { credentials: 'include' });
        if (!response.ok) throw new Error(`Page request failed (${response.status})`);
        doc = new DOMParser().parseFromString(await response.text(), 'text/html');
      }
      updateThreadPageInfo(doc, targetUrl);
      state.items = dedupe(extractFromDocument(doc, targetUrl));
      state.scannedPages = 1;
    } catch (error) {
      console.error('[SimpCity Gallery]', error);
      recordDiagnostic('scanFailures', `Page ${targetPage} scan`, error);
      notify(`Could not load page ${targetPage}: ${error.message}`, 5500);
    } finally {
      state.scanning = false;
      render();
    }
  }

  const style = document.createElement('style');
  style.textContent = `

    /* ============================================================
       GALLERY UI  -  design system for SimpCity Thread Gallery 0.9.7
       Direction: full-bleed media canvas, floating instrument plate,
       mode-morphing dock. One restrained signal colour per theme.
       ============================================================ */

    /* ---- 1. Design tokens ------------------------------------- */
    #${APP_ID},
    #scg-launch,
    #scg-gallery-toast{
      --scg-bg:#08090b;
      --scg-canvas:#0a0b0e;
      --scg-surface:#101216;
      --scg-surface-2:#15181d;
      --scg-surface-3:#1c2027;
      --scg-well:#050506;
      --scg-line:#252a32;
      --scg-line-strong:#333a45;
      --scg-text:#eef1f5;
      --scg-text-soft:#c6ccd4;
      --scg-muted:#98a1ad;
      --scg-accent:#4fb3ff;
      --scg-accent-ink:#04121e;
      --scg-accent-soft:#0d2233;
      --scg-accent-line:#1e5279;
      --scg-success:#4ad4a0;
      --scg-success-soft:#0d2a22;
      --scg-warn:#e0b062;
      --scg-warn-soft:#2a2114;
      --scg-danger:#f06b78;
      --scg-danger-soft:#321316;
      --scg-shadow:0 18px 48px #00000094;
      --scg-shadow-sm:0 6px 18px #0000006b;
      --scg-focus:#7cc8ff;
      --scg-select-arrow:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238e97a3' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
      --scg-r-sm:8px;
      --scg-r-md:11px;
      --scg-r-lg:16px;
      --scg-r-xl:22px;
      color-scheme:dark;
    }

    #${APP_ID}[data-theme="light"],
    #scg-launch[data-theme="light"],
    #scg-gallery-toast[data-theme="light"]{
      --scg-bg:#e9ecf0;
      --scg-canvas:#eef1f5;
      --scg-surface:#ffffff;
      --scg-surface-2:#f5f7fa;
      --scg-surface-3:#e6eaf0;
      --scg-well:#dde2e9;
      --scg-line:#d0d6de;
      --scg-line-strong:#b0b9c5;
      --scg-text:#0f1319;
      --scg-text-soft:#333c48;
      --scg-muted:#4f5a68;
      --scg-accent:#0a5fc4;
      --scg-accent-ink:#ffffff;
      --scg-accent-soft:#e0ecfc;
      --scg-accent-line:#a6c8ef;
      --scg-success:#0d6b4b;
      --scg-success-soft:#dff2e9;
      --scg-warn:#8a5a08;
      --scg-warn-soft:#fbeed5;
      --scg-danger:#af1f3d;
      --scg-danger-soft:#fce1e7;
      --scg-shadow:0 16px 40px #2c3c5226;
      --scg-shadow-sm:0 5px 14px #2c3c5220;
      --scg-focus:#0a5fc4;
      --scg-select-arrow:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%234f5a68' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
      color-scheme:light;
    }

    #${APP_ID}[data-theme="midnight"],
    #scg-launch[data-theme="midnight"],
    #scg-gallery-toast[data-theme="midnight"]{
      --scg-bg:#060912;
      --scg-canvas:#080d1a;
      --scg-surface:#0e1426;
      --scg-surface-2:#131b31;
      --scg-surface-3:#1a2440;
      --scg-well:#03060d;
      --scg-line:#232f52;
      --scg-line-strong:#31406b;
      --scg-text:#e9eefb;
      --scg-text-soft:#bcc7e4;
      --scg-muted:#93a0c2;
      --scg-accent:#8f9dff;
      --scg-accent-ink:#080b1c;
      --scg-accent-soft:#181f4a;
      --scg-accent-line:#39448c;
      --scg-success:#54d9b4;
      --scg-success-soft:#0c2c2c;
      --scg-warn:#dfb066;
      --scg-warn-soft:#2a2317;
      --scg-danger:#ff86a6;
      --scg-danger-soft:#2e1424;
      --scg-focus:#a8b3ff;
      --scg-select-arrow:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2393a0c2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
      color-scheme:dark;
    }

    #${APP_ID}[data-theme="graphite"],
    #scg-launch[data-theme="graphite"],
    #scg-gallery-toast[data-theme="graphite"]{
      --scg-bg:#101011;
      --scg-canvas:#131314;
      --scg-surface:#1a1a1c;
      --scg-surface-2:#202022;
      --scg-surface-3:#292a2d;
      --scg-well:#0a0a0b;
      --scg-line:#34353a;
      --scg-line-strong:#45464c;
      --scg-text:#f1f1f2;
      --scg-text-soft:#cbcbcf;
      --scg-muted:#a1a2a8;
      --scg-accent:#d8b158;
      --scg-accent-ink:#1a1405;
      --scg-accent-soft:#2c2413;
      --scg-accent-line:#5c4c22;
      --scg-success:#63c99a;
      --scg-success-soft:#16261e;
      --scg-warn:#d8b158;
      --scg-warn-soft:#2c2413;
      --scg-danger:#f08a9e;
      --scg-danger-soft:#2b1519;
      --scg-focus:#e3c479;
      --scg-select-arrow:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a1a2a8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
      color-scheme:dark;
    }

    /* ---- 2. Primitives ---------------------------------------- */
    #${APP_ID},#${APP_ID} *,#${APP_ID} *:before,#${APP_ID} *:after,
    #scg-launch,#scg-launch *,#scg-gallery-toast,#scg-gallery-toast *{
      box-sizing:border-box;
      font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
      letter-spacing:normal;
      text-transform:none;
    }
    #${APP_ID} button,#${APP_ID} input,#${APP_ID} select,#${APP_ID} textarea{font:inherit;margin:0}
    #${APP_ID} b,#${APP_ID} strong{font-weight:650}

    #${APP_ID} .scg-icon{
      display:block;width:17px;height:17px;flex:none;
      fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;
    }
    #${APP_ID} .scg-icon-well{
      display:inline-grid;place-items:center;width:17px;height:17px;flex:none;
      color:inherit;font-style:normal;
    }
    #${APP_ID} .scg-icon-well-primary{color:currentColor}
    #${APP_ID} .scg-icon-well-danger{color:var(--scg-danger)}
    #${APP_ID} .scg-close .scg-icon,#${APP_ID} .scg-settings-close .scg-icon{width:18px;height:18px}

    /* Focus: a two-layer ring so it survives on both light and dark. */
    #${APP_ID} :focus-visible,
    #scg-launch:focus-visible{
      outline:2px solid var(--scg-focus);
      outline-offset:2px;
      border-radius:var(--scg-r-sm);
    }
    #${APP_ID} button:focus-visible,#${APP_ID} a:focus-visible,
    #${APP_ID} select:focus-visible,#${APP_ID} input:focus-visible{
      box-shadow:0 0 0 5px color-mix(in srgb,var(--scg-focus) 26%,transparent);
    }
    #${APP_ID} button:disabled,#${APP_ID} select:disabled,#${APP_ID} input:disabled{
      opacity:.4;cursor:not-allowed;box-shadow:none;
    }
    #${APP_ID} button:disabled:hover{background:inherit}

    /* Native dropdowns, restyled but still native. */
    #${APP_ID} select{
      appearance:none;-webkit-appearance:none;
      min-height:34px;padding:6px 30px 6px 10px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background-color:var(--scg-surface-2);color:var(--scg-text);
      background-image:var(--scg-select-arrow);
      background-repeat:no-repeat;background-position:right 9px center;background-size:14px;
      font-size:12px;cursor:pointer;
    }
    #${APP_ID} select:hover:not(:disabled){border-color:var(--scg-line-strong);background-color:var(--scg-surface-3)}
    #${APP_ID} select option{background:var(--scg-surface);color:var(--scg-text)}
    #${APP_ID} select option:checked{background:var(--scg-accent);color:var(--scg-accent-ink)}
    #${APP_ID} input[type="search"]{appearance:none;-webkit-appearance:none}
    #${APP_ID} input[type="search"]::-webkit-search-cancel-button{filter:grayscale(1) opacity(.6)}
    #${APP_ID} input[type="checkbox"]{accent-color:var(--scg-accent);width:15px;height:15px;cursor:pointer}

    /* Tooltips: hover AND keyboard focus, never on coarse pointers. */
    #${APP_ID} [data-tooltip]{position:relative}
    #${APP_ID} [data-tooltip]:hover:after,
    #${APP_ID} [data-tooltip]:focus-visible:after{
      content:attr(data-tooltip);
      position:absolute;z-index:400;top:calc(100% + 8px);left:50%;
      width:max-content;max-width:230px;transform:translateX(-50%);
      padding:6px 9px;border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-sm);
      background:var(--scg-surface-3);color:var(--scg-text);
      box-shadow:var(--scg-shadow-sm);
      font-size:11px;font-weight:550;line-height:1.3;white-space:normal;overflow-wrap:anywhere;pointer-events:none;
    }
    #${APP_ID} .scg-tip-end[data-tooltip]:hover:after,
    #${APP_ID} .scg-tip-end[data-tooltip]:focus-visible:after{left:auto;right:0;transform:none}
    #${APP_ID} .scg-tip-up[data-tooltip]:hover:after,
    #${APP_ID} .scg-tip-up[data-tooltip]:focus-visible:after{top:auto;bottom:calc(100% + 8px)}
    #${APP_ID} .scg-lightbox [data-tooltip]:after{max-width:170px}

    /* Scrollbars */
    #${APP_ID} *{scrollbar-width:thin;scrollbar-color:var(--scg-line-strong) transparent}
    #${APP_ID} ::-webkit-scrollbar{width:11px;height:11px}
    #${APP_ID} ::-webkit-scrollbar-thumb{
      border:3px solid transparent;border-radius:99px;
      background:var(--scg-line-strong);background-clip:content-box;
    }
    #${APP_ID} ::-webkit-scrollbar-track{background:transparent}

    /* ---- 3. Launcher ------------------------------------------ */
    #scg-launch{
      position:fixed;z-index:2147483645;left:20px;bottom:20px;
      display:inline-flex;align-items:center;gap:12px;
      min-height:66px;padding:0 22px 0 12px;
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-xl);
      background:var(--scg-surface);color:var(--scg-text);
      box-shadow:var(--scg-shadow);
      cursor:pointer;
      transition:transform .18s cubic-bezier(.2,.7,.3,1),border-color .18s,box-shadow .18s,background .18s;
    }
    #scg-launch:before{
      content:'';position:absolute;inset:-1px;border-radius:inherit;pointer-events:none;
      background:linear-gradient(150deg,color-mix(in srgb,var(--scg-accent) 26%,transparent),transparent 46%);
      opacity:.5;
    }
    #scg-launch:hover{
      transform:translateY(-2px);
      border-color:color-mix(in srgb,var(--scg-accent) 55%,var(--scg-line-strong));
      box-shadow:var(--scg-shadow),0 0 0 5px color-mix(in srgb,var(--scg-accent) 14%,transparent);
    }
    #scg-launch:active{transform:translateY(0)}
    #scg-launch .scg-launch-icon{
      position:relative;z-index:1;
      display:inline-grid;place-items:center;width:44px;height:44px;flex:none;
      border:1px solid color-mix(in srgb,var(--scg-accent) 45%,transparent);
      border-radius:13px;
      background:color-mix(in srgb,var(--scg-accent) 16%,var(--scg-surface-3));
      color:var(--scg-accent);font-style:normal;
    }
    #scg-launch .scg-launch-icon .scg-icon{width:23px;height:23px;fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
    #scg-launch .scg-launch-copy{position:relative;z-index:1;display:flex;flex-direction:column;gap:3px;text-align:left;min-width:0}
    #scg-launch .scg-launch-copy strong{font-size:14px;font-weight:650;line-height:1.1;letter-spacing:-.005em}
    #scg-launch .scg-launch-copy small{
      color:var(--scg-muted);font-size:11px;line-height:1.1;
      font-variant-numeric:tabular-nums;
    }

    /* ---- 4. Toast --------------------------------------------- */
    #scg-gallery-toast{
      position:fixed;z-index:2147483647;left:50%;bottom:26px;
      max-width:min(640px,calc(100vw - 32px));
      padding:12px 18px;
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-lg);
      background:var(--scg-surface);color:var(--scg-text);
      box-shadow:var(--scg-shadow);
      font-size:13px;line-height:1.45;text-align:center;
      opacity:0;visibility:hidden;
      transform:translate(-50%,14px);
      transition:opacity .2s,transform .2s,visibility .2s;
    }
    #scg-gallery-toast.show{opacity:1;visibility:visible;transform:translate(-50%,0)}

    /* ---- 5. Application shell --------------------------------- */
    #${APP_ID}{
      position:fixed;inset:0;z-index:2147483646;
      display:none;overflow:hidden;
      background:var(--scg-bg);color:var(--scg-text);
      font:400 13px/1.45 ui-sans-serif,system-ui,sans-serif;
    }
    #${APP_ID}.open{display:grid;grid-template-rows:auto minmax(0,1fr)}

    /* The instrument plate: one floating unit instead of four glued bars. */
    #${APP_ID} .scg-plate{
      position:relative;z-index:40;
      margin:12px 12px 0;
      border:1px solid color-mix(in srgb,#ffffff 16%,var(--scg-line));border-radius:var(--scg-r-lg);
      background:linear-gradient(135deg,
        color-mix(in srgb,var(--scg-surface) 60%,transparent),
        color-mix(in srgb,var(--scg-surface-2) 42%,transparent));
      box-shadow:0 14px 38px #00000052,
        inset 0 1px 0 color-mix(in srgb,#ffffff 13%,transparent),
        inset 0 -1px 0 color-mix(in srgb,#000000 14%,transparent);
      backdrop-filter:blur(26px) saturate(160%);
      -webkit-backdrop-filter:blur(26px) saturate(160%);
      isolation:isolate;
    }
    #${APP_ID} .scg-plate:before{
      content:'';position:absolute;z-index:0;inset:1px;border-radius:inherit;pointer-events:none;
      background:linear-gradient(112deg,color-mix(in srgb,#ffffff 9%,transparent),transparent 34%,color-mix(in srgb,var(--scg-accent) 5%,transparent));
    }
    #${APP_ID} .scg-plate>*{position:relative;z-index:1}

    #${APP_ID} .scg-header{
      z-index:2; /* Keep header tooltips above the later-painted filter row. */
      display:grid;
      grid-template-columns:auto minmax(120px,1.1fr) minmax(180px,1.5fr) auto auto;
      align-items:center;gap:14px;
      padding:10px 12px;
    }
    #${APP_ID} .scg-brand{display:flex;align-items:center;gap:10px;min-width:0}
    #${APP_ID} .scg-brand-mark{
      display:grid;place-items:center;width:36px;height:36px;flex:none;
      border:1px solid color-mix(in srgb,var(--scg-accent) 42%,transparent);
      border-radius:12px;
      background:color-mix(in srgb,var(--scg-accent) 15%,var(--scg-surface-3));
      color:var(--scg-accent);
    }
    #${APP_ID} .scg-brand-mark .scg-icon{width:19px;height:19px}
    #${APP_ID} .scg-brand>div:last-child{display:flex;flex-direction:column;gap:3px;min-width:0}
    #${APP_ID} .scg-brand b{font-size:13px;font-weight:650;letter-spacing:-.01em;line-height:1}
    #${APP_ID} .scg-brand small{
      color:var(--scg-muted);font-size:10.5px;line-height:1;
      text-transform:uppercase;letter-spacing:.08em;font-weight:600;
    }

    #${APP_ID} .scg-thread-context{min-width:0;border-left:1px solid var(--scg-line);padding-left:14px}
    #${APP_ID} .scg-thread-context h1{
      margin:0;font-size:13.5px;font-weight:600;line-height:1.25;
      overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    }
    #${APP_ID} .scg-thread-context>div{
      display:flex;align-items:center;gap:8px;margin-top:4px;min-width:0;
      color:var(--scg-muted);font-size:11px;font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-thread-context>div span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    #${APP_ID} .scg-thread-context [data-thread-meta]{
      flex:none;padding:2px 7px;border-radius:99px;
      background:var(--scg-accent-soft);color:var(--scg-accent);
      font-weight:600;font-size:10.5px;
    }


    /* ---- 6. Search + scan ------------------------------------- */
    #${APP_ID} .scg-controls{position:relative;display:flex;align-items:center;gap:10px;min-width:0}
    #${APP_ID} .scg-search{position:relative;display:block;flex:1;min-width:0}
    #${APP_ID} .scg-search>.scg-icon-well{
      position:absolute;z-index:2;top:50%;left:11px;transform:translateY(-50%);
      color:var(--scg-muted);pointer-events:none;
    }
    #${APP_ID} .scg-search input{
      width:100%;height:38px;
      padding:0 12px 0 36px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-text);
      font-size:13px;
    }
    #${APP_ID} .scg-search input::placeholder{color:var(--scg-muted)}
    #${APP_ID} .scg-search input:hover:not(:disabled){border-color:var(--scg-line-strong)}
    #${APP_ID} .scg-search input:focus{border-color:var(--scg-accent);background:var(--scg-surface)}

    #${APP_ID} .scg-scan-actions{
      display:flex;align-items:center;gap:4px;flex:none;
      padding:3px;border:1px solid var(--scg-line);border-radius:13px;
      background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-scan-actions button{
      display:inline-flex;align-items:center;justify-content:center;gap:7px;
      min-height:32px;padding:6px 11px;
      border:1px solid transparent;border-radius:var(--scg-r-sm);
      background:transparent;color:var(--scg-text-soft);
      font-size:12px;font-weight:550;cursor:pointer;white-space:nowrap;
      transition:background .14s,color .14s,border-color .14s;
    }
    #${APP_ID} .scg-scan-actions button:hover:not(:disabled){background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-scan-actions button .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-scan-actions button:hover:not(:disabled) .scg-icon{color:var(--scg-text)}
    #${APP_ID} .scg-scan-actions .scg-scan-thread-danger{
      border-color:#c43a50;background:#8f2034;color:#ffffff;
    }
    #${APP_ID} .scg-scan-actions .scg-scan-thread-danger .scg-icon{color:#ffd7de}
    #${APP_ID} .scg-scan-actions .scg-scan-thread-danger:hover:not(:disabled),
    #${APP_ID} .scg-scan-actions .scg-scan-thread-danger:focus-visible{
      border-color:#ff637b;background:#c92f49;color:#ffffff;
    }
    #${APP_ID} .scg-scan-warning{
      position:absolute;width:1px;height:1px;padding:0;margin:-1px;
      overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;
    }
    #${APP_ID} .scg-scan-actions [data-action="thread"]{
      border-color:color-mix(in srgb,var(--scg-accent) 40%,transparent);
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-scan-actions [data-action="thread"] .scg-icon{color:var(--scg-accent)}
    #${APP_ID} .scg-scan-actions [data-action="thread"]:hover:not(:disabled){
      background:color-mix(in srgb,var(--scg-accent) 22%,var(--scg-surface-3));color:var(--scg-accent);
    }
    #${APP_ID} .scg-source-page{display:flex;align-items:center;gap:4px;padding:0 2px;border-left:1px solid var(--scg-line);border-right:1px solid var(--scg-line)}
    #${APP_ID} .scg-source-page select{min-height:32px;min-width:96px;border-color:transparent;background-color:transparent}
    #${APP_ID} .scg-source-page select:hover:not(:disabled){background-color:var(--scg-surface-3)}

    /* ---- 7. Header utilities + overflow menu ------------------- */
    #${APP_ID} .scg-header-actions{display:flex;align-items:center;gap:3px;flex:none}
    #${APP_ID} .scg-header-actions button{
      position:relative;
      display:inline-grid;grid-auto-flow:column;place-items:center;gap:7px;
      min-width:36px;min-height:36px;padding:8px;
      border:1px solid transparent;border-radius:var(--scg-r-md);
      background:transparent;color:var(--scg-muted);
      font-size:12px;cursor:pointer;
      transition:background .14s,color .14s,border-color .14s;
    }
    #${APP_ID} .scg-header-actions button:hover{background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-header-actions button.active,
    #${APP_ID} .scg-header-actions button[aria-expanded="true"]{
      border-color:var(--scg-line);background:var(--scg-surface-3);color:var(--scg-text);
    }
    #${APP_ID} .scg-header-actions button>span{display:none}
    #${APP_ID} .scg-header-actions .scg-close{color:var(--scg-text-soft)}
    #${APP_ID} .scg-header-actions .scg-close:hover{
      border-color:color-mix(in srgb,var(--scg-danger) 45%,transparent);
      background:var(--scg-danger-soft);color:var(--scg-danger);
    }
    #${APP_ID} .scg-header-rule{width:1px;height:22px;flex:none;margin:0 4px;background:var(--scg-line)}
    #${APP_ID} [data-action="filters-toggle"] .scg-icon{transition:transform .18s ease}
    #${APP_ID}.filters-collapsed [data-action="filters-toggle"] .scg-icon{transform:rotate(-90deg)}

    #${APP_ID} .scg-menu-wrap{position:relative;display:inline-flex}
    #${APP_ID} .scg-menu{
      position:absolute;z-index:300;top:calc(100% + 8px);right:0;
      display:none;flex-direction:column;gap:2px;
      width:246px;padding:6px;
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-lg);
      background:var(--scg-surface);box-shadow:var(--scg-shadow);
    }
    #${APP_ID} .scg-menu.open{display:flex}
    #${APP_ID} .scg-menu-label{
      padding:7px 9px 5px;color:var(--scg-muted);
      font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;
    }
    #${APP_ID} .scg-menu button{
      display:grid !important;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:10px;
      width:100%;min-height:38px;padding:8px 9px;
      border:1px solid transparent;border-radius:var(--scg-r-sm);
      background:transparent;color:var(--scg-text);
      font-size:12.5px;text-align:left;cursor:pointer;
    }
    #${APP_ID} .scg-menu button:hover{background:var(--scg-surface-3)}
    #${APP_ID} .scg-menu button>span{display:block !important;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    #${APP_ID} .scg-menu button .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-menu button:hover .scg-icon{color:var(--scg-accent)}
    #${APP_ID} .scg-menu-sep{height:1px;margin:5px 4px;background:var(--scg-line)}

    /* ---- 8. Filter + view row --------------------------------- */
    #${APP_ID} .scg-filter-panel{
      display:flex;align-items:center;gap:10px;flex-wrap:wrap;
      padding:0 12px 10px;
    }
    #${APP_ID}.filters-collapsed .scg-filter-panel{display:none}

    #${APP_ID} .scg-filters{
      display:flex;align-items:center;gap:2px;flex:none;max-width:100%;overflow-x:auto;
      padding:3px;border:1px solid var(--scg-line);border-radius:13px;
      background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-filters button{
      display:inline-flex;align-items:center;gap:7px;flex:none;
      min-height:32px;padding:6px 11px;
      border:1px solid transparent;border-radius:var(--scg-r-sm);
      background:transparent;color:var(--scg-muted);
      font-size:12px;cursor:pointer;white-space:nowrap;
      transition:background .14s,color .14s;
    }
    #${APP_ID} .scg-filters button .scg-icon{width:15px;height:15px}
    #${APP_ID} .scg-filters button b{font-weight:550;font-size:12px}
    #${APP_ID} .scg-filters button>span{
      min-width:20px;padding:2px 6px;border-radius:99px;
      background:var(--scg-surface-3);color:var(--scg-muted);
      font-size:10.5px;font-weight:650;text-align:center;
      font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-filters button:hover:not(:disabled):not(.active){background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-filters button.active{
      background:var(--scg-accent);color:var(--scg-accent-ink);
      box-shadow:var(--scg-shadow-sm);
    }
    #${APP_ID} .scg-filters button.active>span{background:color-mix(in srgb,var(--scg-accent-ink) 18%,transparent);color:var(--scg-accent-ink)}
    #${APP_ID} .scg-filters button.active .scg-icon{color:var(--scg-accent-ink)}

    #${APP_ID} .scg-layout-switcher{
      display:inline-flex;align-items:center;gap:2px;flex:none;
      padding:3px;border:1px solid var(--scg-line);border-radius:13px;
      background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-layout-switcher button{
      display:inline-flex;align-items:center;gap:7px;
      min-width:34px;min-height:32px;padding:6px 9px;
      border:1px solid transparent;border-radius:var(--scg-r-sm);
      background:transparent;color:var(--scg-muted);
      font-size:12px;cursor:pointer;
    }
    #${APP_ID} .scg-layout-switcher button>span{font-size:12px}
    #${APP_ID} .scg-layout-switcher button:hover:not(:disabled){background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-layout-switcher button.active{
      border-color:var(--scg-line-strong);background:var(--scg-surface);color:var(--scg-text);
      box-shadow:var(--scg-shadow-sm);
    }
    #${APP_ID} .scg-layout-switcher button.active .scg-icon{color:var(--scg-accent)}

    #${APP_ID} .scg-viewbar{display:flex;align-items:center;gap:10px;flex:1;min-width:0;justify-content:flex-end}
    #${APP_ID} .scg-view-summary{
      margin-right:auto;padding-left:2px;
      color:var(--scg-muted);font-size:11.5px;font-variant-numeric:tabular-nums;
      overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    }
    #${APP_ID} .scg-reply-group-actions{display:none;align-items:center;gap:4px}
    #${APP_ID}.grouped-replies .scg-reply-group-actions{display:inline-flex}
    #${APP_ID} .scg-reply-group-actions button{
      display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 8px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-sm);
      background:var(--scg-surface-2);color:var(--scg-text-soft);font-size:11px;cursor:pointer;
    }
    #${APP_ID} .scg-reply-group-actions button:hover{border-color:var(--scg-line-strong);background:var(--scg-surface-3)}
    #${APP_ID} .scg-reply-group-actions [data-action="expand-all"] .scg-icon{transform:rotate(180deg)}
    #${APP_ID} .scg-viewbar label{
      display:inline-flex;align-items:center;gap:7px;flex:none;
      color:var(--scg-muted);font-size:10.5px;font-weight:650;
      text-transform:uppercase;letter-spacing:.08em;
    }
    #${APP_ID} .scg-view-pagination{
      display:inline-flex;align-items:center;gap:3px;flex:none;
      padding:3px;border:1px solid var(--scg-line);border-radius:13px;background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-view-pagination button{
      display:inline-grid;place-items:center;
      min-width:32px;min-height:32px;padding:6px;
      border:1px solid transparent;border-radius:var(--scg-r-sm);
      background:transparent;color:var(--scg-muted);cursor:pointer;
    }
    #${APP_ID} .scg-view-pagination button:hover:not(:disabled){background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-view-pagination button>span{display:none}
    #${APP_ID} .scg-view-pagination select{min-height:32px;min-width:92px;border-color:transparent;background-color:transparent}
    #${APP_ID} .scg-viewbar [data-page-size]{min-width:74px}
    #${APP_ID} .scg-size-control{gap:8px;min-width:178px}
    #${APP_ID} .scg-size-control>span{white-space:nowrap}
    #${APP_ID} [data-card-scale]{
      width:88px;height:20px;padding:0;border:0;background:transparent;accent-color:var(--scg-accent);cursor:pointer;
    }
    #${APP_ID} [data-card-scale]::-webkit-slider-runnable-track{height:4px;border-radius:99px;background:var(--scg-line-strong)}
    #${APP_ID} [data-card-scale]::-webkit-slider-thumb{width:16px;height:16px;margin-top:-6px;border:2px solid var(--scg-surface);border-radius:50%;background:var(--scg-accent);box-shadow:0 1px 5px #0008;-webkit-appearance:none}
    #${APP_ID} [data-card-scale]::-moz-range-track{height:4px;border-radius:99px;background:var(--scg-line-strong)}
    #${APP_ID} [data-card-scale]::-moz-range-thumb{width:14px;height:14px;border:2px solid var(--scg-surface);border-radius:50%;background:var(--scg-accent);box-shadow:0 1px 5px #0008}
    #${APP_ID} [data-card-scale-value]{min-width:38px;color:var(--scg-text-soft);font-size:10.5px;font-weight:700;text-align:right;font-variant-numeric:tabular-nums}
    #${APP_ID} [data-card-scale]:disabled{opacity:.38;cursor:not-allowed}

    /* Refine: progressive disclosure for the low-frequency facets. */
    #${APP_ID} .scg-refine-wrap{position:relative;display:inline-flex;flex:none}
    #${APP_ID} .scg-refine-trigger{
      display:inline-flex;align-items:center;gap:7px;
      min-height:38px;padding:8px 12px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-text-soft);
      font-size:12px;font-weight:550;cursor:pointer;white-space:nowrap;
    }
    #${APP_ID} .scg-refine-trigger:hover:not(:disabled){background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-refine-trigger .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-refine-trigger[aria-expanded="true"],
    #${APP_ID} .scg-refine-trigger.active{
      border-color:color-mix(in srgb,var(--scg-accent) 45%,transparent);
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-refine-trigger[aria-expanded="true"] .scg-icon,
    #${APP_ID} .scg-refine-trigger.active .scg-icon{color:var(--scg-accent)}
    #${APP_ID} .scg-refine-trigger em{
      min-width:18px;padding:1px 6px;border-radius:99px;
      background:var(--scg-accent);color:var(--scg-accent-ink);
      font-style:normal;font-size:10px;font-weight:700;
    }
    #${APP_ID} .scg-refine-popover{
      position:absolute;z-index:300;top:calc(100% + 8px);right:0;
      display:none;
      width:min(430px,calc(100vw - 32px));
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-lg);
      background:var(--scg-surface);box-shadow:var(--scg-shadow);
    }
    #${APP_ID} .scg-refine-popover.open{display:block}
    #${APP_ID} .scg-refine-popover>h2{
      margin:0;padding:12px 14px 0;
      color:var(--scg-muted);font-size:10px;font-weight:700;
      text-transform:uppercase;letter-spacing:.1em;
    }
    #${APP_ID} .scg-refinebar{
      display:grid;grid-template-columns:1fr 1fr;gap:10px;
      padding:12px 14px 14px;
    }
    #${APP_ID} .scg-refinebar label{
      display:flex;flex-direction:column;gap:6px;min-width:0;
      color:var(--scg-muted);font-size:10px;font-weight:700;
      text-transform:uppercase;letter-spacing:.08em;
    }
    #${APP_ID} .scg-refinebar select{width:100%;min-height:36px;font-size:12.5px}
    #${APP_ID} .scg-refinebar button{
      display:inline-flex;align-items:center;justify-content:center;gap:7px;
      min-height:36px;padding:8px 10px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-text-soft);
      font-size:12px;cursor:pointer;
    }
    #${APP_ID} .scg-refinebar button:hover:not(:disabled){background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-refinebar button .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-refinebar button.active{
      border-color:color-mix(in srgb,var(--scg-accent) 45%,transparent);
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-refinebar button.active .scg-icon{color:var(--scg-accent)}
    #${APP_ID} .scg-refine-spacer{display:none}

    /* Frost the plate's larger control wells without fading their content. */
    #${APP_ID} .scg-plate .scg-search input,
    #${APP_ID} .scg-plate .scg-scan-actions,
    #${APP_ID} .scg-plate .scg-filters,
    #${APP_ID} .scg-plate .scg-layout-switcher,
    #${APP_ID} .scg-plate .scg-view-pagination,
    #${APP_ID} .scg-plate .scg-refine-trigger:not(.active):not([aria-expanded="true"]){
      background:color-mix(in srgb,var(--scg-surface-2) 50%,transparent);
    }
    #${APP_ID} .scg-plate select{background-color:color-mix(in srgb,var(--scg-surface-2) 46%,transparent)}
    #${APP_ID} .scg-plate select:hover:not(:disabled){background-color:color-mix(in srgb,var(--scg-surface-3) 58%,transparent)}
    #${APP_ID} .scg-plate .scg-search input:focus{background:color-mix(in srgb,var(--scg-surface) 68%,transparent)}


    /* ---- 9. Canvas -------------------------------------------- */
    #${APP_ID} .scg-stage{position:relative;min-width:0;min-height:0}
    #${APP_ID} .scg-scroll{
      position:absolute;inset:12px 12px 0;
      overflow-x:hidden;overflow-y:auto;
      padding:16px 16px 110px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-lg) var(--scg-r-lg) 0 0;
      border-bottom:0;
      background:var(--scg-canvas);
      box-shadow:inset 0 1px 0 color-mix(in srgb,#ffffff 4%,transparent);
      scroll-behavior:smooth;
    }
    #${APP_ID} .scg-top{
      position:absolute;z-index:26;right:26px;bottom:96px;
      display:none;align-items:center;gap:8px;
      min-height:38px;padding:9px 14px;
      border:1px solid var(--scg-line-strong);border-radius:99px;
      background:var(--scg-surface);color:var(--scg-text);
      box-shadow:var(--scg-shadow);
      font-size:12px;font-weight:550;cursor:pointer;
    }
    #${APP_ID} .scg-top.visible{display:inline-flex}
    #${APP_ID} .scg-top:hover{border-color:color-mix(in srgb,var(--scg-accent) 50%,var(--scg-line-strong))}
    #${APP_ID} .scg-top .scg-icon{color:var(--scg-accent)}

    /* Layout: masonry */
    #${APP_ID}[data-layout="masonry"] .scg-grid:not(.grouped){display:block;columns:5 var(--scg-masonry-width,280px);column-gap:14px}
    #${APP_ID}[data-layout="masonry"].compact .scg-grid:not(.grouped){columns:7 var(--scg-masonry-compact-width,195px);column-gap:10px}
    #${APP_ID}[data-layout="masonry"] .scg-group-grid{columns:5 var(--scg-group-width,250px);column-gap:12px;padding:12px}
    #${APP_ID}[data-layout="masonry"].compact .scg-group-grid{columns:7 var(--scg-group-compact-width,180px);column-gap:9px}
    #${APP_ID}[data-layout="masonry"] .scg-card,
    #${APP_ID}[data-layout="masonry"] .scg-skeleton{break-inside:avoid;margin:0 0 14px}
    #${APP_ID}[data-layout="masonry"].compact .scg-card,
    #${APP_ID}[data-layout="masonry"].compact .scg-skeleton{margin-bottom:10px}

    /* Layout: uniform grid */
    #${APP_ID}[data-layout="grid"] .scg-grid:not(.grouped),
    #${APP_ID}[data-layout="grid"] .scg-group-grid{
      display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--scg-grid-min,250px)),1fr));gap:14px;columns:auto;
    }
    #${APP_ID}[data-layout="grid"].compact .scg-grid:not(.grouped),
    #${APP_ID}[data-layout="grid"].compact .scg-group-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--scg-grid-compact-min,180px)),1fr));gap:10px}
    #${APP_ID}[data-layout="grid"] .scg-group-grid{padding:12px}
    #${APP_ID}[data-layout="grid"] .scg-card{display:flex;height:var(--scg-grid-height,386px);min-width:0;flex-direction:column;margin:0;break-inside:auto}
    #${APP_ID}[data-layout="grid"].compact .scg-card{height:var(--scg-grid-compact-height,312px)}
    #${APP_ID}[data-layout="grid"] .scg-preview,
    #${APP_ID}[data-layout="grid"] .scg-card>video,
    #${APP_ID}[data-layout="grid"] .scg-embed,
    #${APP_ID}[data-layout="grid"] .scg-host{min-height:0;flex:1}
    #${APP_ID}[data-layout="grid"] .scg-preview{overflow:hidden}
    #${APP_ID}[data-layout="grid"] .scg-preview img,
    #${APP_ID}[data-layout="grid"] .scg-card>video{width:100%;height:100%;max-height:none;object-fit:cover}
    #${APP_ID}[data-layout="grid"] .scg-card>video{min-height:190px}
    #${APP_ID}[data-layout="grid"] .scg-embed{aspect-ratio:auto;min-height:190px}
    #${APP_ID}[data-layout="grid"] .scg-host{min-height:180px}
    #${APP_ID}[data-layout="grid"] .scg-actions,
    #${APP_ID}[data-layout="grid"] .scg-meta{flex:none}
    #${APP_ID}[data-layout="grid"] .scg-textcard p{min-height:0;flex:1;-webkit-line-clamp:12}
    #${APP_ID}[data-layout="grid"].compact .scg-textcard p{-webkit-line-clamp:9}
    #${APP_ID}[data-layout="grid"] .scg-skeleton{display:flex;flex-direction:column;height:var(--scg-grid-height,386px);margin:0}
    #${APP_ID}[data-layout="grid"].compact .scg-skeleton{height:var(--scg-grid-compact-height,312px)}
    #${APP_ID}[data-layout="grid"] .scg-skeleton-media{height:auto;min-height:0;flex:1}

    /* Layout: feed */
    #${APP_ID}[data-layout="feed"] .scg-grid:not(.grouped),
    #${APP_ID}[data-layout="feed"] .scg-group-grid{
      display:grid;grid-template-columns:minmax(0,1100px);justify-content:center;gap:14px;columns:auto;
    }
    #${APP_ID}[data-layout="feed"] .scg-group-grid{max-width:1140px;margin:auto;padding:14px}
    #${APP_ID}[data-layout="feed"] .scg-card{width:100%;min-width:0;margin:0;break-inside:auto}
    #${APP_ID}[data-layout="feed"] .scg-media,
    #${APP_ID}[data-layout="feed"] .scg-linkcard{
      display:grid;grid-template-columns:minmax(260px,46%) minmax(0,1fr);
      grid-template-areas:"visual actions" "visual meta";
      grid-template-rows:1fr auto;min-height:252px;
    }
    #${APP_ID}[data-layout="feed"].compact .scg-media,
    #${APP_ID}[data-layout="feed"].compact .scg-linkcard{min-height:206px}
    #${APP_ID}[data-layout="feed"] .scg-preview,
    #${APP_ID}[data-layout="feed"] .scg-card>video,
    #${APP_ID}[data-layout="feed"] .scg-embed,
    #${APP_ID}[data-layout="feed"] .scg-host{
      grid-area:visual;width:100%;height:100%;min-height:252px;border-right:1px solid var(--scg-line);
    }
    #${APP_ID}[data-layout="feed"].compact .scg-preview,
    #${APP_ID}[data-layout="feed"].compact .scg-card>video,
    #${APP_ID}[data-layout="feed"].compact .scg-embed,
    #${APP_ID}[data-layout="feed"].compact .scg-host{min-height:206px}
    #${APP_ID}[data-layout="feed"] .scg-preview{overflow:hidden}
    #${APP_ID}[data-layout="feed"] .scg-preview img,
    #${APP_ID}[data-layout="feed"] .scg-card>video{width:100%;height:100%;max-height:540px;object-fit:contain}
    #${APP_ID}[data-layout="feed"] .scg-embed{aspect-ratio:auto}
    #${APP_ID}[data-layout="feed"] .scg-host{justify-content:center;padding:44px 24px 24px}
    #${APP_ID}[data-layout="feed"] .scg-actions{grid-area:actions;align-self:end;border-top:0;border-left:1px solid var(--scg-line);background:transparent}
    #${APP_ID}[data-layout="feed"] .scg-meta{grid-area:meta;border-left:1px solid var(--scg-line);border-top:1px solid var(--scg-line)}
    #${APP_ID}[data-layout="feed"] .scg-textcard{max-width:900px;justify-self:center}
    #${APP_ID}[data-layout="feed"] .scg-textcard p{-webkit-line-clamp:12}
    #${APP_ID}[data-layout="feed"] .scg-skeleton{display:grid;grid-template-columns:minmax(260px,46%) minmax(0,1fr);height:252px;margin:0}
    #${APP_ID}[data-layout="feed"] .scg-skeleton-media{height:100%;grid-row:1/4}
    #${APP_ID}[data-layout="feed"] .scg-skeleton-line{align-self:end}
    #${APP_ID}[data-layout="feed"] .scg-grid.grouped,
    #${APP_ID}[data-layout="grid"] .scg-grid.grouped{display:block;columns:auto}

    /* ---- 10. Cards -------------------------------------------- */
    #${APP_ID} .scg-card{
      position:relative;overflow:hidden;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-lg);
      background:var(--scg-surface);
      box-shadow:0 3px 12px #00000052;
      contain:paint style;
      transition:transform .18s cubic-bezier(.2,.7,.3,1),border-color .18s,box-shadow .18s;
    }
    #${APP_ID}.compact .scg-card{border-radius:var(--scg-r-md)}
    #${APP_ID} .scg-card:hover{
      transform:translateY(-2px);
      border-color:var(--scg-line-strong);
      box-shadow:0 7px 20px #00000066;
    }
    #${APP_ID} .scg-card:focus-within{border-color:color-mix(in srgb,var(--scg-accent) 55%,var(--scg-line))}
    #${APP_ID} .scg-card.selected{
      border-color:var(--scg-accent);
      box-shadow:0 0 0 2px color-mix(in srgb,var(--scg-accent) 35%,transparent),var(--scg-shadow);
    }
    #${APP_ID} .scg-card.downloaded{border-color:color-mix(in srgb,var(--scg-success) 42%,var(--scg-line))}
    #${APP_ID} .scg-card.download-legacy{border-color:color-mix(in srgb,var(--scg-warn) 42%,var(--scg-line))}

    #${APP_ID} .scg-preview{
      display:block;width:100%;padding:0;border:0;
      background:var(--scg-well);cursor:zoom-in;
    }
    #${APP_ID} .scg-card img,#${APP_ID} .scg-card video{
      display:block;width:100%;max-height:660px;object-fit:contain;background:var(--scg-well);
    }
    #${APP_ID}.compact .scg-card img,#${APP_ID}.compact .scg-card video{max-height:430px}

    #${APP_ID} .scg-badge{
      position:absolute;z-index:7;top:9px;right:9px;
      display:flex;align-items:center;gap:6px;max-width:calc(100% - 60px);
      padding:4px 8px;
      border:1px solid color-mix(in srgb,#ffffff 14%,transparent);
      border-radius:99px;
      background:#0b0d10f2;color:#dfe4ea;
      font-size:10px;font-weight:600;
      box-shadow:0 2px 7px #00000052;
    }
    #${APP_ID} .scg-badge b{
      color:#ffffff;font-size:9.5px;font-weight:700;
      text-transform:uppercase;letter-spacing:.09em;
    }
    #${APP_ID} .scg-badge span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.82}
    #${APP_ID} .scg-saved-status{
      display:none;padding:2px 6px;border-radius:99px;
      background:var(--scg-success);color:#04140e;
      font-size:9px;font-style:normal;font-weight:800;letter-spacing:.05em;
    }
    #${APP_ID} .scg-saved-status.visible{display:inline-block}
    #${APP_ID} .scg-saved-status.legacy{background:var(--scg-warn);color:#1c1405}

    #${APP_ID} .scg-select{position:absolute;z-index:8;top:9px;left:9px;display:none;width:30px;height:30px;cursor:pointer}
    #${APP_ID}.selecting .scg-select{display:block}
    #${APP_ID} .scg-select input{position:absolute;width:100%;height:100%;margin:0;opacity:0;cursor:pointer}
    #${APP_ID} .scg-select span{
      display:grid;place-items:center;width:30px;height:30px;
      border:1.5px solid #ffffffb8;border-radius:10px;
      background:#0b0d10f2;
      box-shadow:0 2px 8px #00000066;
      transition:background .14s,border-color .14s;
    }
    #${APP_ID} .scg-select input:checked+span{background:var(--scg-accent);border-color:var(--scg-accent)}
    #${APP_ID} .scg-select input:checked+span:after{
      content:'';width:11px;height:6px;margin-top:-3px;
      border-left:2.5px solid var(--scg-accent-ink);border-bottom:2.5px solid var(--scg-accent-ink);
      transform:rotate(-45deg);
    }
    #${APP_ID} .scg-select input:focus-visible+span{outline:2px solid var(--scg-focus);outline-offset:2px}

    #${APP_ID} .scg-actions{
      display:flex;gap:6px;padding:8px;
      border-top:1px solid var(--scg-line);background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-actions button{
      display:inline-flex;align-items:center;justify-content:center;gap:7px;flex:1;
      min-height:34px;padding:7px 9px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-sm);
      background:var(--scg-surface);color:var(--scg-text-soft);
      font-size:12px;font-weight:550;cursor:pointer;
      transition:background .14s,border-color .14s,color .14s;
    }
    #${APP_ID} .scg-actions button .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-actions button:hover{background:var(--scg-surface-3);color:var(--scg-text);border-color:var(--scg-line-strong)}
    #${APP_ID} .scg-actions button:hover .scg-icon{color:var(--scg-text)}
    #${APP_ID} .scg-actions .scg-download-action{
      border-color:color-mix(in srgb,var(--scg-accent) 38%,transparent);
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-actions .scg-download-action .scg-icon{color:var(--scg-accent)}
    #${APP_ID} .scg-actions .scg-download-action:hover{
      background:color-mix(in srgb,var(--scg-accent) 24%,var(--scg-surface-3));color:var(--scg-accent);
    }
    #${APP_ID} .scg-card.downloaded .scg-download-action{
      border-color:color-mix(in srgb,var(--scg-success) 40%,transparent);
      background:var(--scg-success-soft);color:var(--scg-success);
    }
    #${APP_ID} .scg-card.downloaded .scg-download-action .scg-icon{color:var(--scg-success)}
    #${APP_ID} .scg-card.download-legacy .scg-download-action{
      border-color:color-mix(in srgb,var(--scg-warn) 40%,transparent);
      background:var(--scg-warn-soft);color:var(--scg-warn);
    }
    #${APP_ID} .scg-card.download-legacy .scg-download-action .scg-icon{color:var(--scg-warn)}

    #${APP_ID} .scg-meta{
      display:flex;align-items:center;justify-content:space-between;gap:10px;
      padding:9px 11px;
      color:var(--scg-muted);font-size:11.5px;
    }
    #${APP_ID} .scg-meta>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    #${APP_ID} .scg-meta a{
      display:inline-flex;align-items:center;gap:6px;flex:none;
      padding:3px 4px;border-radius:6px;
      color:var(--scg-accent);text-decoration:none;font-weight:550;
    }
    #${APP_ID} .scg-meta a:hover{background:var(--scg-accent-soft);text-decoration:underline}
    #${APP_ID} .scg-meta a .scg-icon{width:14px;height:14px}

    #${APP_ID} .scg-host{
      display:flex;flex-direction:column;justify-content:center;gap:6px;
      min-height:140px;padding:44px 18px 18px;
      color:var(--scg-text);text-decoration:none;
      background:linear-gradient(150deg,var(--scg-surface-3),var(--scg-surface-2));
    }
    #${APP_ID} .scg-host strong{font-size:13.5px;word-break:break-word}
    #${APP_ID} .scg-host small{color:var(--scg-muted);font-size:11px}
    #${APP_ID} .scg-host:hover{background:linear-gradient(150deg,var(--scg-accent-soft),var(--scg-surface-2))}

    #${APP_ID} .scg-textcard p{
      display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:8;
      overflow:hidden;margin:0;padding:44px 14px 12px;
      color:var(--scg-text-soft);font-size:12.5px;line-height:1.55;white-space:pre-wrap;
    }
    #${APP_ID} .scg-textcard p.expanded{display:block;-webkit-line-clamp:unset}
    #${APP_ID} .scg-expand{
      display:flex;align-items:center;justify-content:center;gap:7px;
      width:calc(100% - 20px);min-height:32px;margin:0 10px 8px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-sm);
      background:transparent;color:var(--scg-accent);
      font-size:11.5px;font-weight:600;cursor:pointer;
    }
    #${APP_ID} .scg-expand:hover{background:var(--scg-accent-soft)}
    #${APP_ID} .scg-expand .scg-icon{transition:transform .18s ease}
    #${APP_ID} .scg-expand.expanded .scg-icon{transform:rotate(180deg)}

    #${APP_ID} .scg-embed{position:relative;width:100%;aspect-ratio:16/9;background:var(--scg-well)}
    #${APP_ID} .scg-embed iframe{display:block;width:100%;height:100%;border:0}
    #${APP_ID} .scg-embed-placeholder{
      display:grid;place-items:center;min-height:160px;
      background:radial-gradient(circle at 50% 6%,var(--scg-surface-3),var(--scg-well) 72%);
    }
    #${APP_ID} .scg-embed-placeholder button{
      display:flex;flex-direction:column;align-items:center;gap:7px;
      padding:15px 22px;
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-lg);
      background:var(--scg-surface);color:var(--scg-text);
      box-shadow:var(--scg-shadow-sm);cursor:pointer;
      transition:transform .16s,border-color .16s;
    }
    #${APP_ID} .scg-embed-placeholder button:hover{transform:translateY(-1px);border-color:var(--scg-accent)}
    #${APP_ID} .scg-embed-placeholder button .scg-icon-well{
      display:grid;place-items:center;width:38px;height:38px;
      border:1px solid color-mix(in srgb,var(--scg-accent) 40%,transparent);
      border-radius:13px;background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-embed-placeholder button .scg-icon{width:19px;height:19px}
    #${APP_ID} .scg-embed-placeholder button b{font-size:13px}
    #${APP_ID} .scg-embed-placeholder button span{color:var(--scg-muted);font-size:11px}

    /* ---- 11. Empty + skeleton --------------------------------- */
    #${APP_ID} .scg-empty{
      display:flex;flex-direction:column;align-items:center;gap:10px;
      padding:88px 24px;text-align:center;
      color:var(--scg-muted);font-size:13.5px;
      column-span:all;grid-column:1/-1;
    }
    #${APP_ID} .scg-empty .scg-empty-art{
      display:grid;place-items:center;width:64px;height:64px;
      border:1px solid var(--scg-line);border-radius:20px;
      background:var(--scg-surface);color:var(--scg-muted);
    }
    #${APP_ID} .scg-empty .scg-empty-art .scg-icon{width:28px;height:28px}
    #${APP_ID} .scg-empty b{color:var(--scg-text);font-size:15px;font-weight:600}
    #${APP_ID} .scg-empty span{max-width:420px;font-size:12.5px;line-height:1.55}

    #${APP_ID} .scg-skeleton{
      overflow:hidden;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-lg);
      background:var(--scg-surface);
    }
    #${APP_ID} .scg-skeleton-media,#${APP_ID} .scg-skeleton-line{position:relative;overflow:hidden;background:var(--scg-surface-3)}
    #${APP_ID} .scg-skeleton-media{height:var(--scg-skeleton-height,210px)}
    #${APP_ID} .scg-skeleton-line{height:9px;margin:13px;border-radius:99px}
    #${APP_ID} .scg-skeleton-line.short{width:48%;margin-top:0}
    #${APP_ID} .scg-skeleton-media:after,#${APP_ID} .scg-skeleton-line:after{
      content:'';position:absolute;inset:0;transform:translateX(-110%);
      background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--scg-text) 11%,transparent),transparent);
      animation:scg-shimmer 1.4s infinite;
    }
    @keyframes scg-shimmer{to{transform:translateX(110%)}}

    /* ---- 12. Reply groups ------------------------------------- */
    #${APP_ID} .scg-grid.grouped{display:block;columns:auto}
    #${APP_ID} .scg-reply-group{
      margin:0 0 16px;overflow:hidden;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-lg);
      background:var(--scg-surface);box-shadow:var(--scg-shadow-sm);
    }
    #${APP_ID} .scg-reply-group>header{
      display:flex;align-items:center;justify-content:space-between;gap:12px;
      padding:11px 14px;
      border-bottom:1px solid var(--scg-line);background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-reply-group>header>div{display:flex;align-items:center;gap:12px;flex-wrap:wrap;min-width:0}
    #${APP_ID} .scg-reply-group>header b{font-size:12.5px;color:var(--scg-text)}
    #${APP_ID} .scg-reply-group>header span{color:var(--scg-muted);font-size:11.5px}
    #${APP_ID} .scg-reply-group>header a{
      display:inline-flex;align-items:center;gap:6px;flex:none;
      padding:5px 9px;border:1px solid var(--scg-line);border-radius:var(--scg-r-sm);
      color:var(--scg-accent);text-decoration:none;font-size:11.5px;font-weight:550;white-space:nowrap;
    }
    #${APP_ID} .scg-reply-group>header a:hover{background:var(--scg-accent-soft)}
    #${APP_ID} .scg-reply-group>header a .scg-icon{width:14px;height:14px}


    /* ---- 13. The dock (status / selection / downloads) --------- */
    #${APP_ID} .scg-activitybar{
      position:absolute;z-index:30;left:50%;bottom:18px;transform:translateX(-50%);
      display:flex;align-items:center;gap:12px;flex-wrap:nowrap;
      width:min(1240px,calc(100% - 36px));min-height:64px;
      padding:11px 14px;
      border:1px solid color-mix(in srgb,#ffffff 22%,var(--scg-line-strong));border-radius:18px;
      background:linear-gradient(135deg,
        color-mix(in srgb,var(--scg-surface) 52%,transparent),
        color-mix(in srgb,var(--scg-surface-2) 34%,transparent));
      box-shadow:0 20px 54px #00000070,
        inset 0 1px 0 color-mix(in srgb,#ffffff 20%,transparent),
        inset 0 -1px 0 color-mix(in srgb,#000000 18%,transparent);
      backdrop-filter:blur(34px) saturate(180%);
      -webkit-backdrop-filter:blur(34px) saturate(180%);
      isolation:isolate;
    }
    #${APP_ID} .scg-activitybar:before{
      content:'';position:absolute;z-index:0;inset:1px;border-radius:inherit;pointer-events:none;
      background:linear-gradient(115deg,color-mix(in srgb,#ffffff 13%,transparent),transparent 38%,color-mix(in srgb,var(--scg-accent) 7%,transparent));
    }
    #${APP_ID} .scg-activitybar>*{position:relative;z-index:1}

    #${APP_ID} .scg-activity-state{display:flex;align-items:center;gap:10px;min-width:0;flex:0 1 260px;padding-left:4px}
    #${APP_ID} .scg-activity-state>i{
      width:8px;height:8px;flex:none;border-radius:99px;
      background:var(--scg-success);
      box-shadow:0 0 0 4px color-mix(in srgb,var(--scg-success) 18%,transparent);
    }
    #${APP_ID}.scanning .scg-activity-state>i,
    #${APP_ID}.downloading .scg-activity-state>i{
      background:var(--scg-accent);
      box-shadow:0 0 0 4px color-mix(in srgb,var(--scg-accent) 20%,transparent);
      animation:scg-pulse 1.25s ease-in-out infinite;
    }
    @keyframes scg-pulse{50%{opacity:.35}}
    #${APP_ID} .scg-activity-state>div{display:flex;flex-direction:column;gap:3px;min-width:0}
    #${APP_ID} .scg-activity-state .scg-status{
      display:block;margin:0;overflow:hidden;
      color:var(--scg-text);font-size:12px;font-weight:600;
      text-overflow:ellipsis;white-space:nowrap;
    }
    #${APP_ID} .scg-activity-state span{
      overflow:hidden;color:var(--scg-muted);font-size:11px;
      text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-activitybar button{
      display:inline-flex;align-items:center;justify-content:center;gap:7px;
      min-height:40px;padding:9px 12px;
      border:1px solid color-mix(in srgb,#ffffff 10%,var(--scg-line));border-radius:13px;
      background:color-mix(in srgb,var(--scg-surface-2) 50%,transparent);color:var(--scg-text-soft);
      font-size:12px;font-weight:550;cursor:pointer;white-space:nowrap;
      transition:background .14s,border-color .14s,color .14s;
    }
    #${APP_ID} .scg-activitybar button .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-activitybar button:hover:not(:disabled){background:var(--scg-surface-3);color:var(--scg-text);border-color:var(--scg-line-strong)}
    #${APP_ID} .scg-activitybar button:hover:not(:disabled) .scg-icon{color:var(--scg-text)}
    #${APP_ID} .scg-activitybar .scg-primary{
      border-color:color-mix(in srgb,var(--scg-accent) 60%,transparent);
      background:var(--scg-accent);color:var(--scg-accent-ink);font-weight:650;
    }
    #${APP_ID} .scg-activitybar .scg-primary .scg-icon{color:var(--scg-accent-ink)}
    #${APP_ID} .scg-activitybar .scg-primary:hover:not(:disabled){
      background:color-mix(in srgb,var(--scg-accent) 86%,#ffffff);color:var(--scg-accent-ink);
    }
    #${APP_ID} .scg-activitybar .scg-primary:hover:not(:disabled) .scg-icon{color:var(--scg-accent-ink)}

    #${APP_ID} .scg-selection-toggle{flex:none}
    #${APP_ID}.selecting .scg-selection-toggle{
      border-color:color-mix(in srgb,var(--scg-accent) 50%,transparent);
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID}.selecting .scg-selection-toggle .scg-icon{color:var(--scg-accent)}

    #${APP_ID} .scg-bulk-actions{display:none;align-items:center;gap:6px;min-width:0;flex:1}
    #${APP_ID}.selecting .scg-bulk-actions,
    #${APP_ID}.downloading .scg-bulk-actions{display:flex}
    #${APP_ID}.downloading:not(.selecting) .scg-bulk-actions{flex:0 0 auto}
    #${APP_ID}.downloading:not(.selecting) .scg-bulk-actions>*:not([data-action="download-selected"]){display:none}
    #${APP_ID} .scg-activitybar .cancel-download{
      border-color:color-mix(in srgb,var(--scg-danger) 65%,transparent);
      background:var(--scg-danger-soft);color:var(--scg-danger);
    }
    #${APP_ID} .scg-activitybar .cancel-download .scg-icon{color:var(--scg-danger)}
    #${APP_ID} .scg-activitybar .cancel-download:hover:not(:disabled){
      border-color:var(--scg-danger);background:color-mix(in srgb,var(--scg-danger-soft) 72%,var(--scg-danger));color:#ffffff;
    }
    #${APP_ID} .scg-activitybar .cancel-download:hover:not(:disabled) .scg-icon{color:#ffffff}
    #${APP_ID} .scg-bulk-label{
      flex:none;color:var(--scg-muted);
      font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;
    }
    #${APP_ID} .scg-selected-count{
      flex:none;margin-left:auto;padding:0 4px;
      color:var(--scg-text);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;
    }
    #${APP_ID}.selecting .scg-activity-state{flex:0 0 auto}
    #${APP_ID}.selecting .scg-activity-state>div{display:none}

    #${APP_ID} .scg-progress{position:relative;display:flex;min-width:220px;max-width:440px;flex:1 1 300px}
    #${APP_ID}.selecting .scg-progress{display:none}
    #${APP_ID}.selecting.has-downloads .scg-progress,
    #${APP_ID}.selecting.downloading .scg-progress{display:flex;min-width:150px;max-width:320px;flex:1 1 220px}
    #${APP_ID} .scg-progress-summary{
      display:flex !important;align-items:center;gap:10px;width:100%;min-width:0;
      min-height:40px;padding:8px 12px !important;
      border:1px solid color-mix(in srgb,#ffffff 10%,var(--scg-line));border-radius:13px;
      background:color-mix(in srgb,var(--scg-surface-2) 50%,transparent);color:var(--scg-text-soft);cursor:pointer;
    }
    #${APP_ID} .scg-progress-track{
      position:relative;display:block;height:6px;flex:1;min-width:70px;overflow:hidden;
      border-radius:99px;background:var(--scg-surface-3);
    }
    #${APP_ID} .scg-progress-fill{
      display:block;height:100%;width:0;border-radius:inherit;
      background:linear-gradient(90deg,var(--scg-accent),color-mix(in srgb,var(--scg-accent) 45%,var(--scg-success)));
      transition:width .2s ease;
    }
    #${APP_ID} .scg-progress-text{
      flex:none;max-width:220px;overflow:hidden;
      color:var(--scg-muted);font-size:11.5px;text-align:right;
      text-overflow:ellipsis;white-space:nowrap;font-variant-numeric:tabular-nums;
    }
    /* Download queue popover */
    #${APP_ID} .scg-download-popover{
      display:none;position:absolute;z-index:320;right:0;bottom:calc(100% + 10px);
      width:min(520px,calc(100vw - 32px));max-height:min(540px,66vh);
      overflow:hidden;
      border:1px solid color-mix(in srgb,#ffffff 14%,var(--scg-line-strong));border-radius:var(--scg-r-lg);
      background:linear-gradient(145deg,
        color-mix(in srgb,var(--scg-surface) 94%,transparent),
        color-mix(in srgb,var(--scg-surface-2) 90%,transparent));
      color:var(--scg-text);
      box-shadow:0 18px 48px #00000080,
        inset 0 1px 0 color-mix(in srgb,#ffffff 8%,transparent);
      backdrop-filter:blur(20px) saturate(145%);
      -webkit-backdrop-filter:blur(20px) saturate(145%);
      isolation:isolate;
    }
    #${APP_ID} .scg-progress.expanded .scg-download-popover{display:block}
    #${APP_ID} .scg-download-popover>header{
      display:flex;align-items:center;justify-content:space-between;gap:12px;
      padding:11px 13px;
      border-bottom:1px solid var(--scg-line);background:color-mix(in srgb,var(--scg-surface-2) 92%,transparent);
    }
    #${APP_ID} .scg-download-popover>header b{font-size:13px}
    #${APP_ID} .scg-download-popover>header>div{display:flex;align-items:center;gap:5px}
    #${APP_ID} .scg-download-popover>header button{
      display:inline-flex;align-items:center;gap:6px;
      min-height:30px;padding:5px 9px !important;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-sm);
      background:color-mix(in srgb,var(--scg-surface) 94%,transparent);color:var(--scg-text-soft);
      font-size:11px;cursor:pointer;
    }
    #${APP_ID} .scg-download-popover>header button:hover{background:var(--scg-surface-3);color:var(--scg-text)}
    #${APP_ID} .scg-download-overall{
      padding:10px 13px;border-bottom:1px solid var(--scg-line);
      color:var(--scg-muted);font-size:11.5px;font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-download-jobs{max-height:min(420px,48vh);overflow:auto;padding:8px}
    #${APP_ID} .scg-download-job{padding:9px 10px;border-radius:var(--scg-r-md);contain:content}
    #${APP_ID} .scg-download-job+.scg-download-job{border-top:1px solid var(--scg-line)}
    #${APP_ID} .scg-download-job>div:first-child{display:flex;align-items:center;justify-content:space-between;gap:12px}
    #${APP_ID} .scg-download-job b{
      min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
      color:var(--scg-text);font-size:11.5px;font-weight:550;
    }
    #${APP_ID} .scg-download-job span{flex:none;color:var(--scg-muted);font-size:10.5px;font-variant-numeric:tabular-nums}
    #${APP_ID} .scg-job-track{height:4px;margin-top:7px;overflow:hidden;border-radius:99px;background:var(--scg-surface-3)}
    #${APP_ID} .scg-job-track i{
      display:block;height:100%;border-radius:inherit;
      background:var(--scg-accent);transition:width .15s;
    }
    #${APP_ID} .scg-job-track i.indeterminate{animation:scg-indeterminate 1s ease-in-out infinite}
    @keyframes scg-indeterminate{0%{transform:translateX(-120%)}50%{transform:translateX(95%)}100%{transform:translateX(290%)}}
    #${APP_ID} .scg-job-saved .scg-job-track i{background:var(--scg-success)}
    #${APP_ID} .scg-job-failed .scg-job-track i,
    #${APP_ID} .scg-job-verification .scg-job-track i{background:var(--scg-danger)}
    #${APP_ID} .scg-job-duplicate .scg-job-track i{background:var(--scg-warn)}
    #${APP_ID} .scg-job-duplicate,#${APP_ID} .scg-job-skipped{opacity:.72}
    #${APP_ID} .scg-download-empty{padding:28px;text-align:center;color:var(--scg-muted);font-size:12px}

    /* ---- 14. Settings sheet ----------------------------------- */
    #${APP_ID} .scg-settings-panel{
      display:none;position:fixed;inset:0;z-index:2147483647;
      align-items:center;justify-content:center;padding:24px;
      background:color-mix(in srgb,var(--scg-bg) 78%,transparent);
      backdrop-filter:blur(10px);
    }
    #${APP_ID} .scg-settings-panel.open{display:flex}
    #${APP_ID} .scg-settings-dialog{
      display:flex;flex-direction:column;
      width:min(940px,calc(100vw - 32px));max-height:min(820px,calc(100vh - 32px));
      overflow:hidden;outline:none;
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-xl);
      background:var(--scg-surface);box-shadow:var(--scg-shadow);
    }
    #${APP_ID} .scg-settings-head{
      display:flex;align-items:center;justify-content:space-between;gap:16px;
      padding:16px 18px;border-bottom:1px solid var(--scg-line);background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-settings-head>div{display:flex;align-items:center;gap:12px;min-width:0}
    #${APP_ID} .scg-settings-head>div>.scg-icon{width:22px;height:22px;color:var(--scg-accent)}
    #${APP_ID} .scg-settings-head h2{margin:0;font-size:17px;font-weight:650;letter-spacing:-.01em}
    #${APP_ID} .scg-settings-head p{margin:3px 0 0;color:var(--scg-muted);font-size:12px}
    #${APP_ID} .scg-settings-close{
      display:inline-grid;place-items:center;width:38px;height:38px;flex:none;padding:0;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface);color:var(--scg-text-soft);cursor:pointer;
    }
    #${APP_ID} .scg-settings-close:hover{
      border-color:color-mix(in srgb,var(--scg-danger) 45%,transparent);
      background:var(--scg-danger-soft);color:var(--scg-danger);
    }
    #${APP_ID} .scg-settings-body{overflow:auto;padding:16px}
    #${APP_ID} .scg-settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
    #${APP_ID} .scg-settings-card{
      padding:15px;border:1px solid var(--scg-line);border-radius:var(--scg-r-lg);
      background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-settings-card.full{grid-column:1/-1}
    #${APP_ID} .scg-settings-card h3{
      display:flex;align-items:center;gap:9px;margin:0 0 6px;
      color:var(--scg-text);font-size:13.5px;font-weight:650;
    }
    #${APP_ID} .scg-settings-card h3 .scg-icon{color:var(--scg-accent)}
    #${APP_ID} .scg-settings-card>p{margin:0 0 13px;color:var(--scg-muted);font-size:12px;line-height:1.5}
    #${APP_ID} .scg-settings-version{
      display:inline-flex;align-items:center;margin-left:auto;padding:3px 8px;
      border:1px solid var(--scg-accent-line);border-radius:99px;
      background:var(--scg-accent-soft);color:var(--scg-accent);
      font-size:10.5px;font-weight:650;
    }
    #${APP_ID} .scg-setting-fields{display:grid;grid-template-columns:1fr 1fr;gap:10px}
    #${APP_ID} .scg-setting-fields label{
      display:flex;flex-direction:column;gap:6px;min-width:0;
      color:var(--scg-muted);font-size:10px;font-weight:700;
      text-transform:uppercase;letter-spacing:.08em;
    }
    #${APP_ID} .scg-setting-fields select{width:100%;min-height:36px;font-size:12.5px}
    #${APP_ID} .scg-setting-size>span{display:flex;align-items:center;gap:10px;min-height:36px;padding:7px 10px;border:1px solid var(--scg-line);border-radius:var(--scg-r-md);background:var(--scg-surface)}
    #${APP_ID} .scg-setting-size [data-card-scale]{width:100%;flex:1}
    #${APP_ID} .scg-download-settings{grid-template-columns:repeat(4,minmax(0,1fr))}
    #${APP_ID} .scg-archive-checks{display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;margin-top:10px}
    #${APP_ID} .scg-settings-check{
      display:flex;align-items:flex-start;gap:9px;margin:10px 0;
      color:var(--scg-muted);font-size:11.5px;line-height:1.5;cursor:pointer;
    }
    #${APP_ID} .scg-settings-check input{margin-top:1px;flex:none}
    #${APP_ID} .scg-settings-check:hover{color:var(--scg-text-soft)}
    #${APP_ID} .scg-settings-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
    #${APP_ID} .scg-settings-actions button{
      display:inline-flex;align-items:center;justify-content:center;gap:7px;
      min-height:36px;padding:8px 12px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface);color:var(--scg-text);
      font-size:12px;font-weight:550;cursor:pointer;
    }
    #${APP_ID} .scg-settings-actions button .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-settings-actions button:hover{background:var(--scg-surface-3);border-color:var(--scg-line-strong)}
    #${APP_ID} .scg-settings-actions .danger{
      border-color:color-mix(in srgb,var(--scg-danger) 45%,transparent);
      background:var(--scg-danger-soft);color:var(--scg-danger);
    }
    #${APP_ID} .scg-settings-actions .danger .scg-icon{color:var(--scg-danger)}
    #${APP_ID} .scg-settings-file{display:none}
    #${APP_ID} .scg-diag-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:9px}
    #${APP_ID} .scg-diag-stat{
      min-width:0;padding:10px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);background:var(--scg-surface);
    }
    #${APP_ID} .scg-diag-stat span{
      display:block;color:var(--scg-muted);
      font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
    }
    #${APP_ID} .scg-diag-stat b{
      display:block;margin-top:5px;overflow:hidden;text-overflow:ellipsis;
      color:var(--scg-text);font-size:12px;font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-storage-note{
      display:flex;align-items:flex-start;gap:9px;padding:10px 12px;
      border:1px solid color-mix(in srgb,var(--scg-success) 34%,var(--scg-line));
      border-radius:var(--scg-r-md);
      background:var(--scg-success-soft);color:var(--scg-text-soft);font-size:11.5px;line-height:1.5;
    }
    #${APP_ID} .scg-storage-note .scg-icon{color:var(--scg-success);margin-top:1px}
    #${APP_ID} .scg-permission-note{
      padding:11px 13px;border-left:3px solid var(--scg-accent);border-radius:0 var(--scg-r-sm) var(--scg-r-sm) 0;
      background:var(--scg-accent-soft);color:var(--scg-text-soft);font-size:11.5px;line-height:1.55;
    }
    #${APP_ID} .scg-settings-foot{
      display:flex;align-items:center;justify-content:space-between;gap:12px;
      padding:12px 18px;border-top:1px solid var(--scg-line);
      background:var(--scg-surface-2);color:var(--scg-muted);font-size:11.5px;
    }
    #${APP_ID} .scg-settings-foot b{color:var(--scg-accent)}

    /* ---- 15. Confirm dialog ----------------------------------- */
    #${APP_ID} .scg-confirm-panel{
      display:none;position:fixed;inset:0;z-index:2147483647;
      align-items:center;justify-content:center;padding:18px;
      background:color-mix(in srgb,var(--scg-bg) 74%,transparent);backdrop-filter:blur(8px);
    }
    #${APP_ID} .scg-confirm-panel.open{display:flex}
    #${APP_ID} .scg-confirm-dialog{
      width:min(440px,100%);padding:24px;text-align:center;
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-xl);
      background:var(--scg-surface);box-shadow:var(--scg-shadow);
    }
    #${APP_ID} .scg-confirm-icon{
      display:grid;place-items:center;width:46px;height:46px;margin:0 auto 14px;
      border:1px solid var(--scg-accent-line);border-radius:15px;
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-confirm-icon .scg-icon{width:22px;height:22px}
    #${APP_ID} .scg-confirm-dialog h2{margin:0;color:var(--scg-text);font-size:16.5px;font-weight:650}
    #${APP_ID} .scg-confirm-dialog p{margin:9px 0 20px;color:var(--scg-muted);font-size:12.5px;line-height:1.55}
    #${APP_ID} .scg-confirm-preference{
      display:flex;align-items:center;justify-content:center;gap:8px;
      margin:-8px 0 18px;color:var(--scg-text-soft);font-size:12px;cursor:pointer;
    }
    #${APP_ID} .scg-reply-toggle{
      display:inline-flex;align-items:center;gap:5px;flex:none;
      min-height:30px;padding:5px 8px;border:1px solid var(--scg-line);border-radius:var(--scg-r-sm);
      background:var(--scg-surface);color:var(--scg-text-soft);font-size:11px;cursor:pointer;
    }
    #${APP_ID} .scg-reply-select{
      display:none;align-items:center;gap:6px;flex:none;min-height:30px;padding:5px 8px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-sm);
      background:var(--scg-surface);color:var(--scg-text-soft);font-size:11px;cursor:pointer;
    }
    #${APP_ID}.selecting .scg-reply-select{display:inline-flex}
    #${APP_ID} .scg-reply-select input{width:16px;height:16px;margin:0;accent-color:var(--scg-accent);cursor:pointer}
    #${APP_ID} .scg-reply-select .scg-icon{color:var(--scg-accent)}
    #${APP_ID} .scg-reply-toggle:hover{border-color:var(--scg-line-strong);background:var(--scg-surface-3)}
    #${APP_ID} .scg-reply-toggle .scg-icon{transition:transform .18s ease}
    #${APP_ID} .scg-reply-toggle[aria-expanded="false"] .scg-icon{transform:rotate(-90deg)}
    #${APP_ID} .scg-reply-group.collapsed>header{border-bottom:0}
    #${APP_ID} .scg-collapsed-status{color:var(--scg-accent);font-size:10px;font-style:normal;font-weight:650;text-transform:uppercase;letter-spacing:.06em}
    #${APP_ID} .scg-confirm-preference[hidden]{display:none}
    #${APP_ID} .scg-confirm-preference input{accent-color:var(--scg-accent)}
    #${APP_ID} .scg-confirm-dialog>div:last-child{display:flex;justify-content:center;gap:9px}
    #${APP_ID} .scg-confirm-dialog button{
      display:inline-flex;align-items:center;justify-content:center;gap:7px;
      min-width:120px;min-height:40px;padding:10px 15px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-text);
      font-size:12.5px;font-weight:550;cursor:pointer;
    }
    #${APP_ID} .scg-confirm-dialog button:hover{background:var(--scg-surface-3)}
    #${APP_ID} .scg-confirm-accept{
      border-color:var(--scg-accent) !important;
      background:var(--scg-accent) !important;color:var(--scg-accent-ink) !important;font-weight:650;
    }
    #${APP_ID} .scg-confirm-accept .scg-icon{color:var(--scg-accent-ink)}
    #${APP_ID} .scg-confirm-accept.danger{
      border-color:var(--scg-danger) !important;
      background:var(--scg-danger) !important;color:#1b0308 !important;
    }
    #${APP_ID} .scg-confirm-accept.danger .scg-icon{color:#1b0308}


    /* ---- 16. Lightbox: the Theatre ----------------------------- */
    #${APP_ID} .scg-lightbox{
      display:none;position:fixed;inset:0;z-index:2147483647;
      overflow:hidden;background:#000000;color:var(--scg-text);
    }
    #${APP_ID} .scg-lightbox.open{display:block}
    #${APP_ID}[data-theme="light"] .scg-lightbox{background:#c9d1dc}
    #${APP_ID} .scg-viewer-shell{
      display:grid;grid-template-rows:auto minmax(0,1fr) auto;
      width:100%;height:100%;height:100dvh;
      background:radial-gradient(circle at 50% 32%,color-mix(in srgb,var(--scg-surface) 62%,#000000) 0,#050506 62%,#000000 100%);
    }
    #${APP_ID}[data-theme="light"] .scg-viewer-shell{
      background:radial-gradient(circle at 50% 32%,#ffffff 0,#e6ebf2 58%,#cfd7e2 100%);
    }
    #${APP_ID} .scg-lightbox:fullscreen .scg-viewer-shell{height:100vh}

    /* Floating top bar */
    #${APP_ID} .scg-viewer-topbar{
      position:relative;z-index:20;align-self:start;
      display:flex;align-items:center;gap:14px;min-width:0;
      width:calc(100% - 24px);margin:12px;
      padding:8px 10px 8px 14px;
      border:1px solid color-mix(in srgb,#ffffff 15%,var(--scg-line));border-radius:var(--scg-r-lg);
      background:linear-gradient(135deg,
        color-mix(in srgb,var(--scg-surface) 62%,transparent),
        color-mix(in srgb,var(--scg-surface-2) 44%,transparent));
      box-shadow:0 12px 34px #0000005c,inset 0 1px 0 color-mix(in srgb,#ffffff 10%,transparent);
      backdrop-filter:blur(28px) saturate(160%);
      -webkit-backdrop-filter:blur(28px) saturate(160%);
      visibility:visible;will-change:opacity,transform;
      transition:opacity .28s cubic-bezier(.2,.7,.3,1),transform .28s cubic-bezier(.2,.7,.3,1),visibility 0s;
    }
    #${APP_ID} .scg-viewer-identity{display:flex;align-items:center;gap:14px;min-width:0;flex:1}
    #${APP_ID} .scg-viewer-identity>b{
      display:inline-flex;align-items:baseline;gap:3px;flex:none;
      padding:5px 10px;border:1px solid var(--scg-line);border-radius:99px;
      background:var(--scg-surface-2);color:var(--scg-text);
      font-size:14px;font-weight:650;font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-viewer-identity>b span{color:var(--scg-muted);font-size:11px;font-weight:550}
    #${APP_ID} .scg-viewer-identity>div{min-width:0}
    #${APP_ID} .scg-viewer-titleline{display:flex;align-items:center;gap:9px;min-width:0}
    #${APP_ID} .scg-viewer-titleline strong{
      min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
      font-size:13px;font-weight:600;
    }
    #${APP_ID} .scg-viewer-resolution{
      display:inline-flex;align-items:center;flex:none;
      padding:3px 8px;border:1px solid var(--scg-accent-line);border-radius:99px;
      background:var(--scg-accent-soft);color:var(--scg-accent);
      font-size:10.5px;font-weight:650;font-variant-numeric:tabular-nums;white-space:nowrap;
    }
    #${APP_ID} .scg-viewer-identity small{
      display:block;margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
      max-width:min(760px,54vw);color:var(--scg-muted);font-size:11px;
    }

    #${APP_ID} .scg-viewer-tools{position:relative;z-index:21;display:flex;align-items:center;gap:4px;flex:none}
    #${APP_ID} .scg-viewer-tools button{
      display:inline-grid;place-items:center;
      min-width:36px;height:36px;padding:8px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-text-soft);
      cursor:pointer;transition:background .14s,border-color .14s,color .14s;
    }
    #${APP_ID} .scg-viewer-tools button:hover:not(:disabled){background:var(--scg-surface-3);color:var(--scg-text);border-color:var(--scg-line-strong)}
    #${APP_ID} .scg-viewer-tools button[aria-pressed="true"]{
      border-color:color-mix(in srgb,var(--scg-accent) 50%,transparent);
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-viewer-zoom{
      display:flex;align-items:center;gap:3px;
      padding:3px;margin-right:5px;
      border:1px solid var(--scg-line);border-radius:13px;background:var(--scg-surface-2);
    }
    #${APP_ID} .scg-viewer-zoom.unavailable{display:none}
    #${APP_ID} .scg-viewer-zoom button{min-width:32px;height:30px;border-color:transparent;background:transparent}
    #${APP_ID} .scg-viewer-zoom [data-viewer-fit]{
      grid-auto-flow:column;gap:6px;min-width:84px;padding:6px 9px;
    }
    #${APP_ID} .scg-viewer-zoom [data-viewer-fit] span{
      font-size:11px;font-weight:650;font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-viewer-tools .scg-viewer-download{
      border-color:color-mix(in srgb,var(--scg-accent) 55%,transparent);
      background:var(--scg-accent);color:var(--scg-accent-ink);
    }
    #${APP_ID} .scg-viewer-tools .scg-viewer-download:hover{
      background:color-mix(in srgb,var(--scg-accent) 86%,#ffffff);color:var(--scg-accent-ink);
    }
    #${APP_ID} .scg-viewer-tools .scg-lightbox-close{margin-left:5px;color:var(--scg-text)}
    #${APP_ID} .scg-viewer-tools .scg-lightbox-close:hover{
      border-color:color-mix(in srgb,var(--scg-danger) 50%,transparent);
      background:var(--scg-danger-soft);color:var(--scg-danger);
    }

    /* Stage + details */
    #${APP_ID} .scg-viewer-body{
      position:relative;display:grid;grid-template-columns:minmax(0,1fr) 336px;
      min-width:0;min-height:0;overflow:hidden;
      transition:grid-template-columns .22s ease;
    }
    #${APP_ID} .scg-lightbox.details-hidden .scg-viewer-body{grid-template-columns:minmax(0,1fr) 0}
    #${APP_ID} .scg-lightbox-stage{
      position:relative;display:flex;align-items:center;justify-content:center;
      width:100%;height:100%;min-width:0;min-height:0;
      padding:18px;overflow:hidden;
      touch-action:none;user-select:none;
    }
    #${APP_ID} .scg-lightbox-stage .scg-viewer-image{
      position:relative;z-index:1;display:block;
      width:auto;height:auto;max-width:100%;max-height:100%;
      object-fit:contain;border-radius:4px;
      background:var(--scg-well);
      box-shadow:0 24px 80px #000000b3;
      transform:var(--scg-viewer-transform,translate3d(0,0,0) scale(1)) !important;
      transform-origin:center;transition:transform .14s ease;will-change:transform;cursor:zoom-in;
    }
    #${APP_ID}[data-theme="light"] .scg-lightbox-stage .scg-viewer-image{box-shadow:0 20px 60px #35496433}
    #${APP_ID} .scg-lightbox.viewer-zoomed .scg-viewer-image{cursor:grab;transition:none}
    #${APP_ID} .scg-lightbox.viewer-zoomed .scg-viewer-image.dragging{cursor:grabbing}
    #${APP_ID} .scg-lightbox-stage video,#${APP_ID} .scg-lightbox-stage iframe{
      position:relative;z-index:1;display:block;
      width:100%;height:100%;max-width:100%;max-height:100%;
      border:0;border-radius:10px;background:#000000;object-fit:contain;
      box-shadow:0 24px 80px #000000b3;
    }
    #${APP_ID} .scg-lightbox-stage.viewer-buffering:before{
      content:'';position:absolute;z-index:3;width:30px;height:30px;
      border:3px solid color-mix(in srgb,var(--scg-text) 22%,transparent);
      border-top-color:var(--scg-accent);border-radius:99px;
      animation:scg-viewer-spin .75s linear infinite;pointer-events:none;
    }
    @keyframes scg-viewer-loading-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
    @keyframes scg-viewer-spin{to{transform:rotate(360deg)}}
    #${APP_ID} .scg-viewer-loading{
      display:flex;align-items:center;gap:11px;padding:9px 12px;
      border:1px solid color-mix(in srgb,var(--scg-line-strong) 72%,transparent);border-radius:99px;
      background:color-mix(in srgb,var(--scg-surface) 44%,transparent);
      color:var(--scg-muted);font-size:12px;
      backdrop-filter:blur(14px) saturate(145%);
      -webkit-backdrop-filter:blur(14px) saturate(145%);
      animation:scg-viewer-loading-in .2s ease both;
    }
    #${APP_ID} .scg-viewer-loading i{
      display:block;width:19px;height:19px;flex:none;
      border:2px solid color-mix(in srgb,var(--scg-text) 20%,transparent);
      border-top-color:var(--scg-accent);border-radius:99px;
      animation:scg-viewer-spin .75s linear infinite;
      transform-origin:center;will-change:transform;
    }
    #${APP_ID} .scg-viewer-error{
      max-width:470px;padding:24px;
      border:1px solid color-mix(in srgb,var(--scg-danger) 40%,var(--scg-line));
      border-radius:var(--scg-r-lg);
      background:var(--scg-danger-soft);color:var(--scg-danger);
      text-align:center;font-size:12.5px;line-height:1.55;
    }

    #${APP_ID} .scg-viewer-details{
      min-width:0;overflow:auto;
      margin:0 12px 12px 0;padding:18px;
      border:1px solid var(--scg-line-strong);border-radius:var(--scg-r-lg);
      background:color-mix(in srgb,var(--scg-surface) 90%,transparent);
      box-shadow:var(--scg-shadow);
      backdrop-filter:blur(22px) saturate(130%);
      opacity:1;transition:opacity .18s ease,transform .22s ease;
    }
    #${APP_ID} .scg-lightbox.details-hidden .scg-viewer-details{
      overflow:hidden;opacity:0;pointer-events:none;transform:translateX(34px);
    }
    #${APP_ID} .scg-viewer-details-head{
      display:flex;align-items:center;gap:11px;
      padding-bottom:15px;border-bottom:1px solid var(--scg-line);
    }
    #${APP_ID} .scg-viewer-details-head>span{
      display:grid;place-items:center;width:38px;height:38px;flex:none;
      border:1px solid var(--scg-accent-line);border-radius:12px;
      background:var(--scg-accent-soft);color:var(--scg-accent);
    }
    #${APP_ID} .scg-viewer-details-head>div{min-width:0}
    #${APP_ID} .scg-viewer-details-head b,#${APP_ID} .scg-viewer-details-head small{
      display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    }
    #${APP_ID} .scg-viewer-details-head b{font-size:13.5px;font-weight:650}
    #${APP_ID} .scg-viewer-details-head small{margin-top:3px;color:var(--scg-muted);font-size:11px}
    #${APP_ID} .scg-viewer-caption{
      margin:15px 0;padding:13px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-text-soft);
      font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;
    }
    #${APP_ID} .scg-viewer-details dl{margin:15px 0}
    #${APP_ID} .scg-viewer-details dl>div{
      display:grid;grid-template-columns:88px minmax(0,1fr);gap:10px;
      padding:9px 0;border-bottom:1px solid var(--scg-line);
    }
    #${APP_ID} .scg-viewer-details dt{
      color:var(--scg-muted);font-size:10px;font-weight:700;
      text-transform:uppercase;letter-spacing:.08em;
    }
    #${APP_ID} .scg-viewer-details dd{
      min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;
      color:var(--scg-text);font-size:11.5px;font-weight:550;font-variant-numeric:tabular-nums;
    }
    #${APP_ID} .scg-viewer-details-links{display:grid;grid-template-columns:1fr 1fr;gap:8px}
    #${APP_ID} .scg-viewer-details-links a{
      display:flex;align-items:center;justify-content:center;gap:7px;
      min-height:36px;padding:9px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-accent);
      text-decoration:none;font-size:11.5px;font-weight:550;
    }
    #${APP_ID} .scg-viewer-details-links a:hover{background:var(--scg-accent-soft);border-color:var(--scg-accent-line)}
    #${APP_ID} .scg-viewer-details-links a .scg-icon{width:14px;height:14px}
    #${APP_ID} .scg-viewer-shortcuts{
      margin:18px 0 0;padding-top:14px;border-top:1px solid var(--scg-line);
      color:var(--scg-muted);font-size:10.5px;line-height:1.75;
    }

    /* Navigation */
    #${APP_ID} .scg-lightbox .scg-nav{
      position:absolute;z-index:8;top:50%;transform:translateY(-50%);
      display:grid;place-items:center;width:46px;height:46px;padding:0;
      border:1px solid var(--scg-line-strong);border-radius:50%;
      background:color-mix(in srgb,var(--scg-surface) 84%,transparent);
      color:var(--scg-text);
      box-shadow:var(--scg-shadow);backdrop-filter:blur(16px);
      opacity:.78;cursor:pointer;
      transition:opacity .18s,transform .18s,background .18s,border-color .18s;
    }
    #${APP_ID} .scg-lightbox .scg-nav:hover{
      opacity:1;transform:translateY(-50%) scale(1.06);
      border-color:color-mix(in srgb,var(--scg-accent) 55%,var(--scg-line-strong));
      background:var(--scg-surface);
    }
    #${APP_ID} .scg-lightbox .scg-nav .scg-icon{width:22px;height:22px;stroke-width:2.2}
    #${APP_ID} .scg-lightbox .scg-prev{left:14px}
    #${APP_ID} .scg-lightbox .scg-next{right:362px}
    #${APP_ID} .scg-lightbox.details-hidden .scg-next{right:14px}

    /* Filmstrip footer */
    #${APP_ID} .scg-viewer-footer{
      position:relative;z-index:15;align-self:end;
      display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;
      width:calc(100% - 24px);margin:0 12px 12px;
      padding:8px 10px;
      border:1px solid color-mix(in srgb,#ffffff 15%,var(--scg-line));border-radius:var(--scg-r-lg);
      background:linear-gradient(135deg,
        color-mix(in srgb,var(--scg-surface) 62%,transparent),
        color-mix(in srgb,var(--scg-surface-2) 44%,transparent));
      box-shadow:0 12px 34px #0000005c,inset 0 1px 0 color-mix(in srgb,#ffffff 10%,transparent);
      backdrop-filter:blur(28px) saturate(160%);
      -webkit-backdrop-filter:blur(28px) saturate(160%);
      visibility:visible;will-change:opacity,transform;
      transition:opacity .28s cubic-bezier(.2,.7,.3,1),transform .28s cubic-bezier(.2,.7,.3,1),visibility 0s;
    }
    #${APP_ID} .scg-viewer-strip{
      display:flex;align-items:center;gap:7px;min-width:0;
      overflow-x:auto;padding:2px;
    }
    #${APP_ID} .scg-viewer-thumb{
      position:relative;width:56px;height:56px;flex:none;overflow:hidden;padding:0;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-muted);
      opacity:.66;cursor:pointer;transition:opacity .14s,border-color .14s,transform .14s;
    }
    #${APP_ID} .scg-viewer-thumb:hover{opacity:.92;transform:translateY(-1px)}
    #${APP_ID} .scg-viewer-thumb.active{
      border-color:var(--scg-accent);opacity:1;
      box-shadow:0 0 0 2px color-mix(in srgb,var(--scg-accent) 34%,transparent);
    }
    #${APP_ID} .scg-viewer-thumb img{display:block;width:100%;height:100%;object-fit:cover;background:var(--scg-well)}
    #${APP_ID} .scg-viewer-thumb>span{display:grid;place-items:center;width:100%;height:100%}
    #${APP_ID} .scg-viewer-thumb em{
      position:absolute;right:3px;bottom:3px;min-width:16px;padding:1px 4px;
      border-radius:5px;background:#05070ad9;color:#e2e6ec;
      font-size:8.5px;font-style:normal;font-weight:650;text-align:center;
    }
    #${APP_ID} .scg-viewer-footer .scg-lightbox-actions{display:flex;align-items:center;gap:6px;flex:none}
    #${APP_ID} .scg-viewer-footer .scg-lightbox-actions button{
      display:inline-flex;align-items:center;justify-content:center;gap:7px;
      min-height:36px;padding:8px 11px;
      border:1px solid var(--scg-line);border-radius:var(--scg-r-md);
      background:var(--scg-surface-2);color:var(--scg-text-soft);
      font-size:12px;font-weight:550;cursor:pointer;white-space:nowrap;
    }
    #${APP_ID} .scg-viewer-footer .scg-lightbox-actions button .scg-icon{color:var(--scg-muted)}
    #${APP_ID} .scg-viewer-footer .scg-lightbox-actions button:hover{
      background:var(--scg-surface-3);color:var(--scg-text);border-color:var(--scg-line-strong);
    }

    /* The translucent viewer chrome yields to the media after inactivity. */
    #${APP_ID} .scg-lightbox.viewer-idle .scg-viewer-topbar{
      opacity:0;visibility:hidden;pointer-events:none;transform:translate3d(0,-18px,0);
      transition:opacity .28s cubic-bezier(.4,0,1,1),transform .28s cubic-bezier(.4,0,1,1),visibility 0s .28s;
    }
    #${APP_ID} .scg-lightbox.viewer-idle .scg-viewer-footer{
      opacity:0;visibility:hidden;pointer-events:none;transform:translate3d(0,18px,0);
      transition:opacity .28s cubic-bezier(.4,0,1,1),transform .28s cubic-bezier(.4,0,1,1),visibility 0s .28s;
    }
    #${APP_ID} .scg-lightbox.viewer-idle.details-hidden .scg-nav{opacity:0;pointer-events:none}


    /* ---- 17. Responsive: wide laptop ---------------------------- */
    @media(max-width:1340px){
      #${APP_ID} .scg-header{grid-template-columns:auto minmax(0,1fr) auto;row-gap:10px}
      #${APP_ID} .scg-controls{grid-column:1/-1;order:3}
      #${APP_ID}[data-layout="masonry"] .scg-grid:not(.grouped){columns:4 var(--scg-masonry-width,280px)}
      #${APP_ID}[data-layout="masonry"].compact .scg-grid:not(.grouped){columns:6 var(--scg-masonry-compact-width,195px)}
      #${APP_ID} .scg-lightbox .scg-next{right:342px}
    }

    /* ---- 18. Responsive: tablet -------------------------------- */
    @media(max-width:1024px){
      #${APP_ID} .scg-plate{margin:10px 10px 0}
      #${APP_ID} .scg-header{grid-template-columns:auto minmax(0,1fr) auto;gap:10px;padding:9px 10px}
      #${APP_ID} .scg-thread-context{padding-left:10px}
      #${APP_ID} .scg-filter-panel{padding:0 10px 9px;gap:8px}
      #${APP_ID} .scg-viewbar{flex-basis:100%;justify-content:flex-start}
      #${APP_ID} .scg-view-summary{margin-right:auto}
      #${APP_ID} .scg-scroll{inset:10px 10px 0;padding:14px 12px 136px}
      #${APP_ID} .scg-activitybar{width:calc(100% - 20px);bottom:10px;flex-wrap:wrap}
      #${APP_ID} .scg-top{right:20px;bottom:122px}
      #${APP_ID}[data-layout="masonry"] .scg-grid:not(.grouped){columns:3 var(--scg-masonry-width,280px)}
      #${APP_ID}[data-layout="masonry"].compact .scg-grid:not(.grouped){columns:4 var(--scg-masonry-compact-width,195px)}
      #${APP_ID} .scg-settings-grid{grid-template-columns:1fr}
      #${APP_ID} .scg-settings-card.full{grid-column:auto}
      #${APP_ID} .scg-download-settings{grid-template-columns:repeat(2,minmax(0,1fr))}
    }

    @media(max-width:980px){
      #${APP_ID} .scg-viewer-body{display:block}
      #${APP_ID} .scg-viewer-details{
        position:absolute;z-index:9;top:10px;right:10px;bottom:10px;
        width:min(340px,calc(100vw - 76px));margin:0;
      }
      #${APP_ID} .scg-lightbox.details-hidden .scg-viewer-details{transform:translateX(calc(100% + 26px))}
      #${APP_ID} .scg-lightbox .scg-next,
      #${APP_ID} .scg-lightbox.details-hidden .scg-next{right:14px}
      #${APP_ID} .scg-viewer-footer .scg-lightbox-actions button>span{display:none}
      #${APP_ID} .scg-viewer-footer .scg-lightbox-actions button{min-width:36px;padding:8px}
      #${APP_ID} .scg-viewer-identity small{max-width:40vw}
    }

    /* ---- 19. Responsive: narrow window / phone ------------------ */
    @media(max-width:720px){
      #${APP_ID} .scg-plate{margin:8px 8px 0;border-radius:var(--scg-r-md)}
      #${APP_ID} .scg-header{grid-template-columns:auto minmax(0,1fr) auto;gap:9px;padding:8px}
      #${APP_ID} .scg-brand small{display:none}
      #${APP_ID} .scg-brand-mark{width:34px;height:34px;border-radius:11px}
      #${APP_ID} .scg-thread-context{padding-left:9px}
      #${APP_ID} .scg-thread-context h1{font-size:12.5px}
      #${APP_ID} .scg-thread-context [data-source-summary]{display:none}
      #${APP_ID} .scg-controls{flex-wrap:wrap}
      #${APP_ID} .scg-search{flex-basis:100%}
      #${APP_ID} .scg-scan-actions{width:100%;justify-content:space-between}
      #${APP_ID} .scg-scan-actions button{flex:1;min-width:0;padding:6px 8px}
      #${APP_ID} .scg-scan-actions [data-action="page"]>span{display:none}
      #${APP_ID} .scg-source-page{flex:1;min-width:0}
      #${APP_ID} .scg-source-page select{min-width:0;width:100%}
      #${APP_ID} .scg-filter-panel{padding:0 8px 8px}
      #${APP_ID} .scg-filters{width:100%}
      #${APP_ID} .scg-filters button{flex:1;justify-content:center;padding:6px 8px}
      #${APP_ID} .scg-filters button b{display:none}
      #${APP_ID} .scg-layout-switcher button>span{display:none}
      #${APP_ID} .scg-viewbar{flex-wrap:wrap;gap:8px}
      #${APP_ID} .scg-view-summary{flex-basis:100%;margin:0}
      #${APP_ID} .scg-viewbar label{font-size:9.5px}
      #${APP_ID} .scg-refine-popover{
        position:fixed;top:auto;right:8px;left:8px;bottom:auto;
        width:auto;max-height:70vh;overflow:auto;
      }
      #${APP_ID} .scg-refinebar{grid-template-columns:1fr}
      #${APP_ID} .scg-menu{position:fixed;right:8px;left:auto;width:min(280px,calc(100vw - 16px))}
      #${APP_ID} .scg-scroll{inset:8px 8px 0;padding:12px 10px 160px;border-radius:var(--scg-r-md) var(--scg-r-md) 0 0}
      #${APP_ID} .scg-top{right:14px;bottom:146px;padding:8px 12px}
      #${APP_ID} .scg-top>span{display:none}
      #${APP_ID}[data-layout="masonry"] .scg-grid:not(.grouped){columns:2 var(--scg-masonry-width,280px);column-gap:8px}
      #${APP_ID}[data-layout="masonry"].compact .scg-grid:not(.grouped){columns:2 var(--scg-masonry-compact-width,195px);column-gap:8px}
      #${APP_ID}[data-layout="masonry"] .scg-group-grid{columns:2 var(--scg-group-width,250px);padding:8px}
      #${APP_ID}[data-layout="masonry"].compact .scg-group-grid{columns:2 var(--scg-group-compact-width,180px);padding:8px}
      #${APP_ID}[data-layout="grid"] .scg-grid:not(.grouped),
      #${APP_ID}[data-layout="grid"] .scg-group-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--scg-grid-min,250px)),1fr));gap:8px}
      #${APP_ID}[data-layout="grid"].compact .scg-grid:not(.grouped),
      #${APP_ID}[data-layout="grid"].compact .scg-group-grid{grid-template-columns:repeat(auto-fill,minmax(min(100%,var(--scg-grid-compact-min,180px)),1fr));gap:8px}
      #${APP_ID}[data-layout="grid"] .scg-card{height:var(--scg-grid-height,386px)}
      #${APP_ID}[data-layout="grid"].compact .scg-card{height:var(--scg-grid-compact-height,312px)}
      #${APP_ID}[data-layout="feed"] .scg-media,
      #${APP_ID}[data-layout="feed"] .scg-linkcard,
      #${APP_ID}[data-layout="feed"].compact .scg-media,
      #${APP_ID}[data-layout="feed"].compact .scg-linkcard{display:block;min-height:0}
      #${APP_ID}[data-layout="feed"] .scg-preview,
      #${APP_ID}[data-layout="feed"] .scg-card>video,
      #${APP_ID}[data-layout="feed"] .scg-embed,
      #${APP_ID}[data-layout="feed"] .scg-host{height:auto;min-height:182px;border-right:0}
      #${APP_ID}[data-layout="feed"] .scg-actions,
      #${APP_ID}[data-layout="feed"] .scg-meta{border-left:0}
      #${APP_ID}[data-layout="feed"] .scg-skeleton{display:block;height:auto}
      #${APP_ID}[data-layout="feed"] .scg-skeleton-media{height:182px}
      #${APP_ID} .scg-badge span{display:none}
      #${APP_ID} .scg-activitybar{width:calc(100% - 16px);min-height:68px;bottom:8px;gap:8px;padding:9px 10px;border-radius:16px}
      #${APP_ID} .scg-activity-state{flex:1 1 100%}
      #${APP_ID} .scg-progress{flex:1 1 100%;min-width:0;max-width:none}
      #${APP_ID} .scg-progress-text{max-width:130px}
      #${APP_ID} .scg-selection-toggle>span{display:none}
      #${APP_ID}.selecting .scg-bulk-actions,
      #${APP_ID}.downloading .scg-bulk-actions{flex-wrap:wrap;flex-basis:100%}
      #${APP_ID}.selecting .scg-bulk-actions button>span{display:none}
      #${APP_ID}.selecting .scg-bulk-actions .scg-primary>span,
      #${APP_ID}.downloading .scg-bulk-actions .scg-primary>span{display:inline}
      #${APP_ID} .scg-download-popover{position:fixed;right:8px;left:8px;bottom:76px;width:auto;max-height:62vh}
      #${APP_ID} .scg-download-job>div:first-child{align-items:flex-start;flex-direction:column;gap:3px}
      #${APP_ID} .scg-diag-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
      #${APP_ID} .scg-archive-checks{grid-template-columns:1fr}
      #${APP_ID} .scg-settings-panel{padding:8px}
      #${APP_ID} .scg-settings-dialog{width:100%;max-height:calc(100vh - 16px)}
      #${APP_ID} .scg-settings-head{padding:12px}
      #${APP_ID} .scg-settings-body{padding:10px}
      #${APP_ID} .scg-settings-foot{align-items:flex-start;flex-direction:column;gap:5px}
      #${APP_ID} .scg-viewer-topbar{width:calc(100% - 16px);margin:8px;padding:7px 8px 7px 10px;gap:9px}
      #${APP_ID} .scg-viewer-identity{gap:9px}
      #${APP_ID} .scg-viewer-identity>b{padding:4px 8px;font-size:12px}
      #${APP_ID} .scg-viewer-identity small{display:none}
      #${APP_ID} .scg-viewer-titleline strong{font-size:11.5px}
      #${APP_ID} .scg-viewer-resolution{padding:2px 6px;font-size:9.5px}
      #${APP_ID} .scg-viewer-tools{gap:3px}
      #${APP_ID} .scg-viewer-tools button{min-width:34px;height:34px;padding:7px}
      #${APP_ID} .scg-viewer-zoom{gap:2px;margin-right:3px}
      #${APP_ID} .scg-viewer-zoom [data-viewer-fit]{min-width:34px;padding:6px}
      #${APP_ID} .scg-viewer-zoom [data-viewer-fit] span{display:none}
      #${APP_ID} .scg-lightbox-stage{padding:8px}
      #${APP_ID} .scg-lightbox .scg-nav{width:38px;height:38px}
      #${APP_ID} .scg-lightbox .scg-nav .scg-icon{width:19px;height:19px}
      #${APP_ID} .scg-lightbox .scg-prev{left:6px}
      #${APP_ID} .scg-lightbox .scg-next,
      #${APP_ID} .scg-lightbox.details-hidden .scg-next{right:6px}
      #${APP_ID} .scg-viewer-details{top:auto;right:8px;bottom:8px;left:8px;width:auto;max-height:56%;padding:14px;transform:none}
      #${APP_ID} .scg-lightbox.details-hidden .scg-viewer-details{transform:translateY(calc(100% + 22px))}
      #${APP_ID} .scg-viewer-footer{width:calc(100% - 16px);margin:0 8px 8px;gap:7px;padding:7px}
      #${APP_ID} .scg-viewer-thumb{width:48px;height:48px;border-radius:var(--scg-r-sm)}
      #${APP_ID} .scg-viewer-footer .scg-lightbox-actions [data-copy-current]{display:none}
      #scg-launch{left:12px;bottom:12px;min-height:58px;padding:0 17px 0 10px;gap:10px}
      #scg-launch .scg-launch-icon{width:38px;height:38px;border-radius:12px}
      #scg-launch .scg-launch-copy strong{font-size:13px}
      #scg-launch .scg-launch-copy small{font-size:10.5px}
    }

    /* Very short windows: give the canvas the vertical budget. */
    @media(max-height:620px){
      #${APP_ID} .scg-viewer-details{max-height:none}
      #${APP_ID} .scg-viewer-shortcuts{display:none}
      #${APP_ID} .scg-viewer-thumb{width:44px;height:44px}
      #${APP_ID} .scg-download-popover{max-height:52vh}
      #${APP_ID} .scg-download-jobs{max-height:30vh}
    }

    /* ---- 20. Touch targets ------------------------------------- */
    @media(pointer:coarse){
      #${APP_ID} button,#${APP_ID} select,#${APP_ID} .scg-meta a,#${APP_ID} .scg-viewer-details-links a{min-height:44px}
      #${APP_ID} .scg-header-actions button,
      #${APP_ID} .scg-view-pagination button,
      #${APP_ID} .scg-viewer-tools button{min-width:44px}
      #${APP_ID} .scg-select,#${APP_ID} .scg-select span{width:36px;height:36px}
      #${APP_ID} .scg-viewer-thumb{min-height:0}
      #${APP_ID} [data-tooltip]:hover:after{display:none}
      #${APP_ID} .scg-scan-warning{
        position:static;width:auto;height:auto;padding:5px 8px;margin:0;
        overflow:visible;clip:auto;white-space:normal;
        color:var(--scg-muted);font-size:10.5px;line-height:1.35;
      }
    }

    /* ---- 21. Reduced motion ------------------------------------ */
    @media(prefers-reduced-motion:reduce){
      #${APP_ID},#${APP_ID} *,#${APP_ID} *:before,#${APP_ID} *:after,
      #scg-launch,#scg-launch *,#scg-gallery-toast{
        animation-duration:.001ms !important;
        animation-iteration-count:1 !important;
        transition-duration:.001ms !important;
        transition-delay:0s !important;
      }
      #${APP_ID} .scg-scroll{scroll-behavior:auto}
      #${APP_ID} .scg-viewer-loading i,
      #${APP_ID} .scg-lightbox-stage.viewer-buffering:before{
        animation:none !important;border-color:var(--scg-accent);opacity:.72;
      }
      #${APP_ID} .scg-card:hover,#${APP_ID} .scg-viewer-thumb:hover,
      #scg-launch:hover,#${APP_ID} .scg-embed-placeholder button:hover{transform:none}
      #${APP_ID} .scg-lightbox .scg-nav:hover{transform:translateY(-50%)}
    }

    /* ---- 22. Forced colors ------------------------------------- */
    @media(forced-colors:active){
      #${APP_ID} .scg-card,#${APP_ID} .scg-plate,#${APP_ID} .scg-activitybar,
      #${APP_ID} .scg-menu,#${APP_ID} .scg-refine-popover,#${APP_ID} .scg-download-popover{border:1px solid CanvasText}
      #${APP_ID} .scg-icon{stroke:CanvasText}
    }

  `;
  document.head.appendChild(style);

  const launch = document.createElement('button');
  launch.id = 'scg-launch';
  launch.innerHTML = `${iconWell('gallery', 'scg-launch-icon')}<span class="scg-launch-copy"><strong>Open Gallery</strong><small data-launch-detail>Media browser · v${APP_VERSION}</small></span>`;
  launch.setAttribute('aria-label', 'Open SimpCity thread gallery');
  document.body.appendChild(launch);

  function refreshLaunchButton() {
    const count = state.items.filter(isMediaItem).length;
    const detail = launch.querySelector('[data-launch-detail]');
    if (detail) detail.textContent = count ? `Media browser · ${count.toLocaleString()} indexed` : `Media browser · v${APP_VERSION}`;
    launch.setAttribute('aria-label', count ? `Open SimpCity thread gallery, ${count} media items indexed` : 'Open SimpCity thread gallery');
  }

  const app = document.createElement('section');
  app.id = APP_ID;
  app.setAttribute('data-ui', 'gallery');
  app.setAttribute('role', 'dialog');
  app.setAttribute('aria-modal', 'true');
  app.setAttribute('aria-label', 'SimpCity Thread Gallery');
  app.innerHTML = `
    <div class="scg-plate">
      <header class="scg-header">
        <div class="scg-brand">
          <div class="scg-brand-mark" aria-hidden="true">${icon('gallery')}</div>
          <div><b>Gallery</b><small>Media browser</small></div>
        </div>
        <div class="scg-thread-context">
          <h1 data-thread-title>Thread gallery</h1>
          <div><span data-thread-meta>Page 1</span><span data-source-summary>Current page</span></div>
        </div>
        <nav class="scg-controls" aria-label="Search and scanning">
          <label class="scg-search">${iconWell('search')}<input data-search type="search" placeholder="Search author, caption, host or text" aria-label="Search indexed media"></label>
          <div class="scg-scan-actions" role="group" aria-label="Scan sources">
            <button data-action="page" data-tooltip="Index the page you are on">${iconWell('page')}<span>This page</span></button>
            <span class="scg-source-page"><select data-source-page aria-label="Thread page to load"></select><button data-action="load-source-page" data-tooltip="Load the selected thread page" aria-label="Load the selected thread page">${iconWell('download')}</button></span>
            <button class="scg-scan-thread-danger scg-tip-end" data-action="thread" data-tooltip="${escapeHtml(SCAN_WARNING_TEXT)}" aria-describedby="scg-thread-scan-warning">${iconWell('scan')}<span>Scan thread</span></button>
          </div>
          <span class="scg-scan-warning" id="scg-thread-scan-warning" role="note">${escapeHtml(SCAN_WARNING_TEXT)}</span>
        </nav>
        <div class="scg-header-actions">
          <button data-action="filters-toggle" data-tooltip="Hide filters" aria-expanded="true" aria-controls="scg-filter-panel" aria-label="Hide filters">${iconWell('chevron')}<span>Filters</span></button>
          <span class="scg-header-rule" aria-hidden="true"></span>
          <button data-action="settings" data-tooltip="Settings and diagnostics" aria-label="Settings and diagnostics">${iconWell('settings')}<span>Settings</span></button>
          <span class="scg-menu-wrap">
            <button data-action="overflow" class="scg-tip-end" data-tooltip="More options" aria-label="More options" aria-haspopup="true" aria-expanded="false" aria-controls="scg-overflow-menu">${iconWell('more')}<span>More</span></button>
            <div class="scg-menu" id="scg-overflow-menu" role="menu" aria-label="More options">
              <div class="scg-menu-label" role="presentation">Appearance</div>
              <button data-action="theme" role="menuitem">${iconWell('palette')}<span>Theme</span></button>
              <button data-action="density" role="menuitem">${iconWell('layout')}<span>Compact cards</span></button>
              <div class="scg-menu-sep" role="separator"></div>
              <div class="scg-menu-label" role="presentation">Help</div>
              <button data-action="shortcuts" role="menuitem">${iconWell('keyboard')}<span>Keyboard shortcuts</span></button>
            </div>
          </span>
          <button class="scg-close scg-tip-end" data-tooltip="Close gallery (Esc)" aria-label="Close gallery">${iconWell('close', 'scg-icon-well-danger')}</button>
        </div>
      </header>
      <section class="scg-filter-panel" id="scg-filter-panel" aria-label="Filters and view options">
        <div class="scg-filters" role="group" aria-label="Filter by media type">
          <button class="active" data-filter="all" aria-pressed="true">${icon('gallery')}<b>All</b><span aria-hidden="true">0</span></button>
          <button data-filter="image" aria-pressed="false">${icon('image')}<b>Images</b><span aria-hidden="true">0</span></button>
          <button data-filter="video" aria-pressed="false">${icon('video')}<b>Videos</b><span aria-hidden="true">0</span></button>
          <button data-filter="link" aria-pressed="false">${icon('link')}<b>Links</b><span aria-hidden="true">0</span></button>
          <button data-filter="text" aria-pressed="false">${icon('text')}<b>Text</b><span aria-hidden="true">0</span></button>
        </div>
        <div class="scg-viewbar">
          <span class="scg-view-summary" role="status" aria-live="polite">0 matched</span>
          <span class="scg-reply-group-actions" role="group" aria-label="Reply group visibility">
            <button data-action="collapse-all" data-tooltip="Collapse all reply groups in the filtered results">${iconWell('chevron')}<span>Collapse All</span></button>
            <button data-action="expand-all" data-tooltip="Expand all reply groups in the filtered results">${iconWell('chevron')}<span>Expand All</span></button>
          </span>
          <div class="scg-layout-switcher" role="group" aria-label="Gallery layout">
            <button data-layout-mode="masonry" data-tooltip="Masonry layout (L)" aria-pressed="true">${iconWell('masonry')}<span>Masonry</span></button>
            <button data-layout-mode="grid" data-tooltip="Uniform grid layout (L)" aria-pressed="false">${iconWell('grid')}<span>Grid</span></button>
            <button data-layout-mode="feed" data-tooltip="Wide feed layout (L)" aria-pressed="false">${iconWell('feed')}<span>Feed</span></button>
          </div>
          <span class="scg-refine-wrap">
            <button class="scg-refine-trigger" data-action="refine-toggle" aria-haspopup="true" aria-expanded="false" aria-controls="scg-refine-popover">${iconWell('sliders')}<span>Refine</span><em data-refine-count hidden>0</em></button>
            <div class="scg-refine-popover" id="scg-refine-popover" role="group" aria-label="Refine results">
              <h2>Refine results</h2>
              <div class="scg-refinebar">
                <label>Sort <select data-sort><option value="thread-asc">Oldest replies first</option><option value="thread-desc">Newest replies first</option><option value="author">Author A-Z</option><option value="host">Host A-Z</option><option value="type">Media type</option></select></label>
                <label>Group <select data-group-by><option value="none">Individual cards</option><option value="reply">Group by reply</option></select></label>
                <label>Author <select data-author-filter></select></label>
                <label>Host <select data-host-filter></select></label>
                <button data-selected-only aria-pressed="false">${iconWell('select')}<span>Selected only</span></button>
                <button data-action="reset-filters">${iconWell('reset')}<span>Reset all</span></button>
              </div>
            </div>
          </span>
          <span class="scg-view-pagination" role="group" aria-label="Result pages">
            <button data-action="view-prev" class="scg-tip-end" data-tooltip="Previous result page" aria-label="Previous result page">${iconWell('previous')}<span>Previous</span></button>
            <select data-view-page aria-label="Gallery result page"></select>
            <button data-action="view-next" class="scg-tip-end" data-tooltip="Next result page" aria-label="Next result page">${iconWell('next')}<span>Next</span></button>
          </span>
          <label class="scg-size-control"><span>Media size</span><input data-card-scale type="range" min="70" max="160" step="5" value="100" aria-label="Image and video card size"><output data-card-scale-value>100%</output></label>
          <label>Per view <select data-page-size>${VIEW_PAGE_SIZES.map(size => `<option value="${size}">${size}</option>`).join('')}</select></label>
        </div>
      </section>
    </div>
    <div class="scg-stage">
      <main class="scg-scroll" tabindex="-1"><div class="scg-grid"></div></main>
      <footer class="scg-bulkbar scg-activitybar" aria-label="Activity, selection and downloads">
        <div class="scg-activity-state"><i aria-hidden="true"></i><div><b class="scg-status">Ready</b><span data-activity-detail>Open a page or scan the thread</span></div></div>
        <button class="scg-selection-toggle scg-tip-up" data-action="selection-mode" data-tooltip="Turn selection mode on or off">${iconWell('select')}<span>Select media</span></button>
        <div class="scg-bulk-actions" role="group" aria-label="Bulk selection">
          <span class="scg-bulk-label" aria-hidden="true">Select</span>
          <button class="scg-tip-up" data-action="select-visible" data-tooltip="Select every media item in this view">${iconWell('select')}<span>Visible</span></button>
          <button class="scg-tip-up" data-action="select-images" data-tooltip="Select all indexed images">${iconWell('image')}<span>Images</span></button>
          <button class="scg-tip-up" data-action="select-videos" data-tooltip="Select all indexed videos">${iconWell('video')}<span>Videos</span></button>
          <button class="scg-tip-up" data-action="clear-selection" data-tooltip="Clear the selection" disabled>${iconWell('clear')}<span>Clear</span></button>
          <span class="scg-selected-count" role="status" aria-live="polite">0 selected</span>
          <button class="scg-primary scg-tip-up scg-tip-end" data-action="download-selected" disabled>${iconWell('archive', 'scg-icon-well-primary')}<span>Download ZIP (0)</span></button>
        </div>
        <div class="scg-progress">
          <button class="scg-progress-summary scg-tip-up scg-tip-end" data-action="download-details" data-tooltip="Open the download queue" aria-expanded="false" aria-controls="scg-download-queue"><span class="scg-progress-track"><span class="scg-progress-fill"></span></span><span class="scg-progress-text">Downloads idle</span></button>
          <aside class="scg-download-popover" id="scg-download-queue" aria-label="Download queue">
            <header><b>Download queue</b><div><button data-action="clear-download-history" data-tooltip="Forget saved-file markers" aria-label="Clear download history">${iconWell('history')}<span>Clear history</span></button><button data-action="close-download-details" aria-label="Close the download queue">${iconWell('close')}<span>Close</span></button></div></header>
            <div class="scg-download-overall" role="status" aria-live="polite">No downloads yet.</div>
            <div class="scg-download-jobs"></div>
          </aside>
        </div>
      </footer>
      <button class="scg-top scg-tip-up scg-tip-end" data-tooltip="Back to the top of the gallery">${iconWell('up')}<span>Top</span></button>
    </div>
    <div class="scg-lightbox" role="dialog" aria-modal="true" aria-label="Media viewer" tabindex="-1"></div>
    <div class="scg-settings-panel" role="dialog" aria-modal="true" aria-label="Settings and diagnostics">
      <section class="scg-settings-dialog" tabindex="-1">
        <header class="scg-settings-head"><div>${icon('settings')}<div><h2>Settings and diagnostics</h2><p>Local preferences, portability and troubleshooting. Nothing leaves this device.</p></div></div><button class="scg-settings-close" data-action="close-settings" aria-label="Close settings">${iconWell('close', 'scg-icon-well-danger')}</button></header>
        <div class="scg-settings-body"><div class="scg-settings-grid">
          <section class="scg-settings-card"><h3>${icon('palette')} Appearance</h3><p>Choose the gallery layout, theme, card density and remembered filter state.</p><div class="scg-setting-fields"><label>Gallery layout<select data-setting-layout><option value="masonry">Masonry</option><option value="grid">Uniform grid</option><option value="feed">Feed</option></select></label><label>Theme<select data-setting-theme><option value="dark">Darkroom (default dark)</option><option value="light">Daylight (light)</option><option value="midnight">Indigo (dark)</option><option value="graphite">Graphite (dark)</option></select></label><label>Card density<select data-setting-density><option value="comfortable">Comfortable</option><option value="compact">Compact</option></select></label><label class="scg-setting-size">Media card size <span><input data-card-scale type="range" min="70" max="160" step="5" value="100" aria-label="Image and video card size in masonry and grid layouts"><output data-card-scale-value>100%</output></span></label></div><label class="scg-settings-check"><input type="checkbox" data-setting-filters-collapsed><span>Keep filters collapsed</span></label></section>
          <section class="scg-settings-card full"><h3>${icon('archive')} Archive and downloads <span class="scg-settings-version">v${APP_VERSION}</span></h3><p>Control download throughput, ZIP part limits and archive organization. Changes apply to the next queue.</p><div class="scg-setting-fields scg-download-settings"><label>ZIP folders<select data-setting-archive-layout><option value="page-author">Page / author</option><option value="page">Page only</option><option value="reply">Page / reply</option><option value="flat">Flat archive</option></select></label><label>Simultaneous files<select data-setting-download-concurrency><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option></select></label><label>Maximum part size<select data-setting-zip-part-size><option value="100">100 MB</option><option value="300">300 MB</option><option value="600">600 MB</option><option value="1000">1,000 MB</option></select></label><label>Files per part<select data-setting-zip-part-files><option value="24">24</option><option value="50">50</option><option value="100">100</option></select></label></div><div class="scg-archive-checks"><label class="scg-settings-check"><input type="checkbox" data-setting-include-manifest><span>Add manifest.csv with source URL, reply, author, validation method and CRC32.</span></label><label class="scg-settings-check"><input type="checkbox" data-setting-dedupe-content><span>Merge byte-identical reposts using verified size + CRC32 while preserving download history and provenance.</span></label></div></section>
          <section class="scg-settings-card"><h3>${icon('scan')} Scan safety</h3><p>Large full-thread scans index HTML from every detected page and can use substantial memory.</p><label class="scg-settings-check"><input type="checkbox" data-setting-warn-large-scan><span>Warn before scanning threads with ${LARGE_THREAD_WARNING_PAGES} or more pages</span></label></section>
          <section class="scg-settings-card"><h3>${icon('info')} Storage</h3><p>Preferences and verified download history are stored outside the website. Pre-v0.7.3 history remains visible as LEGACY but will not skip a new download.</p><div class="scg-storage-note">${icon('select')}<span><b data-diag-storage></b><br><span data-diag-migration></span></span></div></section>
          <section class="scg-settings-card"><h3>${icon('upload')} Backup and restore</h3><p>Export preferences as JSON or restore a previous backup.</p><label class="scg-settings-check"><input type="checkbox" data-export-history><span>Include download history. This contains thread and media references.</span></label><div class="scg-settings-actions"><button data-action="export-settings">${iconWell('download')}<span>Export</span></button><button data-action="import-settings">${iconWell('upload')}<span>Import</span></button><input class="scg-settings-file" data-settings-file type="file" accept="application/json,.json"></div></section>
          <section class="scg-settings-card full"><h3>${icon('info')} Diagnostics</h3><p>A local, URL-scrubbed summary for troubleshooting. Nothing is transmitted automatically.</p><div class="scg-diag-grid"><div class="scg-diag-stat"><span>Version</span><b data-diag-version></b></div><div class="scg-diag-stat"><span>Thread pages</span><b data-diag-pages></b></div><div class="scg-diag-stat"><span>Indexed items</span><b data-diag-items></b></div><div class="scg-diag-stat"><span>Download history</span><b data-diag-history></b></div><div class="scg-diag-stat"><span>Resolver failures</span><b data-diag-resolvers></b></div><div class="scg-diag-stat"><span>Download failures</span><b data-diag-downloads></b></div><div class="scg-diag-stat"><span>Scan failures</span><b data-diag-scans></b></div><div class="scg-diag-stat"><span>Storage</span><b data-diag-storage></b></div></div><div class="scg-settings-actions" style="margin-top:10px"><button data-action="copy-debug">${iconWell('copy')}<span>Copy debug report</span></button><button data-action="clear-diagnostics">${iconWell('reset')}<span>Clear diagnostics</span></button></div></section>
          <section class="scg-settings-card"><h3>${icon('reset')} Reset</h3><p>Reset visual preferences, or also forget SAVED download markers.</p><div class="scg-settings-actions"><button data-action="reset-preferences">${iconWell('reset')}<span>Reset preferences</span></button><button class="danger" data-action="reset-everything">${iconWell('trash', 'scg-icon-well-danger')}<span>Reset preferences + history</span></button></div></section>
          <section class="scg-settings-card"><h3>${icon('info')} Cross-host permission</h3><p>The wildcard connection permission is intentionally retained for media hosted on changing third-party domains.</p><div class="scg-permission-note">The script only requests URLs discovered in thread replies or known resolver endpoints. It contains no analytics or telemetry.</div></section>
        </div></div>
        <footer class="scg-settings-foot"><span>SimpCity Thread Gallery <b>v${APP_VERSION}</b></span><span>Settings remain on this device unless you export them.</span></footer>
      </section>
    </div>
    <div class="scg-confirm-panel" role="dialog" aria-modal="true" aria-labelledby="scg-confirm-title"><section class="scg-confirm-dialog"><div class="scg-confirm-icon">${icon('info')}</div><h2 id="scg-confirm-title" data-confirm-title>Confirm action</h2><p data-confirm-message></p><label class="scg-confirm-preference" data-confirm-preference hidden><input type="checkbox"><span></span></label><div><button data-confirm-cancel>${iconWell('close')}<span>Cancel</span></button><button class="scg-confirm-accept" data-confirm-accept>${iconWell('check', 'scg-icon-well-primary')}<span>Continue</span></button></div></section></div>`;
  document.body.appendChild(app);
  const toast = document.createElement('div');
  toast.id = 'scg-gallery-toast';
  toast.className = 'scg-toast';
  toast.setAttribute('role', 'status');
  toast.setAttribute('aria-live', 'polite');
  toast.setAttribute('aria-atomic', 'true');
  document.body.appendChild(toast);
  applyShellState();

  function openApp() {
    app.classList.add('open');
    document.documentElement.style.overflow = 'hidden';
    applyShellState();
    requestAnimationFrame(() => app.querySelector('[data-search]')?.focus({ preventScroll: true }));
    if (!state.items.length) scanCurrentPage();
    else render();
    if (migratedLegacyStorage && !openApp.migrationNotified) {
      openApp.migrationNotified = true;
      notify('Your previous gallery settings were moved into private userscript storage.', 5200);
    }
  }

  function closeApp() {
    closeSettingsPanel();
    closeDisclosures();
    app.classList.remove('open');
    document.documentElement.style.overflow = '';
    state.cancelScan = true;
    state.scanCanceling = state.scanning;
    state.scanController?.abort();
    closeLightbox();
    releaseRenderedMedia();
    state.renderedItems = [];
    document.getElementById('scg-launch')?.focus({ preventScroll: true });
  }

  const controllers = Object.freeze({
    scanner: Object.freeze({ current: scanCurrentPage, page: scanSpecificPage, thread: scanThread }),
    gallery: Object.freeze({ open: openApp, close: closeApp, render }),
    viewer: Object.freeze({ open: openLightbox, close: closeLightbox, navigate: navigateLightbox }),
    downloads: Object.freeze({ one: downloadMedia, bulk: bulkDownload }),
    settings: Object.freeze({ open: openSettingsPanel, close: closeSettingsPanel }),
  });

  launch.onclick = controllers.gallery.open;
  app.querySelector('.scg-close').onclick = controllers.gallery.close;
  app.querySelectorAll('[data-filter]').forEach(btn => btn.onclick = () => {
    commitState({ filter: btn.dataset.filter, viewPage: 1 }, { persist: true });
  });
  let searchTimer;
  app.querySelector('[data-search]').oninput = event => {
    clearTimeout(searchTimer);
    const value = event.target.value;
    searchTimer = setTimeout(() => commitState({ query: value, viewPage: 1 }), 140);
  };
  app.querySelector('[data-action="page"]').onclick = () => state.scanning ? notify('Cancel the current scan first.') : controllers.scanner.current();
  app.querySelector('[data-action="load-source-page"]').onclick = () => controllers.scanner.page(app.querySelector('[data-source-page]').value);
  app.querySelector('[data-action="thread"]').onclick = controllers.scanner.thread;
  app.querySelector('[data-sort]').onchange = event => commitState({ sort: event.target.value, viewPage: 1 }, { persist: true });
  app.querySelector('[data-author-filter]').onchange = event => commitState({ authorFilter: event.target.value, viewPage: 1 });
  app.querySelector('[data-host-filter]').onchange = event => commitState({ hostFilter: event.target.value, viewPage: 1 });
  app.querySelector('[data-group-by]').onchange = event => commitState({ groupBy: event.target.value, viewPage: 1 }, { persist: true });
  app.querySelector('[data-selected-only]').onclick = () => commitState({ selectedOnly: !state.selectedOnly, viewPage: 1 });
  app.querySelector('[data-action="collapse-all"]').onclick = () => {
    if (setReplyGroupsCollapsed(visibleItems(), true)) render();
  };
  app.querySelector('[data-action="expand-all"]').onclick = () => {
    if (setReplyGroupsCollapsed(visibleItems(), false)) render();
  };
  app.querySelector('[data-action="reset-filters"]').onclick = () => {
    app.querySelector('[data-search]').value = '';
    commitState({
      filter: 'all', query: '', authorFilter: 'all', hostFilter: 'all', selectedOnly: false,
      sort: 'thread-asc', groupBy: 'none', viewPage: 1,
    }, { persist: true });
  };
  app.querySelector('[data-action="selection-mode"]').onclick = () => {
    if (state.selectionMode) {
      exitSelectionMode();
    } else {
      state.selectionMode = true;
    }
    commitState({}, { render: true });
  };
  app.querySelector('[data-action="filters-toggle"]').onclick = () => {
    commitState({ filtersCollapsed: !state.filtersCollapsed }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-action="density"]').onclick = () => {
    commitState({ compact: !state.compact }, { persist: true, shellOnly: true });
  };
  app.querySelectorAll('[data-layout-mode]').forEach(button => {
    button.onclick = () => commitState({ layoutMode: button.dataset.layoutMode }, { persist: true, shellOnly: true });
  });
  app.querySelector('[data-action="theme"]').onclick = () => {
    const current = Math.max(0, THEME_MODES.indexOf(state.theme));
    commitState({ theme: THEME_MODES[(current + 1) % THEME_MODES.length] }, { persist: true, shellOnly: true });
  };
  const overflowTrigger = app.querySelector('[data-action="overflow"]');
  const overflowMenu = app.querySelector('.scg-menu');
  const refineTrigger = app.querySelector('[data-action="refine-toggle"]');
  const refinePopover = app.querySelector('.scg-refine-popover');
  overflowTrigger.onclick = event => {
    event.stopPropagation();
    toggleDisclosure(overflowTrigger, overflowMenu);
  };
  refineTrigger.onclick = event => {
    event.stopPropagation();
    toggleDisclosure(refineTrigger, refinePopover);
  };
  document.addEventListener('pointerdown', event => {
    if (!app.classList.contains('open')) return;
    if (event.target.closest?.('.scg-menu-wrap, .scg-refine-wrap')) return;
    closeDisclosures();
  }, true);
  app.querySelector('[data-action="shortcuts"]').onclick = () => notify('Gallery: / search · L cycles layout · Shift+A selects this view · Shift+G opens or closes · Esc closes. Viewer: ← → navigate · wheel or + − zoom · 0 fit · I details · F fullscreen · Space play or pause · D download · S select.', 11000);
  app.querySelector('[data-action="shortcuts"]').addEventListener('click', () => closeDisclosures());
  app.querySelector('[data-action="settings"]').onclick = () => {
    closeDisclosures();
    controllers.settings.open();
  };
  app.querySelector('[data-action="close-settings"]').onclick = controllers.settings.close;
  app.querySelector('[data-setting-theme]').onchange = event => {
    commitState({ theme: THEME_MODES.includes(event.target.value) ? event.target.value : DEFAULT_SETTINGS.theme }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-density]').onchange = event => {
    commitState({ compact: event.target.value === 'compact' }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-layout]').onchange = event => {
    commitState({ layoutMode: LAYOUT_MODES.includes(event.target.value) ? event.target.value : 'masonry' }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-filters-collapsed]').onchange = event => {
    commitState({ filtersCollapsed: event.target.checked }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-archive-layout]').onchange = event => {
    commitState({ archiveLayout: ['flat', 'page', 'page-author', 'reply'].includes(event.target.value) ? event.target.value : DEFAULT_SETTINGS.archiveLayout }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-download-concurrency]').onchange = event => {
    commitState({ downloadConcurrency: [1, 2, 3, 4].includes(Number(event.target.value)) ? Number(event.target.value) : DEFAULT_SETTINGS.downloadConcurrency }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-zip-part-size]').onchange = event => {
    commitState({ zipPartSizeMb: [100, 300, 600, 1000].includes(Number(event.target.value)) ? Number(event.target.value) : DEFAULT_SETTINGS.zipPartSizeMb }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-zip-part-files]').onchange = event => {
    commitState({ zipPartMaxFiles: [24, 50, 100].includes(Number(event.target.value)) ? Number(event.target.value) : DEFAULT_SETTINGS.zipPartMaxFiles }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-include-manifest]').onchange = event => {
    commitState({ includeManifest: event.target.checked }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-dedupe-content]').onchange = event => {
    commitState({ dedupeContent: event.target.checked }, { persist: true, shellOnly: true });
  };
  app.querySelector('[data-setting-warn-large-scan]').onchange = event => {
    commitState({ warnLargeThreadScan: event.target.checked }, { persist: true, shellOnly: true });
  };
  app.querySelector('.scg-settings-panel').onclick = event => {
    if (event.target.classList.contains('scg-settings-panel')) controllers.settings.close();
  };
  app.querySelector('[data-action="export-settings"]').onclick = () => exportSettings(app.querySelector('[data-export-history]').checked);
  const settingsFile = app.querySelector('[data-settings-file]');
  app.querySelector('[data-action="import-settings"]').onclick = () => settingsFile.click();
  settingsFile.onchange = async event => {
    await importSettings(event.target.files?.[0]);
    event.target.value = '';
  };
  app.querySelector('[data-action="copy-debug"]').onclick = () => copyText(JSON.stringify(buildDebugReport(), null, 2), 'Debug report copied.');
  app.querySelector('[data-action="clear-diagnostics"]').onclick = clearDiagnostics;
  app.querySelector('[data-action="reset-preferences"]').onclick = async () => {
    if (await confirmAction({ title: 'Reset gallery preferences?', message: 'Theme, density, filters and view preferences will return to their defaults.', confirmLabel: 'Reset preferences' })) resetPreferences(false);
  };
  app.querySelector('[data-action="reset-everything"]').onclick = async () => {
    if (await confirmAction({ title: 'Reset everything?', message: 'Preferences and SAVED markers will be removed. Existing downloaded files will not be deleted.', confirmLabel: 'Reset everything', danger: true })) resetPreferences(true);
  };
  app.querySelector('[data-action="select-visible"]').onclick = () => selectItems(mediaItems());
  app.querySelector('[data-action="select-images"]').onclick = () => selectItems(state.items.filter(item => item.type === 'image'));
  app.querySelector('[data-action="select-videos"]').onclick = () => selectItems(state.items.filter(item => item.type === 'video' || item.type === 'embed'));
  app.querySelector('[data-action="clear-selection"]').onclick = () => {
    clearSelection();
    if (state.selectedOnly) render();
    else updateSelectionUi();
  };
  app.querySelector('[data-action="download-selected"]').onclick = controllers.downloads.bulk;
  app.querySelector('[data-action="download-details"]').onclick = event => {
    event.stopPropagation();
    state.downloadDetailsOpen = !state.downloadDetailsOpen;
    updateDownloadUi();
  };
  app.querySelector('[data-action="close-download-details"]').onclick = event => {
    event.stopPropagation();
    state.downloadDetailsOpen = false;
    updateDownloadUi();
    app.querySelector('[data-action="download-details"]')?.focus({ preventScroll: true });
  };
  app.querySelector('[data-action="clear-download-history"]').onclick = async event => {
    event.stopPropagation();
    if (state.downloading) return notify('Finish or cancel the active download before clearing its history.', 5200);
    if (!await confirmAction({ title: 'Clear download history?', message: 'This removes SAVED and LEGACY markers plus completed queue entries. Existing files will stay in your Downloads folder.', confirmLabel: 'Clear history', danger: true })) return;
    if (!clearDownloadHistory()) return;
    notify('Download history cleared. Existing files were not deleted.');
  };
  const scrollArea = app.querySelector('.scg-scroll');
  const topButton = app.querySelector('.scg-top');
  const changeViewPage = page => {
    state.viewPage = Math.min(Math.max(1, Number(page) || 1), state.viewPages);
    scrollArea.scrollTop = 0;
    render();
  };
  app.querySelector('[data-action="view-prev"]').onclick = () => changeViewPage(state.viewPage - 1);
  app.querySelector('[data-action="view-next"]').onclick = () => changeViewPage(state.viewPage + 1);
  app.querySelector('[data-view-page]').onchange = event => changeViewPage(event.target.value);
  app.querySelector('[data-page-size]').onchange = event => {
    commitState({ perPage: Number(event.target.value), viewPage: 1 }, { persist: true });
  };
  app.querySelectorAll('[data-card-scale]').forEach(slider => {
    slider.oninput = event => {
      const cardScale = Math.min(160, Math.max(70, Math.round(Number(event.target.value) / 5) * 5));
      commitState({ cardScale }, { shellOnly: true });
    };
    slider.onchange = () => persistSettings();
  });
  topButton.onclick = () => {
    scrollArea.scrollTo({ top: 0, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
    app.querySelector('[data-search]')?.focus({ preventScroll: true });
  };
  scrollArea.onscroll = () => topButton.classList.toggle('visible', scrollArea.scrollTop > 650);
  app.querySelector('.scg-grid').onclick = event => {
    const items = state.renderedItems;
    const replySelect = event.target.closest('[data-reply-select]');
    if (replySelect) {
      setReplySelected(replySelect.dataset.replySelect, replySelect.checked);
      if (state.selectedOnly) render();
      else updateSelectionUi();
      return;
    }
    const replyToggle = event.target.closest('[data-reply-toggle]');
    if (replyToggle) {
      setReplyCollapsed(replyToggle.dataset.replyToggle, replyToggle.getAttribute('aria-expanded') === 'true');
      render();
      return;
    }
    const selection = event.target.closest('[data-select]');
    if (selection) {
      const item = items[Number(selection.dataset.select)];
      setSelected(item, selection.checked);
      if (state.selectedOnly) render();
      else updateSelectionUi();
      return;
    }
    const player = event.target.closest('[data-load-player]');
    if (player) {
      const item = items[Number(player.dataset.loadPlayer)];
      const container = player.closest('.scg-embed');
      if (item && container) {
        container.classList.remove('scg-embed-placeholder');
        container.innerHTML = iframeHtml(item);
      }
      return;
    }
    const expand = event.target.closest('[data-expand]');
    if (expand) {
      const paragraph = expand.closest('.scg-textcard').querySelector('p');
      const expanded = paragraph.classList.toggle('expanded');
      setIconButton(expand, 'chevron', expanded ? 'Show less' : 'Show more');
      expand.classList.toggle('expanded', expanded);
      return;
    }
    const download = event.target.closest('[data-download]');
    if (download) {
      downloadMedia(items[Number(download.dataset.download)]);
      return;
    }
    const button = event.target.closest('[data-open]');
    if (!button) return;
    controllers.viewer.open(items[Number(button.dataset.open)]);
  };
  app.querySelector('.scg-lightbox').onclick = event => { if (event.target.classList.contains('scg-lightbox')) closeLightbox(); };
  document.addEventListener('keydown', event => {
    const typing = event.target.matches?.('input,textarea,select,[contenteditable="true"]');
    if (typing && event.key !== 'Escape') return;
    const openDisclosure = app.querySelector('.scg-menu.open, .scg-refine-popover.open');
    if (openDisclosure && event.key === 'Escape') {
      event.preventDefault();
      const trigger = openDisclosure.classList.contains('scg-menu')
        ? app.querySelector('[data-action="overflow"]')
        : app.querySelector('[data-action="refine-toggle"]');
      closeDisclosures();
      trigger?.focus();
      return;
    }
    const confirmPanel = app.querySelector('.scg-confirm-panel');
    if (confirmPanel.classList.contains('open')) {
      if (event.key === 'Escape') confirmPanel.querySelector('[data-confirm-cancel]')?.click();
      else if (event.key === 'Tab') containFocus(confirmPanel, event);
      return;
    }
    const settingsPanel = app.querySelector('.scg-settings-panel');
    if (settingsPanel.classList.contains('open')) {
      if (event.key === 'Escape') controllers.settings.close();
      else if (event.key === 'Tab') containFocus(settingsPanel, event);
      return;
    }
    const box = app.querySelector('.scg-lightbox');
    if (box.classList.contains('open')) {
      wakeViewerChrome();
      const item = currentViewerItem();
      if (event.key === 'Tab') {
        const focusable = [...box.querySelectorAll('button:not(:disabled),a[href],video[controls]')].filter(node => node.offsetParent !== null);
        if (focusable.length) {
          const current = focusable.indexOf(document.activeElement);
          const next = event.shiftKey ? (current <= 0 ? focusable.length - 1 : current - 1) : (current >= focusable.length - 1 ? 0 : current + 1);
          event.preventDefault();
          focusable[next].focus();
        }
      } else if (event.key === 'Escape') closeLightbox();
      else if (event.key === 'ArrowLeft') navigateLightbox(-1);
      else if (event.key === 'ArrowRight') navigateLightbox(1);
      else if ((event.key === '+' || event.key === '=') && item?.type === 'image') setViewerZoom(state.viewerZoom + .25);
      else if (event.key === '-' && item?.type === 'image') setViewerZoom(state.viewerZoom - .25);
      else if (event.key === '0' && item?.type === 'image') setViewerZoom(1);
      else if (event.key.toLowerCase() === 'i') toggleViewerInfo();
      else if (event.key.toLowerCase() === 'f') toggleViewerFullscreen();
      else if (event.key === ' ') {
        const video = box.querySelector('video');
        if (video) {
          event.preventDefault();
          if (video.paused) video.play().catch(() => {});
          else video.pause();
        }
      }
      else if (event.key.toLowerCase() === 'd' && item) downloadMedia(item);
      else if (event.key.toLowerCase() === 's') {
        if (!item) return;
        toggleSelected(item);
        setIconButton(box.querySelector('[data-select-current]'), 'select', `${state.selected.has(item.selectionKey) ? 'Unselect' : 'Select'} (S)`);
      }
      return;
    }
    if (event.key === 'Escape' && app.classList.contains('open')) closeApp();
    else if (event.key === '/' && app.classList.contains('open')) {
      event.preventDefault();
      app.querySelector('[data-search]').focus();
    } else if (!event.shiftKey && event.key.toLowerCase() === 'l' && app.classList.contains('open')) {
      event.preventDefault();
      cycleLayoutMode(1, true);
    } else if (event.shiftKey && event.key.toLowerCase() === 'a' && app.classList.contains('open')) {
      event.preventDefault();
      state.selectionMode = true;
      selectItems(mediaItems());
      render();
    } else if (event.shiftKey && event.key.toLowerCase() === 'g') {
      event.preventDefault();
      if (app.classList.contains('open')) closeApp();
      else openApp();
    } else if (event.key === 'Tab' && app.classList.contains('open')) {
      containFocus(app, event);
    }
  });
})();