<script lang="ts">
  import { X, MagnifyingGlass, CircleNotch, ArrowRight, Check, Warning, Sparkle } from "phosphor-svelte";
  import { untrack } from "svelte";
  import { gql, thumbUrl } from "../../lib/client";
  import { GET_SOURCES, FETCH_SOURCE_MANGA, FETCH_CHAPTERS, UPDATE_MANGA, UPDATE_CHAPTERS_PROGRESS } from "../../lib/queries";
  import type { Manga, Source, Chapter } from "../../lib/types";

  interface Props {
    manga:           Manga;
    currentChapters: Chapter[];
    onClose:         () => void;
    onMigrated:      (newManga: Manga) => void;
  }
  let { manga, currentChapters, onClose, onMigrated }: Props = $props();

  type Step = "source" | "search" | "confirm";

  interface Match {
    manga: Manga;
    chapters: Chapter[];
    readCount: number;
    similarity: number;
  }

  function titleSimilarity(a: string, b: string): number {
    const norm = (s: string) =>
      s.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter(Boolean);
    const wordsA = new Set(norm(a));
    const wordsB = new Set(norm(b));
    if (wordsA.size === 0 || wordsB.size === 0) return 0;
    const intersection = [...wordsA].filter((w) => wordsB.has(w)).length;
    const union = new Set([...wordsA, ...wordsB]).size;
    return intersection / union;
  }

  let step: Step            = $state("source");
  let sources: Source[]     = $state([]);
  let loadingSources        = $state(true);
  let selectedSource: Source | null = $state(null);
  let query                         = $state(untrack(() => manga.title));
  let results: { manga: Manga; similarity: number }[] = $state([]);
  let searching             = $state(false);
  let selectedMatch: Match | null = $state(null);
  let loadingMatchId: number | null = $state(null);
  let migrating             = $state(false);
  let error: string | null  = $state(null);
  const readCount   = $derived(currentChapters.filter((c) => c.isRead).length);
  const totalCount  = $derived(currentChapters.length);
  const chapterDiff = $derived(selectedMatch ? selectedMatch.chapters.length - totalCount : 0);
  const STEPS = ["source", "search", "confirm"] as const satisfies Step[];
  const stepIdx = $derived(STEPS.indexOf(step));

  $effect(() => {
    gql<{ sources: { nodes: Source[] } }>(GET_SOURCES)
      .then((d) => { sources = d.sources.nodes.filter((s) => s.id !== "0" && s.id !== manga.source?.id); })
      .catch(console.error)
      .finally(() => { loadingSources = false; });

    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  });

  function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); }

  async function searchSource(src: Source, q: string) {
    if (!src || !q.trim()) return;
    searching = true; results = []; error = null;
    try {
      const d = await gql<{ fetchSourceManga: { mangas: Manga[] } }>(FETCH_SOURCE_MANGA, {
        source: src.id, type: "SEARCH", page: 1, query: q.trim(),
      });
      const scored = d.fetchSourceManga.mangas.map((m) => ({
        manga: m,
        similarity: titleSimilarity(manga.title, m.title),
      }));
      scored.sort((a, b) => b.similarity - a.similarity);
      results = scored;
    } catch (e: any) {
      error = e.message;
    } finally {
      searching = false;
    }
  }

  function pickSource(src: Source) {
    selectedSource = src;
    step = "search";
    searchSource(src, query);
  }

  async function selectMatch(m: Manga, similarity: number) {
    loadingMatchId = m.id; error = null;
    try {
      const d = await gql<{ fetchChapters: { chapters: Chapter[] } }>(FETCH_CHAPTERS, { mangaId: m.id });
      const chapters = d.fetchChapters.chapters;
      const matchReadCount = chapters.filter((c) => {
        const old = currentChapters.find((o) => Math.abs(o.chapterNumber - c.chapterNumber) < 0.01);
        return old?.isRead;
      }).length;
      selectedMatch = { manga: m, chapters, readCount: matchReadCount, similarity };
      step = "confirm";
    } catch (e: any) {
      error = e.message;
    } finally {
      loadingMatchId = null;
    }
  }

  async function migrate() {
    if (!selectedMatch) return;
    migrating = true; error = null;
    try {
      const { manga: newManga, chapters: newChapters } = selectedMatch;
      const oldByNum = new Map(currentChapters.map((c) => [Math.round(c.chapterNumber * 100), c]));

      const toMarkRead: number[] = [];
      const toMarkBookmarked: number[] = [];
      const progressUpdates: { id: number; lastPageRead: number }[] = [];

      for (const nc of newChapters) {
        const key = Math.round(nc.chapterNumber * 100);
        const old = oldByNum.get(key);
        if (!old) continue;
        if (old.isRead) toMarkRead.push(nc.id);
        if (old.isBookmarked) toMarkBookmarked.push(nc.id);
        if ((old.lastPageRead ?? 0) > 0 && !old.isRead)
          progressUpdates.push({ id: nc.id, lastPageRead: old.lastPageRead! });
      }

      if (toMarkRead.length)
        await gql(UPDATE_CHAPTERS_PROGRESS, { ids: toMarkRead, isRead: true });
      if (toMarkBookmarked.length)
        await gql(UPDATE_CHAPTERS_PROGRESS, { ids: toMarkBookmarked, isBookmarked: true });
      for (const { id, lastPageRead } of progressUpdates)
        await gql(UPDATE_CHAPTERS_PROGRESS, { ids: [id], lastPageRead });

      await gql(UPDATE_MANGA, { id: newManga.id, inLibrary: true });
      await gql(UPDATE_MANGA, { id: manga.id, inLibrary: false });

      onMigrated({ ...newManga, inLibrary: true });
    } catch (e: any) {
      error = e.message;
      migrating = false;
    }
  }
