<script lang="ts">
  import { tick } from "svelte";
  import { X, Book, Image, Sliders, Info, Keyboard, Gear, HardDrives, FolderSimple, Plus, Pencil, Trash, Wrench, PaintBrush, ListChecks, Lock, Eye, EyeSlash, Star, ShieldCheck, Tag, ClockCounterClockwise } from "phosphor-svelte";
  import ThreeDCard from "../shared/ThreeDCard.svelte";
  import { invoke } from "@tauri-apps/api/core";
  import { listen } from "@tauri-apps/api/event";
  import { getVersion } from "@tauri-apps/api/app";
  import { open as openUrl } from "@tauri-apps/plugin-shell";
  import { gql, thumbUrl } from "../../lib/client";
  import { GET_CATEGORIES, CREATE_CATEGORY, UPDATE_CATEGORY, DELETE_CATEGORY, UPDATE_CATEGORY_ORDER, GET_SOURCES } from "../../lib/queries";
  import { GET_DOWNLOADS_PATH, SET_DOWNLOADS_PATH, SET_LOCAL_SOURCE_PATH, GET_TRACKERS, LOGIN_TRACKER_OAUTH, LOGIN_TRACKER_CREDENTIALS, LOGOUT_TRACKER, GET_TRACKER_RECORDS, GET_SERVER_SECURITY, SET_SERVER_AUTH, SET_SOCKS_PROXY, SET_FLARESOLVERR, CREATE_BACKUP, RESTORE_BACKUP, GET_RESTORE_STATUS, VALIDATE_BACKUP } from "../../lib/queries";
  import type { Category, Source } from "../../lib/types";
  import { store, updateSettings, resetKeybinds, clearHistory, wipeAllData, setSettingsOpen, deleteCustomTheme, toggleHiddenCategory, setCategories, addToast } from "../../store/state.svelte";
  import { authSession } from "../../lib/auth";
  import { cache } from "../../lib/cache";
  import { KEYBIND_LABELS, DEFAULT_KEYBINDS, eventToKeybind } from "../../lib/keybinds";
  import type { Settings, FitMode, Theme } from "../../store/state.svelte";
  import type { Keybinds } from "../../lib/keybinds";
  import type { Tracker } from "../../lib/types";
  interface Props {
    onOpenThemeEditor?: (id?: string | null) => void;
  }
  let { onOpenThemeEditor }: Props = $props();
  type Tab = "general" | "appearance" | "reader" | "library" | "performance" | "keybinds" | "storage" | "folders" | "tracking" | "security" | "content" | "about" | "devtools";
  const TABS: { id: Tab; label: string; icon: any }[] = [
    { id: "general",    label: "General",     icon: Gear       },
    { id: "appearance", label: "Appearance",  icon: PaintBrush },
    { id: "reader",     label: "Reader",      icon: Book       },
    { id: "library",    label: "Library",     icon: Image      },
    { id: "performance",label: "Performance", icon: Sliders    },
    { id: "keybinds",   label: "Keybinds",    icon: Keyboard   },
    { id: "storage",    label: "Storage",     icon: HardDrives },
    { id: "folders",    label: "Folders",     icon: FolderSimple },
    { id: "tracking",   label: "Tracking",    icon: ListChecks },
    { id: "security",   label: "Security",    icon: Lock       },
    { id: "content",    label: "Content",     icon: ShieldCheck },
    { id: "about",      label: "About",       icon: Info       },
    { id: "devtools",   label: "Dev Tools",   icon: Wrench     },
  ];
  const THEMES: { id: Theme; label: string; description: string; swatches: string[] }[] = [
    { id: "dark",           label: "Dark",           description: "Default near-black",            swatches: ["#101010","#151515","#a8c4a8","#f0efec"] },
    { id: "high-contrast",  label: "High Contrast",  description: "Darker base, sharper text",     swatches: ["#080808","#111111","#bcd8bc","#ffffff"] },
    { id: "light",          label: "Light",          description: "Warm off-white",                swatches: ["#f4f2ee","#faf8f4","#2a5a2a","#1a1916"] },
    { id: "light-contrast", label: "Light Contrast", description: "Light with maximum contrast",   swatches: ["#ece8e2","#f5f2ec","#183818","#080806"] },
    { id: "midnight",       label: "Midnight",       description: "Deep blue-black tint",          swatches: ["#0c1020","#101428","#a8b4e8","#eeeef8"] },
    { id: "warm",           label: "Warm",           description: "Amber and sepia tones",         swatches: ["#16130c","#1c1810","#e0b860","#f5f0e0"] },
  ];
  const anims = $derived(store.settings.qolAnimations ?? true);

  let tab: Tab          = $state("general");
  let prevTabIndex      = $state(0);
  let tabSlideDir       = $state<"up" | "down">("down");
  let tabIconKey        = $state(0);
  let contentBodyEl: HTMLDivElement;
  $effect(() => { tab; tick().then(() => contentBodyEl?.scrollTo({ top: 0 })); });

  function setTab(id: Tab) {
    if (anims) {
      const next = TABS.findIndex(t => t.id === id);
      tabSlideDir = next > prevTabIndex ? "down" : "up";
      prevTabIndex = next;
      tabIconKey++;
    }
    tab = id;
  }
  function close() { setSettingsOpen(false); }
  function onKey(e: KeyboardEvent) { if (e.key === "Escape" && !listeningKey) close(); }
  $effect(() => {
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  });
  let listeningKey: keyof Keybinds | null = $state(null);
  function startListen(key: keyof Keybinds) {
    listeningKey = listeningKey === key ? null : key;
  }
  function onKeyCapture(e: KeyboardEvent) {
    if (!listeningKey) return;
    e.preventDefault(); e.stopPropagation();
    const bind = eventToKeybind(e);
    if (!bind) return;
    updateSettings({ keybinds: { ...store.settings.keybinds, [listeningKey]: bind } });
    listeningKey = null;
  }
  $effect(() => {
    if (listeningKey) {
      window.addEventListener("keydown", onKeyCapture, true);
      return () => window.removeEventListener("keydown", onKeyCapture, true);
    }
  });
  interface StorageInfo { manga_bytes: number; total_bytes: number; free_bytes: number; path: string; }
  let storageInfo: StorageInfo | null = $state(null);
  let storageLoading = $state(false);
  let storageError: string | null = $state(null);
  let clearing = $state(false);
  let cleared  = $state(false);

  // ── External server detection ─────────────────────────────────────────────────
  // A server is "external" if its URL doesn't point to localhost — in that case we
  // cannot invoke Tauri commands against its filesystem (path validation, disk usage,
  // migration). We can still read the server's paths via GraphQL, but we must never
  // overwrite the server's download directory config without the user explicitly asking.
  const isExternalServer = $derived.by(() => {
    const url = (store.settings.serverUrl ?? "http://localhost:4567").toLowerCase().trim();
    try {
      const host = new URL(url).hostname;
      return host !== "localhost" && host !== "127.0.0.1" && host !== "::1";
    } catch { return false; }
  });

  // ── Download path editing ────────────────────────────────────────────────────
  let downloadsPathInput    = $state(store.settings.serverDownloadsPath ?? "");
  let localSourcePathInput  = $state(store.settings.serverLocalSourcePath ?? "");
  let pathsSaving           = $state(false);
  let pathsError: string | null = $state(null);
  let pathsFieldError: { dl?: string; loc?: string } = $state({});
  let pathsSaved            = $state(false);

  // The actual resolved default path from Rust — shown as placeholder + scanned when dl path is empty.
  // Only meaningful for local servers — Tauri can't stat a remote filesystem.
  // Re-fetches reactively when the server URL changes (e.g. user switches to local).
  let defaultDownloadsPath  = $state("");
  $effect(() => {
    if (!isExternalServer) {
      invoke<string>("get_default_downloads_path").then(p => { defaultDownloadsPath = p; });
    } else {
      defaultDownloadsPath = "";
    }
  });

  // The last confirmed server paths — used to detect a change requiring migration
  let confirmedDownloadsPath   = $state(store.settings.serverDownloadsPath ?? "");
  let confirmedLocalSourcePath = $state(store.settings.serverLocalSourcePath ?? "");

  // ── Migration state ──────────────────────────────────────────────────────────
  let migrateFrom: string | null   = $state(null); // old path that has content
  let migrateTo:   string | null   = $state(null); // new path
  let migrating                    = $state(false);
  let migrateProgress: { done: number; total: number; current: string } | null = $state(null);
  let migrateError: string | null  = $state(null);
  let migrateUnlisten: (() => void) | null = null;

  // ── Extra scan directories (local-only, stored in app settings) ──────────────
  let extraScanDirs: string[]   = $state([...(store.settings.extraScanDirs ?? [])]);
  let newScanDir                = $state("");
  let multiStorageInfos: (StorageInfo & { label: string })[] = $state([]);
  let advStorageOpen            = $state(false);
  let backupSectionOpen         = $state(false);

  async function fetchStorage() {
    storageLoading = true; storageError = null;
    try {
      // Always pull the current paths from the server via GQL — works for local and external.
      const pathData = await gql<{ settings: { downloadsPath: string; localSourcePath: string } }>(GET_DOWNLOADS_PATH);
      const dl  = pathData.settings.downloadsPath  ?? "";
      const loc = pathData.settings.localSourcePath ?? "";

      downloadsPathInput    = dl;
      localSourcePathInput  = loc;
      confirmedDownloadsPath   = dl;
      confirmedLocalSourcePath = loc;
      updateSettings({ serverDownloadsPath: dl, serverLocalSourcePath: loc });

      // Disk usage scanning uses Tauri invoke — only possible when the server is local.
      // For external servers we display the paths pulled above but skip the filesystem scan.
      if (isExternalServer) {
        multiStorageInfos = []; storageInfo = null; return;
      }

      // When dl is empty the server uses the default path — scan that instead
      const effectiveDl = dl || defaultDownloadsPath;

      const dirsToScan: { path: string; label: string }[] = [];
      if (effectiveDl) dirsToScan.push({ path: effectiveDl, label: dl ? "Downloads" : "Downloads (default)" });
      if (loc && loc !== effectiveDl) dirsToScan.push({ path: loc, label: "Local source" });
      for (const p of extraScanDirs) {
        if (p && !dirsToScan.find(d => d.path === p)) dirsToScan.push({ path: p, label: p });
      }

      if (dirsToScan.length === 0) {
        multiStorageInfos = []; storageInfo = null; return;
      }

      const results = await Promise.allSettled(
        dirsToScan.map(d =>
          invoke<StorageInfo>("get_storage_info", { downloadsPath: d.path })
            .then(info => ({ ...info, label: d.label }))
        )
      );

      multiStorageInfos = results
        .filter((r): r is PromiseFulfilledResult<StorageInfo & { label: string }> => r.status === "fulfilled")
        .map(r => r.value);
      storageInfo = multiStorageInfos[0] ?? null;
    } catch (e: any) {
      storageError = e instanceof Error ? e.message : String(e);
    } finally {
      storageLoading = false;
    }
  }

  /** Validate a path exists on disk. Returns error string or null.
   *  Only runs for local servers — we can't stat a remote filesystem via Tauri. */
  async function validatePath(path: string): Promise<string | null> {
    if (!path.trim()) return null; // empty = use default, always valid
    if (isExternalServer) return null; // can't check remote paths locally
    try {
      const exists = await invoke<boolean>("check_path_exists", { path: path.trim() });
      return exists ? null : "Directory does not exist";
    } catch {
      return "Could not check path";
    }
  }

  /** Create a directory on disk via Tauri. Only valid for local servers. */
  async function createDirectory(path: string): Promise<void> {
    if (isExternalServer) throw new Error("Cannot create directories on an external server");
    await invoke("create_directory", { path });
  }

  async function savePaths() {
    const dl  = downloadsPathInput.trim();
    const loc = localSourcePathInput.trim();
    pathsError = null; pathsFieldError = {};

    // Validate paths exist before touching the server (empty = use default = always valid).
    // Skipped for external servers — we can't stat their filesystem.
    const [dlErr, locErr] = await Promise.all([validatePath(dl), validatePath(loc)]);
    if (dlErr || locErr) {
      pathsFieldError = { ...(dlErr ? { dl: dlErr } : {}), ...(locErr ? { loc: locErr } : {}) };
      return;
    }

    pathsSaving = true;
    try {
      // Send each mutation independently — localSourcePath rejects empty string server-side
      await gql(SET_DOWNLOADS_PATH, { path: dl });
      if (loc) await gql(SET_LOCAL_SOURCE_PATH, { path: loc });

      updateSettings({ serverDownloadsPath: dl, serverLocalSourcePath: loc });

      // Migration requires local filesystem access — skip for external servers.
      if (!isExternalServer) {
        const oldDl = confirmedDownloadsPath || defaultDownloadsPath;
        const newDl = dl || defaultDownloadsPath;
        if (newDl && oldDl && newDl !== oldDl) {
          const hadContent = await invoke<boolean>("check_path_exists", { path: oldDl });
          if (hadContent) { migrateFrom = oldDl; migrateTo = newDl; }
        }
      }

      confirmedDownloadsPath   = dl;
      confirmedLocalSourcePath = loc;
      pathsSaved = true;
      setTimeout(() => pathsSaved = false, 2000);
      await fetchStorage();
    } catch (e: any) {
      pathsError = e?.message ?? "Failed to save paths";
    } finally {
      pathsSaving = false;
    }
  }

  async function startMigration() {
    if (!migrateFrom || !migrateTo) return;
    migrating = true; migrateError = null; migrateProgress = { done: 0, total: 0, current: "" };

    // Subscribe to progress events from Tauri
    const { listen } = await import("@tauri-apps/api/event");
    migrateUnlisten = await listen<{ done: number; total: number; current: string }>(
      "migrate_progress",
      (e) => { migrateProgress = e.payload; }
    );

    try {
      await invoke("migrate_downloads", { src: migrateFrom, dst: migrateTo });
      migrateFrom = null; migrateTo = null; migrateProgress = null;
      await fetchStorage();
    } catch (e: any) {
      migrateError = e?.message ?? "Migration failed";
    } finally {
      migrating = false;
      migrateUnlisten?.(); migrateUnlisten = null;
    }
  }

  function dismissMigration() {
    migrateFrom = null; migrateTo = null; migrateError = null; migrateProgress = null;
  }

  async function browseDownloadsFolder() {
    const picked = await invoke<string | null>("pick_downloads_folder");
    if (picked) {
      downloadsPathInput = picked;
      pathsFieldError = { ...pathsFieldError, dl: undefined };
    }
  }

  function addExtraScanDir() {
    const dir = newScanDir.trim();
    if (!dir || extraScanDirs.includes(dir)) return;
    extraScanDirs = [...extraScanDirs, dir];
    updateSettings({ extraScanDirs });
    newScanDir = "";
    fetchStorage();
  }

  function removeExtraScanDir(path: string) {
    extraScanDirs = extraScanDirs.filter(d => d !== path);
    updateSettings({ extraScanDirs });
    fetchStorage();
  }
  $effect(() => { if (tab === "storage" && !storageInfo && !storageLoading) fetchStorage(); });
  function handleClearCache() {
    clearing = true;
    caches.keys().then((names) => Promise.all(names.map((n) => caches.delete(n)))).catch(() => {})
      .finally(() => { clearing = false; cleared = true; setTimeout(() => cleared = false, 2500); fetchStorage(); });
  }
  function fmtBytes(bytes: number): string {
    if (bytes === 0) return "0 B";
    const units = ["B","KB","MB","GB","TB"];
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return `${(bytes / Math.pow(1024, i)).toFixed(i >= 2 ? 1 : 0)} ${units[i]}`;
  }
  interface PerfSnapshot {
    cacheEntries: number;
    cacheKeys:    string[];
    oldestEntryMs: number | null;
    newestEntryMs: number | null;
  }
  let perfSnapshot: PerfSnapshot | null = $state(null);
  function refreshPerfMetrics() {
    const knownPrefixes = ["library", "sources", "popular", "genre:", "manga:", "chapters:", "page:", "pages:"];
    let entries = 0;
    let oldest: number | null = null;
    let newest: number | null = null;
    const foundKeys: string[] = [];
    const checkKey = (k: string) => {
      const age = cache.ageOf(k);
      if (age !== undefined) {
        entries++;
        foundKeys.push(k);
        const ts = Date.now() - age;
        if (oldest === null || ts < oldest) oldest = ts;
        if (newest === null || ts > newest) newest = ts;
      }
    };
    ["library", "sources", "popular"].forEach(checkKey);
    ["Action","Romance","Fantasy","Comedy","Drama","Horror","Sci-Fi","Adventure","Thriller",
     "Isekai","Supernatural","Historical","Psychological","Sports","Mystery","Mecha",
     "Slice of Life","School Life","Martial Arts","Magic","Military"].forEach(g => checkKey(`genre:${g}`));
    perfSnapshot = { cacheEntries: entries, cacheKeys: foundKeys, oldestEntryMs: oldest, newestEntryMs: newest };
  }
  $effect(() => { if (tab === "performance" || tab === "devtools") refreshPerfMetrics(); });
  function fmtAge(ts: number | null): string {
    if (ts === null) return "—";
    const secs = Math.floor((Date.now() - ts) / 1000);
    if (secs < 60) return `${secs}s ago`;
    const mins = Math.floor(secs / 60);
    if (mins < 60) return `${mins}m ago`;
    return `${Math.floor(mins / 60)}h ago`;
  }
  let storageLimitInput = $state(String(store.settings.storageLimitGb ?? ""));
  function applyStorageLimit() {
    const v = storageLimitInput.trim();
    if (v === "" || v === "0") { updateSettings({ storageLimitGb: null }); return; }
    const n = parseFloat(v);
    if (!isNaN(n) && n > 0) updateSettings({ storageLimitGb: n });
  }
  let catsLoading:    boolean      = $state(false);
  let catsError:      string|null  = $state(null);
  let newFolderName   = $state("");
  let editingId: number | null     = $state(null);
  let editingName     = $state("");
  async function loadCategories() {
    catsLoading = true; catsError = null;
    try {
      const res = await gql<{ categories: { nodes: Category[] } }>(GET_CATEGORIES);
      const zeroCat = store.categories.filter(c => c.id === 0);
      const fresh = res.categories.nodes.filter(c => c.id !== 0);
      const merged = fresh.map(f => {
        const existing = store.categories.find(c => c.id === f.id);
        return existing ? { ...existing, ...f } : f;
      });
      setCategories([...zeroCat, ...merged]);
    } catch (e: any) {
      catsError = e?.message ?? "Failed to load folders";
    } finally { catsLoading = false; }
  }
  async function createFolder() {
    const name = newFolderName.trim();
    if (!name) return;
    try {
      const res = await gql<{ createCategory: { category: Category } }>(CREATE_CATEGORY, { name });
      setCategories([...store.categories, res.createCategory.category]);
      newFolderName = "";
    } catch (e: any) { catsError = e?.message ?? "Failed to create folder"; }
  }
  function startEdit(id: number, name: string) { editingId = id; editingName = name; }
  async function commitEdit() {
    if (editingId !== null && editingName.trim()) {
      try {
        await gql(UPDATE_CATEGORY, { id: editingId, name: editingName.trim() });
        setCategories(store.categories.map(c => c.id === editingId ? { ...c, name: editingName.trim() } : c));
      } catch (e: any) { catsError = e?.message ?? "Failed to rename"; }
    }
    editingId = null; editingName = "";
  }
  async function deleteFolder(id: number) {
    try {
      await gql(DELETE_CATEGORY, { id });
      setCategories(store.categories.filter(c => c.id !== id));
    } catch (e: any) { catsError = e?.message ?? "Failed to delete folder"; }
  }
  async function moveCategory(id: number, direction: -1 | 1) {
    const zeroCat  = store.categories.filter(c => c.id === 0);
    const sortable = store.categories
      .filter(c => c.id !== 0)
      .sort((a, b) => a.order - b.order);
    const idx = sortable.findIndex(c => c.id === id);
    if (idx < 0) return;
    const newPos = idx + 1 + direction; // 1-based server position
    if (newPos < 1 || newPos > sortable.length) return;
    const reordered = [...sortable];
    const [moved] = reordered.splice(idx, 1);
    reordered.splice(idx + direction, 0, moved);
    setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]);
    try {
      const res = await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id, position: newPos });
      const updated = res.updateCategoryOrder.categories.filter(c => c.id !== 0);
      setCategories([
        ...zeroCat,
        ...updated
          .sort((a, b) => a.order - b.order)
          .map(fresh => {
            const existing = store.categories.find(c => c.id === fresh.id);
            return existing ? { ...existing, ...fresh } : fresh;
          }),
      ]);
    } catch (e: any) {
      catsError = e?.message ?? "Failed to reorder";
      await loadCategories();
    }
  }
  $effect(() => { if (tab === "folders" && !store.categories.length && !catsLoading) loadCategories(); });
  let selectOpen: string | null = $state(null);
  function toggleSelect(id: string) { selectOpen = selectOpen === id ? null : id; }
  function onSelectOutside(e: MouseEvent) {
    if (selectOpen && !(e.target as HTMLElement).closest(".select-wrap")) selectOpen = null;
  }
  $effect(() => {
    document.addEventListener("mousedown", onSelectOutside);
    return () => document.removeEventListener("mousedown", onSelectOutside);
  });
  let splashTriggered = $state(false);
  let expOpen         = $state(false);
  let showAuthPass     = $state(false);
  let showSocksPass    = $state(false);
  let pinInput         = $state(store.settings.appLockPin ?? "");
  let pinError         = $state("");
  let secLoading       = $state(false);
  let secError         = $state<string | null>(null);
  let secSaved         = $state<string | null>(null);
  let authMode         = $state(store.settings.serverAuthMode ?? "NONE");
  // Warning is based on what the server has confirmed (store value), not the
  // local draft — so it doesn't fire just because the store has a stale value
  // before loadServerSecurity runs, and it clears once the user saves a
  // supported mode.
  const authModeUnsupported = $derived(
    store.settings.serverAuthMode === "SIMPLE_LOGIN" ||
    store.settings.serverAuthMode === "UI_LOGIN"
  );
  let authUsername     = $state(store.settings.serverAuthUser ?? "");
  let authPassword     = $state("");
  let socksEnabled     = $state(store.settings.socksProxyEnabled ?? false);
  let socksHost        = $state(store.settings.socksProxyHost ?? "");
  let socksPort        = $state(store.settings.socksProxyPort ?? "1080");
  let socksVersion     = $state(store.settings.socksProxyVersion ?? 5);
  let socksUsername    = $state(store.settings.socksProxyUsername ?? "");
  let socksPassword    = $state(store.settings.socksProxyPassword ?? "");
  let flareEnabled     = $state(store.settings.flareSolverrEnabled ?? false);
  let flareUrl         = $state(store.settings.flareSolverrUrl ?? "http://localhost:8191");
  let flareTimeout     = $state(store.settings.flareSolverrTimeout ?? 60);
  let flareSession     = $state(store.settings.flareSolverrSessionName ?? "moku");
  let flareTtl         = $state(store.settings.flareSolverrSessionTtl ?? 15);
  let flareFallback    = $state(store.settings.flareSolverrFallback ?? false);
  function showSaved(key: string) {
    secSaved = key; secError = null;
    setTimeout(() => { if (secSaved === key) secSaved = null; }, 2000);
  }
  let secLoaded        = $state(false);
  async function loadServerSecurity() {
    try {
      const res = await gql<{ settings: {
        authMode: string; authUsername: string;
        socksProxyEnabled: boolean; socksProxyHost: string; socksProxyPort: string;
        socksProxyVersion: number; socksProxyUsername: string;
        flareSolverrEnabled: boolean; flareSolverrUrl: string; flareSolverrTimeout: number;
        flareSolverrSessionName: string; flareSolverrSessionTtl: number;
        flareSolverrAsResponseFallback: boolean;
      }}>(GET_SERVER_SECURITY);
      const s = res.settings;
      const mode = (s.authMode ?? "NONE") as "NONE" | "BASIC_AUTH" | "SIMPLE_LOGIN" | "UI_LOGIN";
      authMode     = mode;
      authUsername = s.authUsername;
      updateSettings({ serverAuthMode: mode, serverAuthUser: s.authUsername });
      socksEnabled  = s.socksProxyEnabled;  socksHost    = s.socksProxyHost;
      socksPort     = s.socksProxyPort;     socksVersion = s.socksProxyVersion;
      socksUsername = s.socksProxyUsername;
      flareEnabled  = s.flareSolverrEnabled; flareUrl    = s.flareSolverrUrl;
      flareTimeout  = s.flareSolverrTimeout; flareSession = s.flareSolverrSessionName;
      flareTtl      = s.flareSolverrSessionTtl; flareFallback = s.flareSolverrAsResponseFallback;
      updateSettings({
        socksProxyEnabled: socksEnabled, socksProxyHost: socksHost, socksProxyPort: socksPort,
        socksProxyVersion: socksVersion, socksProxyUsername: socksUsername,
        flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl,
        flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession,
        flareSolverrSessionTtl: flareTtl, flareSolverrFallback: flareFallback,
      });
      secLoaded = true;
    } catch {}
  }
  $effect(() => { if (tab === "security" && !secLoaded) loadServerSecurity(); });
  async function saveAuth() {
    if (authMode === "BASIC_AUTH" && (!authUsername.trim() || !authPassword.trim())) {
      secError = "Username and password are required for Basic Auth"; return;
    }
    secLoading = true; secError = null;

    const prevMode = store.settings.serverAuthMode;
    const prevUser = store.settings.serverAuthUser;
    const prevPass = store.settings.serverAuthPass;
    const newUser  = authMode === "BASIC_AUTH" ? authUsername.trim() : "";
    const newPass  = authMode === "BASIC_AUTH" ? authPassword.trim() : "";

    // The store must contain valid credentials while the mutation request is
    // in-flight so fetchAuthenticated can authenticate it:
    //   - Updating credentials: server still accepts the OLD password, so keep
    //     the old credentials in the store until the server confirms the change.
    //   - First-time enable (store has no pass yet): pre-commit the new
    //     credentials because there is nothing else to send.
    const isFirstTimeEnable = authMode === "BASIC_AUTH" && !prevPass.trim();
    if (isFirstTimeEnable) {
      updateSettings({ serverAuthMode: authMode as any, serverAuthUser: newUser, serverAuthPass: newPass });
    }

    try {
      await gql(SET_SERVER_AUTH, { authMode, authUsername: newUser, authPassword: newPass });
      // On success, commit new credentials (no-op if already pre-committed).
      updateSettings({ serverAuthMode: authMode as any, serverAuthUser: newUser, serverAuthPass: newPass });
      if (authMode === "NONE") { authSession.clearTokens(); authPassword = ""; }
      showSaved("auth");
    } catch (e: any) {
      // Roll back to previous values on failure.
      updateSettings({ serverAuthMode: prevMode, serverAuthUser: prevUser, serverAuthPass: prevPass });
      secError = e?.message ?? "Failed to save authentication settings";
    } finally { secLoading = false; }
  }

  async function clearAuth() {
    secLoading = true; secError = null;
    const prevMode = store.settings.serverAuthMode;
    const prevUser = store.settings.serverAuthUser;
    const prevPass = store.settings.serverAuthPass;
    // Keep existing credentials in the store so the disable-auth mutation
    // goes out authenticated, then clear them on success.
    try {
      await gql(SET_SERVER_AUTH, { authMode: "NONE", authUsername: "", authPassword: "" });
      updateSettings({ serverAuthMode: "NONE", serverAuthUser: "", serverAuthPass: "" });
      authMode = "NONE"; authUsername = ""; authPassword = "";
      authSession.clearTokens();
      showSaved("auth");
    } catch (e: any) {
      updateSettings({ serverAuthMode: prevMode, serverAuthUser: prevUser, serverAuthPass: prevPass });
      secError = e?.message ?? "Failed to disable authentication";
    } finally { secLoading = false; }
  }
  async function saveSocksProxy() {
    secLoading = true; secError = null;
    try {
      await gql(SET_SOCKS_PROXY, {
        socksProxyEnabled: socksEnabled, socksProxyHost: socksHost.trim(),
        socksProxyPort: socksPort.trim(), socksProxyVersion: socksVersion,
        socksProxyUsername: socksUsername.trim(), socksProxyPassword: socksPassword.trim(),
      });
      updateSettings({
        socksProxyEnabled: socksEnabled, socksProxyHost: socksHost,
        socksProxyPort: socksPort, socksProxyVersion: socksVersion,
        socksProxyUsername: socksUsername, socksProxyPassword: socksPassword,
      });
      showSaved("socks");
    } catch (e: any) {
      secError = e?.message ?? "Failed to save SOCKS proxy";
    } finally { secLoading = false; }
  }
  async function saveFlareSolverr() {
    secLoading = true; secError = null;
    try {
      await gql(SET_FLARESOLVERR, {
        flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl.trim(),
        flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession.trim(),
        flareSolverrSessionTtl: flareTtl, flareSolverrAsResponseFallback: flareFallback,
      });
      updateSettings({
        flareSolverrEnabled: flareEnabled, flareSolverrUrl: flareUrl,
        flareSolverrTimeout: flareTimeout, flareSolverrSessionName: flareSession,
        flareSolverrSessionTtl: flareTtl, flareSolverrFallback: flareFallback,
      });
      showSaved("flare");
    } catch (e: any) {
      secError = e?.message ?? "Failed to save FlareSolverr";
    } finally { secLoading = false; }
  }
  function commitPin() {
    const cleaned = pinInput.replace(/\D/g, "").slice(0, 8);
    pinInput = cleaned;
    if (cleaned.length >= 4) {
      updateSettings({ appLockPin: cleaned }); pinError = "";
    } else if (cleaned.length > 0) {
      pinError = "PIN must be at least 4 digits";
    } else {
      updateSettings({ appLockPin: "" }); pinError = "";
    }
  }
  let trackers:        Tracker[]    = $state([]);
  let trackersLoading: boolean      = $state(false);
  let trackersError:   string|null  = $state(null);
  let oauthTrackerId:  number|null  = $state(null);
  let oauthCallbackInput: string    = $state("");
  let oauthSubmitting: boolean      = $state(false);
  let credsTrackerId:  number|null  = $state(null);
  let credsUsername:   string       = $state("");
  let credsPassword:   string       = $state("");
  let credsSubmitting: boolean      = $state(false);
  let loggingOut:      number|null  = $state(null); // trackerId being logged out
  async function loadTrackers() {
    trackersLoading = true; trackersError = null;
    try {
      const res = await gql<{ trackers: { nodes: Tracker[] } }>(GET_TRACKERS);
      trackers = res.trackers.nodes;
    } catch (e: any) {
      trackersError = e?.message ?? "Failed to load trackers";
    } finally {
      trackersLoading = false;
    }
  }
  $effect(() => { if (tab === "tracking" && trackers.length === 0 && !trackersLoading) loadTrackers(); });
  async function startOAuth(tracker: Tracker) {
    if (!tracker.authUrl) return;
    oauthTrackerId     = tracker.id;
    oauthCallbackInput = "";
    await openUrl(tracker.authUrl);
  }
  async function submitOAuth() {
    if (!oauthTrackerId || !oauthCallbackInput.trim()) return;
    oauthSubmitting = true;
    try {
      await gql(LOGIN_TRACKER_OAUTH, {
        trackerId:   oauthTrackerId,
        callbackUrl: oauthCallbackInput.trim(),
      });
      await loadTrackers();
      oauthTrackerId     = null;
      oauthCallbackInput = "";
    } catch (e: any) {
      trackersError = e?.message ?? "Login failed";
    } finally {
      oauthSubmitting = false;
    }
  }
  function cancelOAuth() { oauthTrackerId = null; oauthCallbackInput = ""; }
  function startCredentials(tracker: Tracker) {
    credsTrackerId = tracker.id;
    credsUsername  = "";
    credsPassword  = "";
  }
  async function submitCredentials() {
    if (!credsTrackerId || !credsUsername.trim() || !credsPassword.trim()) return;
    credsSubmitting = true;
    try {
      await gql(LOGIN_TRACKER_CREDENTIALS, {
        trackerId: credsTrackerId,
        username:  credsUsername.trim(),
        password:  credsPassword.trim(),
      });
      await loadTrackers();
      credsTrackerId = null;
      credsUsername  = "";
      credsPassword  = "";
    } catch (e: any) {
      trackersError = e?.message ?? "Login failed";
    } finally {
      credsSubmitting = false;
    }
  }
  function cancelCredentials() { credsTrackerId = null; credsUsername = ""; credsPassword = ""; }
  async function logoutTracker(trackerId: number) {
    loggingOut = trackerId;
    try {
      await gql(LOGOUT_TRACKER, { trackerId });
      await loadTrackers();
    } catch (e: any) {
      trackersError = e?.message ?? "Logout failed";
    } finally {
      loggingOut = null;
    }
  }
  function usesOAuth(t: Tracker): boolean { return !!t.authUrl; }
  interface ReleaseInfo {
    tag_name:     string;
    name:         string;
    body:         string;
    published_at: string;
    html_url:     string;
  }
  type UpdatePhase =
    | "idle"
    | "downloading"
    | "ready"      // downloaded, awaiting restart
    | "error";
  const IS_WINDOWS = navigator.userAgent.includes("Windows");
  let appVersion      = $state("…");
  let releases        = $state<ReleaseInfo[]>([]);
  let releasesLoading = $state(false);
  let releasesError   = $state<string | null>(null);
  let expandedTag     = $state<string | null>(null);
  let updatePhase     = $state<UpdatePhase>("idle");
  let updateError     = $state<string | null>(null);
  let dlBytes         = $state(0);
  let dlTotal         = $state<number | null>(null);
  let targetTag       = $state<string | null>(null); // tag being installed
  let releasesLoaded = false; // plain var — not $state, so effect doesn't re-run on change
  $effect(() => {
    if (tab !== "about") return;
    getVersion().then(v => appVersion = v).catch(() => appVersion = "unknown");
    if (!releasesLoaded) { releasesLoaded = true; loadReleases(); }
  });
  async function loadReleases() {
    releasesLoading = true; releasesError = null;
    try {
      const timeout = new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error("Request timed out after 10s")), 10_000)
      );
      const all = await Promise.race([
        invoke<ReleaseInfo[]>("list_releases"),
        timeout,
      ]);
      releases = all.filter(r => typeof r.tag_name === "string" && r.tag_name.trim());
    } catch (e: any) {
      releasesError = e instanceof Error ? e.message : String(e);
    } finally {
      releasesLoading = false;
    }
  }
  function stripV(v: string) { return v.replace(/^v/, ""); }
  function isCurrentVersion(tag: string) { return stripV(tag) === appVersion; }
  function parseSemver(v: string): number[] {
    return stripV(v).split(".").map(Number);
  }
  function compareSemver(a: string, b: string): number {
    const pa = parseSemver(a), pb = parseSemver(b);
    for (let i = 0; i < 3; i++) {
      if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0);
    }
    return 0;
  }
  let onLatestVersion = $derived((() => {
    if (releasesLoading || releases.length === 0 || !appVersion || appVersion === "…") return false;
    const sorted = releases.slice().sort((a, b) => compareSemver(a.tag_name, b.tag_name));
    return compareSemver(appVersion, sorted[0].tag_name) >= 0;
  })());
  function fmtDate(iso: string): string {
    if (!iso) return "";
    const d = new Date(iso);
    return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
  }
  let unlistenProgress: (() => void) | undefined;
  $effect(() => {
    listen<{ downloaded: number; total: number | null }>("update-progress", (e) => {
      dlBytes = e.payload.downloaded;
      dlTotal = e.payload.total ?? null;
    }).then(fn => { unlistenProgress = fn; });
    return () => unlistenProgress?.();
  });
  async function installUpdate(release: ReleaseInfo) {
    if (updatePhase === "downloading") return;
    targetTag   = release.tag_name;
    updatePhase = "downloading";
    updateError = null;
    dlBytes     = 0;
    dlTotal     = null;
    try {
      if (IS_WINDOWS) {
        try { await invoke("kill_server"); } catch {}
        await invoke("download_and_install_update");
        updatePhase = "ready";
      } else {
        await openUrl(release.html_url);
        updatePhase = "idle";
        targetTag   = null;
      }
    } catch (e: any) {
      updateError = e instanceof Error ? e.message : String(e);
      updatePhase = "error";
    }
  }
  async function restartNow() {
    await invoke("restart_app");
  }
  function cancelUpdate() {
    updatePhase = "idle";
    updateError = null;
    targetTag   = null;
    dlBytes     = 0;
    dlTotal     = null;
  }
  function fmtProgress(): string {
    if (dlTotal) {
      return `${fmtBytes(dlBytes)} / ${fmtBytes(dlTotal)} (${Math.round((dlBytes / dlTotal) * 100)}%)`;
    }
    return fmtBytes(dlBytes);
  }
  function triggerSplash() {
    splashTriggered = true;
    setTimeout(() => splashTriggered = false, 200);
    (window as any).__mokuShowSplash?.();
  }
  let contentSources:        Source[] = $state([]);
  let contentSourcesLoading: boolean  = $state(false);
  let newTagInput:           string   = $state("");
  let tagsRevealed:          boolean  = $state(false);
  let sourceSearch:          string   = $state("");
  $effect(() => {
    if (tab === "content" && contentSources.length === 0 && !contentSourcesLoading) {
      loadContentSources();
    }
  });
  async function loadContentSources() {
    contentSourcesLoading = true;
    try {
      const d = await gql<{ sources: { nodes: Source[] } }>(GET_SOURCES);
      contentSources = d.sources.nodes.filter(s => s.id !== "0");
    } catch (e) { console.error(e); }
    finally { contentSourcesLoading = false; }
  }
  function addTag() {
    const t = newTagInput.trim().toLowerCase();
    if (!t) return;
    const tags = store.settings.nsfwFilteredTags ?? [];
    if (!tags.includes(t)) updateSettings({ nsfwFilteredTags: [...tags, t] });
    newTagInput = "";
  }
  function removeTag(tag: string) {
    updateSettings({ nsfwFilteredTags: (store.settings.nsfwFilteredTags ?? []).filter(t => t !== tag) });
  }
  function resetTags() {
    updateSettings({ nsfwFilteredTags: ["adult", "mature", "hentai", "ecchi", "erotic", "pornograph", "18+", "smut", "lemon", "explicit", "sexual violence"] });
  }
  function toggleSourceAllowed(ids: string[]) {
    const allowed = store.settings.nsfwAllowedSourceIds ?? [];
    const blocked = store.settings.nsfwBlockedSourceIds ?? [];
    const allAllowed = ids.every(id => allowed.includes(id));
    if (allAllowed) {
      updateSettings({ nsfwAllowedSourceIds: allowed.filter(x => !ids.includes(x)) });
    } else {
      updateSettings({
        nsfwAllowedSourceIds: [...allowed.filter(x => !ids.includes(x)), ...ids],
        nsfwBlockedSourceIds: blocked.filter(x => !ids.includes(x)),
      });
    }
  }
  // ── Backup ────────────────────────────────────────────────────────────────────
  let backupLoading    = $state(false);
  let backupError: string | null = $state(null);
  let backupList: { url: string; name: string; deleting?: boolean }[] = $state(
    JSON.parse(localStorage.getItem("moku_backups") ?? "[]")
  );

  function saveBackupList() {
    localStorage.setItem("moku_backups", JSON.stringify(backupList));
  }

  async function createBackup() {
    backupLoading = true; backupError = null;
    try {
      const res = await gql<{ createBackup: { url: string } }>(CREATE_BACKUP);
      const url = res.createBackup.url;
      const name = url.split("/").pop() ?? url;
      backupList = [{ url, name }, ...backupList];
      saveBackupList();
    } catch (e: any) {
      backupError = e?.message ?? "Failed to create backup";
    } finally {
      backupLoading = false;
    }
  }

  async function deleteBackup(url: string) {
    backupList = backupList.map(b => b.url === url ? { ...b, deleting: true } : b);
    try {
      const serverUrl = (store.settings.serverUrl ?? "http://localhost:4567").replace(/\/$/, "");
      const headers: Record<string, string> = {};
      const pass = store.settings.serverAuthPass ?? "";
      const user = store.settings.serverAuthUser ?? "";
      if (store.settings.serverAuthMode === "BASIC_AUTH" && user && pass) {
        headers["Authorization"] = "Basic " + btoa(`${user}:${pass}`);
      }
      await fetch(`${serverUrl}${url}`, { method: "DELETE", headers });
      backupList = backupList.filter(b => b.url !== url);
      saveBackupList();
    } catch (e: any) {
      backupList = backupList.map(b => b.url === url ? { ...b, deleting: false } : b);
      backupError = (e as any)?.message ?? "Failed to delete backup";
    }
  }
  async function downloadBackup(backup: { url: string; name: string }) {
    try {
      const serverUrl = (store.settings.serverUrl ?? "http://localhost:4567").replace(/\/$/, "");
      const headers: Record<string, string> = {};
      const pass = store.settings.serverAuthPass ?? "";
      const user = store.settings.serverAuthUser ?? "";
      if (store.settings.serverAuthMode === "BASIC_AUTH" && user && pass) {
        headers["Authorization"] = "Basic " + btoa(`${user}:${pass}`);
      }
      const resp = await fetch(`${serverUrl}${backup.url}`, { headers });
      if (!resp.ok) throw new Error(`Server returned ${resp.status}`);
      const blob = await resp.blob();

      // Try File System Access API (lets user pick save location)
      if ("showSaveFilePicker" in window) {
        try {
          const handle = await (window as any).showSaveFilePicker({
            suggestedName: backup.name,
            types: [{ description: "Backup file", accept: { "application/octet-stream": [".tachibk", ".proto.gz"] } }],
          });
          const writable = await handle.createWritable();
          await writable.write(blob);
          await writable.close();
          addToast({ kind: "success", title: "Backup saved", body: backup.name });
          return;
        } catch (pickerErr: any) {
          // User cancelled the picker — abort silently
          if (pickerErr?.name === "AbortError") return;
          // Otherwise fall through to classic download
        }
      }

      // Fallback: classic anchor download
      const objectUrl = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = objectUrl;
      a.download = backup.name;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      setTimeout(() => URL.revokeObjectURL(objectUrl), 5000);
      addToast({ kind: "download", title: "Backup downloaded", body: backup.name });
    } catch (e: any) {
      backupError = e?.message ?? "Failed to download backup";
    }
  }

  let restoreLoading = $state(false);
  let restoreError: string | null = $state(null);
  let restoreJobId: string | null = $state(null);
  let restoreStatus: { mangaProgress: number; state: string; totalManga: number } | null = $state(null);
  let restorePollInterval: ReturnType<typeof setInterval> | null = null;

  let validateLoading = $state(false);
  let validateError: string | null = $state(null);
  let validateResult: { missingSources: { id: string; name: string }[]; missingTrackers: { name: string }[] } | null = $state(null);

  let restoreFile: File | null   = $state(null);

  function stopRestorePoll() {
    if (restorePollInterval) { clearInterval(restorePollInterval); restorePollInterval = null; }
  }

  async function pollRestoreStatus(id: string) {
    try {
      const res = await gql<{ restoreStatus: typeof restoreStatus }>(GET_RESTORE_STATUS, { id });
      restoreStatus = res.restoreStatus;
      if (res.restoreStatus?.state === "SUCCESS" || res.restoreStatus?.state === "FAILURE") {
        stopRestorePoll();
      }
    } catch {}
  }

  async function submitRestore() {
    if (!restoreFile) return;
    restoreLoading = true; restoreError = null; restoreStatus = null; restoreJobId = null;
    stopRestorePoll();
    try {
      const serverUrl = (store.settings.serverUrl ?? "http://localhost:4567").replace(/\/$/, "");
      const form = new FormData();
      const operations = JSON.stringify({
        query: `mutation RestoreBackup($backup: Upload!) { restoreBackup(input: { backup: $backup }) { id status { mangaProgress state totalManga } } }`,
        variables: { backup: null }
      });
      form.append("operations", operations);
      form.append("map", JSON.stringify({ "0": ["variables.backup"] }));
      form.append("0", restoreFile, restoreFile.name);

      const headers: Record<string, string> = { "Accept": "application/json" };
      const pass = store.settings.serverAuthPass ?? "";
      const user = store.settings.serverAuthUser ?? "";
      if (store.settings.serverAuthMode === "BASIC_AUTH" && user && pass) {
        headers["Authorization"] = "Basic " + btoa(`${user}:${pass}`);
      }

      const resp = await fetch(`${serverUrl}/api/graphql`, { method: "POST", headers, body: form });
      const json = await resp.json();
      if (json.errors?.length) throw new Error(json.errors[0].message);

      const result = json.data.restoreBackup;
      restoreJobId = result.id;
      restoreStatus = result.status;
      if (result.status?.state !== "SUCCESS" && result.status?.state !== "FAILURE") {
        restorePollInterval = setInterval(() => pollRestoreStatus(result.id), 1500);
      }
    } catch (e: any) {
      restoreError = e?.message ?? "Failed to start restore";
    } finally {
      restoreLoading = false;
    }
  }

  async function submitValidate() {
    if (!restoreFile) return;
    validateLoading = true; validateError = null; validateResult = null;
    try {
      const serverUrl = (store.settings.serverUrl ?? "http://localhost:4567").replace(/\/$/, "");
      const form = new FormData();
      const operations = JSON.stringify({
        query: `query ValidateBackup($backup: Upload!) { validateBackup(input: { backup: $backup }) { missingSources { id name } missingTrackers { name } } }`,
        variables: { backup: null }
      });
      form.append("operations", operations);
      form.append("map", JSON.stringify({ "0": ["variables.backup"] }));
      form.append("0", restoreFile, restoreFile.name);

      const headers: Record<string, string> = { "Accept": "application/json" };
      const pass = store.settings.serverAuthPass ?? "";
      const user = store.settings.serverAuthUser ?? "";
      if (store.settings.serverAuthMode === "BASIC_AUTH" && user && pass) {
        headers["Authorization"] = "Basic " + btoa(`${user}:${pass}`);
      }

      const resp = await fetch(`${serverUrl}/api/graphql`, { method: "POST", headers, body: form });
      const json = await resp.json();
      if (json.errors?.length) throw new Error(json.errors[0].message);
      validateResult = json.data.validateBackup;
    } catch (e: any) {
      validateError = e?.message ?? "Failed to validate backup";
    } finally {
      validateLoading = false;
    }
  }

  $effect(() => { if (tab !== "storage") stopRestorePoll(); });

  function toggleSourceBlocked(ids: string[]) {
    const allowed = store.settings.nsfwAllowedSourceIds ?? [];
    const blocked = store.settings.nsfwBlockedSourceIds ?? [];
    const allBlocked = ids.every(id => blocked.includes(id));
    if (allBlocked) {
      updateSettings({ nsfwBlockedSourceIds: blocked.filter(x => !ids.includes(x)) });
    } else {
      updateSettings({
        nsfwBlockedSourceIds: [...blocked.filter(x => !ids.includes(x)), ...ids],
        nsfwAllowedSourceIds: allowed.filter(x => !ids.includes(x)),
      });
    }
  }
  interface ContentSourceGroup {
    name:    string;
    iconUrl: string;
    isNsfw:  boolean;
    sources: Source[];
  }
  const contentSourcesFiltered = $derived.by(() => {
    const q = sourceSearch.trim().toLowerCase();
    const filtered = q
      ? contentSources.filter(s => s.displayName.toLowerCase().includes(q) || s.lang.toLowerCase().includes(q))
      : contentSources;
    const map = new Map<string, ContentSourceGroup>();
    for (const s of filtered) {
      const key = s.name;
      if (!map.has(key)) map.set(key, { name: s.name, iconUrl: s.iconUrl, isNsfw: s.isNsfw, sources: [] });
      map.get(key)!.sources.push(s);
    }
    return Array.from(map.values());
  });
