<script lang="ts">
  import { onDestroy, untrack } from "svelte";
  import { gql, thumbUrl } from "../../lib/client";
  import { GET_SOURCES, FETCH_SOURCE_MANGA } from "../../lib/queries";
  import { cache, CACHE_KEYS, getPageSet } from "../../lib/cache";
  import { dedupeSources, dedupeMangaById, dedupeMangaByTitle } from "../../lib/util";
  import { store, setSearchPrefill, setPreviewManga } from "../../store/state.svelte";
  import type { Manga, Source } from "../../lib/types";

  type SearchTab = "keyword" | "tag" | "source";
  type TagMode   = "AND" | "OR";

  interface SourceResult {
    source:  Source;
    mangas:  Manga[];
    loading: boolean;
    error:   string | null;
  }

  const CONCURRENCY = 6;

  const COMMON_GENRES = [
    "Action","Adventure","Comedy","Drama","Fantasy","Romance",
    "Sci-Fi","Slice of Life","Horror","Mystery","Thriller","Sports",
    "Supernatural","Mecha","Historical","Psychological","School Life",
    "Shounen","Seinen","Josei","Shoujo","Isekai","Martial Arts",
    "Magic","Music","Cooking","Medical","Military","Harem","Ecchi",
  ];

  async function runConcurrent<T>(items: T[], fn: (item: T) => Promise<void>, signal: AbortSignal): Promise<void> {
    let i = 0;
    async function worker() {
      while (i < items.length) {
        if (signal.aborted) return;
        const item = items[i++];
        await fn(item).catch(() => {});
      }
    }
    await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker));
  }

  function matchesAllTags(m: Manga, tags: string[]): boolean {
    const genres = (m.genre ?? []).map((g: string) => g.toLowerCase());
    return tags.every((t) => genres.includes(t.toLowerCase()));
  }

  function buildGenreFilter(tags: string[], mode: TagMode): Record<string, unknown> {
    if (tags.length === 0) return {};
    if (mode === "AND") return { and: tags.map((t) => ({ genre: { includesInsensitive: t } })) };
    return { or: tags.map((t) => ({ genre: { includesInsensitive: t } })) };
  }

  const MANGAS_BY_GENRE = `
    query MangasByGenre($filter: MangaFilterInput, $first: Int, $offset: Int) {
      mangas(filter: $filter, first: $first, offset: $offset, orderBy: IN_LIBRARY_AT, orderByType: DESC) {
        nodes {
          id title thumbnailUrl inLibrary genre status
          source { id displayName }
        }
        pageInfo { hasNextPage }
        totalCount
      }
    }
  `;

  let tab: SearchTab = $state("keyword");

  let preferredLang = store.settings?.preferredExtensionLang ?? "";

  let allSources:  Source[] = $state([]);
  let loadingSources         = $state(false);
  let pendingPrefill         = $state("");

  $effect(() => {
    if (store.searchPrefill) {
      const prefill = store.searchPrefill;
      untrack(() => {
        pendingPrefill = prefill;
        tab = "keyword";
        setSearchPrefill("");
      });
    }
  });

  loadingSources = true;
  gql<{ sources: { nodes: Source[] } }>(GET_SOURCES)
    .then((d) => { allSources = d.sources.nodes.filter((src: Source) => src.id !== "0"); })
    .catch(console.error)
    .finally(() => { loadingSources = false; });

  const availableLangs   = $derived(Array.from(new Set<string>(allSources.map((s) => s.lang))).sort());
  const hasMultipleLangs = $derived(availableLangs.length > 1);

  // ── Keyword search ────────────────────────────────────────────────────────

  let kw_query        = $state("");
  let kw_submitted    = $state("");
  let kw_results:     SourceResult[] = $state([]);
  let kw_showAdvanced = $state(false);
  let kw_selectedLangs: Set<string>  = $state(new Set());
  let kw_includeNsfw  = $state(false);
  let kw_inputEl:     HTMLInputElement | null = $state(null);
  let kw_abortCtrl:   AbortController | null  = null;

  $effect(() => {
    if (allSources.length) {
      const available = new Set(allSources.map((s) => s.lang));
      kw_selectedLangs = available.has(preferredLang)
        ? new Set([preferredLang])
        : new Set(availableLangs.slice(0, 1));
    }
  });

  $effect(() => {
    if (!loadingSources && pendingPrefill && !kw_submitted && allSources.length) {
      const q = pendingPrefill;
      pendingPrefill = "";
      kw_query = q;
      kwDoSearch(q);
    }
  });

  function kwGetVisibleSources(): Source[] {
    let filtered = allSources;
    if (kw_selectedLangs.size > 0)
      filtered = filtered.filter((s) => kw_selectedLangs.has(s.lang));
    if (!kw_includeNsfw)
      filtered = filtered.filter((s) => !s.isNsfw);
    return filtered;
  }

  async function kwDoSearch(q: string) {
    const trimmed = q.trim();
    if (!trimmed) return;
    const visible = kwGetVisibleSources();
    if (!visible.length) return;
    kw_abortCtrl?.abort();
    const ctrl = new AbortController();
    kw_abortCtrl = ctrl;
    kw_submitted = trimmed;
    kw_results   = visible.map((src) => ({ source: src, mangas: [], loading: true, error: null }));
    await runConcurrent(visible, async (src) => {
      if (ctrl.signal.aborted) return;
      try {
        const d = await gql<{ fetchSourceManga: { mangas: Manga[] } }>(
          FETCH_SOURCE_MANGA, { source: src.id, type: "SEARCH", page: 1, query: trimmed }, ctrl.signal,
        );
        if (ctrl.signal.aborted) return;
        kw_results = kw_results.map((r) =>
          r.source.id === src.id ? { ...r, mangas: d.fetchSourceManga.mangas, loading: false } : r,
        );
      } catch (e: any) {
        if (ctrl.signal.aborted || e?.name === "AbortError") return;
        kw_results = kw_results.map((r) =>
          r.source.id === src.id ? { ...r, loading: false, error: e.message ?? "Error" } : r,
        );
      }
    }, ctrl.signal);
  }

  function kwToggleLang(lang: string) {
    const next = new Set(kw_selectedLangs);
    if (next.has(lang)) { if (next.size === 1) return; next.delete(lang); }
    else next.add(lang);
    kw_selectedLangs = next;
  }

  const kw_visibleCount = $derived(kwGetVisibleSources().length);
  const kw_hasResults   = $derived(kw_results.some((r) => r.mangas.length > 0));
  const kw_allDone      = $derived(kw_results.length > 0 && kw_results.every((r) => !r.loading));

  // ── Tag search ────────────────────────────────────────────────────────────

  let tag_activeTags:       string[] = $state([]);
  let tag_tagMode:          TagMode  = $state("AND");
  let tag_tagFilter                  = $state("");

  let tag_localResults:     Manga[]  = $state([]);
  let tag_totalCount                 = $state(0);
  let tag_loadingLocal               = $state(false);
  let tag_loadingMoreLocal           = $state(false);
  let tag_localOffset                = $state(0);
  let tag_localHasNext               = $state(false);
  let tag_abortLocal:       AbortController | null = null;

  let tag_searchSources              = $state(false);
  let tag_sourceResults:    Manga[]  = $state([]);
  let tag_loadingSourceSearch        = $state(false);
  let tag_loadingMoreSource          = $state(false);
  let tag_srcNextPage: Map<string, number> = $state(new Map());
  let tag_abortSource: AbortController | null = null;

  const tag_filteredGenres = $derived.by(() => {
    const q = tag_tagFilter.trim().toLowerCase();
    return q ? COMMON_GENRES.filter((g) => g.toLowerCase().includes(q)) : COMMON_GENRES;
  });

  const tag_hasActiveTags = $derived(tag_activeTags.length > 0);
  const tag_localIds      = $derived(new Set(tag_localResults.map((m) => m.id)));
  const tag_mergedResults = $derived(dedupeMangaByTitle(dedupeMangaById(
    tag_searchSources
      ? [...tag_localResults, ...tag_sourceResults.filter((m) => !tag_localIds.has(m.id))]
      : tag_localResults
  ), store.settings.mangaLinks));
  const tag_totalVisible  = $derived(tag_mergedResults.length);
  const tag_sourceHasMore = $derived(tag_searchSources && [...tag_srcNextPage.values()].some((p) => p > 0));

  $effect(() => {
    const _activeTags = tag_activeTags;
    const _tagMode    = tag_tagMode;
    untrack(() => tagFetchLocal(_activeTags, _tagMode));
  });

  let tag_autoSearchFired = $state(false);
  $effect(() => {
    if (!tag_loadingLocal && tag_activeTags.length > 0 && !tag_autoSearchFired && !tag_searchSources && !loadingSources) {
      if (tag_localResults.length < 20) {
        untrack(() => { tag_autoSearchFired = true; tag_searchSources = true; });
      }
    }
  });
  $effect(() => { const _ = tag_activeTags; untrack(() => { tag_autoSearchFired = false; }); });
  $effect(() => {
    if (tag_searchSources && tag_activeTags.length > 0 && !loadingSources) {
      const tags = tag_activeTags;
      untrack(() => tagFetchSources(tags));
    }
  });

  async function tagFetchLocal(activeTags: string[], tagMode: TagMode) {
    if (activeTags.length === 0) {
      tag_localResults = []; tag_totalCount = 0; tag_localHasNext = false; tag_localOffset = 0;
      return;
    }
    tag_abortLocal?.abort();
    const ctrl = new AbortController();
    tag_abortLocal = ctrl;
    tag_localResults = []; tag_totalCount = 0; tag_localOffset = 0; tag_localHasNext = false;
    tag_loadingLocal = true;
    gql<{ mangas: { nodes: Manga[]; pageInfo: { hasNextPage: boolean }; totalCount: number } }>(
      MANGAS_BY_GENRE,
      { filter: buildGenreFilter(activeTags, tagMode), first: (store.settings.renderLimit ?? 48), offset: 0 },
      ctrl.signal,
    ).then((d) => {
      if (ctrl.signal.aborted) return;
      tag_localResults  = d.mangas.nodes;
      tag_totalCount    = d.mangas.totalCount;
      tag_localHasNext  = d.mangas.pageInfo.hasNextPage;
      tag_localOffset   = (store.settings.renderLimit ?? 48);
    }).catch((e: any) => {
      if (e?.name !== "AbortError") console.error(e);
    }).finally(() => {
      if (!ctrl.signal.aborted) tag_loadingLocal = false;
    });
  }

  async function tagFetchSources(activeTags: string[]) {
    tag_abortSource?.abort();
    const ctrl = new AbortController();
    tag_abortSource = ctrl;
    tag_srcNextPage       = new Map();
    tag_loadingSourceSearch = true;
    const sources    = dedupeSources(allSources, preferredLang);
    const primaryTag = activeTags[0];
    for (const src of sources) tag_srcNextPage.set(src.id, -1);
    runConcurrent(sources, async (src) => {
      if (ctrl.signal.aborted) return;
      const ps      = getPageSet(src.id, "SEARCH", activeTags);
      const pageKey = CACHE_KEYS.sourceMangaPage(src.id, "SEARCH", 1, activeTags);
      const result = await cache
        .get<{ mangas: Manga[]; hasNextPage: boolean }>(
          pageKey,
          () => gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>(
            FETCH_SOURCE_MANGA, { source: src.id, type: "SEARCH", page: 1, query: primaryTag }, ctrl.signal,
          ).then((d) => d.fetchSourceManga),
        )
        .catch((e: any) => { if (e?.name !== "AbortError") console.error(e); return null; });
      if (!result || ctrl.signal.aborted) return;
      ps.add(1);
      tag_srcNextPage.set(src.id, result.hasNextPage ? 2 : -1);
      tag_srcNextPage = new Map(tag_srcNextPage);
      const matching = activeTags.length > 1
        ? result.mangas.filter((m) => matchesAllTags(m, activeTags))
        : result.mangas;
      if (matching.length > 0) {
        tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks);
        tag_loadingSourceSearch = false;
      }
    }, ctrl.signal).finally(() => {
      if (!ctrl.signal.aborted) tag_loadingSourceSearch = false;
    });
  }

  async function tagLoadMoreLocal() {
    if (tag_loadingMoreLocal || !tag_localHasNext) return;
    tag_loadingMoreLocal = true;
    tag_abortLocal?.abort();
    const ctrl = new AbortController();
    tag_abortLocal = ctrl;
    try {
      const d = await gql<{ mangas: { nodes: Manga[]; pageInfo: { hasNextPage: boolean } } }>(
        MANGAS_BY_GENRE,
        { filter: buildGenreFilter(tag_activeTags, tag_tagMode), first: (store.settings.renderLimit ?? 48), offset: tag_localOffset },
        ctrl.signal,
      );
      if (ctrl.signal.aborted) return;
      tag_localResults  = [...tag_localResults, ...d.mangas.nodes];
      tag_localHasNext  = d.mangas.pageInfo.hasNextPage;
      tag_localOffset  += (store.settings.renderLimit ?? 48);
    } catch (e: any) {
      if (e?.name !== "AbortError") console.error(e);
    } finally {
      if (!ctrl.signal.aborted) tag_loadingMoreLocal = false;
    }
  }

  async function tagLoadMoreSource() {
    if (tag_loadingMoreSource || !tag_sourceHasMore) return;
    tag_loadingMoreSource = true;
    tag_abortSource?.abort();
    const ctrl = new AbortController();
    tag_abortSource = ctrl;
    const sources    = dedupeSources(allSources, preferredLang).filter((src) => (tag_srcNextPage.get(src.id) ?? -1) > 0);
    const primaryTag = tag_activeTags[0];
    try {
      await runConcurrent(sources, async (src) => {
        const page    = tag_srcNextPage.get(src.id)!;
        if (ctrl.signal.aborted) return;
        const ps      = getPageSet(src.id, "SEARCH", tag_activeTags);
        const pageKey = CACHE_KEYS.sourceMangaPage(src.id, "SEARCH", page, tag_activeTags);
        const result = await cache
          .get<{ mangas: Manga[]; hasNextPage: boolean }>(
            pageKey,
            () => gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>(
              FETCH_SOURCE_MANGA, { source: src.id, type: "SEARCH", page, query: primaryTag }, ctrl.signal,
            ).then((d) => d.fetchSourceManga),
          )
          .catch((e: any) => { if (e?.name !== "AbortError") tag_srcNextPage.set(src.id, -1); return null; });
        if (!result || ctrl.signal.aborted) return;
        ps.add(page);
        tag_srcNextPage.set(src.id, result.hasNextPage ? page + 1 : -1);
        tag_srcNextPage = new Map(tag_srcNextPage);
        const matching = tag_activeTags.length > 1
          ? result.mangas.filter((m) => matchesAllTags(m, tag_activeTags))
          : result.mangas;
        if (matching.length > 0) {
          tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks);
        }
      }, ctrl.signal);
    } finally {
      if (!ctrl.signal.aborted) tag_loadingMoreSource = false;
    }
  }

  function tagToggleTag(tag: string) {
    tag_srcNextPage   = new Map();
    tag_sourceResults = [];
    tag_activeTags    = tag_activeTags.includes(tag)
      ? tag_activeTags.filter((t) => t !== tag)
      : [...tag_activeTags, tag];
  }

  function tagToggleSearchSources() {
    tag_searchSources = !tag_searchSources;
    if (tag_searchSources && tag_activeTags.length > 0 && !loadingSources) {
      tagFetchSources(tag_activeTags);
    }
  }

  // ── Source browse ─────────────────────────────────────────────────────────

  let src_selectedLang  = $state("all");
  let src_activeSource: Source | null = $state(null);
  let src_browseResults: Manga[]       = $state([]);
  let src_loadingBrowse                = $state(false);
  let src_browseQuery                  = $state("");
  let src_submitted                    = $state("");
  let src_hasNextPage                  = $state(false);
  let src_currentPage                  = $state(1);
  let src_abortCtrl: AbortController | null = null;
  let src_langPocketOpen = $state(true);
  let src_expandedGroups: Set<string> = $state(new Set());

  // Group sources by displayName — sources with same name but different langs get grouped
  interface SourceGroup {
    name:     string;
    iconUrl:  string;
    sources:  Source[];
    isNsfw:   boolean;
  }

  const src_visibleSources = $derived(src_selectedLang === "all"
    ? allSources
    : allSources.filter((s) => s.lang === src_selectedLang));

  const src_groupedSources = $derived.by(() => {
    const filtered = src_visibleSources;
    const map = new Map<string, SourceGroup>();
    for (const src of filtered) {
      const key = src.displayName;
      if (!map.has(key)) {
        map.set(key, { name: src.displayName, iconUrl: src.iconUrl, sources: [], isNsfw: src.isNsfw });
      }
      map.get(key)!.sources.push(src);
    }
    return Array.from(map.values());
  });

  function srcToggleGroup(name: string) {
    const next = new Set(src_expandedGroups);
    if (next.has(name)) next.delete(name); else next.add(name);
    src_expandedGroups = next;
  }

  async function srcFetchBrowse(src: Source, type: "POPULAR" | "SEARCH", q?: string, page = 1) {
    src_abortCtrl?.abort();
    const ctrl = new AbortController();
    src_abortCtrl = ctrl;
    if (page === 1) { src_loadingBrowse = true; src_browseResults = []; }
    try {
      const d = await gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>(
        FETCH_SOURCE_MANGA, { source: src.id, type, page, query: q ?? null }, ctrl.signal,
      );
      if (ctrl.signal.aborted) return;
      src_browseResults = page === 1 ? d.fetchSourceManga.mangas : [...src_browseResults, ...d.fetchSourceManga.mangas];
      src_hasNextPage = d.fetchSourceManga.hasNextPage;
      src_currentPage = page;
    } catch (e: any) {
      if (e?.name !== "AbortError") console.error(e);
    } finally {
      if (!ctrl.signal.aborted) src_loadingBrowse = false;
    }
  }

  function srcSelectSource(src: Source) {
    src_activeSource = src; src_browseQuery = ""; src_submitted = "";
    srcFetchBrowse(src, "POPULAR");
  }

  function srcHandleSearch() {
    if (!src_activeSource || !src_browseQuery.trim()) return;
    src_submitted = src_browseQuery.trim();
    srcFetchBrowse(src_activeSource, "SEARCH", src_browseQuery.trim());
  }

  function srcClearSearch() {
    src_browseQuery = ""; src_submitted = "";
    if (src_activeSource) srcFetchBrowse(src_activeSource, "POPULAR");
  }

  onDestroy(() => {
    kw_abortCtrl?.abort();
    tag_abortLocal?.abort();
    tag_abortSource?.abort();
    src_abortCtrl?.abort();
  });