</script>

<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
  <div class="modal">

    <!-- Header -->
    <div class="modal-header">
      <div class="modal-title">
        <span class="modal-title-label">Migrate source</span>
        <span class="modal-title-manga">{manga.title}</span>
      </div>
      <button class="close-btn" onclick={onClose}><X size={14} weight="light" /></button>
    </div>

    <!-- Step indicators -->
    <div class="steps">
      {#each STEPS as st, i}
        <div class="step" class:step-active={step === st} class:step-done={i < stepIdx}>
          <span class="step-dot">
            {#if i < stepIdx}<Check size={9} weight="bold" />{:else}{i + 1}{/if}
          </span>
          <span class="step-label">
            {st === "source" ? "Pick source" : st === "search" ? (selectedSource ? selectedSource.displayName : "Search") : "Confirm"}
          </span>
        </div>
      {/each}
    </div>

    <!-- Body -->
    <div class="body">

      <!-- Step 1: Pick source -->
      {#if step === "source"}
        <div class="source-list">
          {#if loadingSources}
            <div class="centered">
              <CircleNotch size={16} weight="light" class="anim-spin" style="color:var(--text-faint)" />
            </div>
          {:else if sources.length === 0}
            <div class="centered"><span class="hint">No other sources installed.</span></div>
          {:else}
            {#each sources as src}
              <button
                class="source-row"
                class:source-row-active={selectedSource?.id === src.id}
                onclick={() => pickSource(src)}>
                <img src={thumbUrl(src.iconUrl)} alt={src.name} class="source-icon"
                  onerror={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
                <div class="source-info">
                  <span class="source-name">{src.displayName}</span>
                  <span class="source-meta">{src.lang.toUpperCase()}{src.isNsfw ? " · NSFW" : ""}</span>
                </div>
                <ArrowRight size={13} weight="light" class="source-arrow" />
              </button>
            {/each}
          {/if}
        </div>

      <!-- Step 2: Search & pick match -->
      {:else if step === "search"}
        <div class="search-step">

          <!-- Source context pill -->
          {#if selectedSource}
            <div class="search-context">
              <img src={thumbUrl(selectedSource.iconUrl)} alt="" class="search-context-icon"
                onerror={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
              <span class="search-context-name">{selectedSource.displayName}</span>
              <button class="search-context-change" onclick={() => { step = "source"; results = []; }}>Change</button>
            </div>
          {/if}

          <div class="search-row">
            <div class="search-bar">
              <MagnifyingGlass size={13} weight="light" class="search-icon" />
                <input class="search-input" bind:value={query}
                onkeydown={(e) => e.key === "Enter" && selectedSource && searchSource(selectedSource, query)}
                placeholder="Search title…" use:focusOnMount />
            </div>
            <button class="search-btn"
              onclick={() => selectedSource && searchSource(selectedSource, query)}
              disabled={searching || !selectedSource}>
              {#if searching}
                <CircleNotch size={13} weight="light" class="anim-spin" />
              {:else}
                <MagnifyingGlass size={12} weight="bold" /> Search
              {/if}
            </button>
          </div>

          {#if error}<p class="error"><Warning size={13} weight="light" /> {error}</p>{/if}

          <div class="results">
            {#if searching}
              {#each Array(6) as _, i}
                <div class="sk-result">
                  <div class="skeleton sk-cover"></div>
                  <div class="sk-meta">
                    <div class="skeleton sk-title"></div>
                    <div class="skeleton sk-title" style="width:40%"></div>
                  </div>
                </div>
              {/each}
            {:else}
              {#each results as { manga: m, similarity }, idx}
                <button class="result-row"
                  onclick={() => selectMatch(m, similarity)}
                  disabled={loadingMatchId !== null}>
                  <div class="result-cover-wrap">
                    <img src={thumbUrl(m.thumbnailUrl)} alt={m.title} class="result-cover" />
                  </div>
                  <div class="result-info">
                    <span class="result-title">{m.title}</span>
                    <div class="result-meta">
                      {#if idx === 0 && similarity > 0.5}
                        <span class="best-match-badge"><Sparkle size={9} weight="fill" /> Best match</span>
                      {/if}
                      <span class="sim-bar"><span class="sim-fill" style="width:{Math.round(similarity * 100)}%"></span></span>
                      <span class="sim-label">{Math.round(similarity * 100)}% match</span>
                    </div>
                  </div>
                  {#if loadingMatchId === m.id}
                    <CircleNotch size={13} weight="light" class="anim-spin" style="color:var(--text-faint);flex-shrink:0" />
                  {:else}
                    <ArrowRight size={13} weight="light" style="color:var(--text-faint);flex-shrink:0;opacity:0.5" />
                  {/if}
                </button>
              {/each}
              {#if !searching && results.length === 0 && !error}
                <div class="centered">
                  <span class="hint">{query ? "No results — try a different title." : "Enter a title to search."}</span>
                </div>
              {/if}
            {/if}
          </div>
        </div>

      <!-- Step 3: Confirm -->
      {:else if step === "confirm" && selectedMatch}
        <div class="confirm-step">
          <div class="confirm-row">
            <div class="confirm-manga">
              <div class="confirm-cover-wrap">
                <img src={thumbUrl(manga.thumbnailUrl)} alt={manga.title} class="confirm-cover" />
              </div>
              <p class="confirm-title">{manga.title}</p>
              <p class="confirm-source">{manga.source?.displayName ?? "Unknown"}</p>
              <span class="confirm-tag">Current</span>
            </div>

            <div class="confirm-divider">
              <ArrowRight size={16} weight="light" class="confirm-arrow" />
            </div>

            <div class="confirm-manga">
              <div class="confirm-cover-wrap">
                <img src={thumbUrl(selectedMatch.manga.thumbnailUrl)} alt={selectedMatch.manga.title} class="confirm-cover" />
              </div>
              <p class="confirm-title">{selectedMatch.manga.title}</p>
              <p class="confirm-source">{selectedSource?.displayName ?? "Unknown"}</p>
              <span class="confirm-tag confirm-tag-new">New</span>
            </div>
          </div>

          <div class="confirm-stats">
            <div class="stat-row">
              <span class="stat-label">Title match</span>
              <span class="stat-val"
                class:stat-good={selectedMatch.similarity > 0.7}
                class:stat-warn={selectedMatch.similarity > 0.4 && selectedMatch.similarity <= 0.7}
                class:stat-bad={selectedMatch.similarity <= 0.4}>
                {Math.round(selectedMatch.similarity * 100)}%
              </span>
            </div>
            <div class="stat-row">
              <span class="stat-label">Chapters on new source</span>
              <span class="stat-val" class:stat-warn={chapterDiff < -5}>
                {selectedMatch.chapters.length}
                {#if chapterDiff !== 0}
                  <span class="chapter-diff">{chapterDiff > 0 ? `+${chapterDiff}` : chapterDiff} vs current</span>
                {/if}
              </span>
            </div>
            <div class="stat-row">
              <span class="stat-label">Read progress to carry over</span>
              <span class="stat-val">{selectedMatch.readCount} / {readCount} chapters</span>
            </div>
          </div>

          {#if chapterDiff < -5}
            <div class="warn-box">
              <Warning size={13} weight="light" />
              New source has {Math.abs(chapterDiff)} fewer chapters — some content may be missing.
            </div>
          {/if}

          <p class="confirm-note">The current entry will be removed from your library. Downloads are not transferred.</p>

          {#if error}<p class="error"><Warning size={13} weight="light" /> {error}</p>{/if}

          <div class="confirm-actions">
            <button class="back-btn" onclick={() => step = "search"} disabled={migrating}>Back</button>
            <button class="migrate-btn" onclick={migrate} disabled={migrating}>
              {#if migrating}
                <CircleNotch size={13} weight="light" class="anim-spin" /> Migrating…
              {:else}
                <Check size={13} weight="bold" /> Migrate
              {/if}
            </button>
          </div>
        </div>
      {/if}

    </div>
  </div>
</div>

<style>
  .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 200; animation: fadeIn 0.1s ease both; }
  .modal { background: var(--bg-base); border: 1px solid var(--border-base); border-radius: var(--radius-xl); width: 520px; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 40px rgba(0,0,0,0.5); }
  .modal-header { display: flex; align-items: flex-start; justify-content: space-between; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
  .modal-title { display: flex; flex-direction: column; gap: 2px; }
  .modal-title-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
  .modal-title-manga { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); letter-spacing: var(--tracking-tight); }
  .close-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: var(--radius-md); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; }
  .close-btn:hover { color: var(--text-muted); background: var(--bg-raised); }

  /* Steps */
  .steps { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-3) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
  .step { display: flex; align-items: center; gap: var(--sp-2); opacity: 0.4; transition: opacity var(--t-base); }
  .step + .step::before { content: "›"; color: var(--text-faint); margin-right: var(--sp-1); font-size: var(--text-sm); }
  .step-active { opacity: 1; }
  .step-done   { opacity: 0.6; }
  .step-dot { width: 18px; height: 18px; border-radius: 50%; background: var(--bg-raised); border: 1px solid var(--border-base); display: flex; align-items: center; justify-content: center; font-family: var(--font-ui); font-size: 10px; color: var(--text-faint); flex-shrink: 0; }
  .step-active .step-dot { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
  .step-label { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-muted); }
  .step-active .step-label { color: var(--text-secondary); }

  /* Body */
  .body { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
  .centered { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-8); }
  .hint { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }

  /* Source list */
  .source-list { flex: 1; overflow-y: auto; padding: var(--sp-2); display: flex; flex-direction: column; gap: 1px; }
  .source-row { display: flex; align-items: center; gap: var(--sp-3); padding: 9px var(--sp-3); border-radius: var(--radius-md); border: 1px solid transparent; background: none; text-align: left; width: 100%; cursor: pointer; transition: background var(--t-fast), border-color var(--t-fast); }
  .source-row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
  .source-row-active { background: var(--accent-muted); border-color: var(--accent-dim); }
  .source-icon { width: 28px; height: 28px; border-radius: var(--radius-md); object-fit: cover; flex-shrink: 0; background: var(--bg-raised); }
  .source-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
  .source-name { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .source-meta { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
  :global(.source-arrow) { color: var(--text-faint); opacity: 0; transition: opacity var(--t-base); }
  .source-row:hover :global(.source-arrow) { opacity: 1; }

  /* Search step */
  .search-step { flex: 1; overflow: hidden; display: flex; flex-direction: column; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); }
  .search-context { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); flex-shrink: 0; }
  .search-context-icon { width: 18px; height: 18px; border-radius: var(--radius-sm); object-fit: cover; flex-shrink: 0; }
  .search-context-name { flex: 1; font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .search-context-change { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--accent-fg); background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; transition: opacity var(--t-base); }
  .search-context-change:hover { opacity: 0.75; }
  .search-row { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; }
  .search-bar { flex: 1; 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); }
  .search-bar:focus-within { border-color: var(--border-strong); }
  :global(.search-icon) { color: var(--text-faint); flex-shrink: 0; }
  .search-input { flex: 1; background: none; border: none; outline: none; color: var(--text-primary); font-size: var(--text-sm); padding: 7px 0; }
  .search-input::placeholder { color: var(--text-faint); }
  .search-btn { 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); }
  .search-btn:hover:not(:disabled) { filter: brightness(1.1); }
  .search-btn:disabled { opacity: 0.4; cursor: default; }
  .results { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; }
  .result-row { display: flex; align-items: center; gap: var(--sp-3); padding: 7px var(--sp-2); border-radius: var(--radius-md); border: none; background: none; text-align: left; width: 100%; cursor: pointer; transition: background var(--t-fast); }
  .result-row:hover:not(:disabled) { background: var(--bg-raised); }
  .result-row:disabled { opacity: 0.5; cursor: default; }
  .result-cover-wrap { width: 36px; height: 54px; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); flex-shrink: 0; }
  .result-cover { width: 100%; height: 100%; object-fit: cover; }
  .result-info { flex: 1; display: flex; flex-direction: column; gap: 4px; overflow: hidden; min-width: 0; }
  .result-title { font-size: var(--text-sm); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .result-meta { display: flex; align-items: center; gap: var(--sp-2); }
  .best-match-badge { display: inline-flex; align-items: center; gap: 3px; font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--accent-fg); background: var(--accent-muted); border: 1px solid var(--accent-dim); padding: 1px 5px; border-radius: var(--radius-sm); flex-shrink: 0; }
  .sim-bar { width: 48px; height: 3px; background: var(--bg-overlay); border-radius: var(--radius-full); overflow: hidden; flex-shrink: 0; display: inline-block; }
  .sim-fill { display: block; height: 100%; background: var(--accent); border-radius: var(--radius-full); transition: width 0.2s ease; }
  .sim-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; }

  /* Skeletons */
  .sk-result { display: flex; align-items: center; gap: var(--sp-3); padding: 7px var(--sp-2); }
  .sk-cover { width: 36px; height: 54px; border-radius: var(--radius-sm); flex-shrink: 0; }
  .sk-meta { flex: 1; display: flex; flex-direction: column; gap: var(--sp-2); }
  .sk-title { height: 13px; width: 65%; border-radius: var(--radius-sm); }

  /* Confirm step */
  .confirm-step { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-4); padding: var(--sp-4) var(--sp-5); }
  .confirm-row { display: flex; align-items: center; justify-content: center; gap: var(--sp-4); }
  .confirm-manga { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); flex: 1; max-width: 160px; }
  .confirm-cover-wrap { width: 100%; aspect-ratio: 2/3; border-radius: var(--radius-md); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); }
  .confirm-cover { width: 100%; height: 100%; object-fit: cover; }
  .confirm-title { font-size: var(--text-xs); color: var(--text-secondary); text-align: center; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: var(--leading-snug); }
  .confirm-source { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); text-align: center; }
  .confirm-divider { display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
  :global(.confirm-arrow) { color: var(--text-faint); }
  .confirm-tag { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 2px 7px; border-radius: var(--radius-full); background: var(--bg-raised); border: 1px solid var(--border-dim); color: var(--text-faint); }
  .confirm-tag-new { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
  .confirm-stats { display: flex; flex-direction: column; gap: var(--sp-2); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: var(--sp-3) var(--sp-4); }
  .stat-row { display: flex; justify-content: space-between; align-items: center; }
  .stat-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); }
  .stat-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); }
  .stat-good { color: var(--color-success) !important; }
  .stat-warn { color: #d97706 !important; }
  .stat-bad  { color: var(--color-error) !important; }
  .chapter-diff { font-family: var(--font-ui); font-size: var(--text-2xs); color: #d97706; letter-spacing: var(--tracking-wide); margin-left: var(--sp-2); }
  .warn-box { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); background: rgba(217,119,6,0.08); border: 1px solid rgba(217,119,6,0.25); border-radius: var(--radius-md); font-size: var(--text-xs); color: #d97706; line-height: var(--leading-snug); }
  .confirm-note { font-size: var(--text-xs); color: var(--text-faint); line-height: var(--leading-base); }
  .confirm-actions { display: flex; justify-content: flex-end; gap: var(--sp-2); flex-shrink: 0; }
  .back-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 6px 10px; border-radius: var(--radius-md); background: none; color: var(--text-muted); border: 1px solid var(--border-dim); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
  .back-btn:hover:not(:disabled) { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised); }
  .back-btn:disabled { opacity: 0.4; cursor: default; }
  .migrate-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: 7px 16px; border-radius: var(--radius-md); background: var(--accent-dim); border: 1px solid var(--accent); color: var(--accent-fg); cursor: pointer; transition: background var(--t-base), border-color var(--t-base); }
  .migrate-btn:hover:not(:disabled) { background: var(--accent-muted); border-color: var(--accent-bright); }
  .migrate-btn:disabled { opacity: 0.5; cursor: default; }

  /* Error */
  .error { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-xs); color: var(--color-error); padding: var(--sp-2) var(--sp-3); background: rgba(180,60,60,0.08); border-radius: var(--radius-md); border: 1px solid rgba(180,60,60,0.2); }

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

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