<script lang="ts">
  import { onMount, untrack } from "svelte";
  import { MagnifyingGlass, Books, DownloadSimple, Folder, FolderSimple, FolderSimplePlus, Trash, Star, CheckSquare, X, ArrowSquareOut, SortAscending, Funnel, CaretUp, CaretDown, Check, ArrowsClockwise } from "phosphor-svelte";
  import { invoke } from "@tauri-apps/api/core";
  import { gql, thumbUrl } from "../../lib/client";
  import { GET_CATEGORIES, GET_LIBRARY, UPDATE_MANGA, UPDATE_MANGAS, GET_CHAPTERS, DELETE_DOWNLOADED_CHAPTERS, DEQUEUE_DOWNLOAD, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_CATEGORY_ORDER, UPDATE_LIBRARY, LIBRARY_UPDATE_STATUS } from "../../lib/queries";
  import { cache, CACHE_KEYS, CACHE_GROUPS, DEFAULT_TTL_MS } from "../../lib/cache";
  import { dedupeMangaById, dedupeMangaByTitle, shouldHideNsfw } from "../../lib/util";
  import { store, setLibraryFilter, checkAndMarkCompleted as storeCheckAndMarkCompleted, updateSettings, setCategories, setLibraryUpdates, addToast } from "../../store/state.svelte";
  import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter, LibraryContentFilter, LibraryUpdateEntry } from "../../store/state.svelte";
  import type { Manga, Category, Chapter } from "../../lib/types";
  import ContextMenu, { type MenuEntry } from "../shared/ContextMenu.svelte";
  import Thumbnail from "../shared/Thumbnail.svelte";

  const CARD_MIN_W     = 130;
  const CARD_GAP       = 16;
  const COMPLETED_NAME = "Completed";

  // Drag type discriminators (tab reorder only — manga cards no longer use drag).
  const DT_TAB = "application/x-moku-tab";

  let activeDragKind: "tab" | null = $state(null);
  let dragInsertIdx:  number       = $state(-1);

  // ── Sort / filter panel ───────────────────────────────────────────────────

  let sortPanelOpen:   boolean = $state(false);
  let filterPanelOpen: boolean = $state(false);

  function openSortPanel() {
    sortPanelOpen = !sortPanelOpen;
    filterPanelOpen = false;
  }

  function openFilterPanel() {
    filterPanelOpen = !filterPanelOpen;
    sortPanelOpen = false;
  }

  const SORT_LABELS: Record<LibrarySortMode, string> = {
    az:             "A–Z",
    unreadCount:    "Unread chapters",
    totalChapters:  "Total chapters",
    recentlyAdded:  "Recently added",
    recentlyRead:   "Recently read",
    latestFetched:  "Latest fetched chapter",
    latestUploaded: "Latest uploaded chapter",
  };

  const STATUS_LABELS: Record<LibraryStatusFilter, string> = {
    ALL:       "All statuses",
    ONGOING:   "Ongoing",
    COMPLETED: "Completed",
    CANCELLED: "Cancelled",
    HIATUS:    "Hiatus",
    UNKNOWN:   "Unknown",
  };

  const ALL_SORT_MODES: LibrarySortMode[] = [
    "az", "unreadCount", "totalChapters", "recentlyAdded",
    "recentlyRead", "latestFetched", "latestUploaded",
  ];

  const ALL_STATUS_FILTERS: LibraryStatusFilter[] = [
    "ALL", "ONGOING", "COMPLETED", "CANCELLED", "HIATUS", "UNKNOWN",
  ];

  // Per-tab reactive state — $derived so Svelte tracks changes to libraryFilter and settings
  const tabSortMode = $derived(
    store.settings.libraryTabSort[store.libraryFilter]?.mode ?? "az" as LibrarySortMode
  );
  const tabSortDir = $derived(
    store.settings.libraryTabSort[store.libraryFilter]?.dir ?? "asc" as LibrarySortDir
  );
  const tabStatus = $derived(
    store.settings.libraryTabStatus[store.libraryFilter] ?? "ALL" as LibraryStatusFilter
  );
  const tabFilters = $derived(
    store.settings.libraryTabFilters?.[store.libraryFilter] ?? {} as Partial<Record<LibraryContentFilter, boolean>>
  );
  const hasActiveFilters = $derived(
    tabStatus !== "ALL" || Object.values(tabFilters).some(Boolean)
  );

  function setTabSort(mode: LibrarySortMode, dir?: LibrarySortDir) {
    const prev   = store.settings.libraryTabSort[store.libraryFilter];
    const newDir = dir ?? prev?.dir ?? "asc";
    updateSettings({
      libraryTabSort: {
        ...store.settings.libraryTabSort,
        [store.libraryFilter]: { mode, dir: newDir },
      },
    });
  }

  function toggleTabSortDir() {
    setTabSort(tabSortMode, tabSortDir === "asc" ? "desc" : "asc");
  }

  function setTabStatus(status: LibraryStatusFilter) {
    updateSettings({
      libraryTabStatus: {
        ...store.settings.libraryTabStatus,
        [store.libraryFilter]: status,
      },
    });
  }

  function toggleTabFilter(filter: LibraryContentFilter) {
    const current = store.settings.libraryTabFilters?.[store.libraryFilter] ?? {};
    updateSettings({
      libraryTabFilters: {
        ...(store.settings.libraryTabFilters ?? {}),
        [store.libraryFilter]: {
          ...current,
          [filter]: !current[filter],
        },
      },
    });
  }

  function clearAllFilters() {
    updateSettings({
      libraryTabStatus: {
        ...store.settings.libraryTabStatus,
        [store.libraryFilter]: "ALL",
      },
      libraryTabFilters: {
        ...(store.settings.libraryTabFilters ?? {}),
        [store.libraryFilter]: {},
      },
    });
  }

  let allManga:       Manga[]      = $state([]);
  let loading:        boolean      = $state(true);
  let error:          string|null  = $state(null);
  let retryCount:     number       = $state(0);
  let search:         string       = $state("");
  let renderVisible:  number       = $state(0);
  let scrollEl:       HTMLDivElement;
  let containerWidth: number       = $state(800);
  let ctx:     { x: number; y: number; manga: Manga } | null = $state(null);
  let emptyCtx:{ x: number; y: number } | null               = $state(null);

  // ── Multi-select ──────────────────────────────────────────────────────────

  let selectedIds:   Set<number> = $state(new Set());
  let selectMode:    boolean     = $state(false);
  let bulkWorking:   boolean     = $state(false);
  // Which folder-move popup is open (shows inline folder list)
  let bulkMoveOpen:  boolean     = $state(false);

  function enterSelectMode(id?: number) {
    selectMode = true;
    if (id !== undefined) selectedIds = new Set([id]);
  }

  function exitSelectMode() {
    selectMode  = false;
    selectedIds = new Set();
    bulkMoveOpen = false;
  }

  function toggleSelect(id: number) {
    const next = new Set(selectedIds);
    if (next.has(id)) next.delete(id); else next.add(id);
    selectedIds = next;
    if (next.size === 0) exitSelectMode();
  }

  function selectAll() {
    selectedIds = new Set(visibleManga.map(m => m.id));
  }

  // Long-press to enter select mode on touch devices
  let longPressTimer: ReturnType<typeof setTimeout> | null = null;
  function onCardPointerDown(e: PointerEvent, m: Manga) {
    if (e.button !== 0) return; // only primary
    longPressTimer = setTimeout(() => {
      longPressTimer = null;
      enterSelectMode(m.id);
    }, 500);
  }
  function onCardPointerUp() {
    if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
  }
  function onCardPointerLeave() {
    if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
  }

  function onCardClick(e: MouseEvent, m: Manga) {
    if (selectMode) {
      toggleSelect(m.id);
      return;
    }
    // Cmd/Ctrl+click or Shift+click enters select mode
    if (e.metaKey || e.ctrlKey || e.shiftKey) {
      e.preventDefault();
      enterSelectMode(m.id);
      return;
    }
    store.activeManga = m;
  }

  // ── Bulk mutations ────────────────────────────────────────────────────────

  async function bulkMoveToCategory(cat: Category) {
    bulkWorking  = true;
    bulkMoveOpen = false;
    try {
      await Promise.all(
        [...selectedIds].map(id => {
          const manga = allManga.find(m => m.id === id);
          if (!manga) return Promise.resolve();
          return toggleMangaCategory(manga, cat);
        })
      );
    } finally {
      bulkWorking = false;
      exitSelectMode();
    }
  }

  async function bulkRemoveFromLibrary() {
    bulkWorking = true;
    try {
      await Promise.all(
        [...selectedIds].map(id => {
          const manga = allManga.find(m => m.id === id);
          if (!manga) return Promise.resolve();
          return removeFromLibrary(manga);
        })
      );
    } finally {
      bulkWorking = false;
      exitSelectMode();
    }
  }

  // ── Completed category auto-create ────────────────────────────────────────

  async function ensureCompletedCategory(cats: Category[]): Promise<Category[]> {
    if (cats.some(c => c.name === COMPLETED_NAME)) return cats;
    try {
      const res = await gql<{ createCategory: { category: Category } }>(CREATE_CATEGORY, { name: COMPLETED_NAME });
      return [...cats, res.createCategory.category];
    } catch { return cats; }
  }

  // ── Data loading ──────────────────────────────────────────────────────────

  async function reloadCategories() {
    try {
      const d = await gql<{ categories: { nodes: Category[] } }>(GET_CATEGORIES);
      const cats = await ensureCompletedCategory(d.categories.nodes);
      setCategories(cats);
    } catch (e) { console.error(e); }
  }

  function loadLibrary() {
    return cache.get(
      CACHE_KEYS.LIBRARY,
      () => gql<{ mangas: { nodes: Manga[] } }>(GET_LIBRARY).then(d => d.mangas.nodes),
      DEFAULT_TTL_MS,
      CACHE_GROUPS.LIBRARY,
    );
  }

  async function loadData() {
    try {
      const [nodes] = await Promise.all([loadLibrary(), reloadCategories()]);
      const mapped = nodes.map((m: any) => ({
        ...m,
        chapterCount: m.chapters?.totalCount ?? m.chapterCount ?? 0,
      }));
      allManga = dedupeMangaByTitle(dedupeMangaById(mapped), store.settings.mangaLinks);
      error    = null;
      await migrateCategorizedToLibrary();
    } catch (e: any) {
      error = e.message;
    } finally {
      loading = false;
    }
  }

  async function migrateCategorizedToLibrary() {
    const allCatManga = store.categories.flatMap(c => c.mangas?.nodes ?? []);
    const orphanIds   = [...new Set(allCatManga.filter(m => !m.inLibrary).map(m => m.id))];
    if (!orphanIds.length) return;
    await gql(UPDATE_MANGAS, { ids: orphanIds, inLibrary: true }).catch(console.error);
    allManga = allManga.map(m => orphanIds.includes(m.id) ? { ...m, inLibrary: true } : m);
    cache.clearGroup(CACHE_GROUPS.LIBRARY);
  }

  $effect(() => {
    retryCount;
    loading = true; error = null;
    if (retryCount > 0) cache.clear(CACHE_KEYS.LIBRARY);
    untrack(() => loadData());
  });

  $effect(() => { if (scrollEl) scrollEl.scrollTo({ top: 0 }); });

  $effect(() => {
    const f = store.libraryFilter;
    if (f === "library" || f === "downloaded") return;
    const id = Number(f);
    if (!store.categories.some(c => c.id === id)) {
      untrack(() => { store.libraryFilter = "library"; });
    }
  });

  // Exit select mode when the filter changes
  $effect(() => { store.libraryFilter; untrack(() => exitSelectMode()); });

  let prevChapterId: number | null = null;
  $effect(() => {
    const wasOpen = prevChapterId !== null;
    prevChapterId = store.activeChapter?.id ?? null;
    if (wasOpen && !store.activeChapter) {
      cache.clear(CACHE_KEYS.LIBRARY);
      untrack(() => loadData());
    }
  });

  // ── Derived ───────────────────────────────────────────────────────────────

  const visibleCategories = $derived((() => {
    const defaultId = store.settings.defaultLibraryCategoryId ?? null;
    return store.categories
      .filter(c => c.id !== 0 && !(store.settings.hiddenCategoryIds ?? []).includes(c.id))
      .sort((a, b) => {
        if (a.id === defaultId) return -1;
        if (b.id === defaultId) return  1;
        return a.order - b.order;
      });
  })());

  const categoryMangaMap = $derived((() => {
    const map = new Map<number, Manga[]>();
    for (const cat of store.categories) {
      const nodes = cat.mangas?.nodes ?? [];
      map.set(cat.id, nodes);
    }
    return map;
  })());

  const filtered = $derived((() => {
    const q      = search.trim().toLowerCase();
    const mode   = tabSortMode;
    const dir    = tabSortDir;
    const status = tabStatus;

    // 1. Pick the right base list for this tab
    let items: Manga[];
    if (store.libraryFilter === "library") {
      // "Saved" shows all in-library manga so that manga in folders are still visible here.
      // If the user prefers the old behaviour (only uncategorised), they can toggle it off in settings.
      if (store.settings.libraryShowAllInSaved ?? true) {
        items = allManga.filter(m => m.inLibrary);
      } else {
        items = categoryMangaMap.get(0) ?? [];
      }
    } else if (store.libraryFilter === "downloaded") {
      items = allManga.filter(m => (m.downloadCount ?? 0) > 0);
    } else {
      items = categoryMangaMap.get(Number(store.libraryFilter)) ?? [];
    }

    // 2. NSFW filter — always applied before text search or sort
    items = items.filter(m => !shouldHideNsfw(m, store.settings));

    // 3. Text search
    if (q) items = items.filter(m => m.title.toLowerCase().includes(q));

    // 4. Status filter
    if (status !== "ALL") {
      items = items.filter(m => {
        const s = m.status?.toUpperCase().replace(/\s+/g, "_") ?? "UNKNOWN";
        return s === status;
      });
    }

    // 4b. Content filters (additive — each active filter further narrows the list)
    const filters = store.settings.libraryTabFilters?.[store.libraryFilter] ?? {};
    if (filters.unread)     items = items.filter(m => (m.unreadCount ?? 0) > 0);
    if (filters.started)    items = items.filter(m => (m.unreadCount ?? 0) > 0 && (m.chapterCount ?? 0) > (m.unreadCount ?? 0));
    if (filters.downloaded) items = items.filter(m => (m.downloadCount ?? 0) > 0);
    if (filters.bookmarked) items = items.filter(m => !!(m as any).hasBookmark);

    // 5. Sort
    const recentlyReadMap = new Map<number, number>();
    if (mode === "recentlyRead") {
      for (const h of store.history) {
        if (!recentlyReadMap.has(h.mangaId)) recentlyReadMap.set(h.mangaId, h.readAt);
      }
    }

    const sorted = [...items].sort((a, b) => {
      let cmp = 0;
      switch (mode) {
        case "az":
          cmp = a.title.localeCompare(b.title, undefined, { sensitivity: "base" });
          break;
        case "unreadCount":
          cmp = (a.unreadCount ?? 0) - (b.unreadCount ?? 0);
          break;
        case "totalChapters":
          cmp = (a.chapterCount ?? 0) - (b.chapterCount ?? 0);
          break;
        case "recentlyAdded":
          // id is monotonically increasing on Suwayomi — higher = newer
          cmp = a.id - b.id;
          break;
        case "recentlyRead": {
          const ra = recentlyReadMap.get(a.id) ?? 0;
          const rb = recentlyReadMap.get(b.id) ?? 0;
          cmp = ra - rb;
          break;
        }
        // latestFetched / latestUploaded: no per-manga date available at list level;
        // fall back to id ordering so the option still does something sensible.
        case "latestFetched":
        case "latestUploaded":
          cmp = a.id - b.id;
          break;
      }
      return dir === "asc" ? cmp : -cmp;
    });

    return sorted;
  })());

  const cols           = $derived(Math.max(1, Math.floor((containerWidth + CARD_GAP) / (CARD_MIN_W + CARD_GAP))));
  const visibleManga   = $derived(filtered.slice(0, renderVisible));
  const hasMore        = $derived(filtered.length > renderVisible);
  const remainingCount = $derived(filtered.length - renderVisible);

  $effect(() => { filtered; untrack(() => { renderVisible = store.settings.renderLimit ?? 48; }); });

  const counts = $derived((() => {
    const m: Record<string, number> = {
      library:    (store.settings.libraryShowAllInSaved ?? true)
        ? allManga.filter(x => x.inLibrary).length
        : (categoryMangaMap.get(0) ?? []).length,
      downloaded: allManga.filter(m => (m.downloadCount ?? 0) > 0).length,
    };
    for (const cat of visibleCategories) {
      m[String(cat.id)] = (categoryMangaMap.get(cat.id) ?? []).length;
    }
    return m;
  })());

  function loadMore() { renderVisible += store.settings.renderLimit ?? 48; }

  // ── Drag: tab reorder ─────────────────────────────────────────────────────

  let dragTabId:     number | null = $state(null);
  let dragOverTabId: number | null = $state(null);
  let dropTargetTabId: number | null = $state(null);

  function onTabDragStart(e: DragEvent, cat: Category) {
    activeDragKind = "tab";
    dragTabId = cat.id;
    e.dataTransfer!.effectAllowed = "move";
    e.dataTransfer!.setData(DT_TAB, String(cat.id));
    e.dataTransfer!.setData("text/plain", `tab:${cat.id}`);
  }

  function onTabDragOver(e: DragEvent, cat: Category, idx: number) {
    if (activeDragKind !== "tab") return;
    if (dragTabId === null || dragTabId === cat.id) return;
    e.preventDefault();
    e.dataTransfer!.dropEffect = "move";
    dragOverTabId = cat.id;
    dragInsertIdx = idx;
  }

  function onTabDragLeave() {
    dragOverTabId = null;
  }

  async function onTabDrop(e: DragEvent, dropCat: Category) {
    e.preventDefault();
    dragOverTabId = null;
    dragInsertIdx = -1;

    if (activeDragKind !== "tab") return;
    if (dragTabId === null || dragTabId === dropCat.id) { dragTabId = null; return; }

    const dragId = dragTabId;
    dragTabId = null;
    activeDragKind = null;

    const sorted = [...store.categories]
      .filter(c => c.id !== 0)
      .sort((a, b) => a.order - b.order);

    const fromIdx = sorted.findIndex(c => c.id === dragId);
    const toIdx   = sorted.findIndex(c => c.id === dropCat.id);
    if (fromIdx < 0 || toIdx < 0) return;

    const reordered = [...sorted];
    const [moved]   = reordered.splice(fromIdx, 1);
    reordered.splice(toIdx, 0, moved);
    const withNewOrder = reordered.map((c, i) => ({ ...c, order: i + 1 }));
    setCategories(store.categories.map(c => withNewOrder.find(u => u.id === c.id) ?? c));

    const newPos = toIdx + 1;
    try {
      await gql<{ updateCategoryOrder: { categories: Category[] } }>(
        UPDATE_CATEGORY_ORDER,
        { id: dragId, position: newPos },
      );
    } catch (err) {
      console.error("Tab reorder failed:", err);
      await reloadCategories();
    }
  }

  function onTabDragEnd() {
    activeDragKind = null;
    dragTabId = null;
    dragOverTabId = null;
    dragInsertIdx = -1;
  }

  // ── Mutations ─────────────────────────────────────────────────────────────

  async function removeFromLibrary(manga: Manga) {
    await gql(UPDATE_MANGA, { id: manga.id, inLibrary: false }).catch(console.error);
    allManga = allManga.filter(m => m.id !== manga.id);
    cache.clearGroup(CACHE_GROUPS.LIBRARY);
    await reloadCategories();
  }

  async function deleteAllDownloads(manga: Manga) {
    try {
      const data = await gql<{ chapters: { nodes: Chapter[] } }>(GET_CHAPTERS, { mangaId: manga.id });
      const ids = data.chapters.nodes.filter(c => c.isDownloaded).map(c => c.id);
      if (!ids.length) return;
      await gql(DELETE_DOWNLOADED_CHAPTERS, { ids });
      await Promise.allSettled(ids.map(id => gql(DEQUEUE_DOWNLOAD, { chapterId: id })));
      allManga = allManga.map(m => m.id === manga.id ? { ...m, downloadCount: 0 } : m);
    } catch (e) { console.error(e); }
  }

  async function toggleMangaCategory(manga: Manga, cat: Category) {
    const inCat = (categoryMangaMap.get(cat.id) ?? []).some(m => m.id === manga.id);
    setCategories(store.categories.map(c => {
      if (c.id !== cat.id || !c.mangas) return c;
      const nodes = inCat
        ? c.mangas.nodes.filter(m => m.id !== manga.id)
        : [...c.mangas.nodes, manga];
      return { ...c, mangas: { nodes } };
    }));
    try {
      await gql(UPDATE_MANGA_CATEGORIES, {
        mangaId:    manga.id,
        addTo:      inCat ? [] : [cat.id],
        removeFrom: inCat ? [cat.id] : [],
      });
      if (!inCat && !manga.inLibrary) {
        await gql(UPDATE_MANGA, { id: manga.id, inLibrary: true });
        allManga = allManga.map(m => m.id === manga.id ? { ...m, inLibrary: true } : m);
        cache.clearGroup(CACHE_GROUPS.LIBRARY);
      }
      await reloadCategories();
    } catch (e) {
      console.error(e);
      await reloadCategories();
    }
  }

  async function createAndAssign(manga: Manga) {
    const name = prompt("Folder name:");
    if (!name?.trim()) return;
    try {
      const res = await gql<{ createCategory: { category: Category } }>(CREATE_CATEGORY, { name: name.trim() });
      const cat = res.createCategory.category;
      await gql(UPDATE_MANGA_CATEGORIES, { mangaId: manga.id, addTo: [cat.id], removeFrom: [] });
      if (!manga.inLibrary) {
        await gql(UPDATE_MANGA, { id: manga.id, inLibrary: true });
        allManga = allManga.map(m => m.id === manga.id ? { ...m, inLibrary: true } : m);
        cache.clearGroup(CACHE_GROUPS.LIBRARY);
      }
      await reloadCategories();
    } catch (e) { console.error(e); }
  }

  // ── Context menu ──────────────────────────────────────────────────────────

  function openCtx(e: MouseEvent, m: Manga) {
    if (selectMode) { toggleSelect(m.id); return; }
    e.preventDefault();
    ctx = { x: e.clientX, y: e.clientY, manga: m };
  }

  function sanitize(s: string) { return s.replace(/[\/\\?%*:|"<>]/g, "_"); }

  async function openMangaFolder(m: Manga) {
    let base = store.settings.serverDownloadsPath?.trim();
    if (!base) {
      try { base = await invoke<string>("get_default_downloads_path"); } catch {}
    }
    if (!base) {
      addToast({ kind: "error", title: "No downloads path set", body: "Configure it in Settings → Storage" });
      return;
    }
    const source = m.source?.displayName ?? m.source?.name ?? "";
    const path   = source
      ? `${base}/mangas/${sanitize(source)}/${sanitize(m.title)}`
      : `${base}/mangas/${sanitize(m.title)}`;
    try {
      await invoke("open_path", { path });
    } catch (e: any) {
      addToast({ kind: "error", title: "Could not open folder", body: e?.toString?.() ?? path });
    }
  }

  async function openDownloadsFolder() {
    let path = store.settings.serverDownloadsPath?.trim();
    if (!path) {
      try { path = await invoke<string>("get_default_downloads_path"); } catch {}
    }
    if (!path) {
      addToast({ kind: "error", title: "No downloads path set", body: "Configure it in Settings → Storage" });
      return;
    }
    try {
      await invoke("open_path", { path });
    } catch (e: any) {
      addToast({ kind: "error", title: "Could not open folder", body: e?.toString?.() ?? path });
    }
  }

    function buildCtxItems(m: Manga): MenuEntry[] {
    const catEntries: MenuEntry[] = visibleCategories.map(cat => {
      const inCat = (categoryMangaMap.get(cat.id) ?? []).some(x => x.id === m.id);
      return {
        label: inCat ? `Remove from ${cat.name}` : `Add to ${cat.name}`,
        icon:  Folder,
        onClick: () => toggleMangaCategory(m, cat),
      };
    });
    return [
      { label: m.inLibrary ? "Remove from library" : "Add to library", icon: Books, onClick: () => m.inLibrary ? removeFromLibrary(m) : gql(UPDATE_MANGA, { id: m.id, inLibrary: true }).then(() => { allManga = allManga.map(x => x.id === m.id ? { ...x, inLibrary: true } : x); cache.clear(CACHE_KEYS.LIBRARY); }).catch(console.error) },
      { label: "Open in file manager", icon: ArrowSquareOut, disabled: !(m.downloadCount && m.downloadCount > 0), onClick: () => openMangaFolder(m) },
      { label: "Delete all downloads", icon: Trash, danger: true, disabled: !(m.downloadCount && m.downloadCount > 0), onClick: () => deleteAllDownloads(m) },
      { separator: true },
      { label: "Select this manga", icon: CheckSquare, onClick: () => enterSelectMode(m.id) },
      ...(catEntries.length ? [{ separator: true } as MenuEntry, ...catEntries] : []),
      { separator: true },
      { label: "New folder", icon: FolderSimplePlus, onClick: () => createAndAssign(m) },
    ];
  }

  function buildEmptyCtx(): MenuEntry[] {
    return [{
      label: "New folder",
      icon:  FolderSimplePlus,
      onClick: async () => {
        const name = prompt("Folder name:");
        if (!name?.trim()) return;
        try {
          await gql(CREATE_CATEGORY, { name: name.trim() });
          await reloadCategories();
        } catch (e) { console.error(e); }
      },
    }];
  }

  // ── Completed auto-assign ─────────────────────────────────────────────────

  export async function checkAndMarkCompleted(mangaId: number, chaps: Chapter[]) {
    await storeCheckAndMarkCompleted(mangaId, chaps, store.categories, gql, UPDATE_MANGA_CATEGORIES, UPDATE_MANGA);
    await reloadCategories();
  }

  let refreshing:     boolean = $state(false);
  let refreshProgress = $state({ finished: 0, total: 0 });
  let pollTimer:      ReturnType<typeof setTimeout> | null = null;
  let refreshDone:    boolean = $state(false); // brief "done" flash on button
  let refreshDoneTimer: ReturnType<typeof setTimeout> | null = null;

  function showToast(newChapters: number, totalUpdated: number) {
    if (newChapters > 0) {
      addToast({ kind: "success", title: "Library updated", body: `${newChapters} new chapter${newChapters !== 1 ? "s" : ""} across ${totalUpdated} series` });
    } else {
      addToast({ kind: "info", title: "Already up to date", body: "No new chapters found" });
    }
  }

  async function startLibraryRefresh() {
    if (refreshing) return;
    refreshing = true;
    refreshProgress = { finished: 0, total: 0 };

    const prevCounts = new Map(allManga.map(m => [m.id, m.unreadCount ?? 0]));

    let seenWork = false;
    try {
      const updateRes = await gql<{ updateLibrary: { updateStatus: { jobsInfo: { isRunning: boolean; totalJobs: number } } } }>(UPDATE_LIBRARY, {});
      seenWork = updateRes.updateLibrary.updateStatus.jobsInfo.totalJobs > 0;
    } catch {
      refreshing = false;
      return;
    }

    pollTimer = setTimeout(function poll() {
      gql<{ libraryUpdateStatus: {
        jobsInfo: { isRunning: boolean; finishedJobs: number; totalJobs: number };
        mangaUpdates: { status: string; manga: { id: number; title: string; thumbnailUrl: string; unreadCount: number } }[];
      } }>(LIBRARY_UPDATE_STATUS, {})
        .then(d => {
          const { jobsInfo, mangaUpdates } = d.libraryUpdateStatus;
          refreshProgress = { finished: jobsInfo.finishedJobs, total: jobsInfo.totalJobs };

          if (jobsInfo.totalJobs > 0) seenWork = true;

          if (!jobsInfo.isRunning && seenWork) {
            refreshing = false;
            pollTimer  = null;

            const entries: LibraryUpdateEntry[] = mangaUpdates
              .filter(u => u.status === "FINISHED")
              .reduce<LibraryUpdateEntry[]>((acc, u) => {
                const prev       = prevCounts.get(u.manga.id) ?? 0;
                const newChapters = Math.max(0, (u.manga.unreadCount ?? 0) - prev);
                if (newChapters > 0) {
                  acc.push({
                    mangaId:      u.manga.id,
                    mangaTitle:   u.manga.title,
                    thumbnailUrl: u.manga.thumbnailUrl,
                    newChapters,
                    checkedAt:    Date.now(),
                  });
                }
                return acc;
              }, []);

            setLibraryUpdates(entries);
            cache.clearGroup(CACHE_GROUPS.LIBRARY);
            loadData();

            // Done flash on button
            refreshDone = true;
            if (refreshDoneTimer) clearTimeout(refreshDoneTimer);
            refreshDoneTimer = setTimeout(() => { refreshDone = false; }, 2500);

            // Toast summary
            const totalNew = entries.reduce((s, e) => s + e.newChapters, 0);
            showToast(totalNew, entries.length);
            return;
          }

          pollTimer = setTimeout(poll, 3000);
        })
        .catch(() => {
          refreshing = false;
          pollTimer  = null;
        });
    }, 2000);
  }

  onMount(() => {
    const ro = new ResizeObserver(([e]) => containerWidth = e.contentRect.width);
    ro.observe(scrollEl);
    const unsub = cache.subscribe(CACHE_KEYS.LIBRARY, () => loadData());

    const defaultId = store.settings.defaultLibraryCategoryId;
    if (defaultId && store.libraryFilter === "library") {
      store.libraryFilter = String(defaultId);
    }

    // Escape key exits select mode
    function onKeyDown(e: KeyboardEvent) {
      if (e.key === "Escape" && (sortPanelOpen || filterPanelOpen)) {
        sortPanelOpen = false; filterPanelOpen = false; return;
      }
      if (e.key === "Escape" && selectMode) exitSelectMode();
      if ((e.key === "a" && (e.metaKey || e.ctrlKey)) && selectMode) {
        e.preventDefault();
        selectAll();
      }
    }

    function onDocMouseDown(e: MouseEvent) {
      const target = e.target as HTMLElement;
      if (sortPanelOpen   && !target.closest(".sort-panel-wrap, .sort-panel"))   sortPanelOpen   = false;
      if (filterPanelOpen && !target.closest(".filter-panel-wrap, .filter-panel")) filterPanelOpen = false;
    }

    window.addEventListener("keydown", onKeyDown);
    document.addEventListener("mousedown", onDocMouseDown, true);

    return () => {
      ro.disconnect();
      unsub();
      if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
      window.removeEventListener("keydown", onKeyDown);
      document.removeEventListener("mousedown", onDocMouseDown, true);
    };
  });
</script>

<div
  class="root"
  role="presentation"
  bind:this={scrollEl}
  oncontextmenu={(e) => {
    if ((e.target as HTMLElement).closest("button")) return;
    e.preventDefault();
    emptyCtx = { x: e.clientX, y: e.clientY };
  }}
>
  {#if store.settings.libraryBranches ?? true}
    <svg class="branches" viewBox="0 0 400 600" preserveAspectRatio="xMaxYMid slice" aria-hidden="true">
      <g stroke="var(--accent)" stroke-width="0.6" fill="none" opacity="0.13">
        <path d="M380 600 C380 500 340 460 310 400 C280 340 300 280 270 220"/>
        <path d="M270 220 C255 190 230 175 210 150"/>
        <path d="M270 220 C290 195 310 185 330 165"/>
        <path d="M310 400 C290 375 265 368 245 350"/>
        <path d="M310 400 C330 370 355 362 370 340"/>
        <path d="M210 150 C195 128 185 108 175 80"/>
        <path d="M210 150 C225 130 240 122 258 105"/>
        <path d="M245 350 C228 330 215 315 205 290"/>
        <path d="M175 80  C168 60  162 42  158 20"/>
        <path d="M175 80  C185 62  195 50  208 35"/>
        <path d="M205 290 C196 268 190 250 186 225"/>
        <path d="M258 105 C268 88  278 72  292 52"/>
        <path class="anim-branch" d="M186 225 C180 205 176 185 174 160"/>
        <path class="anim-branch" d="M292 52  C300 36  308 20  318 0"/>
      </g>
    </svg>
  {/if}

  {#if error}
    <div class="center">
      <p class="error-msg">Could not reach Suwayomi</p>
      <p class="error-detail">Make sure the server is running, then retry.</p>
      <button class="retry-btn" onclick={() => retryCount++}>Retry</button>
    </div>
  {:else}
    <div class="header">
      <div class="header-left">
        <span class="heading">Library</span>
        <div class="tabs">
          {#each [["library","Saved"], ["downloaded","Downloaded"]] as [f, label]}
            <button class="tab" class:active={store.libraryFilter === f} onclick={() => store.libraryFilter = f}>
              {#if f === "library"}<Books size={11} weight="bold" />
              {:else if f === "downloaded"}<DownloadSimple size={11} weight="bold" />{/if}
              {label}
              <span class="tab-count">{counts[f] ?? 0}</span>
            </button>
          {/each}
          {#each visibleCategories as cat, idx}
            {@const isDefault = (store.settings.defaultLibraryCategoryId ?? null) === cat.id}
            {#if dragInsertIdx === idx && activeDragKind === "tab"}
              <div class="tab-insert-bar" aria-hidden="true"></div>
            {/if}
            <button
              class="tab"
              class:active={store.libraryFilter === String(cat.id)}
              class:tab-dragging={dragTabId === cat.id}
              class:tab-drop-target={dropTargetTabId === cat.id}
              class:tab-default={isDefault}
              draggable="true"
              onclick={() => store.libraryFilter = String(cat.id)}
              ondragstart={(e) => onTabDragStart(e, cat)}
              ondragover={(e) => onTabDragOver(e, cat, idx)}
              ondragleave={onTabDragLeave}
              ondrop={(e) => onTabDrop(e, cat)}
              ondragend={onTabDragEnd}
            >
              {#if isDefault}
                <Star size={11} weight="fill" style="color:var(--accent-fg)" />
              {:else}
                <Folder size={11} weight="bold" />
              {/if}
              {cat.name}
              <span class="tab-count">{counts[String(cat.id)] ?? 0}</span>
            </button>
            {#if dragInsertIdx === idx + 1 && activeDragKind === "tab" && idx === visibleCategories.length - 1}
              <div class="tab-insert-bar" aria-hidden="true"></div>
            {/if}
          {/each}
        </div>
      </div>
      <div class="header-right">
        <div class="search-wrap">
          <MagnifyingGlass size={13} class="search-icon" weight="light" />
          <input class="search" placeholder="Search" bind:value={search} />
        </div>

        <button
          class="icon-btn refresh-btn"
          class:icon-btn-active={refreshing}
          class:refresh-btn-done={refreshDone}
          title={refreshing ? `Checking… ${refreshProgress.finished}/${refreshProgress.total}` : refreshDone ? "Library updated" : "Check for updates"}
          disabled={refreshing}
          onclick={startLibraryRefresh}
        >
          <ArrowsClockwise size={15} weight="bold" class={refreshing ? "anim-spin" : ""} />
          {#if refreshing && refreshProgress.total > 0}
            <span class="refresh-progress">{refreshProgress.finished}/{refreshProgress.total}</span>
          {/if}
        </button>
        <button
          class="icon-btn"
          title="Open downloads folder"
          onclick={openDownloadsFolder}
        >
          <FolderSimple size={15} weight="bold" />
        </button>
        <div class="sort-panel-wrap">
          <button
            class="icon-btn"
            class:icon-btn-active={tabSortMode !== "az" || tabSortDir !== "asc"}
            title="Sort"
            onclick={openSortPanel}
          >
            <SortAscending size={15} weight="bold" />
          </button>
          {#if sortPanelOpen}
            <div class="dropdown-panel sort-panel" role="menu">
              <div class="panel-header">
                <span class="panel-heading">Sort</span>
              </div>
              <div class="panel-divider"></div>
              <p class="panel-label">Order by</p>
              {#each ALL_SORT_MODES as m}
                <button
                  class="panel-item"
                  class:panel-item-active={tabSortMode === m}
                  role="menuitem"
                  onclick={() => { setTabSort(m); sortPanelOpen = false; }}
                >
                  {SORT_LABELS[m]}
                  {#if tabSortMode === m}
                    {#if tabSortDir === "asc"}
                      <CaretUp size={11} weight="bold" class="sort-caret" />
                    {:else}
                      <CaretDown size={11} weight="bold" class="sort-caret" />
                    {/if}
                  {/if}
                </button>
              {/each}
              <button
                class="panel-item dir-toggle"
                role="menuitem"
                onclick={toggleTabSortDir}
              >
                {tabSortDir === "asc" ? "Ascending" : "Descending"}
                {#if tabSortDir === "asc"}
                  <CaretUp size={11} weight="bold" />
                {:else}
                  <CaretDown size={11} weight="bold" />
                {/if}
              </button>
            </div>
          {/if}
        </div>

        <!-- Filter panel -->
        <div class="filter-panel-wrap">
          <button
            class="icon-btn"
            class:icon-btn-active={hasActiveFilters}
            title="Filter"
            onclick={openFilterPanel}
          >
            <Funnel size={15} weight={hasActiveFilters ? "fill" : "bold"} />
          </button>
          {#if filterPanelOpen}
            <div class="dropdown-panel filter-panel" role="menu">
              <div class="panel-header">
                <span class="panel-heading">Filter</span>
                {#if hasActiveFilters}
                  <button class="panel-clear-btn" onclick={clearAllFilters}>Clear all</button>
                {/if}
              </div>
              <div class="panel-divider"></div>
              <p class="panel-label">Content</p>
              {#each ([["unread","Unread"],["started","Started"],["downloaded","Downloaded"],["bookmarked","Bookmarked"]] as const) as [f, label]}
                <button
                  class="panel-item panel-item-check"
                  class:panel-item-active={tabFilters[f]}
                  role="menuitem"
                  onclick={() => toggleTabFilter(f)}
                >
                  <span class="panel-check" class:panel-check-on={tabFilters[f]}>
                    {#if tabFilters[f]}<Check size={9} weight="bold" />{/if}
                  </span>
                  {label}
                </button>
              {/each}
              <div class="panel-divider"></div>
              <p class="panel-label">Status</p>
              {#each ALL_STATUS_FILTERS.filter(s => s !== "ALL") as s}
                <button
                  class="panel-item panel-item-check"
                  class:panel-item-active={tabStatus === s}
                  role="menuitem"
                  onclick={() => setTabStatus(tabStatus === s ? "ALL" : s)}
                >
                  <span class="panel-check" class:panel-check-on={tabStatus === s}>
                    {#if tabStatus === s}<Check size={9} weight="bold" />{/if}
                  </span>
                  {STATUS_LABELS[s]}
                </button>
              {/each}
            </div>
          {/if}
        </div>
      </div>
    </div>

    <!-- ── Refresh progress bar ──────────────────────────────────────────────── -->
    {#if refreshing && refreshProgress.total > 0}
      {@const pct = Math.round((refreshProgress.finished / refreshProgress.total) * 100)}
      <div class="refresh-bar-wrap" aria-hidden="true">
        <div class="refresh-bar-fill" style="width:{pct}%"></div>
      </div>
    {/if}

    <!-- ── Selection toolbar ───────────────────────────────────────────────── -->
    {#if selectMode}
      <div class="select-bar">
        <div class="select-bar-left">
          <button class="sel-btn sel-cancel" onclick={exitSelectMode} title="Cancel (Esc)">
            <X size={13} weight="bold" />
          </button>
          <span class="sel-count">{selectedIds.size} selected</span>
          <button class="sel-btn sel-all" onclick={selectAll} title="Select all (⌘A)">
            Select all
          </button>
        </div>
        <div class="select-bar-right">
          {#if visibleCategories.length}
            <div class="bulk-move-wrap">
              <button
                class="sel-btn sel-move"
                disabled={selectedIds.size === 0 || bulkWorking}
                onclick={() => bulkMoveOpen = !bulkMoveOpen}
              >
                <Folder size={13} weight="bold" />
                Move to folder
              </button>
              {#if bulkMoveOpen}
                <div class="bulk-folder-list">
                  {#each visibleCategories as cat}
                    <button class="bulk-folder-item" onclick={() => bulkMoveToCategory(cat)}>
                      <Folder size={11} weight="bold" />
                      {cat.name}
                    </button>
                  {/each}
                </div>
              {/if}
            </div>
          {/if}
          <button
            class="sel-btn sel-remove"
            disabled={selectedIds.size === 0 || bulkWorking}
            onclick={bulkRemoveFromLibrary}
          >
            <Trash size={13} weight="bold" />
            Remove
          </button>
        </div>
      </div>
    {/if}

    <div class="content">
    {#if loading}
      <div class="grid">
        {#each Array(12) as _}
          <div class="card-skeleton">
            <div class="cover-skeleton skeleton"></div>
            <div class="title-skeleton skeleton"></div>
          </div>
        {/each}
      </div>
    {:else if filtered.length === 0}
      <div class="center">
        {store.libraryFilter === "library" ? "No manga saved to library — browse sources to add some."
          : store.libraryFilter === "downloaded" ? "No downloaded manga."
          : "No manga in this folder yet. Right-click manga anywhere to assign them."}
      </div>
    {:else}
      <div class="grid" style="--cols:{cols}">
        {#each visibleManga as m (m.id)}
          {@const isSelected = selectedIds.has(m.id)}
          <button
            class="card"
            class:card-selected={isSelected}
            class:select-mode={selectMode}
            onclick={(e) => onCardClick(e, m)}
            oncontextmenu={(e) => openCtx(e, m)}
            onpointerdown={(e) => onCardPointerDown(e, m)}
            onpointerup={onCardPointerUp}
            onpointerleave={onCardPointerLeave}
          >
            <div class="cover-wrap">
              <Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" style="object-fit:{store.settings.libraryCropCovers ? 'cover' : 'contain'}" draggable="false" />
              {#if m.downloadCount}<span class="badge-dl">{m.downloadCount}</span>{/if}
              {#if m.unreadCount}<span class="badge-unread">{m.unreadCount}</span>{/if}
              {#if selectMode}
                <div class="select-overlay" aria-hidden="true">
                  <div class="select-check" class:checked={isSelected}>
                    {#if isSelected}
                      <CheckSquare size={20} weight="fill" />
                    {:else}
                      <div class="select-check-empty"></div>
                    {/if}
                  </div>
                </div>
              {/if}
            </div>
            <p class="title">{m.title}</p>
          </button>
        {/each}
      </div>
      {#if hasMore}
        <div class="load-more-row">
          <button class="load-more-btn" onclick={loadMore}>
            Show {Math.min(remainingCount, store.settings.renderLimit ?? 48)} more
            <span class="load-more-count">({remainingCount} remaining)</span>
          </button>
        </div>
      {/if}
    {/if}
    </div><!-- .content -->
  {/if}
</div>

{#if ctx}
  <ContextMenu x={ctx.x} y={ctx.y} items={buildCtxItems(ctx.manga)} onClose={() => ctx = null} />
{/if}
{#if emptyCtx}
  <ContextMenu x={emptyCtx.x} y={emptyCtx.y} items={buildEmptyCtx()} onClose={() => emptyCtx = null} />
{/if}

<style>
  .root { position: relative; display: flex; flex-direction: column; height: 100%; overflow: visible; animation: fadeIn 0.14s ease both; }
  .content { flex: 1; overflow-y: auto; padding: var(--sp-5) var(--sp-6) var(--sp-6); will-change: scroll-position; -webkit-overflow-scrolling: touch; }
  .branches { position: absolute; top: 0; right: 0; width: 400px; height: 600px; pointer-events: none; z-index: 0; }
  .branches :global(.anim-branch) { stroke-dasharray: 60; stroke-dashoffset: 60; animation: branchGrow 2.4s ease forwards; }
  @keyframes branchGrow { to { stroke-dashoffset: 0; } }
  .header { position: relative; z-index: 100; display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); gap: var(--sp-4); flex-wrap: wrap; flex-shrink: 0; }
  .header-left { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
  .heading { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; flex-shrink: 0; }
  .tabs { display: flex; gap: 2px; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 2px; }
  .tab { display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-sm); color: var(--text-faint); white-space: nowrap; transition: background var(--t-base), color var(--t-base); cursor: grab; }
  .tab:hover { color: var(--text-muted); }
  .tab.active { background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); }
  .tab-default { color: var(--text-muted); }
  .tab-dragging { opacity: 0.4; cursor: grabbing; }
  .tab-drop-target { background: var(--accent-muted) !important; color: var(--accent-fg) !important; outline: 1px dashed var(--accent); }
  .tab-insert-bar { width: 2px; height: 22px; background: var(--accent); border-radius: 2px; flex-shrink: 0; box-shadow: 0 0 6px var(--accent); pointer-events: none; }
  .tab-count { font-size: var(--text-2xs); opacity: 0.6; }
  .search-wrap { position: relative; display: flex; align-items: center; }
  .search-wrap :global(.search-icon) { position: absolute; left: 10px; color: var(--text-faint); pointer-events: none; }
  .search { background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px 5px 28px; color: var(--text-primary); font-size: var(--text-sm); width: 180px; outline: none; transition: border-color var(--t-base); }
  .search::placeholder { color: var(--text-faint); }
  .search:focus { border-color: var(--border-strong); }

  /* ── Header right cluster ───────────────────────────────────────────────── */
  .header-right { display: flex; align-items: center; gap: var(--sp-2); }

  /* ── Icon buttons (sort / filter triggers) ──────────────────────────────── */
  .icon-btn { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-faint); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
  .icon-btn:hover { color: var(--text-primary); border-color: var(--border-strong); }
  .icon-btn-active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
  .refresh-btn { gap: var(--sp-1); width: auto; padding: 0 8px; }
  .refresh-btn:disabled { cursor: default; }
  .refresh-progress { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); }

  /* ── Dropdown panels (shared) ───────────────────────────────────────────── */
  .sort-panel-wrap,
  .filter-panel-wrap { position: relative; }
  .dropdown-panel { position: absolute; top: calc(100% + 6px); right: 0; z-index: 9999; min-width: 220px; background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-lg); padding: var(--sp-1); box-shadow: 0 8px 32px rgba(0,0,0,0.5); animation: fadeIn 0.1s ease both; }
  .panel-label { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wider); text-transform: uppercase; color: var(--text-faint); padding: 4px 8px 8px; }
  .panel-item { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 7px 10px; border-radius: var(--radius-sm); border: none; background: transparent; color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-xs); cursor: pointer; text-align: left; transition: background var(--t-base), color var(--t-base); gap: var(--sp-2); }
  .panel-item:hover { background: var(--bg-overlay); color: var(--text-primary); }
  .panel-item-active { color: var(--accent-fg); background: var(--accent-muted); font-weight: var(--weight-medium, 500); }
  .panel-item-active:hover { background: var(--accent-dim); }
  .panel-divider { height: 1px; background: var(--border-dim); margin: 4px 2px; }
  /* Panel header row — shared by sort + filter panels */
  .panel-header { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px 4px; }
  .panel-heading { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); color: var(--text-secondary); font-weight: var(--weight-medium, 500); }
  .panel-clear-btn { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-faint); background: none; border: none; cursor: pointer; padding: 0; transition: color var(--t-base); }
  .panel-clear-btn:hover { color: var(--color-error); }
  /* Check items */
  .panel-item-check { justify-content: flex-start; gap: var(--sp-2); }
  .panel-check { width: 13px; height: 13px; border-radius: 2px; border: 1px solid var(--border-strong); background: transparent; flex-shrink: 0; transition: background var(--t-base), border-color var(--t-base); display: flex; align-items: center; justify-content: center; color: var(--bg-base); }
  .panel-check-on { background: var(--accent); border-color: var(--accent); }
  .dir-toggle { color: var(--text-secondary); justify-content: flex-start; gap: var(--sp-2); border-top: 1px solid var(--border-dim); border-radius: 0 0 var(--radius-sm) var(--radius-sm); margin-top: 2px; padding-top: 9px; }
  :global(.sort-caret) { flex-shrink: 0; }

  /* ── Selection toolbar ──────────────────────────────────────────────────── */
  .select-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) var(--sp-6); background: var(--bg-raised); border-bottom: 1px solid var(--accent-dim); flex-shrink: 0; animation: fadeIn 0.1s ease both; }
  .select-bar-left { display: flex; align-items: center; gap: var(--sp-3); }
  .select-bar-right { display: flex; align-items: center; gap: var(--sp-2); position: relative; }
  .sel-count { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--accent-fg); letter-spacing: var(--tracking-wide); }
  .sel-btn { display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: var(--bg-base); color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); white-space: nowrap; }
  .sel-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
  .sel-btn:disabled { opacity: 0.4; cursor: not-allowed; }
  .sel-cancel { border-color: transparent; background: transparent; }
  .sel-cancel:hover { background: var(--bg-raised); border-color: var(--border-dim); }
  .sel-move { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
  .sel-move:hover:not(:disabled) { background: var(--accent-dim); }
  .sel-remove { color: var(--color-error, #e05c5c); border-color: color-mix(in srgb, var(--color-error, #e05c5c) 30%, transparent); }
  .sel-remove:hover:not(:disabled) { background: color-mix(in srgb, var(--color-error, #e05c5c) 12%, transparent); }
  .sel-all { border-color: transparent; background: transparent; }

  /* Bulk folder dropdown */
  .bulk-move-wrap { position: relative; }
  .bulk-folder-list { position: absolute; top: calc(100% + 4px); right: 0; z-index: 200; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 4px; min-width: 160px; box-shadow: 0 8px 24px rgba(0,0,0,0.35); animation: fadeIn 0.1s ease both; }
  .bulk-folder-item { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 10px; border-radius: var(--radius-sm); border: none; background: transparent; color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-xs); cursor: pointer; text-align: left; transition: background var(--t-base), color var(--t-base); }
  .bulk-folder-item:hover { background: var(--bg-hover, var(--bg-base)); color: var(--text-primary); }

  /* ── Grid & cards ───────────────────────────────────────────────────────── */
  .grid { position: relative; z-index: 1; isolation: isolate; display: grid; grid-template-columns: repeat(var(--cols, auto-fill), minmax(130px, 1fr)); gap: var(--sp-4); }
  .card { background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
  .card:hover .cover { filter: brightness(1.07); }
  .card:hover .title { color: var(--text-primary); }
  .card.select-mode { cursor: default; }
  .card.card-selected .cover-wrap { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-md); }
  .card.card-selected .title { color: var(--accent-fg); }
  .cover-wrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); }
  .cover { width: 100%; height: 100%; transition: filter var(--t-base); will-change: filter; }
  .badge-dl { position: absolute; bottom: var(--sp-1); right: var(--sp-1); min-width: 18px; height: 18px; padding: 0 3px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; background: var(--accent-dim); color: var(--accent-fg); border-radius: var(--radius-sm); border: 1px solid var(--accent-muted); }
  .badge-unread { position: absolute; top: var(--sp-1); left: var(--sp-1); min-width: 18px; height: 18px; padding: 0 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; background: var(--bg-void); color: var(--text-primary); border-radius: var(--radius-sm); border: 1px solid var(--border-strong); }

  /* Select overlay (checkbox) */
  .select-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.18); display: flex; align-items: flex-start; justify-content: flex-end; padding: 6px; pointer-events: none; }
  .select-check { color: var(--text-faint); opacity: 0.7; transition: color var(--t-base), opacity var(--t-base); }
  .select-check.checked { color: var(--accent-fg); opacity: 1; }
  .select-check-empty { width: 20px; height: 20px; border-radius: 4px; border: 2px solid var(--text-faint); background: rgba(0,0,0,0.3); }

  .title { margin-top: var(--sp-2); font-size: var(--text-sm); color: var(--text-secondary); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: color var(--t-base); }
  .card-skeleton { padding: 0; }
  .cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); }
  .title-skeleton { height: 12px; margin-top: var(--sp-2); width: 80%; border-radius: var(--radius-sm); }
  .load-more-row { display: flex; justify-content: center; padding: var(--sp-5) 0 var(--sp-2); position: relative; z-index: 1; }
  .load-more-btn { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 8px 20px; border-radius: var(--radius-full); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
  .load-more-btn:hover { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
  .load-more-count { color: var(--text-faint); font-size: var(--text-2xs); }
  .center { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 60%; color: var(--text-muted); font-size: var(--text-sm); gap: var(--sp-2); text-align: center; line-height: var(--leading-base); }
  .error-msg { color: var(--color-error); font-size: var(--text-base); }
  .error-detail { color: var(--text-faint); font-size: var(--text-sm); }
  .retry-btn { margin-top: var(--sp-3); padding: 6px 16px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-muted); cursor: pointer; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); }
  /* ── Refresh progress bar ───────────────────────────────────────────────── */
  .refresh-bar-wrap { height: 2px; background: var(--border-dim); flex-shrink: 0; overflow: hidden; }
  .refresh-bar-fill { height: 100%; background: var(--accent); border-radius: 0 2px 2px 0; transition: width 0.6s ease; }

  /* Done flash on button */
  .refresh-btn-done { color: var(--color-success, #5cae6e) !important; border-color: color-mix(in srgb, var(--color-success, #5cae6e) 40%, transparent) !important; background: color-mix(in srgb, var(--color-success, #5cae6e) 10%, transparent) !important; }

  @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
</style>