</script>

<div class="root">
  
  <div class="header">
    <h1 class="heading">Search</h1>
    <div class="tabs">
      <button
        class="tab"
        class:tabActive={tab === "keyword"}
        onclick={() => (tab = "keyword")}
      >
        
        <svg width="11" height="11" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
          <path d="M229.66,218.34l-50.07-50.07a88,88,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.31ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>
        </svg>
        Keyword
      </button>
      <button
        class="tab"
        class:tabActive={tab === "tag"}
        onclick={() => (tab = "tag")}
      >
        
        <svg width="11" height="11" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
          <path d="M224,104H200l8-48a8,8,0,0,0-15.79-2.67L183.79,104H136l8-48a8,8,0,0,0-15.79-2.67L119.79,104H72a8,8,0,0,0,0,16h45.33L105.6,200H56a8,8,0,0,0,0,16H103l-8,48a8,8,0,0,0,15.79,2.67L119.21,216H168l-8,48a8,8,0,0,0,15.79,2.67L184.21,216H232a8,8,0,0,0,0-16H186.67l11.73-80H224a8,8,0,0,0,0-16Zm-69.33,96H101.6L113.33,120h53.07Z"/>
        </svg>
        Tags
      </button>
      <button
        class="tab"
        class:tabActive={tab === "source"}
        onclick={() => (tab = "source")}
      >
        
        <svg width="11" height="11" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
          <path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"/>
        </svg>
        Sources
      </button>
    </div>
  </div>

  

  {#if tab === "keyword"}
    <div class="keywordBar">
      <div class="searchBar">
        <svg width="14" height="14" viewBox="0 0 256 256" fill="currentColor" class="searchIcon" aria-hidden="true">
          <path d="M229.66,218.34l-50.07-50.07a88,88,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.31ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>
        </svg>
        
        <input
          bind:this={kw_inputEl}
          bind:value={kw_query}
          use:focusOnMount
          class="searchInput"
          placeholder="Search across sources…"
          onkeydown={(e) => e.key === "Enter" && kwDoSearch(kw_query)}
        />
        {#if kw_query}
          <button
            class="clearBtn"
            title="Clear"
            onclick={() => { kw_query = ""; kw_inputEl?.focus(); }}
          >×</button>
        {/if}
        {#if hasMultipleLangs}
          <button
            class="advancedBtn"
            class:advancedBtnActive={kw_showAdvanced}
            title="Language & filter options"
            onclick={() => (kw_showAdvanced = !kw_showAdvanced)}
          >
            
            <svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
              <path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H183a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h81a32,32,0,0,0,62,0h33a8,8,0,0,0,0-16Zm-64,24a16,16,0,1,1,16-16A16,16,0,0,1,152,192Z"/>
            </svg>
          </button>
        {/if}
        <button
          class="searchBtn"
          onclick={() => kwDoSearch(kw_query)}
          disabled={!kw_query.trim() || loadingSources}
        >
          {#if loadingSources}
            <svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" aria-hidden="true">
              <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
            </svg>
          {:else}
            Search
          {/if}
        </button>
      </div>

      {#if hasMultipleLangs && kw_showAdvanced}
        <div class="advancedPanel">
          <div class="advancedHeader">
            <span class="advancedTitle">Languages</span>
            <div class="advancedActions">
              <button class="advancedLink" onclick={() => (kw_selectedLangs = new Set(availableLangs))}>All</button>
              <button class="advancedLink" onclick={() => (kw_selectedLangs = new Set([preferredLang]))}>Reset</button>
            </div>
          </div>
          <div class="langGrid">
            {#each availableLangs as lang (lang)}
              <button
                class="langChip"
                class:langChipActive={kw_selectedLangs.has(lang)}
                onclick={() => kwToggleLang(lang)}
              >
                {lang === preferredLang ? `${lang.toUpperCase()} ★` : lang.toUpperCase()}
              </button>
            {/each}
          </div>
          <div class="advancedDivider"></div>
          <label class="advancedCheck">
            <input type="checkbox" bind:checked={kw_includeNsfw} class="checkbox" />
            Include NSFW sources
          </label>
          <div class="advancedFooter">
            Searching <strong>{kw_visibleCount}</strong> source{kw_visibleCount !== 1 ? "s" : ""}
          </div>
        </div>
      {/if}
    </div>

    {#if !kw_submitted}
      <div class="empty">
        <svg width="36" height="36" viewBox="0 0 256 256" fill="currentColor" class="emptyIcon" aria-hidden="true">
          <path d="M229.66,218.34l-50.07-50.07a88,88,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.31ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>
        </svg>
        <p class="emptyText">Search across sources</p>
        <p class="emptyHint">
          {#if hasMultipleLangs}
            {kw_visibleCount} source{kw_visibleCount !== 1 ? "s" : ""} · {kw_selectedLangs.size} language{kw_selectedLangs.size !== 1 ? "s" : ""}
          {:else}
            {kw_visibleCount} source{kw_visibleCount !== 1 ? "s" : ""}
          {/if}
        </p>
        {#if hasMultipleLangs && !kw_showAdvanced}
          <button class="advancedLinkStandalone" onclick={() => (kw_showAdvanced = true)}>
            <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
              <path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H183a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h81a32,32,0,0,0,62,0h33a8,8,0,0,0,0-16Zm-64,24a16,16,0,1,1,16-16A16,16,0,0,1,152,192Z"/>
            </svg>
            Adjust language filters
          </button>
        {/if}
      </div>
    {:else}
      <div class="results">
        {#if kw_results.length === 0}
          <div class="empty">
            <svg width="20" height="20" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" style="color:var(--text-faint)" aria-hidden="true">
              <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
            </svg>
          </div>
        {/if}

        {#each kw_results.filter((r) => r.mangas.length > 0 || r.loading || r.error) as { source, mangas, loading, error } (source.id)}
          <div class="sourceSection">
            <div class="sourceHeader">
              <img
                src={thumbUrl(source.iconUrl)}
                alt={source.displayName}
                class="sourceIcon"
                onerror={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
              />
              <span class="sourceName">{source.displayName}</span>
              {#if hasMultipleLangs}
                <span class="sourceLang">{source.lang.toUpperCase()}</span>
              {/if}
              {#if loading}
                <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" style="color:var(--text-faint);margin-left:auto" aria-hidden="true">
                  <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
                </svg>
              {:else if mangas.length > 0}
                <span class="resultCount">{mangas.length} results</span>
              {/if}
            </div>

            {#if error}
              <p class="sourceError">{error}</p>
            {:else if loading}
              
              <div class="sourceRow">
                {#each Array(4) as _, i (i)}
                  <div class="skCard">
                    <div class="skeleton skCover"></div>
                    <div class="skeleton skTitle"></div>
                  </div>
                {/each}
              </div>
            {:else if mangas.length > 0}
              <div class="sourceRow">
                {#each mangas.slice(0, (store.settings.renderLimit ?? 48)) as m (m.id)}
                  <button class="card" onclick={() => setPreviewManga(m)}>
                    <div class="coverWrap">
                      <img
                        src={thumbUrl(m.thumbnailUrl)}
                        alt={m.title}
                        class="cover"
                        loading="lazy"
                        decoding="async"
                      />
                      {#if m.inLibrary}<span class="inLibBadge">Saved</span>{/if}
                    </div>
                    <p class="cardTitle">{m.title}</p>
                  </button>
                {/each}
              </div>
            {/if}
          </div>
        {/each}

        {#if kw_allDone && !kw_hasResults}
          <div class="empty">
            <p class="emptyText">No results for "{kw_submitted}"</p>
            <p class="emptyHint">Try a different spelling or fewer words</p>
          </div>
        {/if}
      </div>
    {/if}

  

  {:else if tab === "tag"}
    <div class="splitRoot">
      
      <div class="splitSidebar">
        <div class="splitSearchWrap">
          <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" class="splitSearchIcon" aria-hidden="true">
            <path d="M229.66,218.34l-50.07-50.07a88,88,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.31ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>
          </svg>
          <input
            bind:value={tag_tagFilter}
            class="splitSearchInput"
            placeholder="Filter tags…"
          />
          {#if tag_tagFilter}
            <button class="splitSearchClear" title="Clear" onclick={() => (tag_tagFilter = "")}>×</button>
          {/if}
        </div>
        <div class="splitList">
          {#each tag_filteredGenres as tag (tag)}
            <button
              class="splitItem"
              class:splitItemActive={tag_activeTags.includes(tag)}
              onclick={() => tagToggleTag(tag)}
            >
              <span class="splitItemLabel">{tag}</span>
              {#if tag_activeTags.includes(tag)}<span class="tagCheckMark">✓</span>{/if}
            </button>
          {/each}
          {#if tag_filteredGenres.length === 0}
            <p class="splitEmpty">No matching tags</p>
          {/if}
        </div>
      </div>

      
      <div class="splitContent">
        {#if !tag_hasActiveTags}
          <div class="empty">
            <svg width="32" height="32" viewBox="0 0 256 256" fill="currentColor" class="emptyIcon" aria-hidden="true">
              <path d="M224,104H200l8-48a8,8,0,0,0-15.79-2.67L183.79,104H136l8-48a8,8,0,0,0-15.79-2.67L119.79,104H72a8,8,0,0,0,0,16h45.33L105.6,200H56a8,8,0,0,0,0,16H103l-8,48a8,8,0,0,0,15.79,2.67L119.21,216H168l-8,48a8,8,0,0,0,15.79,2.67L184.21,216H232a8,8,0,0,0,0-16H186.67l11.73-80H224a8,8,0,0,0,0-16Zm-69.33,96H101.6L113.33,120h53.07Z"/>
            </svg>
            <p class="emptyText">Browse by tag</p>
            <p class="emptyHint">Select one or more genre tags to find matching manga.</p>
          </div>
        {:else}
          
          <div class="tagActiveBar">
            <div class="tagPillRow">
              {#each tag_activeTags as tag (tag)}
                <span class="tagPill">
                  {tag}
                  <button class="tagPillRemove" title="Remove {tag}" onclick={() => tagToggleTag(tag)}>×</button>
                </span>
              {/each}
            </div>
            <div class="tagBarRight">
              {#if tag_activeTags.length > 1}
                <div class="tagModeToggle">
                  <button
                    class="tagModeBtn"
                    class:tagModeBtnActive={tag_tagMode === "AND"}
                    title="Match ALL tags"
                    onclick={() => (tag_tagMode = "AND")}
                  >AND</button>
                  <button
                    class="tagModeBtn"
                    class:tagModeBtnActive={tag_tagMode === "OR"}
                    title="Match ANY tag"
                    onclick={() => (tag_tagMode = "OR")}
                  >OR</button>
                </div>
              {/if}
              <button
                class="tagModeBtn"
                class:tagModeBtnActive={tag_searchSources}
                title="Also search across sources (slower, requires network)"
                disabled={loadingSources}
                onclick={tagToggleSearchSources}
              >
                
                <svg width="11" height="11" viewBox="0 0 256 256" fill="currentColor" style="margin-right:3px;vertical-align:middle" aria-hidden="true">
                  <path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24ZM101.63,168h52.74C149,186.34,140,202.87,128,215.89,116,202.87,107,186.34,101.63,168ZM98,152a145.72,145.72,0,0,1,0-48h60a145.72,145.72,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.79a161.79,161.79,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154.37,88H101.63C107,69.66,116,53.13,128,40.11,140,53.13,149,69.66,154.37,88ZM174.21,104h38.46a88.15,88.15,0,0,1,0,48H174.21a161.79,161.79,0,0,0,0-48Zm32.32-16H170.71a133.32,133.32,0,0,0-22.7-45.8A88.21,88.21,0,0,1,206.53,88ZM108,42.2A133.32,133.32,0,0,0,85.29,88H49.47A88.21,88.21,0,0,1,108,42.2ZM49.47,168H85.29A133.32,133.32,0,0,0,108,213.8,88.21,88.21,0,0,1,49.47,168Zm98.53,45.8A133.32,133.32,0,0,0,170.71,168h35.82A88.21,88.21,0,0,1,148,213.8Z"/>
                </svg>
                Sources
              </button>
              <button class="tagClearAll" onclick={() => (tag_activeTags = [])}>Clear all</button>
            </div>
          </div>

          <div class="splitContentHeader">
            <span class="splitContentTitle">
              {tag_activeTags.length === 1 ? tag_activeTags[0] : `${tag_activeTags.length} tags (${tag_tagMode})`}
              {#if tag_searchSources}
                <span style="margin-left:6px;font-weight:400;opacity:0.55;font-size:0.9em">+ sources</span>
              {/if}
            </span>
            {#if tag_loadingLocal || tag_loadingSourceSearch}
              <svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" style="color:var(--text-faint)" aria-hidden="true">
                <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
              </svg>
            {:else}
              <span class="splitResultCount">
                {tag_totalVisible}{tag_localHasNext || tag_sourceHasMore ? "+" : ""} of {tag_totalCount + tag_sourceResults.length} results
              </span>
            {/if}
          </div>

          {#if tag_loadingLocal}
            
            <div class="tagGrid">
              {#each Array(48) as _, i (i)}
                <div class="skCard">
                  <div class="skeleton skCover"></div>
                  <div class="skeleton skTitle"></div>
                </div>
              {/each}
            </div>
          {:else if tag_mergedResults.length > 0}
            <div class="tagGrid">
              {#each tag_mergedResults as m (m.id)}
                <button class="card" onclick={() => setPreviewManga(m)}>
                  <div class="coverWrap">
                    <img src={thumbUrl(m.thumbnailUrl)} alt={m.title} class="cover" loading="lazy" decoding="async" />
                    {#if m.inLibrary}<span class="inLibBadge">Saved</span>{/if}
                  </div>
                  <p class="cardTitle">{m.title}</p>
                </button>
              {/each}

              {#if tag_loadingSourceSearch}
                {#each Array(8) as _, i (i)}
                  <div class="skCard">
                    <div class="skeleton skCover"></div>
                    <div class="skeleton skTitle"></div>
                  </div>
                {/each}
              {/if}

              {#if tag_localHasNext || tag_sourceHasMore}
                <div class="showMoreCell">
                  {#if tag_localHasNext}
                    <button class="showMoreBtn" onclick={tagLoadMoreLocal} disabled={tag_loadingMoreLocal}>
                      {#if tag_loadingMoreLocal}
                        <svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" aria-hidden="true">
                          <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
                        </svg> Loading…
                      {:else}
                        Show more (library)
                      {/if}
                    </button>
                  {/if}
                  {#if tag_sourceHasMore}
                    <button class="showMoreBtn" onclick={tagLoadMoreSource} disabled={tag_loadingMoreSource}>
                      {#if tag_loadingMoreSource}
                        <svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" aria-hidden="true">
                          <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
                        </svg> Loading…
                      {:else}
                        Show more (sources)
                      {/if}
                    </button>
                  {/if}
                </div>
              {/if}
            </div>
          {:else}
            <div class="empty">
              <p class="emptyText">No results for {tag_activeTags.join(` ${tag_tagMode} `)}</p>
              <p class="emptyHint">
                {#if tag_searchSources}
                  Try OR mode or broader tags.
                {:else}
                  Try OR mode, enable Sources, or check that these manga are in your library.
                {/if}
              </p>
            </div>
          {/if}
        {/if}
      </div>
    </div>

  

  {:else if tab === "source"}
    <div class="splitRoot">
      
      <div class="splitSidebar">
        <button class="langPocketToggle" onclick={() => (src_langPocketOpen = !src_langPocketOpen)}>
            <span class="langPocketLabel">Languages</span>
            <svg width="9" height="9" viewBox="0 0 256 256" fill="currentColor"
              style="transition: transform 0.2s ease; transform: rotate({src_langPocketOpen ? 180 : 0}deg)"
              aria-hidden="true">
              <path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/>
            </svg>
          </button>
          {#if src_langPocketOpen}
            <div class="langPocket">
              {#each ["all", ...availableLangs] as lang (lang)}
                <button
                  class="langChip"
                  class:langChipActive={src_selectedLang === lang}
                  onclick={() => (src_selectedLang = lang)}
                >
                  {lang === "all" ? "All" : lang.toUpperCase()}
                </button>
              {/each}
            </div>
          {/if}

        {#if loadingSources}
          <div class="splitLoading">
            <svg width="16" height="16" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" style="color:var(--text-faint)" aria-hidden="true">
              <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
            </svg>
          </div>
        {:else}
          <div class="splitList">
            {#each src_groupedSources as group (group.name)}
              {#if group.sources.length === 1}
                <button
                  class="splitItem splitItemSource"
                  class:splitItemActive={src_activeSource?.id === group.sources[0].id}
                  onclick={() => srcSelectSource(group.sources[0])}
                >
                  <img src={thumbUrl(group.iconUrl)} alt="" class="splitSourceIcon"
                    onerror={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
                  <span class="splitItemLabel">{group.name}</span>
                  <span class="sourceLang" style="margin-left:auto;margin-right:4px">{group.sources[0].lang.toUpperCase()}</span>
                  {#if group.isNsfw}<span class="nsfwBadge">18+</span>{/if}
                </button>
              {:else}
                <button
                  class="splitItem splitItemSource splitItemGroup"
                  class:splitItemGroupOpen={src_expandedGroups.has(group.name)}
                  onclick={() => srcToggleGroup(group.name)}
                >
                  <img src={thumbUrl(group.iconUrl)} alt="" class="splitSourceIcon"
                    onerror={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
                  <span class="splitItemLabel">{group.name}</span>
                  <span class="groupLangCount">{group.sources.length}</span>
                  <svg width="8" height="8" viewBox="0 0 256 256" fill="currentColor"
                    class="groupChevron"
                    style="transform: rotate({src_expandedGroups.has(group.name) ? 180 : 0}deg)"
                    aria-hidden="true">
                    <path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/>
                  </svg>
                </button>
                {#if src_expandedGroups.has(group.name)}
                  {#each group.sources as src (src.id)}
                    <button
                      class="splitItem splitItemSource splitItemLangOption"
                      class:splitItemActive={src_activeSource?.id === src.id}
                      onclick={() => srcSelectSource(src)}
                    >
                      <span class="langOptionDot"></span>
                      <span class="splitItemLabel">{src.lang.toUpperCase()}</span>
                      {#if src.isNsfw}<span class="nsfwBadge">18+</span>{/if}
                    </button>
                  {/each}
                {/if}
              {/if}
            {/each}
            {#if src_groupedSources.length === 0}
              <p class="splitEmpty">No sources for this language</p>
            {/if}
          </div>
        {/if}
      </div>

      
      <div class="splitContent">
        {#if !src_activeSource}
          <div class="empty">
            <svg width="32" height="32" viewBox="0 0 256 256" fill="currentColor" class="emptyIcon" aria-hidden="true">
              <path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"/>
            </svg>
            <p class="emptyText">Browse a source</p>
            <p class="emptyHint">Select a source to see its popular titles, or search within it.</p>
          </div>
        {:else}
          <div class="splitContentHeader">
            <div class="splitSourceTitle">
              <img
                src={thumbUrl(src_activeSource.iconUrl)}
                alt=""
                class="splitSourceIcon"
                onerror={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
              />
              <span class="splitContentTitle">{src_activeSource.displayName}</span>
              {#if src_loadingBrowse}
                <svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" style="color:var(--text-faint)" aria-hidden="true">
                  <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
                </svg>
              {:else if src_browseResults.length > 0}
                <span class="splitResultCount">{src_browseResults.length} results</span>
              {/if}
            </div>
          </div>

          <div class="sourceBrowseBar">
            <div class="searchBar" style="flex:1">
              <svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" class="searchIcon" aria-hidden="true">
                <path d="M229.66,218.34l-50.07-50.07a88,88,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.31ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>
              </svg>
              <input
                bind:value={src_browseQuery}
                class="searchInput"
                placeholder="Search {src_activeSource.displayName}…"
                onkeydown={(e) => e.key === "Enter" && srcHandleSearch()}
              />
              {#if src_submitted}
                <button class="clearBtn" title="Clear search" onclick={srcClearSearch}>×</button>
              {/if}
            </div>
            <button
              class="searchBtn"
              onclick={srcHandleSearch}
              disabled={!src_browseQuery.trim() || src_loadingBrowse}
            >
              Search
            </button>
          </div>

          {#if src_loadingBrowse && src_browseResults.length === 0}
            
            <div class="tagGrid">
              {#each Array(18) as _, i (i)}
                <div class="skCard">
                  <div class="skeleton skCover"></div>
                  <div class="skeleton skTitle"></div>
                </div>
              {/each}
            </div>
          {:else if src_browseResults.length > 0}
            <div class="tagGrid">
              {#each src_browseResults as m (m.id)}
                <button class="card" onclick={() => setPreviewManga(m)}>
                  <div class="coverWrap">
                    <img src={thumbUrl(m.thumbnailUrl)} alt={m.title} class="cover" loading="lazy" decoding="async" />
                    {#if m.inLibrary}<span class="inLibBadge">Saved</span>{/if}
                  </div>
                  <p class="cardTitle">{m.title}</p>
                </button>
              {/each}
            </div>
            {#if src_hasNextPage}
              <div class="loadMoreRow">
                <button
                  class="showMoreBtn"
                  disabled={src_loadingBrowse}
                  onclick={() => src_activeSource && srcFetchBrowse(
                    src_activeSource,
                    src_submitted ? "SEARCH" : "POPULAR",
                    src_submitted || undefined,
                    src_currentPage + 1,
                  )}
                >
                  {#if src_loadingBrowse}
                    <svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" aria-hidden="true">
                      <path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
                    </svg> Loading…
                  {:else}
                    Load more
                  {/if}
                </button>
              </div>
            {/if}
          {:else if !src_loadingBrowse}
            <div class="empty">
              <p class="emptyText">
                {src_submitted ? `No results for "${src_submitted}"` : "No results"}
              </p>
            </div>
          {/if}
        {/if}
      </div>
    </div>
  {/if}
</div>

<style>
  /* ── Root ──────────────────────────────────────────────────────────────── */

  .root {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
    animation: fadeIn 0.14s ease both;
  }

  /* ── Header ────────────────────────────────────────────────────────────── */

  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--sp-4) var(--sp-6);
    flex-shrink: 0;
    border-bottom: 1px solid var(--border-dim);
  }

  .heading {
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: var(--weight-normal);
    color: var(--text-faint);
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
  }

  /* ── Tabs ──────────────────────────────────────────────────────────────── */

  .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);
    background: none;
    border: none;
    color: var(--text-faint);
    cursor: pointer;
    transition: background var(--t-base), color var(--t-base);
    white-space: nowrap;
  }
  .tab:hover { color: var(--text-muted); }

  .tabActive {
    background: var(--accent-muted);
    color: var(--accent-fg);
    border: 1px solid var(--accent-dim);
  }
  .tabActive:hover { color: var(--accent-fg); }

  /* ── Keyword bar ───────────────────────────────────────────────────────── */

  .keywordBar {
    padding: var(--sp-3) var(--sp-4);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
  }

  .searchBar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    background: var(--bg-raised);
    border: 1px solid var(--border-dim);
    border-radius: var(--radius-md);
    padding: 0 var(--sp-3) 0 var(--sp-2);
    transition: border-color var(--t-base);
  }
  .searchBar:focus-within { border-color: var(--border-strong); }

  .searchIcon { color: var(--text-faint); flex-shrink: 0; }

  .searchInput {
    flex: 1;
    background: none;
    border: none;
    outline: none;
    color: var(--text-primary);
    font-size: var(--text-sm);
    padding: 7px 0;
  }
  .searchInput::placeholder { color: var(--text-faint); }

  .clearBtn {
    color: var(--text-faint);
    font-size: 14px;
    line-height: 1;
    background: none;
    border: none;
    cursor: pointer;
    padding: 2px;
    transition: color var(--t-base);
  }
  .clearBtn:hover { color: var(--text-muted); }

  .advancedBtn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    border-radius: var(--radius-sm);
    background: none;
    border: none;
    color: var(--text-faint);
    flex-shrink: 0;
    cursor: pointer;
    transition: color var(--t-base), background var(--t-base);
  }
  .advancedBtn:hover { color: var(--text-muted); background: var(--bg-overlay); }
  .advancedBtnActive { color: var(--accent-fg); background: var(--accent-muted); }
  .advancedBtnActive:hover { color: var(--accent-fg); background: var(--accent-muted); }

  .searchBtn {
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-wide);
    padding: 6px 12px;
    border-radius: var(--radius-md);
    background: var(--accent-muted);
    color: var(--accent-fg);
    border: 1px solid var(--accent-dim);
    cursor: pointer;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    transition: filter var(--t-base);
  }
  .searchBtn:hover:not(:disabled) { filter: brightness(1.1); }
  .searchBtn:disabled { opacity: 0.4; cursor: default; }

  /* ── Advanced filter panel ─────────────────────────────────────────────── */

  .advancedPanel {
    background: var(--bg-raised);
    border: 1px solid var(--border-dim);
    border-radius: var(--radius-md);
    padding: var(--sp-3);
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    animation: fadeIn 0.1s ease both;
  }

  .advancedHeader {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .advancedTitle {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    color: var(--text-faint);
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
  }

  .advancedActions { display: flex; gap: var(--sp-1); }

  .advancedLink {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    color: var(--accent-fg);
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    opacity: 0.7;
    transition: opacity var(--t-base);
  }
  .advancedLink:hover { opacity: 1; }

  .langGrid {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
  }

  .langChip {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    padding: 3px 8px;
    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);
  }
  .langChip:hover { color: var(--text-muted); border-color: var(--border-strong); }

  .langChipActive {
    background: var(--accent-muted);
    border-color: var(--accent-dim);
    color: var(--accent-fg);
  }
  .langChipActive:hover { background: var(--accent-muted); color: var(--accent-fg); }

  .advancedDivider {
    height: 1px;
    background: var(--border-dim);
    margin: 2px 0;
  }

  .advancedCheck {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    color: var(--text-muted);
    cursor: pointer;
  }

  .checkbox { accent-color: var(--accent-fg); cursor: pointer; }

  .advancedFooter {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    color: var(--text-faint);
    letter-spacing: var(--tracking-wide);
  }

  .advancedLinkStandalone {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    color: var(--accent-fg);
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    opacity: 0.7;
    transition: opacity var(--t-base);
  }
  .advancedLinkStandalone:hover { opacity: 1; }

  /* ── Empty states ──────────────────────────────────────────────────────── */

  .empty {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--sp-2);
  }

  .emptyIcon { color: var(--text-faint); }
  .emptyText { font-size: var(--text-base); color: var(--text-muted); }
  .emptyHint { font-size: var(--text-sm); color: var(--text-faint); }

  /* ── Keyword results ───────────────────────────────────────────────────── */

  .results {
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
  }

  .sourceSection {
    padding: var(--sp-1) var(--sp-4) var(--sp-3);
    border-bottom: 1px solid var(--border-dim);
  }
  .sourceSection:last-child { border-bottom: none; }

  .sourceHeader {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) 0;
  }

  .sourceIcon {
    width: 18px;
    height: 18px;
    border-radius: var(--radius-sm);
    object-fit: cover;
    flex-shrink: 0;
    background: var(--bg-raised);
  }

  .sourceName {
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    color: var(--text-secondary);
  }

  .sourceLang {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    color: var(--text-faint);
    letter-spacing: var(--tracking-wide);
    background: var(--bg-raised);
    border: 1px solid var(--border-dim);
    border-radius: var(--radius-sm);
    padding: 1px 5px;
  }

  .resultCount {
    margin-left: auto;
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    color: var(--text-faint);
    letter-spacing: var(--tracking-wide);
  }

  .sourceError {
    font-size: var(--text-xs);
    color: var(--color-error);
    padding: var(--sp-1) 0;
    margin: 0;
  }

  /* Horizontal scroll row */
  .sourceRow {
    display: flex;
    gap: var(--sp-3);
    overflow-x: auto;
    padding-bottom: var(--sp-1);
    scrollbar-width: none;
  }
  .sourceRow::-webkit-scrollbar { display: none; }

  /* ── Manga card ────────────────────────────────────────────────────────── */

  .card {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    cursor: pointer;
    flex-shrink: 0;
    width: 110px;
    text-align: left;
    background: none;
    border: none;
    padding: 0;
  }
  .card:hover .cover { filter: brightness(1.06); }
  .card:hover .cardTitle { color: var(--text-primary); }

  .coverWrap {
    position: relative;
    width: 100%;
    aspect-ratio: 2 / 3;
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--bg-raised);
    border: 1px solid var(--border-dim);
    transform: translateZ(0);
  }

  .cover {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: filter var(--t-base);
  }

  .inLibBadge {
    position: absolute;
    bottom: var(--sp-1);
    right: var(--sp-1);
    background: var(--accent-dim);
    color: var(--accent-fg);
    font-family: var(--font-ui);
    font-size: 9px;
    font-weight: var(--weight-medium);
    letter-spacing: var(--tracking-wide);
    padding: 1px 5px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--accent-muted);
  }

  .cardTitle {
    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);
  }

  /* ── Skeleton ──────────────────────────────────────────────────────────── */

  .skCard {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    flex-shrink: 0;
    width: 110px;
  }

  .tagGrid .card  { width: 100%; }
  .tagGrid .skCard { width: 100%; }

  .skeleton { border-radius: var(--radius-sm); }

  .skCover {
    aspect-ratio: 2 / 3;
    width: 100%;
    border-radius: var(--radius-md);
  }

  .skTitle { height: 10px; width: 80%; }

  /* ── Split root (Tag + Source tabs) ────────────────────────────────────── */

  .splitRoot {
    flex: 1;
    display: flex;
    overflow: hidden;
  }

  /* ── Split sidebar ─────────────────────────────────────────────────────── */

  .splitSidebar {
    width: 180px;
    flex-shrink: 0;
    border-right: 1px solid var(--border-dim);
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }

  .splitSearchWrap {
    display: flex;
    align-items: center;
    gap: var(--sp-1);
    padding: var(--sp-2) var(--sp-3);
    border-bottom: 1px solid var(--border-dim);
    flex-shrink: 0;
  }

  .splitSearchIcon { color: var(--text-faint); flex-shrink: 0; }

  .splitSearchInput {
    flex: 1;
    background: none;
    border: none;
    outline: none;
    font-size: var(--text-xs);
    color: var(--text-primary);
    font-family: var(--font-ui);
    min-width: 0;
  }
  .splitSearchInput::placeholder { color: var(--text-faint); }

  .splitSearchClear {
    color: var(--text-faint);
    font-size: 13px;
    line-height: 1;
    background: none;
    border: none;
    cursor: pointer;
    padding: 2px;
    transition: color var(--t-base);
  }
  .splitSearchClear:hover { color: var(--text-muted); }

  .splitList {
    flex: 1;
    overflow-y: auto;
    padding: var(--sp-1);
    scrollbar-width: thin;
    scrollbar-color: var(--border-dim) transparent;
  }

  .splitItem {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    width: 100%;
    padding: 7px var(--sp-3);
    border-radius: var(--radius-md);
    border: 1px solid transparent;
    background: none;
    text-align: left;
    cursor: pointer;
    transition: background var(--t-fast), border-color var(--t-fast);
  }
  .splitItem:hover { background: var(--bg-raised); border-color: var(--border-dim); }

  .splitItemActive {
    background: var(--accent-muted);
    border-color: var(--accent-dim);
  }
  .splitItemActive:hover { background: var(--accent-muted); }

  .splitItemLabel {
    font-size: var(--text-xs);
    color: var(--text-muted);
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .splitItemActive .splitItemLabel { color: var(--accent-fg); font-weight: var(--weight-medium); }

  .splitItemSource { gap: var(--sp-2); }

  .splitEmpty {
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    color: var(--text-faint);
    padding: var(--sp-3);
    margin: 0;
  }

  .splitLoading {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--sp-6);
  }

  /* ── Split content ─────────────────────────────────────────────────────── */

  .splitContent {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  .splitContentHeader {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--border-dim);
    flex-shrink: 0;
    gap: var(--sp-2);
  }

  .splitSourceTitle {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex: 1;
    min-width: 0;
  }

  .splitContentTitle {
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    letter-spacing: var(--tracking-tight);
  }

  .splitResultCount {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    color: var(--text-faint);
    letter-spacing: var(--tracking-wide);
    flex-shrink: 0;
  }

  .splitSourceIcon {
    width: 18px;
    height: 18px;
    border-radius: var(--radius-sm);
    object-fit: cover;
    flex-shrink: 0;
    background: var(--bg-raised);
  }

  /* ── Tag active bar ────────────────────────────────────────────────────── */

  .tagActiveBar {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    border-bottom: 1px solid var(--border-dim);
    flex-shrink: 0;
    flex-wrap: wrap;
  }

  .tagPillRow {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
    flex: 1;
    min-width: 0;
  }

  .tagPill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 7px;
    background: var(--accent-muted);
    border: 1px solid var(--accent-dim);
    border-radius: var(--radius-sm);
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    color: var(--accent-fg);
  }

  .tagPillRemove {
    color: var(--accent-fg);
    opacity: 0.6;
    font-size: 13px;
    line-height: 1;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
    transition: opacity var(--t-base);
  }
  .tagPillRemove:hover { opacity: 1; }

  .tagBarRight {
    display: flex;
    align-items: center;
    gap: 4px;
    flex-shrink: 0;
  }

  .tagModeToggle {
    display: flex;
    border: 1px solid var(--border-dim);
    border-radius: var(--radius-md);
    overflow: hidden;
  }

  .tagModeBtn {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 4px 8px;
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    color: var(--text-faint);
    background: none;
    border: none;
    border-right: 1px solid var(--border-dim);
    cursor: pointer;
    transition: color var(--t-base), background var(--t-base);
  }
  .tagModeBtn:last-child { border-right: none; }
  .tagModeBtn:hover { color: var(--text-muted); background: var(--bg-raised); }
  .tagModeBtnActive { color: var(--accent-fg); background: var(--accent-muted); }
  .tagModeBtnActive:hover { color: var(--accent-fg); background: var(--accent-muted); }

  .tagClearAll {
    display: flex;
    align-items: center;
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    color: var(--text-faint);
    padding: 4px 8px;
    border-radius: var(--radius-md);
    border: 1px solid var(--border-dim);
    background: none;
    cursor: pointer;
    transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
  }
  .tagClearAll:hover {
    color: var(--color-error);
    border-color: color-mix(in srgb, var(--color-error) 40%, transparent);
    background: var(--color-error-bg, color-mix(in srgb, var(--color-error) 8%, transparent));
  }

  .tagCheckMark {
    font-size: var(--text-xs);
    color: var(--accent-fg);
    margin-left: auto;
  }

  /* ── Grid results ──────────────────────────────────────────────────────── */

  .tagGrid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
    gap: var(--sp-4);
    padding: var(--sp-4);
    overflow-y: auto;
    flex: 1;
    align-content: start;
  }

  /* ── Show more / load more ─────────────────────────────────────────────── */

  .showMoreCell {
    grid-column: 1 / -1;
    display: flex;
    justify-content: center;
    gap: var(--sp-2);
    padding: var(--sp-2) 0;
  }

  .showMoreBtn {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 5px 12px;
    border-radius: var(--radius-md);
    border: 1px solid var(--border-dim);
    background: none;
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
    cursor: pointer;
    transition: background var(--t-base), color var(--t-base), border-color var(--t-base);
  }
  .showMoreBtn:hover:not(:disabled) {
    background: var(--bg-raised);
    color: var(--text-secondary);
    border-color: var(--border-strong);
  }
  .showMoreBtn:disabled { opacity: 0.4; cursor: default; }

  .loadMoreRow {
    display: flex;
    justify-content: center;
    padding: var(--sp-3) var(--sp-4);
    flex-shrink: 0;
    border-top: 1px solid var(--border-dim);
  }

  /* ── Source tab: lang filter + browse bar ──────────────────────────────── */

  .sourceBrowseBar {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-4);
    border-bottom: 1px solid var(--border-dim);
    flex-shrink: 0;
  }

  /* ── NSFW badge ────────────────────────────────────────────────────────── */

  .nsfwBadge {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    letter-spacing: var(--tracking-wide);
    color: var(--color-error);
    background: var(--color-error-bg, rgba(180, 60, 60, 0.08));
    border: 1px solid rgba(180, 60, 60, 0.25);
    border-radius: var(--radius-sm);
    padding: 1px 5px;
    margin-left: auto;
    flex-shrink: 0;
  }

  /* ── Language pocket ───────────────────────────────────────────────────── */

  .langPocketToggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    padding: var(--sp-2) var(--sp-3);
    border-bottom: 1px solid var(--border-dim);
    border-top: none;
    border-left: none;
    border-right: none;
    background: none;
    cursor: pointer;
    flex-shrink: 0;
    transition: background var(--t-fast);
  }
  .langPocketToggle:hover { background: var(--bg-raised); }

  .langPocketLabel {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    color: var(--text-faint);
    letter-spacing: var(--tracking-wider);
    text-transform: uppercase;
  }

  .langPocket {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
    padding: var(--sp-2) var(--sp-3);
    border-bottom: 1px solid var(--border-dim);
    flex-shrink: 0;
    animation: fadeIn 0.1s ease both;
  }

  /* ── Source group (multi-lang) ─────────────────────────────────────────── */

  .splitItemGroup { }
  .splitItemGroupOpen { background: var(--bg-raised); }

  .groupLangCount {
    font-family: var(--font-ui);
    font-size: var(--text-2xs);
    color: var(--text-faint);
    background: var(--bg-overlay);
    border: 1px solid var(--border-dim);
    border-radius: var(--radius-sm);
    padding: 0px 5px;
    flex-shrink: 0;
    letter-spacing: var(--tracking-wide);
  }

  .groupChevron {
    color: var(--text-faint);
    flex-shrink: 0;
    transition: transform 0.2s ease;
  }

  .splitItemLangOption {
    padding-left: var(--sp-5);
    background: var(--bg-overlay);
  }
  .splitItemLangOption:hover { background: var(--bg-raised); }

  .langOptionDot {
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: var(--border-strong);
    flex-shrink: 0;
  }
  .splitItemActive .langOptionDot { background: var(--accent-fg); }
</style>

<script module>
  function focusOnMount(node: HTMLElement) { node.focus(); }
</script>
