SimpCity Thread Gallery

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

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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

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

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

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

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

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

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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);
    }
  });
})();