</script>
<div class="backdrop" role="presentation" tabindex="-1" onclick={(e) => { if (e.target === e.currentTarget) close(); }} onkeydown={(e) => { if (e.key === "Escape") close(); }}>
  <div class="modal" role="dialog" aria-label="Settings">
    <div class="sidebar">
      <p class="modal-title">Settings</p>
      <nav class="nav">
        {#each TABS as t}
          <button class="nav-item" class:active={tab === t.id} class:anims onclick={() => setTab(t.id)}>
            <span class="nav-item-icon" class:slide-down={anims && tab === t.id && tabSlideDir === "down"} class:slide-up={anims && tab === t.id && tabSlideDir === "up"}>
              {#key anims && tab === t.id ? tabIconKey : 0}
                <t.icon size={14} weight={tab === t.id ? "regular" : "light"} />
              {/key}
            </span>
            <span>{t.label}</span>
          </button>
        {/each}
      </nav>
    </div>
    <div class="content">
      <div class="content-header">
        <div class="content-header-left">
          <span class="header-icon-wrap" class:slide-down={anims && tabSlideDir === "down"} class:slide-up={anims && tabSlideDir === "up"}>
            {#key tabIconKey}
              {#each TABS as t}
                {#if t.id === tab}
                  <t.icon size={13} weight="light" class="content-header-icon" />
                {/if}
              {/each}
            {/key}
          </span>
          <p class="content-title">{TABS.find((t) => t.id === tab)?.label}</p>
        </div>
        <button class="close-btn" aria-label="Close settings" onclick={close}><X size={15} weight="light" /></button>
      </div>
      <div class="content-body" bind:this={contentBodyEl}>
        {#if tab === "general"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Interface Scale</p>
              <div class="scale-row">
                <input type="range" min={50} max={200} step={5}
                  value={Math.round((store.settings.uiZoom ?? 1.0) * 100)}
                  oninput={(e) => updateSettings({ uiZoom: Number(e.currentTarget.value) / 100 })}
                  class="scale-slider" />
                <input
                  type="number" min={50} max={200} step={1}
                  class="scale-val-input"
                  value={Math.round((store.settings.uiZoom ?? 1.0) * 100)}
                  oninput={(e) => {
                    const n = parseInt(e.currentTarget.value, 10);
                    if (!isNaN(n) && n >= 50 && n <= 200) updateSettings({ uiZoom: n / 100 });
                  }}
                  onblur={(e) => {
                    const n = parseInt(e.currentTarget.value, 10);
                    if (isNaN(n) || n < 50) { updateSettings({ uiZoom: 0.5 }); e.currentTarget.value = "50"; }
                    else if (n > 200) { updateSettings({ uiZoom: 2.0 }); e.currentTarget.value = "200"; }
                  }}
                />
                <span class="scale-pct">%</span>
                <button class="step-btn" onclick={() => updateSettings({ uiZoom: 1.0 })} disabled={(store.settings.uiZoom ?? 1.0) === 1.0} title="Reset to 100%">↺</button>
              </div>
              <p class="scale-hint">
                {#each [50,60,70,80,90,100,110,125,150,175,200] as v}
                  <button class="scale-preset" class:active={Math.round((store.settings.uiZoom ?? 1.0) * 100) === v} onclick={() => updateSettings({ uiZoom: v / 100 })}>{v}%</button>
                {/each}
              </p>
            </div>
            <div class="section">
              <p class="section-title">Server</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Server URL</span><span class="toggle-desc">Base URL of your Suwayomi instance</span></div>
                <input class="text-input" value={store.settings.serverUrl ?? "http://localhost:4567"} oninput={(e) => updateSettings({ serverUrl: e.currentTarget.value })} placeholder="http://localhost:4567" spellcheck="false" />
              </div>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Auto-start server</span><span class="toggle-desc">Launch tachidesk-server when Moku opens</span></div>
                <button role="switch" aria-checked={store.settings.autoStartServer} aria-label="Auto-start server" class="toggle" class:on={store.settings.autoStartServer} onclick={() => updateSettings({ autoStartServer: !store.settings.autoStartServer })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Inactivity</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Idle screen timeout</span><span class="toggle-desc">Show the Moku idle splash after this much inactivity.</span></div>
                <div class="select-wrap" id="idle-timeout">
                  <button class="select-btn" onclick={() => toggleSelect("idle-timeout")}>
                    <span>{{ "0":"Never","1":"1 minute","2":"2 minutes","5":"5 minutes","10":"10 minutes","15":"15 minutes","30":"30 minutes" }[String(store.settings.idleTimeoutMin ?? 5)] ?? `${store.settings.idleTimeoutMin} min`}</span>
                    <svg class="select-caret" class:open={selectOpen === "idle-timeout"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
                  </button>
                  {#if selectOpen === "idle-timeout"}
                    <div class="select-menu">
                      {#each [["0","Never"],["1","1 minute"],["2","2 minutes"],["5","5 minutes"],["10","10 minutes"],["15","15 minutes"],["30","30 minutes"]] as [v, l]}
                        <button class="select-option" class:active={String(store.settings.idleTimeoutMin ?? 5) === v} onclick={() => { updateSettings({ idleTimeoutMin: Number(v) }); selectOpen = null; }}>{l}</button>
                      {/each}
                    </div>
                  {/if}
                </div>
              </div>
            </div>
            <div class="section">
              <p class="section-title">Integrations</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Discord Rich Presence</span><span class="toggle-desc">Show what you're reading in your Discord status</span></div>
                <button role="switch" aria-checked={store.settings.discordRpc} aria-label="Discord Rich Presence" class="toggle" class:on={store.settings.discordRpc} onclick={() => updateSettings({ discordRpc: !store.settings.discordRpc })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Animations</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">QOL Animations</span><span class="toggle-desc">Subtle motion effects across the interface — hover lifts, active-tab transitions, and icon micro-animations</span></div>
                <button role="switch" aria-checked={store.settings.qolAnimations ?? true} aria-label="QOL Animations" class="toggle" class:on={store.settings.qolAnimations ?? true} onclick={() => updateSettings({ qolAnimations: !(store.settings.qolAnimations ?? true) })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Language</p>
              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Preferred source language</span>
                  <span class="toggle-desc">Used to pre-select languages in Search and deduplicate sources</span>
                </div>
                <input
                  class="text-input"
                  value={store.settings.preferredExtensionLang ?? ""}
                  oninput={(e) => updateSettings({ preferredExtensionLang: e.currentTarget.value.trim().toLowerCase() })}
                  placeholder="e.g. en"
                  spellcheck="false"
                  style="width:72px;text-align:center;text-transform:uppercase"
                />
              </div>
            </div>
          </div>
        {:else if tab === "appearance"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Theme</p>
              <div class="theme-grid">
                {#each THEMES as theme}
                  {@const active = (store.settings.theme ?? "dark") === theme.id}
                  <div class="theme-card{active ? ' active' : ''}">
                    <button class="theme-card-inner" onclick={() => updateSettings({ theme: theme.id })} title={theme.description}>
                      <div class="theme-preview">
                        <div class="theme-preview-bg" style="background:{theme.swatches[0]}">
                          <div class="theme-preview-sidebar" style="background:{theme.swatches[1]}"></div>
                          <div class="theme-preview-content">
                            <div class="theme-preview-accent" style="background:{theme.swatches[2]}"></div>
                            <div class="theme-preview-text" style="background:{theme.swatches[3]}55"></div>
                            <div class="theme-preview-text" style="background:{theme.swatches[3]}33;width:60%"></div>
                          </div>
                        </div>
                      </div>
                      <div class="theme-card-info">
                        <span class="theme-card-label">{theme.label}</span>
                        <span class="theme-card-desc">{theme.description}</span>
                      </div>
                      {#if active}<span class="theme-card-check">✓</span>{/if}
                    </button>
                  </div>
                {/each}
                {#each store.settings.customThemes ?? [] as custom}
                  {@const active = store.settings.theme === custom.id}
                  <div class="theme-card custom-theme-card" class:active>
                    <button
                      class="custom-theme-select"
                      onclick={() => updateSettings({ theme: custom.id })}
                      title="Apply {custom.name}"
                    >
                      <div class="theme-preview">
                        <div class="theme-preview-bg" style="background:{custom.tokens['bg-base']}">
                          <div class="theme-preview-sidebar" style="background:{custom.tokens['bg-surface']}"></div>
                          <div class="theme-preview-content">
                            <div class="theme-preview-accent" style="background:{custom.tokens['accent']}"></div>
                            <div class="theme-preview-text" style="background:{custom.tokens['text-primary']}55"></div>
                            <div class="theme-preview-text" style="background:{custom.tokens['text-primary']}33;width:60%"></div>
                          </div>
                        </div>
                      </div>
                      <div class="theme-card-info">
                        <span class="theme-card-label">{custom.name}</span>
                        <span class="theme-card-desc custom-badge">Custom</span>
                      </div>
                    </button>
                    <div class="custom-theme-actions">
                      <button
                        class="custom-theme-edit-btn"
                        onclick={() => onOpenThemeEditor?.(custom.id)}
                        title="Edit theme"
                      >
                        <Pencil size={10} />
                      </button>
                      <button
                        class="custom-theme-delete-btn"
                        onclick={() => {
                          if (confirm(`Delete theme "${custom.name}"?`)) deleteCustomTheme(custom.id);
                        }}
                        title="Delete theme"
                      >
                        <Trash size={10} />
                      </button>
                    </div>
                    {#if active}<span class="theme-card-check">✓</span>{/if}
                  </div>
                {/each}
                <button
                  class="theme-card new-theme-card"
                  onclick={() => onOpenThemeEditor?.(null)}
                  title="Create a custom theme"
                >
                  <div class="new-theme-icon">
                    <Plus size={18} weight="light" />
                  </div>
                  <div class="theme-card-info">
                    <span class="theme-card-label">New Theme</span>
                    <span class="theme-card-desc">Create custom</span>
                  </div>
                </button>
              </div>
            </div>
          </div>
        {:else if tab === "reader"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Page Layout</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Default layout</span><span class="toggle-desc">How chapters open by default</span></div>
                <div class="select-wrap" id="page-style">
                  <button class="select-btn" onclick={() => toggleSelect("page-style")}>
                    <span>{{ "single":"Single page","longstrip":"Long strip" }[store.settings.pageStyle === "double" ? "single" : store.settings.pageStyle]}</span>
                    <svg class="select-caret" class:open={selectOpen === "page-style"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
                  </button>
                  {#if selectOpen === "page-style"}
                    <div class="select-menu">
                      {#each [["single","Single page"],["longstrip","Long strip"]] as [v, l]}
                        <button class="select-option" class:active={(store.settings.pageStyle === "double" ? "single" : store.settings.pageStyle) === v} onclick={() => { updateSettings({ pageStyle: v as Settings["pageStyle"] }); selectOpen = null; }}>{l}</button>
                      {/each}
                    </div>
                  {/if}
                </div>
              </div>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Reading direction</span><span class="toggle-desc">Left-to-right for most manga, right-to-left for Japanese</span></div>
                <div class="select-wrap" id="reading-dir">
                  <button class="select-btn" onclick={() => toggleSelect("reading-dir")}>
                    <span>{{ "ltr":"Left to right","rtl":"Right to left" }[store.settings.readingDirection]}</span>
                    <svg class="select-caret" class:open={selectOpen === "reading-dir"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
                  </button>
                  {#if selectOpen === "reading-dir"}
                    <div class="select-menu">
                      {#each [["ltr","Left to right"],["rtl","Right to left"]] as [v, l]}
                        <button class="select-option" class:active={store.settings.readingDirection === v} onclick={() => { updateSettings({ readingDirection: v as Settings["readingDirection"] }); selectOpen = null; }}>{l}</button>
                      {/each}
                    </div>
                  {/if}
                </div>
              </div>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Page gap</span><span class="toggle-desc">Adds spacing between pages in single-page mode</span></div>
                <button role="switch" aria-checked={store.settings.pageGap} aria-label="Page gap" class="toggle" class:on={store.settings.pageGap} onclick={() => updateSettings({ pageGap: !store.settings.pageGap })}><span class="toggle-thumb"></span></button>
              </label>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Overlay bars</span><span class="toggle-desc">Floats the nav and chapter bars over the page instead of pushing content</span></div>
                <button role="switch" aria-checked={store.settings.overlayBars ?? false} aria-label="Overlay bars" class="toggle" class:on={store.settings.overlayBars ?? false} onclick={() => updateSettings({ overlayBars: !(store.settings.overlayBars ?? false) })}><span class="toggle-thumb"></span></button>
              </label>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Tap to toggle bar</span><span class="toggle-desc">Double-tap the center of the reader to show or hide the bars — ideal for touchscreens</span></div>
                <button role="switch" aria-checked={store.settings.tapToToggleBar ?? false} aria-label="Tap to toggle bar" class="toggle" class:on={store.settings.tapToToggleBar ?? false} onclick={() => updateSettings({ tapToToggleBar: !(store.settings.tapToToggleBar ?? false) })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Fit &amp; Zoom</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Default fit mode</span><span class="toggle-desc">How pages are scaled to fill the reader on open</span></div>
                <div class="select-wrap" id="fit-mode">
                  <button class="select-btn" onclick={() => toggleSelect("fit-mode")}>
                    <span>{{ "width":"Fit width","height":"Fit height","screen":"Fit screen","original":"Original (1:1)" }[store.settings.fitMode ?? "width"]}</span>
                    <svg class="select-caret" class:open={selectOpen === "fit-mode"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
                  </button>
                  {#if selectOpen === "fit-mode"}
                    <div class="select-menu">
                      {#each [["width","Fit width"],["height","Fit height"],["screen","Fit screen"],["original","Original (1:1)"]] as [v, l]}
                        <button class="select-option" class:active={(store.settings.fitMode ?? "width") === v} onclick={() => { updateSettings({ fitMode: v as FitMode }); selectOpen = null; }}>{l}</button>
                      {/each}
                    </div>
                  {/if}
                </div>
              </div>
              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Default zoom</span>
                  <span class="toggle-desc">100% = fills the reader</span>
                </div>
                <div class="scale-row">
                  <input type="range" min={10} max={400} step={5}
                    value={Math.round((store.settings.readerZoom ?? 0.5) * 100)}
                    oninput={(e) => updateSettings({ readerZoom: Number(e.currentTarget.value) / 100 })}
                    class="scale-slider" />
                  <input
                    type="number" min={10} max={400} step={5}
                    class="scale-val-input"
                    value={Math.round((store.settings.readerZoom ?? 0.5) * 100)}
                    oninput={(e) => {
                      const n = parseInt(e.currentTarget.value, 10);
                      if (!isNaN(n) && n >= 10 && n <= 400) updateSettings({ readerZoom: n / 100 });
                    }}
                    onblur={(e) => {
                      const n = parseInt(e.currentTarget.value, 10);
                      if (isNaN(n) || n < 10) { updateSettings({ readerZoom: 0.1 }); e.currentTarget.value = "10"; }
                      else if (n > 400) { updateSettings({ readerZoom: 4.0 }); e.currentTarget.value = "400"; }
                    }}
                  />
                  <span class="scale-pct">%</span>
                  <button class="step-btn" onclick={() => updateSettings({ readerZoom: 0.5 })} disabled={(store.settings.readerZoom ?? 0.5) === 0.5} title="Reset to 100%">↺</button>
                </div>
              </div>
              <p class="scale-hint">
                {#each [50, 75, 100, 125, 150, 200] as v}
                  <button class="scale-preset"
                    class:active={Math.round((store.settings.readerZoom ?? 0.5) * 100) === v}
                    onclick={() => updateSettings({ readerZoom: v / 100 })}>{v}%</button>
                {/each}
              </p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Optimize contrast</span><span class="toggle-desc">Sharpens dark lines on light pages; best for black-and-white manga</span></div>
                <button role="switch" aria-checked={store.settings.optimizeContrast} aria-label="Optimize contrast" class="toggle" class:on={store.settings.optimizeContrast} onclick={() => updateSettings({ optimizeContrast: !store.settings.optimizeContrast })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Behaviour</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Auto-mark read</span><span class="toggle-desc">Marks a chapter as read when you reach the last page</span></div>
                <button role="switch" aria-checked={store.settings.autoMarkRead} aria-label="Auto-mark chapters read" class="toggle" class:on={store.settings.autoMarkRead} onclick={() => updateSettings({ autoMarkRead: !store.settings.autoMarkRead })}><span class="toggle-thumb"></span></button>
              </label>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Auto-advance chapters</span><span class="toggle-desc">Automatically loads the next chapter when you pass the last page</span></div>
                <button role="switch" aria-checked={store.settings.autoNextChapter ?? false} aria-label="Auto-advance chapters" class="toggle" class:on={store.settings.autoNextChapter} onclick={() => updateSettings({ autoNextChapter: !(store.settings.autoNextChapter ?? false) })}><span class="toggle-thumb"></span></button>
              </label>
              {#if !(store.settings.autoNextChapter ?? false)}
                <label class="toggle-row">
                  <div class="toggle-info"><span class="toggle-label">Mark read when skipping</span><span class="toggle-desc">Marks the current chapter read when you manually jump to the next</span></div>
                  <button role="switch" aria-checked={store.settings.markReadOnNext ?? true} aria-label="Mark read when skipping" class="toggle" class:on={store.settings.markReadOnNext ?? true} onclick={() => updateSettings({ markReadOnNext: !(store.settings.markReadOnNext ?? true) })}><span class="toggle-thumb"></span></button>
                </label>
              {/if}
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Auto-bookmark</span><span class="toggle-desc">Automatically saves your page position as you read</span></div>
                <button role="switch" aria-checked={store.settings.autoBookmark ?? true} aria-label="Enable auto-bookmark" class="toggle" class:on={store.settings.autoBookmark ?? true} onclick={() => {
                    updateSettings({ autoBookmark: !(store.settings.autoBookmark ?? true) });
                  }}><span class="toggle-thumb"></span></button>
              </label>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Pages to preload</span><span class="toggle-desc">How many pages ahead to fetch in the background while reading</span></div>
                <div class="step-controls">
                  <button class="step-btn" onclick={() => updateSettings({ preloadPages: Math.max(0, store.settings.preloadPages - 1) })} disabled={store.settings.preloadPages <= 0}>−</button>
                  <span class="step-val">{store.settings.preloadPages}</span>
                  <button class="step-btn" onclick={() => updateSettings({ preloadPages: Math.min(10, store.settings.preloadPages + 1) })} disabled={store.settings.preloadPages >= 10}>+</button>
                </div>
              </div>
            </div>
          </div>
        {:else if tab === "library"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Display</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Crop cover images</span><span class="toggle-desc">Fills the card with the cover art instead of letterboxing</span></div>
                <button role="switch" aria-checked={store.settings.libraryCropCovers} aria-label="Crop cover images" class="toggle" class:on={store.settings.libraryCropCovers} onclick={() => updateSettings({ libraryCropCovers: !store.settings.libraryCropCovers })}><span class="toggle-thumb"></span></button>
              </label>
              <label class="toggle-row">
                <div class="toggle-info">
                  <span class="toggle-label">Show all in Saved tab</span>
                  <span class="toggle-desc">Include manga that are in folders — lets you see your whole library in one place</span>
                </div>
                <button role="switch" aria-checked={store.settings.libraryShowAllInSaved ?? true} aria-label="Show all manga in Saved tab" class="toggle" class:on={store.settings.libraryShowAllInSaved ?? true} onclick={() => updateSettings({ libraryShowAllInSaved: !(store.settings.libraryShowAllInSaved ?? true) })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Chapters</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Default sort direction</span><span class="toggle-desc">Initial chapter list order when opening a manga</span></div>
                <div class="select-wrap" id="sort-dir">
                  <button class="select-btn" onclick={() => toggleSelect("sort-dir")}>
                    <span>{{ "desc":"Newest first","asc":"Oldest first" }[store.settings.chapterSortDir]}</span>
                    <svg class="select-caret" class:open={selectOpen === "sort-dir"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
                  </button>
                  {#if selectOpen === "sort-dir"}
                    <div class="select-menu">
                      {#each [["desc","Newest first"],["asc","Oldest first"]] as [v, l]}
                        <button class="select-option" class:active={store.settings.chapterSortDir === v} onclick={() => { updateSettings({ chapterSortDir: v as Settings["chapterSortDir"] }); selectOpen = null; }}>{l}</button>
                      {/each}
                    </div>
                  {/if}
                </div>
              </div>
            </div>
            <div class="section">
              <p class="section-title">History</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Reading history</span><span class="toggle-desc">{store.history.length} entries</span></div>
                <button class="danger-btn" onclick={clearHistory} disabled={store.history.length === 0}>Clear</button>
              </div>
              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Wipe all data</span>
                  <span class="toggle-desc">History, stats, pins, and manga links</span>
                </div>
                <button class="danger-btn" onclick={wipeAllData}>Wipe</button>
              </div>
            </div>
          </div>
        {:else if tab === "performance"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Render Limit</p>
              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Items per page</span>
                  <span class="toggle-desc">Lower = faster on large libraries</span>
                </div>
                <div class="step-controls">
                  <button class="step-btn" onclick={() => updateSettings({ renderLimit: Math.max(12, (store.settings.renderLimit ?? 48) - 12) })} disabled={(store.settings.renderLimit ?? 48) <= 12}>−</button>
                  <span class="step-val">{store.settings.renderLimit ?? 48}</span>
                  <button class="step-btn" onclick={() => updateSettings({ renderLimit: Math.min(200, (store.settings.renderLimit ?? 48) + 12) })} disabled={(store.settings.renderLimit ?? 48) >= 200}>+</button>
                </div>
              </div>
              <p class="scale-hint">
                {#each [12, 24, 48, 96, 200] as v}
                  <button class="scale-preset" class:active={(store.settings.renderLimit ?? 48) === v} onclick={() => updateSettings({ renderLimit: v })}>{v}</button>
                {/each}
              </p>
            </div>
            <div class="section">
              <p class="section-title">Rendering</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">GPU acceleration</span><span class="toggle-desc">Uses the GPU for rendering; disable if you see visual glitches</span></div>
                <button role="switch" aria-checked={store.settings.gpuAcceleration} aria-label="GPU acceleration" class="toggle" class:on={store.settings.gpuAcceleration} onclick={() => updateSettings({ gpuAcceleration: !store.settings.gpuAcceleration })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Idle / Splash Screen</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Animated card background</span><span class="toggle-desc">Shows cover art cards floating in the background on the idle screen</span></div>
                <button role="switch" aria-checked={store.settings.splashCards ?? true} aria-label="Animated card background" class="toggle" class:on={store.settings.splashCards ?? true} onclick={() => updateSettings({ splashCards: !(store.settings.splashCards ?? true) })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Interface</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Compact sidebar</span><span class="toggle-desc">Collapses the sidebar to icons only</span></div>
                <button role="switch" aria-checked={store.settings.compactSidebar} aria-label="Compact sidebar" class="toggle" class:on={store.settings.compactSidebar} onclick={() => updateSettings({ compactSidebar: !store.settings.compactSidebar })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Session Cache</p>
              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Cache entries</span>
                  <span class="toggle-desc">In-memory, cleared on restart</span>
                </div>
                <div class="perf-stat-group">
                  <span class="perf-stat">{perfSnapshot?.cacheEntries ?? 0} entries</span>
                  <button class="kb-reset" onclick={refreshPerfMetrics} title="Refresh">↺</button>
                </div>
              </div>
              {#if perfSnapshot && perfSnapshot.cacheEntries > 0}
                <div class="step-row">
                  <div class="toggle-info"><span class="toggle-label">Oldest entry</span></div>
                  <span class="perf-stat">{fmtAge(perfSnapshot.oldestEntryMs)}</span>
                </div>
                <div class="step-row">
                  <div class="toggle-info"><span class="toggle-label">Newest entry</span></div>
                  <span class="perf-stat">{fmtAge(perfSnapshot.newestEntryMs)}</span>
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Cached keys</span>
                    <span class="toggle-desc">{perfSnapshot.cacheKeys.join(", ")}</span>
                  </div>
                </div>
              {/if}
            </div>
          </div>
        {:else if tab === "keybinds"}
          <div class="panel">
            <div class="section">
              <div class="section-title-row">
                <p class="section-title">Keyboard shortcuts</p>
                <button class="sec-action-btn" onclick={resetKeybinds}>Reset all</button>
              </div>
              <p class="kb-hint">Click a binding to rebind, then press the new key combination.</p>
              <div class="kb-list">
                {#each Object.keys(KEYBIND_LABELS) as key}
                  {@const k = key as keyof Keybinds}
                  {@const isListening = listeningKey === k}
                  {@const isDefault   = store.settings.keybinds[k] === DEFAULT_KEYBINDS[k]}
                  <div class="kb-row">
                    <span class="kb-label">{KEYBIND_LABELS[k]}</span>
                    <div class="kb-right">
                      <button class="kb-bind" class:listening={isListening} onclick={() => startListen(k)}>
                        {isListening ? "Press key…" : store.settings.keybinds[k]}
                      </button>
                      <button class="kb-reset" onclick={() => updateSettings({ keybinds: { ...store.settings.keybinds, [k]: DEFAULT_KEYBINDS[k] } })} disabled={isDefault} title="Reset">↺</button>
                    </div>
                  </div>
                {/each}
              </div>
            </div>
          </div>
        {:else if tab === "storage"}
          <div class="panel">


            {#if migrateFrom && !isExternalServer}
              <div class="migrate-banner">
                <div class="migrate-banner-body">
                  <span class="migrate-title">Manga found at previous path — move to new location?</span>
                  <span class="migrate-paths">{migrateFrom} → {migrateTo}</span>
                  {#if migrateProgress && migrateProgress.total > 0}
                    <div class="migrate-progress">
                      <div class="migrate-progress-labels">
                        <span class="migrate-current">{migrateProgress.current}</span>
                        <span class="migrate-count">{migrateProgress.done} / {migrateProgress.total}</span>
                      </div>
                      <div class="migrate-bar"><div class="migrate-bar-fill" style="width:{Math.round((migrateProgress.done/migrateProgress.total)*100)}%"></div></div>
                    </div>
                  {/if}
                  {#if migrateError}<span class="migrate-error">{migrateError}</span>{/if}
                </div>
                <div class="migrate-banner-actions">
                  <button class="sec-action-btn sec-action-primary" onclick={startMigration} disabled={migrating}>
                    {migrating ? (migrateProgress ? `Moving… ${migrateProgress.done}/${migrateProgress.total}` : "Starting…") : "Move files"}
                  </button>
                  <button class="sec-action-btn" onclick={dismissMigration} disabled={migrating}>Skip</button>
                </div>
              </div>
            {/if}


            <div class="section">
              <div class="section-title-row"><p class="section-title">Disk Usage</p><button class="sec-action-btn" onclick={fetchStorage} disabled={storageLoading}>{storageLoading ? "…" : "↻"}</button></div>
              {#if storageLoading}
                <p class="storage-loading">Reading filesystem…</p>
              {:else if storageError}
                <p class="storage-loading" style="color:var(--color-error)">{storageError}</p>
              {:else if isExternalServer}
                <p class="storage-loading">Disk usage is unavailable for external servers — filesystem access requires a local connection.</p>
              {:else if multiStorageInfos.length > 0}
                {#each multiStorageInfos as info}
                  {@const limitGb    = store.settings.storageLimitGb ?? null}
                  {@const limitBytes = limitGb !== null ? limitGb * 1024 ** 3 : null}
                  {@const available  = info.manga_bytes + info.free_bytes}
                  {@const cap        = limitBytes !== null ? Math.min(limitBytes, available) : available}
                  {@const pct        = cap > 0 ? Math.min(100, (info.manga_bytes / cap) * 100) : 0}
                  <div class="storage-bar-wrap">
                    <div class="storage-bar-header">
                      <span class="storage-bar-label">{info.label}</span>
                      <span class="storage-bar-used">{fmtBytes(info.manga_bytes)} of {fmtBytes(cap)}</span>
                    </div>
                    <div class="storage-bar">
                      <div class="storage-bar-fill" class:critical={pct > 90} class:warn={pct > 75 && pct <= 90} style="width:{pct}%"></div>
                    </div>
                    <div class="storage-bar-labels">
                      <span class="storage-path-note" style="margin:0">{info.path}</span>
                      <span class="storage-bar-free">{fmtBytes(info.free_bytes)} free</span>
                    </div>
                  </div>
                {/each}
              {:else}
                <p class="storage-loading">No download path configured.</p>
              {/if}
            </div>


            <div class="section">
              <p class="section-title">Downloads Path</p>
              {#if isExternalServer}
                <p class="toggle-desc" style="display:block;padding:0 var(--sp-3) var(--sp-2)">
                  Connected to an external server. The path below is read from the server — changes here will update the server's config directly. Make sure the path is valid on the server's filesystem.
                </p>
              {/if}
              <div class="path-row">
                <input
                  class="text-input path-input"
                  class:path-input-error={!!pathsFieldError.dl}
                  bind:value={downloadsPathInput}
                  placeholder={isExternalServer ? "Server default" : (defaultDownloadsPath || "Default location")}
                  spellcheck="false"
                  onkeydown={(e) => e.key === "Enter" && savePaths()}
                  oninput={() => { pathsFieldError = { ...pathsFieldError, dl: undefined }; }}
                />
                {#if !isExternalServer}
                  <button class="sec-action-btn" onclick={browseDownloadsFolder} title="Browse for folder">
                    Browse
                  </button>
                {/if}
                <div class="path-actions">
                  {#if pathsFieldError.dl}
                    <span class="path-field-error">{pathsFieldError.dl}</span>
                    {#if !isExternalServer}
                      <button class="sec-action-btn" onclick={async () => {
                        try { await createDirectory(downloadsPathInput.trim()); pathsFieldError = { ...pathsFieldError, dl: undefined }; }
                        catch (e: any) { pathsFieldError = { ...pathsFieldError, dl: e?.message ?? "Failed" }; }
                      }}>Create</button>
                    {/if}
                  {/if}
                  {#if pathsError}<span class="path-field-error">{pathsError}</span>{/if}
                  <button class="sec-action-btn sec-action-primary" onclick={savePaths} disabled={pathsSaving}>
                    {pathsSaved ? "Saved ✓" : pathsSaving ? "Saving…" : "Save"}
                  </button>
                </div>
              </div>
            </div>


            <div class="section">
              <p class="section-title">Storage Limit</p>
              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Warn when limit is reached</span>
                  <span class="toggle-desc">{store.settings.storageLimitGb === null ? "No limit set" : `Warn above ${store.settings.storageLimitGb} GB`}</span>
                </div>
                {#if store.settings.storageLimitGb === null}
                  <button class="step-btn" style="width:auto;padding:0 var(--sp-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide)"
                    onclick={() => updateSettings({ storageLimitGb: 10 })}>Set limit</button>
                {:else}
                  <div class="step-controls">
                    <button class="step-btn" onclick={() => updateSettings({ storageLimitGb: Math.max(1, (store.settings.storageLimitGb ?? 10) - 1) })} disabled={(store.settings.storageLimitGb ?? 10) <= 1}>−</button>
                    <input type="number" min="1" step="1" class="storage-limit-input" value={store.settings.storageLimitGb}
                      oninput={(e) => { const n = parseFloat(e.currentTarget.value); if (!isNaN(n) && n > 0) updateSettings({ storageLimitGb: n }); }} />
                    <span class="storage-limit-unit">GB</span>
                    <button class="step-btn" onclick={() => updateSettings({ storageLimitGb: (store.settings.storageLimitGb ?? 10) + 1 })}>+</button>
                    <button class="kb-reset" title="Remove limit" onclick={() => updateSettings({ storageLimitGb: null })}>↺</button>
                  </div>
                {/if}
              </div>
            </div>


            <div class="section">
              <p class="section-title">Cache</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Image cache</span><span class="toggle-desc">Webview page image cache</span></div>
                <button class="danger-btn" onclick={handleClearCache} disabled={clearing}>
                  {cleared ? "Cleared" : clearing ? "Clearing…" : "Clear"}
                </button>
              </div>
            </div>


            <div class="section adv-section">
              <button class="adv-toggle" onclick={() => advStorageOpen = !advStorageOpen}>
                <span class="section-title" style="padding:0">Advanced</span>
                <svg class="adv-caret" class:open={advStorageOpen} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
              </button>
              {#if advStorageOpen}
                <div class="adv-body">

                  <div class="step-row">
                    <div class="toggle-info">
                      <span class="toggle-label">Local source path</span>
                      <span class="toggle-desc">Read manga already on disk without an extension. Leave blank if unused.</span>
                    </div>
                    <div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0">
                      <div style="display:flex;align-items:center;gap:var(--sp-2)">
                        <input class="text-input" style="width:200px;font-family:monospace;font-size:var(--text-xs);{pathsFieldError.loc?'border-color:var(--color-error)':''}"
                          bind:value={localSourcePathInput} placeholder="Optional" spellcheck="false"
                          onkeydown={(e) => e.key === "Enter" && savePaths()}
                          oninput={() => { pathsFieldError = { ...pathsFieldError, loc: undefined }; }} />
                        {#if pathsFieldError.loc && !isExternalServer}
                          <button class="sec-action-btn" onclick={async () => {
                            try { await createDirectory(localSourcePathInput.trim()); pathsFieldError = { ...pathsFieldError, loc: undefined }; }
                            catch (e: any) { pathsFieldError = { ...pathsFieldError, loc: e?.message ?? "Failed" }; }
                          }}>Create</button>
                        {/if}
                      </div>
                      {#if pathsFieldError.loc}<span style="font-family:var(--font-ui);font-size:10px;color:var(--color-error)">{pathsFieldError.loc}</span>{/if}
                    </div>
                  </div>

                  {#each extraScanDirs as dir}
                    <div class="step-row">
                      <div class="toggle-info">
                        <span class="toggle-label" style="font-family:monospace;font-size:var(--text-xs)">{dir}</span>
                        <span class="toggle-desc">Extra scan directory</span>
                      </div>
                      <button class="danger-btn" onclick={() => removeExtraScanDir(dir)}>Remove</button>
                    </div>
                  {/each}

                  <div class="step-row">
                    <div class="toggle-info">
                      <span class="toggle-label">Additional scan path</span>
                      <span class="toggle-desc">Include an extra directory in disk usage readings</span>
                    </div>
                    <div style="display:flex;gap:var(--sp-2);align-items:center;flex-shrink:0">
                      <input class="text-input" style="width:200px;font-family:monospace;font-size:var(--text-xs)"
                        bind:value={newScanDir} placeholder="/path/to/dir" spellcheck="false"
                        onkeydown={(e) => e.key === "Enter" && addExtraScanDir()} />
                      <button class="sec-action-btn" onclick={addExtraScanDir} disabled={!newScanDir.trim() || extraScanDirs.includes(newScanDir.trim())}>Add</button>
                    </div>
                  </div>

                  <div class="step-row" style="padding-top:0">
                    <div class="toggle-info"></div>
                    <button class="sec-action-btn sec-action-primary" onclick={savePaths} disabled={pathsSaving}>
                      {pathsSaved ? "Saved ✓" : pathsSaving ? "Saving…" : "Save"}
                    </button>
                  </div>
                </div>
              {/if}
            </div>


            <div class="section adv-section">
              <button class="adv-toggle" onclick={() => backupSectionOpen = !backupSectionOpen}>
                <span class="section-title" style="padding:0">Backup</span>
                <svg class="adv-caret" class:open={backupSectionOpen} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
              </button>
              {#if backupSectionOpen}
                <div class="adv-body">


                  <div class="step-row">
                    <div class="toggle-info">
                      <span class="toggle-label">Create backup</span>
                      <span class="toggle-desc">Snapshot your library, categories, and tracker links</span>
                    </div>
                    <button class="sec-action-btn sec-action-primary" onclick={createBackup} disabled={backupLoading}>
                      {backupLoading ? "Creating…" : "Create backup"}
                    </button>
                  </div>

                  {#if backupError}
                    <p class="storage-loading" style="color:var(--color-error)">{backupError}</p>
                  {/if}


                  {#if backupList.length === 0}
                    <p class="storage-loading">No backups yet — create one above.</p>
                  {:else}
                    <div class="folder-list" style="margin:0 0 var(--sp-2)">
                      {#each backupList as backup}
                        <div class="folder-row">
                          <ClockCounterClockwise size={14} weight="light" style="color:var(--text-faint);flex-shrink:0" />
                          <span class="folder-row-name" style="font-family:monospace;font-size:var(--text-xs)">{backup.name}</span>
                          <button
                            class="kb-reset"
                            onclick={() => downloadBackup(backup)}
                            title="Download"
                            style="display:flex;align-items:center"
                          >↓</button>
                          <button
                            class="kb-reset folder-delete"
                            onclick={() => deleteBackup(backup.url)}
                            disabled={backup.deleting}
                            title="Delete"
                          ><Trash size={12} weight="light" /></button>
                        </div>
                      {/each}
                    </div>
                  {/if}


                  <div class="step-row">
                    <div class="toggle-info">
                      <span class="toggle-label">Restore from file</span>
                      <span class="toggle-desc">{restoreFile ? restoreFile.name : "Select a .tachibk file"}</span>
                    </div>
                    <label class="sec-action-btn" style="cursor:pointer">
                      Browse
                      <input type="file" accept=".tachibk,.proto.gz" style="display:none"
                        onchange={(e) => {
                          const f = (e.currentTarget as HTMLInputElement).files?.[0] ?? null;
                          restoreFile = f; restoreStatus = null; restoreError = null; validateResult = null; validateError = null;
                        }} />
                    </label>
                  </div>
                  {#if restoreFile}
                    <div class="step-row">
                      <div class="toggle-info"></div>
                      <div class="sec-btn-row">
                        <button class="sec-action-btn" onclick={submitValidate} disabled={validateLoading || restoreLoading}>
                          {validateLoading ? "Checking…" : "Validate"}
                        </button>
                        <button class="sec-action-btn sec-action-primary" onclick={submitRestore} disabled={restoreLoading || validateLoading}>
                          {restoreLoading ? "Restoring…" : "Restore"}
                        </button>
                      </div>
                    </div>
                  {/if}
                  {#if validateError}
                    <p class="storage-loading" style="color:var(--color-error)">{validateError}</p>
                  {/if}
                  {#if validateResult}
                    {#if validateResult.missingSources.length === 0 && validateResult.missingTrackers.length === 0}
                      <p class="storage-loading" style="color:var(--color-success, #4caf50)">✓ All sources and trackers present</p>
                    {:else}
                      {#if validateResult.missingSources.length > 0}
                        <div class="step-row">
                          <div class="toggle-info">
                            <span class="toggle-label" style="color:var(--color-error)">Missing sources</span>
                            <span class="toggle-desc">{validateResult.missingSources.map(s => s.name).join(", ")}</span>
                          </div>
                        </div>
                      {/if}
                      {#if validateResult.missingTrackers.length > 0}
                        <div class="step-row">
                          <div class="toggle-info">
                            <span class="toggle-label" style="color:var(--color-error)">Missing trackers</span>
                            <span class="toggle-desc">{validateResult.missingTrackers.map(t => t.name).join(", ")}</span>
                          </div>
                        </div>
                      {/if}
                    {/if}
                  {/if}
                  {#if restoreError}
                    <p class="storage-loading" style="color:var(--color-error)">{restoreError}</p>
                  {/if}
                  {#if restoreStatus}
                    <div class="step-row">
                      <div class="toggle-info">
                        <span class="toggle-label">
                          {restoreStatus.state === "SUCCESS" ? "✓ Restore complete" :
                           restoreStatus.state === "FAILURE" ? "✗ Restore failed"  : "Restoring…"}
                        </span>
                        {#if restoreStatus.totalManga > 0}
                          <span class="toggle-desc">{restoreStatus.mangaProgress} / {restoreStatus.totalManga} manga</span>
                        {/if}
                      </div>
                      {#if restoreStatus.state !== "SUCCESS" && restoreStatus.state !== "FAILURE" && restoreStatus.totalManga > 0}
                        <div class="storage-bar" style="width:160px;flex-shrink:0">
                          <div class="storage-bar-fill" style="width:{Math.round((restoreStatus.mangaProgress / restoreStatus.totalManga) * 100)}%"></div>
                        </div>
                      {/if}
                    </div>
                  {/if}

                </div>
              {/if}
            </div>
          </div>
        {:else if tab === "folders"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Manage Folders</p>
              <p class="toggle-desc" style="padding:0 var(--sp-3) var(--sp-3);display:block">Folders are stored as Suwayomi categories. Changes sync across all clients.</p>
              {#if catsError}
                <p class="toggle-desc" style="padding:0 var(--sp-3) var(--sp-2);color:var(--color-error);display:block">{catsError}</p>
              {/if}
              <div class="folder-create-row">
                <input class="text-input" placeholder="New folder name…" bind:value={newFolderName}
                  onkeydown={(e) => e.key === "Enter" && createFolder()} style="flex:1;width:auto" />
                <button class="folder-create-btn" onclick={createFolder} disabled={!newFolderName.trim()}>
                  <Plus size={13} weight="bold" /> Create
                </button>
              </div>
              {#if catsLoading}
                <p class="storage-loading">Loading folders…</p>
              {:else if store.categories.filter(c => c.id !== 0).length === 0}
                <p class="storage-loading">No folders yet. Create one above.</p>
              {:else}
                {@const displayCats = store.categories
                    .filter(c => c.id !== 0)
                    .sort((a, b) => {
                      const defaultId = store.settings.defaultLibraryCategoryId ?? null;
                      if (a.id === defaultId) return -1;
                      if (b.id === defaultId) return  1;
                      return a.order - b.order;
                    })}
                <div class="folder-list">
                  {#each displayCats as cat, i}
                    <div class="folder-row">
                      {#if editingId === cat.id}
                        <input class="text-input" bind:value={editingName}
                          onkeydown={(e) => { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") editingId = null; }}
                          onblur={commitEdit} style="flex:1;width:auto" use:focusInput />
                        <button class="kb-reset" onclick={commitEdit} title="Save">✓</button>
                      {:else}
                        <FolderSimple size={14} weight="light" style="color:var(--text-faint);flex-shrink:0" />
                        <span class="folder-row-name">{cat.name}</span>
                        <span class="folder-row-count">{cat.mangas?.nodes.length ?? 0} manga</span>
                        <button
                          class="kb-reset"
                          class:folder-default-active={(store.settings.defaultLibraryCategoryId ?? null) === cat.id}
                          onclick={() => updateSettings({ defaultLibraryCategoryId: (store.settings.defaultLibraryCategoryId ?? null) === cat.id ? null : cat.id })}
                          title={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "Remove as default folder" : "Set as default folder — opens first when you visit Library"}
                        ><Star size={13} weight={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "fill" : "light"} /></button>
                        <button
                          class="kb-reset"
                          class:folder-hidden={(store.settings.hiddenCategoryIds ?? []).includes(cat.id)}
                          onclick={() => toggleHiddenCategory(cat.id)}
                          title={(store.settings.hiddenCategoryIds ?? []).includes(cat.id) ? "Show in Saved tab" : "Hide from Saved tab"}
                        >{#if (store.settings.hiddenCategoryIds ?? []).includes(cat.id)}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}</button>
                        <button class="kb-reset" onclick={() => moveCategory(cat.id, -1)} disabled={i === 0} title="Move up">↑</button>
                        <button class="kb-reset" onclick={() => moveCategory(cat.id, 1)} disabled={i === displayCats.length - 1} title="Move down">↓</button>
                        <button class="kb-reset" onclick={() => startEdit(cat.id, cat.name)} title="Rename"><Pencil size={12} weight="light" /></button>
                        <button class="kb-reset folder-delete" onclick={() => deleteFolder(cat.id)} title="Delete"><Trash size={12} weight="light" /></button>
                      {/if}
                    </div>
                  {/each}
                </div>
              {/if}
            </div>
          </div>
        {:else if tab === "tracking"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Connected Trackers</p>
              {#if trackersError}
                <div class="tracker-error">{trackersError}</div>
              {/if}
              {#if trackersLoading}
                <p class="storage-loading">Loading trackers…</p>
              {:else}
                <div class="tracker-list">
                  {#each trackers as tracker}
                    <div class="tracker-row" class:tracker-row-active={tracker.isLoggedIn}>
                      <div class="tracker-identity">
                        <img src={thumbUrl(tracker.icon)} alt={tracker.name} class="tracker-logo" />
                        <div class="tracker-name-block">
                          <span class="tracker-label">{tracker.name}</span>
                          <span class="tracker-status-pill" class:pill-on={tracker.isLoggedIn}>
                            {tracker.isLoggedIn ? "Connected" : "Not connected"}
                          </span>
                        </div>
                      </div>
                      <div class="tracker-action">
                        {#if tracker.isLoggedIn}
                          <div class="tracker-connected-btns">
                            <button
                              class="danger-btn"
                              onclick={() => logoutTracker(tracker.id)}
                              disabled={loggingOut === tracker.id}
                            >
                              {loggingOut === tracker.id ? "Disconnecting…" : "Disconnect"}
                            </button>
                          </div>
                        {:else if oauthTrackerId === tracker.id}
                          <div class="oauth-flow">
                            <p class="oauth-hint">
                              Browser opened {tracker.name} login — after authorising, copy the full callback URL and paste it below.
                            </p>
                            <input
                              class="oauth-input"
                              placeholder="https://suwayomi.org/tracker-oauth#access_token=…"
                              bind:value={oauthCallbackInput}
                              onkeydown={(e) => { if (e.key === "Enter") submitOAuth(); if (e.key === "Escape") cancelOAuth(); }}
                              use:focusEl
                            />
                            <div class="oauth-btns">
                              <button class="step-btn" onclick={submitOAuth} disabled={oauthSubmitting || !oauthCallbackInput.trim()}>
                                {oauthSubmitting ? "Connecting…" : "Connect"}
                              </button>
                              <button class="kb-reset" onclick={cancelOAuth}>Cancel</button>
                            </div>
                          </div>
                        {:else if credsTrackerId === tracker.id}
                          <div class="oauth-flow">
                            <input
                              class="oauth-input"
                              placeholder="Username / Email"
                              bind:value={credsUsername}
                              onkeydown={(e) => e.key === "Escape" && cancelCredentials()}
                              use:focusEl
                            />
                            <input
                              class="oauth-input"
                              type="password"
                              placeholder="Password"
                              bind:value={credsPassword}
                              onkeydown={(e) => { if (e.key === "Enter") submitCredentials(); if (e.key === "Escape") cancelCredentials(); }}
                            />
                            <div class="oauth-btns">
                              <button class="step-btn" onclick={submitCredentials} disabled={credsSubmitting || !credsUsername.trim() || !credsPassword.trim()}>
                                {credsSubmitting ? "Connecting…" : "Connect"}
                              </button>
                              <button class="kb-reset" onclick={cancelCredentials}>Cancel</button>
                            </div>
                          </div>
                        {:else}
                          <button
                            class="step-btn"
                            style="width:auto;padding:0 var(--sp-4)"
                            onclick={() => usesOAuth(tracker) ? startOAuth(tracker) : startCredentials(tracker)}
                          >
                            {usesOAuth(tracker) ? "Connect via browser →" : "Connect"}
                          </button>
                        {/if}
                      </div>
                    </div>
                  {/each}
                </div>
              {/if}
            </div>
          </div>
        {:else if tab === "security"}
          <div class="panel">
            {#if secError}
              <div class="sec-banner sec-banner-error">{secError}</div>
            {/if}
            <div class="section">
              <div class="section-title-row">
                <p class="section-title">Server Authentication</p>
                <span class="sec-status-pill" class:sec-pill-on={store.settings.serverAuthMode === "BASIC_AUTH"} class:sec-pill-warn={authModeUnsupported}>
                  {store.settings.serverAuthMode === "BASIC_AUTH"   ? "Basic Auth"   :
                   store.settings.serverAuthMode === "SIMPLE_LOGIN" ? "Simple Login — unsupported" :
                   store.settings.serverAuthMode === "UI_LOGIN"     ? "UI Login — unsupported"     : "Disabled"}
                </span>
              </div>

              {#if authModeUnsupported}
                <div class="sec-banner sec-banner-warn" style="margin: var(--sp-2) var(--sp-3) 0;">
                  <strong>{store.settings.serverAuthMode === "SIMPLE_LOGIN" ? "Simple Login" : "UI Login"}</strong> is not supported by this client — only <strong>Basic Auth</strong> works here.
                  Switch your Suwayomi server to <code>basic_auth</code> and set the mode below to <strong>Basic</strong>, then save.
                </div>
              {/if}

              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Mode</span>
                  <span class="toggle-desc">How Suwayomi verifies requests</span>
                </div>
                <div class="auth-mode-group">
                  {#each [
                    { value: "NONE",       label: "None"  },
                    { value: "BASIC_AUTH", label: "Basic" },
                  ] as opt}
                    <button
                      class="auth-mode-btn"
                      class:auth-mode-active={authMode === opt.value}
                      onclick={() => authMode = opt.value as any}
                      disabled={secLoading}
                    >{opt.label}</button>
                  {/each}
                </div>
              </div>

              {#if authMode !== "NONE"}
                <div class="step-row">
                  <div class="toggle-info"><span class="toggle-label">Username</span></div>
                  <input class="text-input" bind:value={authUsername} placeholder="Username" autocomplete="off" spellcheck="false" disabled={secLoading} />
                </div>
                <div class="step-row">
                  <div class="toggle-info"><span class="toggle-label">Password</span></div>
                  <div class="sec-field-wrap">
                    <input class="text-input" type={showAuthPass ? "text" : "password"} bind:value={authPassword} placeholder="Password" autocomplete="off" spellcheck="false" disabled={secLoading} />
                    <button class="sec-eye-btn" onclick={() => showAuthPass = !showAuthPass} title={showAuthPass ? "Hide" : "Show"} tabindex="-1">
                      {#if showAuthPass}
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
                      {:else}
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
                      {/if}
                    </button>
                  </div>
                </div>


              {/if}

              {#if store.settings.serverAuthMode === "BASIC_AUTH"}
                <div class="step-row">
                  <div class="toggle-info"></div>
                  <p class="auth-perf-note">Images are proxied through Tauri when Basic Auth is active, which reduces loading speed.</p>
                </div>
              {/if}

              <div class="step-row">
                <div class="toggle-info"></div>
                <div class="sec-btn-row">
                  {#if store.settings.serverAuthMode !== "NONE"}
                    <button class="sec-action-btn sec-action-danger" onclick={clearAuth} disabled={secLoading}>
                      {secLoading ? "Saving…" : "Disable"}
                    </button>
                  {/if}
                  <button
                    class="sec-action-btn sec-action-primary"
                    onclick={saveAuth}
                    disabled={secLoading || (authMode === "BASIC_AUTH" && (!authUsername.trim() || !authPassword.trim()))}
                  >
                    {secLoading ? "Saving…" : secSaved === "auth" ? "Saved ✓" : store.settings.serverAuthMode === "BASIC_AUTH" ? "Update" : authMode === "NONE" ? "Save" : "Enable"}
                  </button>
                </div>
              </div>
            </div>
            <div class="section">
              <div class="section-title-row">
                <p class="section-title">App Lock</p>
              </div>
              <label class="toggle-row">
                <div class="toggle-info">
                  <span class="toggle-label">PIN lock</span>
                  <span class="toggle-desc">Require a PIN on launch and after idle timeout</span>
                </div>
                <button role="switch" aria-checked={store.settings.appLockEnabled ?? false} aria-label="Enable PIN lock" class="toggle" class:on={store.settings.appLockEnabled} onclick={() => updateSettings({ appLockEnabled: !store.settings.appLockEnabled })}><span class="toggle-thumb"></span></button>
              </label>
              {#if store.settings.appLockEnabled}
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">PIN</span>
                    <span class="toggle-desc">4–8 digits, saved on Enter or Save button</span>
                  </div>
                  <div class="sec-btn-row">
                    <input
                      class="text-input"
                      type="password"
                      inputmode="numeric"
                      maxlength={8}
                      placeholder="4–8 digits"
                      value={pinInput}
                      oninput={(e) => { pinInput = e.currentTarget.value.replace(/\D/g, "").slice(0, 8); pinError = ""; }}
                      onkeydown={(e) => e.key === "Enter" && commitPin()}
                      autocomplete="off"
                      aria-label="Enter PIN"
                      style="width:120px;letter-spacing:0.2em"
                    />
                    <button class="sec-action-btn sec-action-primary"
                      onclick={commitPin}
                      disabled={pinInput.length > 0 && pinInput.length < 4}>
                      {store.settings.appLockPin && pinInput === store.settings.appLockPin ? "Saved ✓" : "Save"}
                    </button>
                  </div>
                  {#if pinError}<span class="sec-pin-error">{pinError}</span>{/if}
                </div>
              {/if}
            </div>
            <div class="section">
              <div class="section-title-row">
                <p class="section-title">SOCKS Proxy</p>
              </div>
              <label class="toggle-row">
                <div class="toggle-info">
                  <span class="toggle-label">Enable SOCKS proxy</span>
                  <span class="toggle-desc">Route Suwayomi traffic through a SOCKS4/5 proxy</span>
                </div>
                <button role="switch" aria-checked={socksEnabled} aria-label="Enable SOCKS proxy" class="toggle" class:on={socksEnabled} onclick={() => { socksEnabled = !socksEnabled; saveSocksProxy(); }}><span class="toggle-thumb"></span></button>
              </label>
              {#if socksEnabled}
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Version</span>
                  </div>
                  <div class="select-wrap" id="socks-ver">
                    <button class="select-btn" onclick={() => toggleSelect("socks-ver")}>
                      <span>SOCKS{socksVersion}</span>
                      <svg class="select-caret" class:open={selectOpen === "socks-ver"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
                    </button>
                    {#if selectOpen === "socks-ver"}
                      <div class="select-menu">
                        {#each [[4,"SOCKS4"],[5,"SOCKS5"]] as [v, l]}
                          <button class="select-option" class:active={socksVersion === v} onclick={() => { socksVersion = v as number; selectOpen = null; }}>{l}</button>
                        {/each}
                      </div>
                    {/if}
                  </div>
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Host</span>
                  </div>
                  <input class="text-input" bind:value={socksHost} placeholder="127.0.0.1" autocomplete="off" spellcheck="false" />
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Port</span>
                  </div>
                  <input class="text-input sec-port-input" bind:value={socksPort} placeholder="1080" autocomplete="off" spellcheck="false" />
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Username</span>
                    <span class="toggle-desc">Optional</span>
                  </div>
                  <input class="text-input" bind:value={socksUsername} placeholder="Username" autocomplete="off" spellcheck="false" />
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Password</span>
                    <span class="toggle-desc">Optional</span>
                  </div>
                  <div class="sec-field-wrap">
                    <input class="text-input" type={showSocksPass ? "text" : "password"} bind:value={socksPassword} placeholder="Password" autocomplete="off" spellcheck="false" />
                    <button class="sec-eye-btn" onclick={() => showSocksPass = !showSocksPass} title={showSocksPass ? "Hide password" : "Show password"} tabindex="-1">
                      {#if showSocksPass}
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
                      {:else}
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
                      {/if}
                    </button>
                  </div>
                </div>
                <div class="step-row">
                  <div class="toggle-info"></div>
                  <button class="sec-action-btn sec-action-primary" onclick={saveSocksProxy} disabled={secLoading}>
                    {secLoading ? "Saving…" : secSaved === "socks" ? "Saved ✓" : "Save"}
                  </button>
                </div>
              {/if}
            </div>
            <div class="section">
              <div class="section-title-row">
                <p class="section-title">FlareSolverr</p>
              </div>
              <label class="toggle-row">
                <div class="toggle-info">
                  <span class="toggle-label">Enable FlareSolverr</span>
                  <span class="toggle-desc">Bypass Cloudflare challenges for sources that require it</span>
                </div>
                <button role="switch" aria-checked={flareEnabled} aria-label="Enable FlareSolverr" class="toggle" class:on={flareEnabled} onclick={() => { flareEnabled = !flareEnabled; saveFlareSolverr(); }}><span class="toggle-thumb"></span></button>
              </label>
              {#if flareEnabled}
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">URL</span>
                    <span class="toggle-desc">FlareSolverr instance address</span>
                  </div>
                  <input class="text-input" bind:value={flareUrl} placeholder="http://localhost:8191" autocomplete="off" spellcheck="false" />
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Timeout</span>
                    <span class="toggle-desc">Max wait per request, in seconds</span>
                  </div>
                  <div class="step-controls">
                    <button class="step-btn" onclick={() => flareTimeout = Math.max(10, flareTimeout - 10)}>−</button>
                    <span class="step-val">{flareTimeout}s</span>
                    <button class="step-btn" onclick={() => flareTimeout = Math.min(300, flareTimeout + 10)}>+</button>
                  </div>
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Session name</span>
                    <span class="toggle-desc">Reuse browser session across requests</span>
                  </div>
                  <input class="text-input" bind:value={flareSession} placeholder="moku" autocomplete="off" spellcheck="false" />
                </div>
                <div class="step-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Session TTL</span>
                    <span class="toggle-desc">Minutes before session is refreshed</span>
                  </div>
                  <div class="step-controls">
                    <button class="step-btn" onclick={() => flareTtl = Math.max(1, flareTtl - 1)}>−</button>
                    <span class="step-val">{flareTtl}m</span>
                    <button class="step-btn" onclick={() => flareTtl = Math.min(60, flareTtl + 1)}>+</button>
                  </div>
                </div>
                <label class="toggle-row">
                  <div class="toggle-info">
                    <span class="toggle-label">Response fallback</span>
                    <span class="toggle-desc">Use FlareSolverr's response when the direct request fails</span>
                  </div>
                  <button role="switch" aria-checked={flareFallback} aria-label="Use as response fallback" class="toggle" class:on={flareFallback} onclick={() => flareFallback = !flareFallback}><span class="toggle-thumb"></span></button>
                </label>
                <div class="step-row">
                  <div class="toggle-info"></div>
                  <button class="sec-action-btn sec-action-primary" onclick={saveFlareSolverr} disabled={secLoading}>
                    {secLoading ? "Saving…" : secSaved === "flare" ? "Saved ✓" : "Save"}
                  </button>
                </div>
              {/if}
            </div>
          </div>
        {:else if tab === "content"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Content Filter</p>
              <label class="toggle-row">
                <div class="toggle-info"><span class="toggle-label">Show adult content</span><span class="toggle-desc">Sources and manga matching blocked tags are hidden when off</span></div>
                <button role="switch" aria-checked={store.settings.showNsfw} aria-label="Show adult content" class="toggle" class:on={store.settings.showNsfw} onclick={() => updateSettings({ showNsfw: !store.settings.showNsfw })}><span class="toggle-thumb"></span></button>
              </label>
            </div>
            <div class="section">
              <p class="section-title">Blocked Genre Tags</p>
              <div class="section-title-row" style="padding-top:0;padding-bottom:var(--sp-2)">
                <span class="toggle-desc" style="flex:1">Manga matching any of these substrings are filtered. Case-insensitive, partial match.</span>
                <button class="kb-reset" style="font-size:var(--text-xs);padding:2px 10px;flex-shrink:0" onclick={() => tagsRevealed = !tagsRevealed}>
                  {tagsRevealed ? "Hide" : `Show (${(store.settings.nsfwFilteredTags ?? []).length})`}
                </button>
              </div>
              {#if tagsRevealed}
              <div class="content-tag-grid">
                {#each (store.settings.nsfwFilteredTags ?? []) as tag}
                  <span class="content-tag">
                    <Tag size={10} weight="light" />
                    {tag}
                    <button class="content-tag-remove" onclick={() => removeTag(tag)} title="Remove tag">×</button>
                  </span>
                {/each}
              </div>
              {/if}
              <div class="content-tag-add">
                <input
                  class="text-input"
                  placeholder="Add tag substring…"
                  bind:value={newTagInput}
                  onkeydown={(e) => { if (e.key === "Enter") addTag(); }}
                  style="flex:1;width:auto"
                />
                <button class="folder-create-btn" onclick={addTag} disabled={!newTagInput.trim()}>
                  <Plus size={13} weight="bold" /> Add
                </button>
                <button class="kb-reset" onclick={resetTags} title="Reset to defaults" style="padding:0 var(--sp-3);font-size:var(--text-xs);color:var(--text-faint)">Reset</button>
              </div>
            </div>
            <div class="section">
              <p class="section-title">Source Overrides</p>
              <p class="toggle-desc" style="padding:0 var(--sp-3) var(--sp-2);display:block">
                Allow lets a source through even if flagged NSFW. Block always hides it.
              </p>
              <div class="content-source-search-wrap">
                <input class="text-input" placeholder="Filter sources…" bind:value={sourceSearch} style="width:100%" />
              </div>
              {#if contentSourcesLoading}
                <p class="storage-loading">Loading sources…</p>
              {:else if contentSources.length === 0}
                <p class="storage-loading">No sources found — check your server connection.</p>
              {:else}
                <div class="content-source-list">
                  {#each contentSourcesFiltered as group (group.name)}
                    {@const ids = group.sources.map(s => s.id)}
                    {@const allowed = store.settings.nsfwAllowedSourceIds ?? []}
                    {@const blocked = store.settings.nsfwBlockedSourceIds ?? []}
                    {@const isAllowed = ids.every(id => allowed.includes(id))}
                    {@const isBlocked = ids.every(id => blocked.includes(id))}
                    <div class="content-source-row" class:content-source-allowed={isAllowed} class:content-source-blocked={isBlocked}>
                      <img src={thumbUrl(group.iconUrl)} alt="" class="content-source-icon" loading="lazy" decoding="async" />
                      <div class="content-source-info">
                        <span class="content-source-name">{group.name}</span>
                        <span class="content-source-lang">{group.sources[0].isNsfw ? "NSFW · " : ""}{group.sources.length > 1 ? `${group.sources.length} languages` : group.sources[0].lang.toUpperCase()}</span>
                      </div>
                      <div class="content-source-actions">
                        <button
                          class="content-action-btn"
                          class:content-action-active-allow={isAllowed}
                          onclick={() => toggleSourceAllowed(ids)}
                          title={isAllowed ? "Remove allow override" : "Allow this source through regardless of NSFW flag"}
                        >Allow</button>
                        <button
                          class="content-action-btn"
                          class:content-action-active-block={isBlocked}
                          onclick={() => toggleSourceBlocked(ids)}
                          title={isBlocked ? "Remove block override" : "Always block this source"}
                        >Block</button>
                      </div>
                    </div>
                  {/each}
                </div>
              {/if}
            </div>
          </div>
        {:else if tab === "about"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Moku</p>
              <div class="about-block">
                <p class="about-line">A manga reader frontend for Suwayomi / Tachidesk.</p>
                <p class="about-line" style="color:var(--text-faint);margin-top:var(--sp-2)">Built with Tauri + Svelte.</p>
              </div>
            </div>
            <div class="section">
              <p class="section-title">Version</p>
              <div class="step-row">
                <div class="toggle-info">
                  <span class="toggle-label">Installed</span>
                  <span class="toggle-desc">v{appVersion}</span>
                </div>
                <button class="step-btn" style="width:auto;padding:0 var(--sp-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide)"
                  onclick={() => { releasesError = null; loadReleases(); }} disabled={releasesLoading}>
                  {releasesLoading ? "Loading…" : "Refresh"}
                </button>
              </div>
              {#if onLatestVersion}
                <div class="step-row">
                  <span class="toggle-desc" style="padding:0 var(--sp-3);color:var(--accent-fg)">✓ You're on the latest version.</span>
                </div>
              {/if}
              {#if updatePhase === "downloading" && IS_WINDOWS}
                <div class="update-progress-wrap">
                  <div class="update-progress-bar">
                    <div class="update-progress-fill"
                      style="width:{dlTotal ? Math.round((dlBytes / dlTotal) * 100) : 0}%"></div>
                  </div>
                  <div class="update-progress-row">
                    <span class="update-progress-label">Downloading {targetTag ?? "update"}…</span>
                    <span class="update-progress-val">{fmtProgress()}</span>
                  </div>
                </div>
              {/if}
              {#if updatePhase === "ready"}
                <div class="update-ready-row">
                  <span class="update-ready-label">
                    {targetTag} downloaded — restart to finish installing.
                  </span>
                  <button class="update-action-btn primary" onclick={restartNow}>Restart now</button>
                  <button class="kb-reset" onclick={cancelUpdate} title="Dismiss">✕</button>
                </div>
              {/if}
              {#if updatePhase === "error"}
                <div class="update-error-row">
                  <span style="color:var(--color-error);font-family:var(--font-ui);font-size:var(--text-xs)">{updateError}</span>
                  <button class="kb-reset" onclick={cancelUpdate}>Dismiss</button>
                </div>
              {/if}
            </div>
            <div class="section">
              <p class="section-title">Releases</p>
              {#if releasesError}
                <p class="storage-loading" style="color:var(--color-error)">{releasesError}</p>
              {:else if releasesLoading}
                <p class="storage-loading">Fetching releases…</p>
              {:else if releases.length === 0}
                <p class="storage-loading">No releases found.</p>
              {:else}
                <div class="release-list-scroll">
                  <div class="release-list">
                  {#each releases as release}
                    {@const isCurrent   = isCurrentVersion(release.tag_name)}
                    {@const isExpanded  = expandedTag === release.tag_name}
                    {@const isTarget    = targetTag === release.tag_name}
                    {@const isInstalling = isTarget && updatePhase === "downloading"}
                    <div class="release-row" class:current={isCurrent}>
                      <div class="release-header">
                        <div class="release-meta">
                          <span class="release-tag">{release.tag_name}</span>
                          {#if isCurrent}
                            <span class="release-badge current-badge">installed</span>
                          {/if}
                          {#if release.published_at}
                            <span class="release-date">{fmtDate(release.published_at)}</span>
                          {/if}
                        </div>
                        <div class="release-actions">
                          {#if release.body.trim()}
                            <button class="release-changelog-btn"
                              onclick={() => expandedTag = isExpanded ? null : release.tag_name}>
                              {isExpanded ? "Hide" : "Changelog"}
                            </button>
                          {/if}
                          {#if !isCurrent}
                            {#if IS_WINDOWS}
                              <button class="update-action-btn"
                                class:primary={!isInstalling}
                                disabled={updatePhase === "downloading"}
                                onclick={() => installUpdate(release)}>
                                {isInstalling ? "Downloading…" : "Install"}
                              </button>
                            {:else}
                              <button class="update-action-btn"
                                onclick={() => installUpdate(release)}>
                                Open on GitHub
                              </button>
                            {/if}
                          {/if}
                        </div>
                      </div>
                      {#if isExpanded && release.body.trim()}
                        <div class="release-body">
                          <pre class="release-body-pre">{release.body.trim()}</pre>
                        </div>
                      {/if}
                    </div>
                  {/each}
                  </div>
                </div>
              {/if}
            </div>
            <div class="section">
              <p class="section-title">Links</p>
              <div class="about-block">
                <a href="https://github.com/Youwes09/Moku" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none">GitHub →</a>
                <a href="https://discord.gg/Jq3pwuNqPp" target="_blank" class="about-line" style="color:var(--accent-fg);text-decoration:none;margin-top:var(--sp-1)">Discord →</a>
              </div>
            </div>
          </div>
        {:else if tab === "devtools"}
          <div class="panel">
            <div class="section">
              <p class="section-title">Toasts</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Fire test toast</span><span class="toggle-desc">Triggers each kind with realistic content</span></div>
                <div class="dev-pill-group">
                  {#each ([["success","S"],["error","E"],["info","I"],["download","D"]] as const) as [kind, label]}
                    <button class="dev-pill dev-pill-{kind}" onclick={() => addToast({
                      kind,
                      title: kind === "success" ? "Library updated" : kind === "error" ? "Could not reach server" : kind === "info" ? "Already up to date" : "Download complete",
                      body:  kind === "success" ? "3 new chapters across 2 series" : kind === "error" ? "Connection refused on port 4567" : kind === "info" ? "No new chapters found" : "Berserk · Ch. 372 ready to read",
                    })}>{label}</button>
                  {/each}
                </div>
              </div>
            </div>
            <div class="section">
              <p class="section-title">Previews</p>
              <div class="step-row">
                <div class="toggle-info"><span class="toggle-label">Idle splash</span><span class="toggle-desc">Dismiss with any click or key</span></div>
                <button class="dev-btn" onclick={triggerSplash} style={splashTriggered ? "background:var(--accent-fg);color:var(--bg-base);border-color:var(--accent-fg)" : ""}>Show</button>
              </div>
            </div>
            <div class="section">
              <button class="exp-disclosure" onclick={() => expOpen = !expOpen} aria-expanded={expOpen}>
                <span class="exp-disclosure-label">Experimental</span>
                <svg class="exp-caret" class:open={expOpen} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
              </button>
              {#if expOpen}
                <div class="exp-body">
                  <p class="exp-hint">3D tilt cards — hover to preview</p>
                  <div class="exp-demo-wrap">
                    {#each [
                      { title: "Berserk",         sub: "Ch. 372", hue: "265" },
                      { title: "Vinland Saga",    sub: "Ch. 208", hue: "200" },
                      { title: "Dungeon Meshi",   sub: "Ch. 97",  hue: "140" },
                    ] as card}
                      <ThreeDCard class="exp-demo-card">
                        <div class="exp-demo-cover" style="--hue:{card.hue}"></div>
                        <div class="exp-demo-info">
                          <span class="exp-demo-title">{card.title}</span>
                          <span class="exp-demo-sub">{card.sub}</span>
                        </div>
                      </ThreeDCard>
                    {/each}
                  </div>
                </div>
              {/if}
            </div>
            <div class="section">
              <p class="section-title">Runtime</p>
              <div class="dev-grid">
                <span class="dev-key">Filter</span>
                <span class="dev-val">{store.libraryFilter}</span>
                <span class="dev-key">Folders</span>
                <span class="dev-val">{store.categories.filter(c => c.id !== 0).map(c => c.name).join(", ") || "none"}</span>
                <span class="dev-key">History</span>
                <span class="dev-val">{store.history.length} entries</span>
                <span class="dev-key">Cache</span>
                <span class="dev-val">{perfSnapshot?.cacheEntries ?? "—"} entries</span>
                <span class="dev-key">Toasts</span>
                <span class="dev-val">{store.toasts.length} queued</span>
                <span class="dev-key">Version</span>
                <span class="dev-val">{appVersion} · {import.meta.env.MODE}</span>
              </div>
            </div>
          </div>
        {/if}
      </div>
    </div>
  </div>
</div>
<script module>
  function focusInput(node: HTMLElement) { node.focus(); }
  function focusEl(node: HTMLElement) { setTimeout(() => node.focus(), 0); }
</script>
<style>
  /* ── Animations ──────────────────────────────────────────────────────────── */
  @keyframes fadeIn  { from { opacity: 0 }                              to { opacity: 1 } }
  @keyframes scaleIn { from { transform: scale(0.97); opacity: 0 }      to { transform: scale(1); opacity: 1 } }
  @keyframes pulse   { 0%,100% { opacity: 1 }                           50% { opacity: 0.6 } }
  @keyframes ripple  { 0%,100% { transform: scale(1); opacity: 0.18 }   50% { transform: scale(1.7); opacity: 0.5 } }
  @keyframes divider-travel { 0% { background-position: -200% 0 } 100% { background-position: 200% 0 } }

  /* ── Modal shell ─────────────────────────────────────────────────────────── */
  .backdrop {
    position: fixed; inset: 0;
    background: rgba(0,0,0,0.55);
    z-index: var(--z-settings);
    display: flex; align-items: center; justify-content: center;
    animation: fadeIn 0.12s ease both;
    backdrop-filter: blur(6px);
  }
  .modal {
    width: min(740px, calc(100vw - 40px));
    height: min(620px, calc(100vh - 72px));
    display: flex;
    background: var(--bg-surface);
    border: 1px solid var(--border-base);
    border-radius: var(--radius-xl);
    overflow: hidden;
    animation: scaleIn 0.18s cubic-bezier(0.16,1,0.3,1) both;
    box-shadow: 0 32px 80px rgba(0,0,0,0.65), 0 0 0 1px rgba(255,255,255,0.03) inset;
  }

  /* ── Sidebar ─────────────────────────────────────────────────────────────── */
  .sidebar {
    width: 160px; flex-shrink: 0;
    background: var(--bg-base);
    border-right: 1px solid var(--border-dim);
    padding: var(--sp-4) var(--sp-2);
    display: flex; flex-direction: column; gap: 2px;
    overflow-y: auto;
  }
  .modal-title {
    font-family: var(--font-ui); font-size: var(--text-2xs);
    color: var(--text-faint); letter-spacing: var(--tracking-wider);
    text-transform: uppercase; padding: 0 var(--sp-2) var(--sp-3);
  }
  .nav { display: flex; flex-direction: column; gap: 1px; }
  .nav-item {
    display: flex; align-items: center; gap: 7px;
    padding: 6px var(--sp-2); border-radius: var(--radius-md);
    font-size: var(--text-sm); color: var(--text-faint);
    background: none; border: none; cursor: pointer; text-align: left;
    transition: background var(--t-fast), color var(--t-fast);
    width: 100%;
  }
  .nav-item:hover  { background: var(--bg-raised); color: var(--text-secondary); }
  .nav-item.active { background: var(--accent-muted); color: var(--accent-fg); }
  .nav-item.anims  { transition: background var(--t-base), color var(--t-base), transform 80ms ease; }
  .nav-item.anims:hover { transform: translateX(1px); }
  .nav-item.anims:active { transform: scale(0.97); }

  .nav-item-icon { display: flex; align-items: center; flex-shrink: 0; }
  .nav-item-icon.slide-down { animation: icon-slide-down 160ms cubic-bezier(0.22, 1, 0.36, 1) both; }
  .nav-item-icon.slide-up   { animation: icon-slide-up   160ms cubic-bezier(0.22, 1, 0.36, 1) both; }

  .header-icon-wrap { display: flex; align-items: center; color: var(--text-faint); }
  .header-icon-wrap.slide-down { animation: icon-slide-down 180ms cubic-bezier(0.22, 1, 0.36, 1) both; }
  .header-icon-wrap.slide-up   { animation: icon-slide-up   180ms cubic-bezier(0.22, 1, 0.36, 1) both; }

  @keyframes icon-slide-down {
    from { transform: translateY(-5px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
  }
  @keyframes icon-slide-up {
    from { transform: translateY(5px); opacity: 0; }
    to   { transform: translateY(0);   opacity: 1; }
  }

  /* ── Content area ────────────────────────────────────────────────────────── */
  .content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
  .content-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: var(--sp-3) var(--sp-5) 0;
    flex-shrink: 0; position: relative;
  }
  .content-header::after {
    content: ""; position: absolute; bottom: 0; left: var(--sp-5); right: var(--sp-5);
    height: 1px;
    background: linear-gradient(to right, transparent, var(--border-strong) 20%, var(--border-strong) 80%, transparent);
  }
  .content-header-left {
    display: flex; align-items: center; gap: var(--sp-2);
    padding-bottom: var(--sp-3); color: var(--text-faint);
  }
  .content-title {
    font-size: var(--text-sm); font-weight: var(--weight-medium);
    color: var(--text-primary); letter-spacing: 0.01em;
  }
  .close-btn {
    display: flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border-radius: var(--radius-sm);
    color: var(--text-faint); background: none; border: none; cursor: pointer;
    transition: color var(--t-base), background var(--t-base);
    margin-bottom: var(--sp-3);
  }
  .close-btn:hover { color: var(--text-muted); background: var(--bg-raised); }
  .content-body { flex: 1; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--border-base) transparent; }
  .content-body::-webkit-scrollbar { width: 4px; }
  .content-body::-webkit-scrollbar-track { background: transparent; }
  .content-body::-webkit-scrollbar-thumb { background: var(--border-base); border-radius: 2px; }

  /* ── Panel / Section ─────────────────────────────────────────────────────── */
  .panel { display: flex; flex-direction: column; padding: var(--sp-3) var(--sp-5) var(--sp-5); }
  .section {
    display: flex; flex-direction: column;
    border-bottom: 1px solid var(--border-dim);
    padding-bottom: var(--sp-3); margin-bottom: var(--sp-3);
  }
  .section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
  .section-title {
    font-family: var(--font-ui); font-size: var(--text-2xs);
    color: var(--text-faint); letter-spacing: var(--tracking-wider);
    text-transform: uppercase; padding: var(--sp-3) var(--sp-3) var(--sp-2);
  }

  /* ── Row primitives ──────────────────────────────────────────────────────── */
  .toggle-row, .step-row {
    display: flex; align-items: center; justify-content: space-between;
    padding: 8px var(--sp-3); border-radius: var(--radius-md);
    gap: var(--sp-3); transition: background var(--t-fast);
  }
  .toggle-row { cursor: default; }
  .toggle-row:hover, .step-row:hover { background: var(--bg-raised); }
  .toggle-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
  .toggle-label { font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.3; }
  .toggle-desc  { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); line-height: var(--leading-snug); }

  /* ── Toggle switch ───────────────────────────────────────────────────────── */
  .toggle {
    position: relative; width: 32px; height: 18px;
    border-radius: var(--radius-full); border: 1px solid var(--border-strong);
    background: var(--bg-overlay); cursor: pointer; flex-shrink: 0;
    transition: background var(--t-base), border-color var(--t-base);
  }
  .toggle.on { background: var(--accent); border-color: var(--accent); }
  .toggle-thumb {
    position: absolute; top: 2px; left: 2px;
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--text-faint);
    transition: transform var(--t-base), background var(--t-base);
  }
  .toggle.on .toggle-thumb { transform: translateX(14px); background: var(--bg-void); }

  /* ── Stepper controls ────────────────────────────────────────────────────── */
  .step-controls { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
  .step-btn {
    font-family: var(--font-ui); font-size: var(--text-sm);
    width: 26px; height: 26px; border-radius: var(--radius-sm);
    border: 1px solid var(--border-dim); background: none; color: var(--text-muted);
    cursor: pointer; display: flex; align-items: center; justify-content: center;
    transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
  }
  .step-btn:hover:not(:disabled) { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised); }
  .step-btn:disabled { opacity: 0.3; cursor: default; }
  .step-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); min-width: 40px; text-align: center; }

  /* ── Select dropdown ─────────────────────────────────────────────────────── */
  .select-wrap { position: relative; flex-shrink: 0; }
  .select-btn {
    display: flex; align-items: center; gap: var(--sp-2);
    font-size: var(--text-sm); color: var(--text-secondary);
    background: var(--bg-raised); border: 1px solid var(--border-dim);
    border-radius: var(--radius-md); padding: 5px 10px;
    cursor: pointer; min-width: 130px;
    transition: border-color var(--t-base), background var(--t-base);
  }
  .select-btn:hover { border-color: var(--border-strong); background: var(--bg-overlay); }
  .select-caret { color: var(--text-faint); transition: transform var(--t-base); flex-shrink: 0; margin-left: auto; }
  .select-caret.open { transform: rotate(180deg); }
  .select-menu {
    position: absolute; top: calc(100% + 4px); right: 0; min-width: 100%;
    background: var(--bg-raised); border: 1px solid var(--border-base);
    border-radius: var(--radius-md); padding: var(--sp-1);
    z-index: 200; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
    animation: scaleIn 0.1s ease both; transform-origin: top right;
  }
  .select-option {
    display: block; width: 100%; padding: 6px var(--sp-3);
    border-radius: var(--radius-sm); font-size: var(--text-sm);
    color: var(--text-secondary); background: none; border: none;
    cursor: pointer; text-align: left;
    transition: background var(--t-fast), color var(--t-fast);
  }
  .select-option:hover  { background: var(--bg-overlay); color: var(--text-primary); }
  .select-option.active { color: var(--accent-fg); background: var(--accent-muted); }

  /* ── Text input ──────────────────────────────────────────────────────────── */
  .text-input {
    background: var(--bg-raised); border: 1px solid var(--border-dim);
    border-radius: var(--radius-md); padding: 5px 10px;
    color: var(--text-primary); font-size: var(--text-sm);
    outline: none; width: 200px; flex-shrink: 0;
    transition: border-color var(--t-base), background var(--t-base);
  }
  .text-input:focus { border-color: var(--accent-dim); background: var(--bg-overlay); }
  .text-input::placeholder { color: var(--text-faint); }

  /* ── Unified button system ───────────────────────────────────────────────── */
  /* Base: .btn  Variants: --accent  --danger  (no modifier = ghost)           */
  .btn, .danger-btn, .dev-btn, .sec-action-btn, .folder-create-btn,
  .release-changelog-btn, .update-action-btn {
    display: inline-flex; align-items: center; gap: var(--sp-1);
    font-family: var(--font-ui); font-size: var(--text-xs);
    letter-spacing: var(--tracking-wide); white-space: nowrap;
    padding: 5px 13px; border-radius: var(--radius-md);
    border: 1px solid var(--border-dim);
    background: none; color: var(--text-muted);
    cursor: pointer; flex-shrink: 0;
    transition: color var(--t-base), border-color var(--t-base), background var(--t-base), filter var(--t-base);
  }
  .btn:hover:not(:disabled),
  .dev-btn:hover:not(:disabled),
  .sec-action-btn:hover:not(:disabled),
  .release-changelog-btn:hover:not(:disabled) {
    color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised);
  }
  .btn:disabled, .danger-btn:disabled, .dev-btn:disabled,
  .sec-action-btn:disabled, .folder-create-btn:disabled, .update-action-btn:disabled {
    opacity: 0.35; cursor: default;
  }

  /* Accent variant */
  .sec-action-primary, .folder-create-btn,
  .update-action-btn.primary {
    background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg);
  }
  .sec-action-primary:hover:not(:disabled),
  .folder-create-btn:hover:not(:disabled),
  .update-action-btn.primary:hover:not(:disabled) { filter: brightness(1.12); }

  /* Danger variant */
  .danger-btn, .sec-action-danger {
    border-color: color-mix(in srgb, var(--color-error) 50%, transparent);
    color: var(--color-error); background: none;
  }
  .danger-btn:hover:not(:disabled), .sec-action-danger:hover:not(:disabled) {
    background: var(--color-error-bg);
    border-color: var(--color-error);
  }
  /* ── Dev tools ───────────────────────────────────────────────────────────── */
  .dev-mono { font-family: monospace; font-size: 11px; color: var(--text-faint); flex-shrink: 0; }
  .dev-pill-group { display: flex; gap: 4px; flex-shrink: 0; }
  .dev-pill {
    font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider);
    font-weight: var(--weight-medium); width: 26px; height: 26px;
    border-radius: var(--radius-sm); border: 1px solid var(--border-dim);
    background: none; color: var(--text-faint); cursor: pointer;
    transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
    display: flex; align-items: center; justify-content: center;
  }
  .dev-pill:hover                { background: var(--bg-overlay); color: var(--text-secondary); border-color: var(--border-strong); }
  .dev-pill-success:hover        { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
  .dev-pill-error:hover          { color: var(--color-error); border-color: color-mix(in srgb, var(--color-error) 40%, transparent); background: var(--color-error-bg); }
  .dev-pill-info:hover           { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-overlay); }
  .dev-pill-download:hover       { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
  .dev-grid { display: grid; grid-template-columns: 64px 1fr; gap: 1px 12px; padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  .dev-key { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-faint); padding: 4px 0; display: flex; align-items: center; }
  .dev-val { font-family: monospace; font-size: 11px; color: var(--text-secondary); padding: 4px 0; display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* ── Experimental disclosure ─────────────────────────────────────────────── */
  .exp-disclosure {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; padding: var(--sp-2) var(--sp-3);
    background: none; border: none; cursor: pointer;
    color: var(--text-faint); transition: color var(--t-fast);
  }
  .exp-disclosure:hover { color: var(--text-secondary); }
  .exp-disclosure-label { font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-medium); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
  .exp-caret { transition: transform 0.18s ease; flex-shrink: 0; }
  .exp-caret.open { transform: rotate(180deg); }
  .exp-body { padding: 0 var(--sp-3) var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); }
  .exp-hint { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  .exp-demo-wrap { display: flex; justify-content: center; gap: var(--sp-3); padding: var(--sp-2) 0; }
  :global(.exp-demo-card) {
    width: 90px; border-radius: var(--radius-lg);
    background: var(--bg-raised); border: 1px solid var(--border-dim);
  }
  .exp-demo-cover {
    height: 120px;
    background: linear-gradient(160deg,
      hsl(var(--hue, 220) 30% 18%) 0%,
      hsl(var(--hue, 220) 40% 28%) 100%
    );
    border-bottom: 1px solid var(--border-dim);
  }
  .exp-demo-info { padding: 6px 8px; display: flex; flex-direction: column; gap: 2px; }
  .exp-demo-title { font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .exp-demo-sub   { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }

  /* ── Scale / zoom control ────────────────────────────────────────────────── */
  .scale-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); }
  .scale-slider { flex: 1; accent-color: var(--accent); }
  .scale-val-input {
    font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary);
    width: 42px; text-align: center; padding: 3px 4px;
    background: var(--bg-raised); border: 1px solid var(--border-dim);
    border-radius: var(--radius-sm); outline: none; transition: border-color var(--t-base);
    -moz-appearance: textfield;
  }
  .scale-val-input::-webkit-inner-spin-button,
  .scale-val-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
  .scale-val-input:focus { border-color: var(--accent-dim); }
  .scale-pct { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); margin-left: calc(var(--sp-1) * -1); }
  .scale-hint { padding: 0 var(--sp-3) var(--sp-2); display: flex; gap: var(--sp-1); flex-wrap: wrap; }
  .scale-preset {
    font-family: var(--font-ui); font-size: var(--text-2xs); padding: 2px 7px;
    border-radius: var(--radius-sm); border: 1px solid var(--border-dim);
    background: none; color: var(--text-faint); cursor: pointer;
    transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
  }
  .scale-preset:hover        { color: var(--text-muted); border-color: var(--border-strong); }
  .scale-preset.active       { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }

  /* ── Theme grid ──────────────────────────────────────────────────────────── */
  .theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(134px, 1fr)); gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); }
  :global(.theme-card) {
    background: var(--bg-raised); border: 1px solid var(--border-dim);
    border-radius: var(--radius-lg); overflow: hidden; cursor: pointer;
    text-align: left; transition: border-color var(--t-base), box-shadow var(--t-base);
    position: relative;
  }
  :global(.theme-card:hover) { border-color: var(--border-strong); box-shadow: 0 2px 12px rgba(0,0,0,0.2); }
  :global(.theme-card.active) { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
  .theme-card-inner { display: flex; flex-direction: column; width: 100%; background: none; border: none; cursor: pointer; text-align: left; }
  .theme-preview { height: 68px; overflow: hidden; }
  .theme-preview-bg { width: 100%; height: 100%; display: flex; }
  .theme-preview-sidebar { width: 20%; height: 100%; flex-shrink: 0; }
  .theme-preview-content { flex: 1; padding: 8px 6px; display: flex; flex-direction: column; gap: 5px; }
  .theme-preview-accent { height: 6px; width: 50%; border-radius: 3px; }
  .theme-preview-text   { height: 4px; width: 100%; border-radius: 2px; }
  .theme-card-info { padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; }
  .theme-card-label { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); }
  .theme-card-desc  { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  .theme-card-check { position: absolute; top: 6px; right: 6px; font-size: 10px; color: var(--accent-fg); background: var(--accent-muted); border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 1px 5px; }
  .custom-badge { color: var(--accent-fg) !important; }
  .custom-theme-card { position: relative; display: flex; flex-direction: column; padding: 0; cursor: default; }
  .custom-theme-select { flex: 1; text-align: left; cursor: pointer; display: flex; flex-direction: column; background: none; border: none; color: inherit; font-family: inherit; }
  .custom-theme-actions { display: none; position: absolute; top: 5px; left: 5px; flex-direction: row; gap: 3px; z-index: 1; }
  .custom-theme-card:hover .custom-theme-actions { display: flex; }
  .custom-theme-edit-btn,
  .custom-theme-delete-btn {
    display: flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border-radius: 3px; font-size: 10px; cursor: pointer;
    border: 1px solid var(--border-base); background: var(--bg-overlay);
    transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
  }
  .custom-theme-edit-btn   { color: var(--text-muted); }
  .custom-theme-edit-btn:hover   { color: var(--accent-fg); background: var(--accent-muted); border-color: var(--accent-dim); }
  .custom-theme-delete-btn { color: var(--text-faint); }
  .custom-theme-delete-btn:hover { color: var(--color-error); background: var(--color-error-bg); border-color: var(--color-error); }
  .new-theme-card { display: flex; flex-direction: column; border-style: dashed !important; border-color: var(--border-base) !important; background: transparent !important; transition: border-color var(--t-base) !important, background var(--t-base) !important; }
  .new-theme-card:hover { border-color: var(--accent-dim) !important; background: var(--accent-muted) !important; }
  .new-theme-icon { height: 68px; display: flex; align-items: center; justify-content: center; color: var(--text-faint); transition: color var(--t-base); }
  :global(.new-theme-card:hover) .new-theme-icon { color: var(--accent-fg); }

  /* ── Keybinds ────────────────────────────────────────────────────────────── */
  .kb-hint  { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: 0 var(--sp-3) var(--sp-3); }
  .kb-list  { display: flex; flex-direction: column; gap: 1px; }
  .kb-row   { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-md); transition: background var(--t-fast); }
  .kb-row:hover { background: var(--bg-raised); }
  .kb-label { font-size: var(--text-sm); color: var(--text-secondary); flex: 1; }
  .kb-right { display: flex; align-items: center; gap: var(--sp-2); }
  .kb-bind  {
    font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
    padding: 4px 10px; border-radius: var(--radius-sm);
    border: 1px solid var(--border-dim); background: var(--bg-raised);
    color: var(--text-secondary); cursor: pointer; min-width: 90px; text-align: center;
    transition: border-color var(--t-base), color var(--t-base);
  }
  .kb-bind:hover              { border-color: var(--border-strong); }
  .kb-bind.listening          { border-color: var(--accent); color: var(--accent-fg); background: var(--accent-muted); animation: pulse 1s ease infinite; }
  .kb-reset {
    font-size: var(--text-sm); color: var(--text-faint);
    padding: 3px 7px; border-radius: var(--radius-sm);
    border: 1px solid transparent; background: none; cursor: pointer;
    transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
  }
  .kb-reset:hover:not(:disabled) { color: var(--text-muted); border-color: var(--border-dim); background: var(--bg-overlay); }
  .kb-reset:disabled { opacity: 0.3; cursor: default; }

  /* ── Storage ─────────────────────────────────────────────────────────────── */
  .storage-loading { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: var(--sp-3); }
  .storage-bar-wrap   { padding: var(--sp-2) var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); }
  .storage-bar-header { display: flex; justify-content: space-between; align-items: baseline; }
  .storage-bar-label  { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); }
  .storage-bar        { height: 5px; background: var(--bg-overlay); border-radius: var(--radius-full); overflow: hidden; }
  .storage-bar-fill   { height: 100%; background: var(--accent); border-radius: var(--radius-full); transition: width 0.4s ease; }
  .storage-bar-fill.warn     { background: #d97706; }
  .storage-bar-fill.critical { background: var(--color-error); }
  .storage-bar-labels { display: flex; justify-content: space-between; }
  .storage-bar-used, .storage-bar-free { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  .storage-path-note  { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: var(--sp-1) var(--sp-3) 0; word-break: break-all; }
  .storage-limit-input {
    width: 64px; text-align: center;
    background: var(--bg-raised); border: 1px solid var(--border-dim);
    border-radius: var(--radius-sm); padding: 3px 6px;
    color: var(--text-secondary); font-family: var(--font-ui); font-size: var(--text-xs);
    outline: none; transition: border-color var(--t-base); -moz-appearance: textfield;
  }
  .storage-limit-input::-webkit-inner-spin-button,
  .storage-limit-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
  .storage-limit-input:focus { border-color: var(--accent-dim); }
  .storage-limit-unit { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; }

  /* ── Migration banner ────────────────────────────────────────────────────── */
  .migrate-banner { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-4); margin-bottom: var(--sp-2); padding: var(--sp-3) var(--sp-4); background: color-mix(in srgb, var(--color-info) 7%, transparent); border: 1px solid color-mix(in srgb, var(--color-info) 22%, transparent); border-radius: var(--radius-md); }
  .migrate-banner-body    { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
  .migrate-title          { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--color-info); letter-spacing: var(--tracking-wide); }
  .migrate-paths          { font-family: monospace; font-size: 10px; color: var(--text-faint); word-break: break-all; }
  .migrate-error          { font-size: var(--text-xs); color: var(--color-error); }
  .migrate-progress       { display: flex; flex-direction: column; gap: 4px; margin-top: 2px; }
  .migrate-progress-labels { display: flex; justify-content: space-between; }
  .migrate-current        { font-family: monospace; font-size: 10px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 70%; }
  .migrate-count          { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
  .migrate-bar            { height: 3px; background: var(--bg-overlay); border-radius: 2px; overflow: hidden; }
  .migrate-bar-fill       { height: 100%; background: var(--color-info); border-radius: 2px; transition: width 0.15s; }
  .migrate-banner-actions { display: flex; flex-direction: column; gap: var(--sp-1); flex-shrink: 0; align-items: flex-end; }

  /* ── Downloads path ──────────────────────────────────────────────────────── */
  .path-row         { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3) var(--sp-3); }
  .path-input       { flex: 1; width: 0 !important; min-width: 0; font-family: monospace !important; font-size: var(--text-xs) !important; }
  .path-input-error { border-color: var(--color-error) !important; }
  .path-field-error { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--color-error); letter-spacing: var(--tracking-wide); white-space: nowrap; }
  .path-actions     { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }

  /* ── Advanced / Backup collapsible ──────────────────────────────────────── */
  .adv-section  { padding-bottom: var(--sp-1); }
  .adv-toggle   { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: var(--sp-2) var(--sp-3); background: none; border: none; cursor: pointer; border-radius: var(--radius-md); transition: background var(--t-fast); }
  .adv-toggle:hover { background: var(--bg-raised); }
  .adv-caret    { color: var(--text-faint); transition: transform var(--t-base); flex-shrink: 0; }
  .adv-caret.open { transform: rotate(180deg); }
  .adv-body     { display: flex; flex-direction: column; gap: 1px; padding-top: var(--sp-1); }

  /* ── Folders / categories ────────────────────────────────────────────────── */
  .folder-create-row { display: flex; gap: var(--sp-2); padding: 0 var(--sp-3) var(--sp-3); }
  .folder-list  { display: flex; flex-direction: column; gap: 1px; padding: 0 var(--sp-2); }
  .folder-row   { display: flex; align-items: center; gap: var(--sp-2); padding: 7px var(--sp-2); border-radius: var(--radius-md); transition: background var(--t-fast); }
  .folder-row:hover     { background: var(--bg-raised); }
  .folder-row-name      { flex: 1; font-size: var(--text-sm); color: var(--text-secondary); }
  .folder-row-count     { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
  .folder-delete:hover:not(:disabled)  { color: var(--color-error) !important; }
  .folder-hidden        { opacity: 0.35; }
  .folder-default-active { color: var(--accent-fg) !important; }

  /* ── Trackers ────────────────────────────────────────────────────────────── */
  .tracker-list         { display: flex; flex-direction: column; gap: var(--sp-2); padding: 0 var(--sp-3) var(--sp-3); }
  .tracker-row          { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3); border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); transition: border-color var(--t-fast); gap: var(--sp-3); }
  .tracker-row-active   { border-color: color-mix(in srgb, var(--accent) 30%, transparent); }
  .tracker-identity     { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; flex: 1; }
  .tracker-logo         { width: 32px; height: 32px; border-radius: var(--radius-sm); object-fit: cover; flex-shrink: 0; border: 1px solid var(--border-dim); }
  .tracker-name-block   { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
  .tracker-label        { font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .tracker-status-pill  { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 1px 6px; border-radius: var(--radius-full); border: 1px solid var(--border-dim); color: var(--text-faint); background: transparent; width: fit-content; }
  .pill-on              { border-color: color-mix(in srgb, var(--accent) 40%, transparent); color: var(--accent-fg); background: var(--accent-muted); }
  .tracker-action       { flex-shrink: 0; }
  .tracker-connected-btns { display: flex; gap: var(--sp-2); }
  .tracker-error        { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--color-error); padding: var(--sp-2) var(--sp-3); letter-spacing: var(--tracking-wide); }
  .oauth-panel          { display: flex; flex-direction: column; gap: var(--sp-3); padding: 0 var(--sp-3) var(--sp-3); }
  .oauth-label          { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  .oauth-input-row      { display: flex; gap: var(--sp-2); }
  .oauth-input-row .text-input { flex: 1; width: 0; }
  .creds-panel          { display: flex; flex-direction: column; gap: var(--sp-2); padding: 0 var(--sp-3) var(--sp-3); }

  /* ── About / releases ────────────────────────────────────────────────────── */
  .about-block  { padding: 0 var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-1); }
  .about-line   { font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-base); }
  .release-list-scroll { max-height: 320px; overflow-y: auto; padding: 0 var(--sp-1); scrollbar-width: thin; scrollbar-color: var(--border-base) transparent; }
  .release-list-scroll::-webkit-scrollbar       { width: 4px; }
  .release-list-scroll::-webkit-scrollbar-track { background: transparent; }
  .release-list-scroll::-webkit-scrollbar-thumb { background: var(--border-base); border-radius: 2px; }
  .release-list         { display: flex; flex-direction: column; gap: 2px; }
  .release-row          { border-radius: var(--radius-md); border: 1px solid transparent; overflow: hidden; transition: border-color var(--t-fast); }
  .release-row.current  { border-color: color-mix(in srgb, var(--accent) 25%, transparent); }
  .release-header       { display: flex; align-items: center; justify-content: space-between; padding: 8px var(--sp-3); gap: var(--sp-3); }
  .release-header:hover { background: var(--bg-raised); }
  .release-meta         { display: flex; align-items: center; gap: var(--sp-2); flex: 1; min-width: 0; }
  .release-tag          { font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--text-secondary); letter-spacing: var(--tracking-wide); }
  .release-badge        { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 1px 6px; border-radius: var(--radius-full); border: 1px solid; }
  .current-badge        { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
  .release-date         { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  .release-actions      { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
  .release-changelog-btn { padding: 3px 10px; }
  .release-body         { padding: var(--sp-2) var(--sp-3) var(--sp-3); border-top: 1px solid var(--border-dim); }
  .release-body-text    { font-size: var(--text-xs); color: var(--text-faint); line-height: var(--leading-base); white-space: pre-wrap; word-break: break-word; }
  .update-action-btn    { padding: 4px 12px; }
  .update-progress-wrap { padding: var(--sp-2) var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); }
  .update-progress-bar  { height: 3px; background: var(--bg-overlay); border-radius: 2px; overflow: hidden; }
  .update-progress-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.3s ease; }
  .update-progress-row  { display: flex; justify-content: space-between; }
  .update-progress-label, .update-progress-val { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  .update-ready-row     { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3); background: color-mix(in srgb, var(--accent) 8%, transparent); border-radius: var(--radius-md); margin: 0 var(--sp-1); }
  .update-ready-label   { flex: 1; font-family: var(--font-ui); font-size: var(--text-xs); color: var(--accent-fg); letter-spacing: var(--tracking-wide); }
  .update-error-row     { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3); }

  /* ── Performance ─────────────────────────────────────────────────────────── */
  .perf-stat-group { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
  .perf-stat { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); flex-shrink: 0; }

  /* ── Security ────────────────────────────────────────────────────────────── */
  .sec-banner        { font-family: var(--font-ui); font-size: var(--text-xs); line-height: var(--leading-snug); border-radius: var(--radius-md); padding: var(--sp-3); letter-spacing: var(--tracking-wide); }
  .sec-banner-error  { color: var(--color-error); background: var(--color-error-bg); border: 1px solid var(--color-error); }
  .sec-banner-warn   { color: var(--color-warn, var(--color-error)); background: var(--color-warn-bg, var(--color-error-bg)); border: 1px solid var(--color-warn, var(--color-error)); }
  .sec-banner-warn code { font-family: monospace; font-size: 10px; background: color-mix(in srgb, var(--color-warn, var(--color-error)) 12%, transparent); padding: 1px 4px; border-radius: 3px; }
  .sec-pill-warn    { border-color: var(--color-error); color: var(--color-error); background: var(--color-error-bg); }
  .section-title-row { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-3) var(--sp-2); }
  .section-title-row .section-title { padding: 0; }
  .sec-status-pill { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 2px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); color: var(--text-faint); background: var(--bg-overlay); flex-shrink: 0; cursor: default; }
  .sec-pill-on     { border-color: var(--accent-dim); color: var(--accent-fg); background: var(--accent-muted); }
  .sec-field-wrap  { position: relative; flex-shrink: 0; }
  .sec-field-wrap .text-input { padding-right: 34px; }
  .sec-eye-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); display: flex; align-items: center; justify-content: center; padding: 0; border: none; background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-base); }
  .sec-eye-btn:hover { color: var(--text-muted); }
  .sec-btn-row  { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
  .auth-perf-note { font-size: var(--text-xs); color: var(--text-faint); max-width: 260px; line-height: var(--leading-snug); }
  .auth-mode-group { display: flex; gap: 2px; background: var(--bg-overlay); border: 1px solid var(--border-base); border-radius: var(--radius-md); padding: 2px; flex-shrink: 0; }
  .auth-mode-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 4px 10px; border-radius: var(--radius-sm); border: none; background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-fast), background var(--t-fast); white-space: nowrap; }
  .auth-mode-btn:hover:not(:disabled)  { color: var(--text-secondary); background: var(--bg-raised); }
  .auth-mode-btn.auth-mode-active      { background: var(--bg-surface); color: var(--text-primary); box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
  .auth-mode-btn:disabled { opacity: 0.4; cursor: default; }
  .sec-pin-error { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--color-error); letter-spacing: var(--tracking-wide); }

  /* ── Content filter ──────────────────────────────────────────────────────── */
  .content-tag-grid    { display: flex; flex-wrap: wrap; gap: var(--sp-2); padding: 0 var(--sp-3) var(--sp-3); }
  .content-tag         { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 4px 8px 4px 7px; border-radius: var(--radius-full); border: 1px solid var(--border-base); background: var(--bg-raised); color: var(--text-secondary); }
  .content-tag-remove  { display: flex; align-items: center; justify-content: center; width: 14px; height: 14px; border-radius: 50%; border: none; background: none; color: var(--text-faint); cursor: pointer; font-size: 14px; line-height: 1; padding: 0; margin-left: 1px; transition: color var(--t-fast), background var(--t-fast); }
  .content-tag-remove:hover { color: var(--color-error); background: var(--color-error-bg); }
  .content-tag-add     { display: flex; align-items: center; gap: var(--sp-2); padding: 0 var(--sp-3); }
  .content-source-search-wrap { padding: 0 var(--sp-3) var(--sp-3); }
  .content-source-list { display: flex; flex-direction: column; gap: 2px; padding: 0 var(--sp-2); }
  .content-source-row  { display: flex; align-items: center; gap: var(--sp-3); padding: 8px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; transition: background var(--t-fast), border-color var(--t-fast); }
  .content-source-row:hover      { background: var(--bg-raised); }
  .content-source-allowed        { background: color-mix(in srgb, var(--color-success) 6%, transparent); border-color: color-mix(in srgb, var(--color-success) 25%, transparent) !important; }
  .content-source-blocked        { background: color-mix(in srgb, var(--color-error)   6%, transparent); border-color: color-mix(in srgb, var(--color-error)   25%, transparent) !important; }
  .content-source-icon  { width: 28px; height: 28px; border-radius: var(--radius-sm); object-fit: cover; flex-shrink: 0; border: 1px solid var(--border-dim); background: var(--bg-overlay); }
  .content-source-info  { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .content-source-name  { font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .content-source-lang  { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  .content-source-actions { display: flex; gap: var(--sp-1); flex-shrink: 0; }
  .content-action-btn   { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 3px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
  .content-action-btn:hover              { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-overlay); }
  .content-action-active-allow { color: var(--color-success) !important; border-color: color-mix(in srgb, var(--color-success) 40%, transparent) !important; background: color-mix(in srgb, var(--color-success) 10%, transparent) !important; }
  .content-action-active-block { color: var(--color-error)   !important; border-color: color-mix(in srgb, var(--color-error)   40%, transparent) !important; background: color-mix(in srgb, var(--color-error)   10%, transparent) !important; }
</style>