<script lang="ts">
    import { onMount, onDestroy } from 'svelte';
    import { ApiError } from '$lib/api/client';
    import {
        reenqueueAnalysis,
        analyzePage,
        getAnalysisMangaCoverage,
        getAnalysisChapterCoverage,
        getAnalysisChapterPages,
        getAnalysisPageDetail,
        analysisStatusStreamUrl,
        type MangaCoverage,
        type ChapterCoverage,
        type PageStatusItem,
        type PageAnalysisDetail,
        type ReenqueueAnalysisOptions,
        type AnalysisEvent
    } from '$lib/api/admin';
    import { chapterLabel } from '$lib/api/chapters';
    import CoverageBadge from '$lib/components/CoverageBadge.svelte';
    import Modal from '$lib/components/Modal.svelte';

    const LIMIT = 25;

    // Global enqueue toggle.
    let includeAnalyzed = $state(false);

    // Coverage overview / search.
    let query = $state('');
    let searchTimer: ReturnType<typeof setTimeout> | null = null;
    let mangas = $state<MangaCoverage[]>([]);
    let total = $state(0);
    let offset = $state(0);
    let loading = $state(true);
    let loadingMore = $state(false);

    // Drill-down state.
    let expandedManga = $state<string | null>(null);
    let chapters = $state<Record<string, 'loading' | ChapterCoverage[]>>({});
    let expandedChapter = $state<string | null>(null);
    let pages = $state<Record<string, 'loading' | PageStatusItem[]>>({});

    // Page-detail modal.
    let detailOpen = $state(false);
    let detail = $state<PageAnalysisDetail | null>(null);
    let detailLoading = $state(false);
    let detailError = $state<string | null>(null);
    let detailQueueBusy = $state(false);

    // Enqueue feedback.
    let busyKey = $state<string | null>(null);
    let notice = $state<string | null>(null);
    let error = $state<string | null>(null);

    // --- Live status (SSE) ---
    type LiveStatus = 'analyzing' | 'done' | 'failed' | 'queued';
    // Per-page override applied on top of the fetched grid status.
    let liveStatus = $state<Record<string, LiveStatus>>({});
    // Pages we've already live-counted into coverage, so a repeat
    // `completed` can't double-bump a badge.
    let counted = new Set<string>();
    let ticker = $state<{ id: number; text: string }[]>([]);
    let tickerSeq = 0;
    let live = $state(false);
    let source: EventSource | null = null;
    let sseErrors = 0;

    onMount(() => {
        void loadCoverage(true);
        openStream();
    });

    onDestroy(() => {
        source?.close();
        source = null;
    });

    function pushTicker(text: string) {
        ticker = [{ id: ++tickerSeq, text }, ...ticker].slice(0, 8);
    }

    /** Effective chip status: live override wins over the fetched value. */
    function chipStatus(p: PageStatusItem): string {
        return liveStatus[p.page_id] ?? p.status;
    }

    /** mangaId that owns a loaded chapter, via the chapters cache. */
    function mangaOfChapter(chapterId: string): string | null {
        for (const [mid, val] of Object.entries(chapters)) {
            if (Array.isArray(val) && val.some((c) => c.chapter_id === chapterId)) {
                return mid;
            }
        }
        return null;
    }

    function bumpCoverage(mangaId: string, chapterId: string) {
        mangas = mangas.map((m) =>
            m.manga_id === mangaId
                ? {
                      ...m,
                      analyzed_pages: Math.min(m.total_pages, m.analyzed_pages + 1)
                  }
                : m
        );
        const chs = chapters[mangaId];
        if (Array.isArray(chs)) {
            chapters = {
                ...chapters,
                [mangaId]: chs.map((c) =>
                    c.chapter_id === chapterId
                        ? {
                              ...c,
                              analyzed_pages: Math.min(
                                  c.total_pages,
                                  c.analyzed_pages + 1
                              )
                          }
                        : c
                )
            };
        }
    }

    /** Mark loaded, in-scope, not-yet-done pages as queued. */
    function markQueued(ev: Extract<AnalysisEvent, { kind: 'enqueued' }>) {
        const next = { ...liveStatus };
        for (const [chapterId, val] of Object.entries(pages)) {
            if (!Array.isArray(val)) continue;
            const inScope =
                ev.chapter_id != null
                    ? chapterId === ev.chapter_id
                    : ev.manga_id != null
                      ? mangaOfChapter(chapterId) === ev.manga_id
                      : true; // whole library
            if (!inScope) continue;
            for (const p of val) {
                const cur: string = next[p.page_id] ?? p.status;
                if (cur === 'none' || cur === 'failed') next[p.page_id] = 'queued';
            }
        }
        liveStatus = next;
    }

    function applyEvent(ev: AnalysisEvent) {
        if (ev.kind === 'enqueued') {
            const scope = ev.chapter_id
                ? ' (chapter)'
                : ev.manga_id
                  ? ' (manga)'
                  : ' (library)';
            pushTicker(`Queued ${ev.count} page${ev.count === 1 ? '' : 's'}${scope}`);
            markQueued(ev);
            return;
        }
        if (ev.kind === 'started') {
            liveStatus = { ...liveStatus, [ev.page_id]: 'analyzing' };
            pushTicker(`Analyzing page ${ev.page_number}`);
        } else if (ev.kind === 'completed') {
            liveStatus = { ...liveStatus, [ev.page_id]: 'done' };
            if (!counted.has(ev.page_id)) {
                counted.add(ev.page_id);
                bumpCoverage(ev.manga_id, ev.chapter_id);
            }
            pushTicker(`✓ Analyzed page ${ev.page_number}`);
        } else if (ev.kind === 'failed') {
            liveStatus = { ...liveStatus, [ev.page_id]: 'failed' };
            pushTicker(`✗ Failed page ${ev.page_number}`);
        }
    }

    function openStream() {
        if (source) return;
        const es = new EventSource(analysisStatusStreamUrl(), { withCredentials: true });
        es.addEventListener('analysis', (e) => {
            try {
                applyEvent(JSON.parse((e as MessageEvent).data) as AnalysisEvent);
                live = true;
                sseErrors = 0;
            } catch {
                // ignore a malformed frame
            }
        });
        // Dropped frames — resync the overview from the server.
        es.addEventListener('lagged', () => void loadCoverage(true));
        es.onopen = () => {
            live = true;
            sseErrors = 0;
        };
        es.onerror = () => {
            live = false;
            sseErrors += 1;
            // EventSource hides the status code; after several failures,
            // probe a normal admin call so the global on401 hook can fire.
            if (sseErrors >= 4) {
                sseErrors = 0;
                getAnalysisMangaCoverage({ limit: 1 }).catch(() => {});
            }
        };
        source = es;
    }

    function onSearchInput() {
        if (searchTimer) clearTimeout(searchTimer);
        searchTimer = setTimeout(() => loadCoverage(true), 300);
    }

    async function loadCoverage(reset: boolean) {
        if (reset) {
            offset = 0;
            loading = true;
            // Collapse open rows so stale chapter/page state doesn't linger.
            expandedManga = null;
            expandedChapter = null;
            // Server numbers are authoritative again — drop live overrides.
            liveStatus = {};
            counted = new Set();
        } else {
            loadingMore = true;
        }
        error = null;
        try {
            const r = await getAnalysisMangaCoverage({
                search: query.trim() || undefined,
                limit: LIMIT,
                offset
            });
            mangas = reset ? r.items : [...mangas, ...r.items];
            total = r.page.total ?? mangas.length;
        } catch (e) {
            error = e instanceof ApiError ? e.message : 'Failed to load coverage.';
        } finally {
            loading = false;
            loadingMore = false;
        }
    }

    async function loadMore() {
        offset += LIMIT;
        await loadCoverage(false);
    }

    async function toggleManga(id: string) {
        if (expandedManga === id) {
            expandedManga = null;
            return;
        }
        expandedManga = id;
        expandedChapter = null;
        if (!chapters[id]) {
            chapters[id] = 'loading';
            try {
                chapters[id] = await getAnalysisChapterCoverage(id);
            } catch (e) {
                delete chapters[id];
                expandedManga = null;
                error = e instanceof ApiError ? e.message : 'Failed to load chapters.';
            }
        }
    }

    async function toggleChapter(id: string) {
        if (expandedChapter === id) {
            expandedChapter = null;
            return;
        }
        expandedChapter = id;
        await loadPages(id);
    }

    async function loadPages(id: string) {
        pages[id] = 'loading';
        try {
            const fresh = await getAnalysisChapterPages(id);
            // Backend status is authoritative for just-fetched pages; drop
            // any live overrides for them so the grid reflects the server.
            const next = { ...liveStatus };
            for (const p of fresh) delete next[p.page_id];
            liveStatus = next;
            pages[id] = fresh;
        } catch (e) {
            delete pages[id];
            error = e instanceof ApiError ? e.message : 'Failed to load pages.';
        }
    }

    async function openDetail(pageId: string) {
        detailOpen = true;
        detailLoading = true;
        detailError = null;
        detail = null;
        try {
            detail = await getAnalysisPageDetail(pageId);
        } catch (e) {
            detailError = e instanceof ApiError ? e.message : 'Failed to load page.';
        } finally {
            detailLoading = false;
        }
    }

    function closeDetail() {
        detailOpen = false;
    }

    async function queueFromDetail() {
        if (!detail || detailQueueBusy) return;
        detailQueueBusy = true;
        detailError = null;
        try {
            await analyzePage(detail.page_id);
            // Reflect the new queued state in the grid + modal.
            if (expandedChapter) await loadPages(expandedChapter);
            notice = `Queued page ${detail.page_number} for analysis.`;
            closeDetail();
        } catch (e) {
            detailError =
                e instanceof ApiError && e.status === 503
                    ? 'Analysis worker is disabled (ANALYSIS_ENABLED=false).'
                    : e instanceof ApiError
                      ? e.message
                      : 'Failed to queue page.';
        } finally {
            detailQueueBusy = false;
        }
    }

    async function enqueue(
        key: string,
        scope: Omit<ReenqueueAnalysisOptions, 'onlyUnanalyzed'>,
        label: string
    ) {
        if (busyKey) return;
        busyKey = key;
        notice = null;
        error = null;
        try {
            const r = await reenqueueAnalysis({
                ...scope,
                onlyUnanalyzed: !includeAnalyzed
            });
            notice = `Enqueued ${r.enqueued} page${r.enqueued === 1 ? '' : 's'} for analysis (${label}).`;
            // Refresh the open chapter's page grid so queued pages show.
            if (expandedChapter) await loadPages(expandedChapter);
        } catch (e) {
            if (e instanceof ApiError) {
                error =
                    e.status === 503
                        ? 'Analysis worker is disabled (ANALYSIS_ENABLED=false).'
                        : e.message;
            } else {
                error = 'Failed to enqueue analysis.';
            }
        } finally {
            busyKey = null;
        }
    }

    const enqueueLibrary = () => enqueue('library', {}, 'whole library');
    const enqueueManga = (m: MangaCoverage) =>
        enqueue(`manga:${m.manga_id}`, { mangaId: m.manga_id }, m.title);
    const enqueueChapter = (m: MangaCoverage, c: ChapterCoverage) =>
        enqueue(
            `chapter:${c.chapter_id}`,
            { chapterId: c.chapter_id },
            `${m.title} · ${chapterLabel({ number: c.number, title: c.title })}`
        );

    function fmtDate(iso: string | null): string {
        if (!iso) return '';
        return new Date(iso).toLocaleString();
    }
</script>

<svelte:head>
    <title>Mangalord | Admin — Analysis</title>
</svelte:head>

<div class="title-row">
    <h1 class="heading">Content analysis</h1>
    <span
        class="live-pill"
        class:on={live}
        data-testid="admin-analysis-live"
        title={live ? 'Live updates connected' : 'Reconnecting…'}
    >
        <span class="live-dot"></span>
        {live ? 'Live' : 'Reconnecting…'}
    </span>
</div>
<p class="lede">
    Coverage of the AI analysis worker (OCR, auto-tags, scene description,
    NSFW moderation). Browse what's analyzed, queue more, and inspect any
    page's result. Updates stream live as pages are queued and processed.
</p>

{#if ticker.length > 0}
    <ul class="ticker" data-testid="admin-analysis-ticker" aria-live="polite">
        {#each ticker as t (t.id)}
            <li>{t.text}</li>
        {/each}
    </ul>
{/if}

<label class="checkbox">
    <input
        type="checkbox"
        bind:checked={includeAnalyzed}
        data-testid="admin-analysis-include-analyzed"
    />
    <span>
        Include already-analyzed pages
        <span class="muted">
            — re-runs analysis on pages that already have results (default
            off). Applies to every "Queue" action.
        </span>
    </span>
</label>

<div class="row library-row">
    <span class="row-title">Whole library</span>
    <button
        type="button"
        disabled={busyKey !== null}
        onclick={enqueueLibrary}
        data-testid="admin-analysis-enqueue-library"
    >
        {busyKey === 'library' ? 'Queueing…' : 'Queue all'}
    </button>
</div>

<label class="field">
    <span>Search</span>
    <input
        type="text"
        bind:value={query}
        oninput={onSearchInput}
        placeholder="Filter mangas by title…"
        data-testid="admin-analysis-search"
    />
</label>

{#if loading}
    <p class="muted" data-testid="admin-analysis-loading">Loading coverage…</p>
{:else if mangas.length === 0}
    <p class="muted" data-testid="admin-analysis-empty">
        No mangas with pages{query.trim() ? ` match "${query.trim()}"` : ''}.
    </p>
{:else}
    <ul class="results" data-testid="admin-analysis-results">
        {#each mangas as m (m.manga_id)}
            <li class="manga" data-testid={`admin-analysis-manga-${m.manga_id}`}>
                <div class="row">
                    <button
                        type="button"
                        class="disclosure"
                        aria-expanded={expandedManga === m.manga_id}
                        onclick={() => toggleManga(m.manga_id)}
                        data-testid={`admin-analysis-expand-${m.manga_id}`}
                    >
                        <span class="caret" class:open={expandedManga === m.manga_id}>▸</span>
                        <span class="row-main">
                            <span class="row-title">{m.title}</span>
                        </span>
                    </button>
                    <CoverageBadge
                        analyzed={m.analyzed_pages}
                        total={m.total_pages}
                        testid={`admin-analysis-coverage-manga-${m.manga_id}`}
                    />
                    <button
                        type="button"
                        disabled={busyKey !== null}
                        onclick={() => enqueueManga(m)}
                        data-testid={`admin-analysis-enqueue-manga-${m.manga_id}`}
                    >
                        {busyKey === `manga:${m.manga_id}` ? 'Queueing…' : 'Queue manga'}
                    </button>
                </div>

                {#if expandedManga === m.manga_id}
                    {#if chapters[m.manga_id] === 'loading'}
                        <p class="muted indent">Loading chapters…</p>
                    {:else if Array.isArray(chapters[m.manga_id])}
                        {@const chs = chapters[m.manga_id] as ChapterCoverage[]}
                        <ul class="chapters">
                            {#each chs as c (c.chapter_id)}
                                <li
                                    class="chapter"
                                    data-testid={`admin-analysis-chapter-${c.chapter_id}`}
                                >
                                    <div class="row">
                                        <button
                                            type="button"
                                            class="disclosure"
                                            aria-expanded={expandedChapter === c.chapter_id}
                                            onclick={() => toggleChapter(c.chapter_id)}
                                            data-testid={`admin-analysis-expand-chapter-${c.chapter_id}`}
                                        >
                                            <span
                                                class="caret"
                                                class:open={expandedChapter === c.chapter_id}
                                                >▸</span
                                            >
                                            <span class="row-main">
                                                <span class="chapter-label">
                                                    {chapterLabel({
                                                        number: c.number,
                                                        title: c.title
                                                    })}
                                                </span>
                                            </span>
                                        </button>
                                        <CoverageBadge
                                            analyzed={c.analyzed_pages}
                                            total={c.total_pages}
                                            testid={`admin-analysis-coverage-chapter-${c.chapter_id}`}
                                        />
                                        <button
                                            type="button"
                                            disabled={busyKey !== null}
                                            onclick={() => enqueueChapter(m, c)}
                                            data-testid={`admin-analysis-enqueue-chapter-${c.chapter_id}`}
                                        >
                                            {busyKey === `chapter:${c.chapter_id}`
                                                ? 'Queueing…'
                                                : 'Queue chapter'}
                                        </button>
                                    </div>

                                    {#if expandedChapter === c.chapter_id}
                                        {#if pages[c.chapter_id] === 'loading'}
                                            <p class="muted indent">Loading pages…</p>
                                        {:else if Array.isArray(pages[c.chapter_id])}
                                            {@const ps = pages[c.chapter_id] as PageStatusItem[]}
                                            <div
                                                class="page-grid"
                                                data-testid={`admin-analysis-pages-${c.chapter_id}`}
                                            >
                                                {#each ps as p (p.page_id)}
                                                    <button
                                                        type="button"
                                                        class="page-chip {chipStatus(p)}"
                                                        title={`Page ${p.page_number} — ${chipStatus(p)}`}
                                                        onclick={() => openDetail(p.page_id)}
                                                        data-testid={`admin-analysis-page-${p.page_id}`}
                                                        data-status={chipStatus(p)}
                                                    >
                                                        {p.page_number}
                                                    </button>
                                                {/each}
                                            </div>
                                            <p class="legend">
                                                <span class="dot done"></span> analyzed
                                                <span class="dot analyzing"></span> analyzing
                                                <span class="dot queued"></span> queued
                                                <span class="dot failed"></span> failed
                                                <span class="dot none"></span> not analyzed
                                            </p>
                                        {/if}
                                    {/if}
                                </li>
                            {/each}
                        </ul>
                    {/if}
                {/if}
            </li>
        {/each}
    </ul>

    {#if mangas.length < total}
        <button
            type="button"
            class="load-more"
            disabled={loadingMore}
            onclick={loadMore}
            data-testid="admin-analysis-load-more"
        >
            {loadingMore ? 'Loading…' : `Load more (${mangas.length}/${total})`}
        </button>
    {/if}
{/if}

{#if notice}
    <p class="notice" role="status" data-testid="admin-analysis-notice">{notice}</p>
{/if}
{#if error}
    <p class="error" role="alert" data-testid="admin-analysis-error">{error}</p>
{/if}

<Modal
    open={detailOpen}
    title={detail ? `Page ${detail.page_number}` : 'Page analysis'}
    onClose={closeDetail}
    size="lg"
    closeOnBackdrop
    testid="admin-analysis-detail"
>
    {#if detailLoading}
        <p class="muted">Loading…</p>
    {:else if detailError}
        <p class="error" role="alert">{detailError}</p>
    {:else if detail}
        <div class="detail-meta">
            <span class="status-pill {detail.status}" data-testid="admin-analysis-detail-status">
                {detail.status === 'done'
                    ? 'Analyzed'
                    : detail.status === 'failed'
                      ? 'Failed'
                      : 'Not analyzed'}
            </span>
            {#if detail.model}<span class="muted">model {detail.model}</span>{/if}
            {#if detail.analyzed_at}<span class="muted">{fmtDate(detail.analyzed_at)}</span>{/if}
        </div>

        {#if detail.status === 'none'}
            <p class="muted">This page hasn't been analyzed yet.</p>
        {:else if detail.status === 'failed'}
            <p class="error">{detail.error ?? 'Analysis failed.'}</p>
        {/if}

        {#if detail.is_nsfw || detail.content_warnings.length > 0}
            <div class="warnings" data-testid="admin-analysis-detail-warnings">
                <span class="cw-label">⚠ NSFW</span>
                {#each detail.content_warnings as w (w)}
                    <span class="cw-chip">{w}</span>
                {/each}
            </div>
        {/if}

        {#if detail.scene_description}
            <section class="block">
                <h3>Scene</h3>
                <p>{detail.scene_description}</p>
            </section>
        {/if}

        {#if detail.tags.length > 0}
            <section class="block">
                <h3>Tags</h3>
                <div class="tag-row">
                    {#each detail.tags as t (t)}<span class="tag">{t}</span>{/each}
                </div>
            </section>
        {/if}

        {#if detail.ocr.length > 0}
            <section class="block">
                <h3>OCR text</h3>
                <ul class="ocr">
                    {#each detail.ocr as line, i (i)}
                        <li><span class="ocr-kind">{line.kind}</span> {line.text}</li>
                    {/each}
                </ul>
            </section>
        {/if}
    {/if}

    {#snippet footer()}
        {#if detail && detail.status !== 'done'}
            <button
                type="button"
                disabled={detailQueueBusy}
                onclick={queueFromDetail}
                data-testid="admin-analysis-detail-queue"
            >
                {detailQueueBusy ? 'Queueing…' : 'Queue this page'}
            </button>
        {/if}
        <button type="button" onclick={closeDetail}>Close</button>
    {/snippet}
</Modal>

<style>
    .title-row {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        margin-bottom: var(--space-2);
    }
    .heading {
        margin-bottom: var(--space-2);
    }
    .title-row .heading {
        margin-bottom: 0;
    }
    .live-pill {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        font-size: var(--font-xs);
        font-weight: var(--weight-semibold);
        padding: 2px var(--space-2);
        border-radius: var(--radius-pill);
        background: var(--surface-elevated);
        color: var(--text-muted);
        border: 1px solid var(--border);
    }
    .live-pill.on {
        color: #2e7d32;
        border-color: color-mix(in srgb, #2e7d32 40%, transparent);
    }
    .live-dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: currentColor;
    }
    .live-pill.on .live-dot {
        animation: pulse 1.6s ease-in-out infinite;
    }
    .ticker {
        list-style: none;
        margin: 0 0 var(--space-3);
        padding: var(--space-2) var(--space-3);
        max-width: 44rem;
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        background: var(--surface);
        font-size: var(--font-sm);
        color: var(--text-muted);
        display: flex;
        flex-direction: column;
        gap: 2px;
        max-height: 9rem;
        overflow: hidden;
    }
    @keyframes pulse {
        0%,
        100% {
            opacity: 1;
        }
        50% {
            opacity: 0.35;
        }
    }
    .lede {
        color: var(--text-muted);
        margin-bottom: var(--space-4);
        max-width: 44rem;
    }
    .checkbox {
        display: grid;
        grid-template-columns: auto 1fr;
        gap: var(--space-2);
        align-items: start;
        font-size: var(--font-sm);
        margin-bottom: var(--space-3);
        max-width: 44rem;
    }
    .muted {
        color: var(--text-muted);
    }
    .indent {
        margin-left: var(--space-5);
    }
    .library-row {
        max-width: 44rem;
        padding: var(--space-2) var(--space-3);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        margin-bottom: var(--space-3);
    }
    .field {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        margin-bottom: var(--space-3);
        max-width: 44rem;
    }
    .field > span {
        color: var(--text-muted);
        font-size: var(--font-sm);
    }
    .row {
        display: flex;
        align-items: center;
        gap: var(--space-2);
    }
    .row-main {
        display: flex;
        flex-direction: column;
        min-width: 0;
        flex: 1;
        text-align: left;
    }
    .row-title,
    .chapter-label {
        font-weight: var(--weight-semibold);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .results {
        list-style: none;
        margin: 0;
        padding: 0;
        max-width: 44rem;
    }
    .manga {
        border-top: 1px solid var(--border);
        padding: var(--space-2) 0;
    }
    .disclosure {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        flex: 1;
        min-width: 0;
        background: transparent;
        border: 0;
        padding: 0;
        cursor: pointer;
        color: inherit;
    }
    .caret {
        transition: transform 0.12s ease;
        color: var(--text-muted);
    }
    .caret.open {
        transform: rotate(90deg);
    }
    .chapters {
        list-style: none;
        margin: var(--space-2) 0 0 var(--space-4);
        padding: 0;
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
    }
    .page-grid {
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
        margin: var(--space-2) 0 0 var(--space-5);
    }
    .page-chip {
        min-width: 28px;
        height: 28px;
        padding: 0 4px;
        font-size: var(--font-xs);
        border-radius: var(--radius-sm);
        border: 1px solid var(--border);
        cursor: pointer;
        background: var(--surface-elevated);
        color: var(--text-muted);
    }
    .page-chip.done {
        background: color-mix(in srgb, #2e7d32 18%, transparent);
        border-color: color-mix(in srgb, #2e7d32 45%, transparent);
        color: #2e7d32;
    }
    .page-chip.queued {
        background: color-mix(in srgb, #2563eb 18%, transparent);
        border-color: color-mix(in srgb, #2563eb 45%, transparent);
        color: #2563eb;
    }
    .page-chip.analyzing {
        background: color-mix(in srgb, #d4762a 22%, transparent);
        border-color: color-mix(in srgb, #d4762a 55%, transparent);
        color: #b85e1a;
        animation: pulse 1.2s ease-in-out infinite;
    }
    .page-chip.failed {
        background: color-mix(in srgb, var(--danger) 18%, transparent);
        border-color: color-mix(in srgb, var(--danger) 45%, transparent);
        color: var(--danger);
    }
    .legend {
        display: flex;
        align-items: center;
        gap: var(--space-2);
        margin: var(--space-2) 0 0 var(--space-5);
        font-size: var(--font-xs);
        color: var(--text-muted);
    }
    .dot {
        display: inline-block;
        width: 10px;
        height: 10px;
        border-radius: 2px;
        margin-right: 2px;
        vertical-align: middle;
    }
    .dot.done {
        background: #2e7d32;
    }
    .dot.queued {
        background: #2563eb;
    }
    .dot.analyzing {
        background: #d4762a;
    }
    .dot.failed {
        background: var(--danger);
    }
    .dot.none {
        background: var(--surface-elevated);
        border: 1px solid var(--border);
    }
    .load-more {
        margin-top: var(--space-3);
    }
    .notice {
        color: var(--success, #2e7d32);
        max-width: 44rem;
    }
    .error {
        color: var(--danger);
        max-width: 44rem;
    }

    /* Detail modal */
    .detail-meta {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
        font-size: var(--font-sm);
        margin-bottom: var(--space-3);
    }
    .status-pill {
        font-weight: var(--weight-semibold);
        font-size: var(--font-xs);
        padding: 1px var(--space-2);
        border-radius: var(--radius-pill);
        background: var(--surface-elevated);
        color: var(--text-muted);
    }
    .status-pill.done {
        background: color-mix(in srgb, #2e7d32 16%, transparent);
        color: #2e7d32;
    }
    .status-pill.failed {
        background: color-mix(in srgb, var(--danger) 16%, transparent);
        color: var(--danger);
    }
    .warnings {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
        padding: var(--space-2) var(--space-3);
        border-radius: var(--radius-md);
        background: color-mix(in srgb, #d4762a 12%, transparent);
        margin-bottom: var(--space-3);
    }
    .cw-label {
        font-weight: 600;
        color: #b85e1a;
        font-size: var(--font-sm);
    }
    .cw-chip {
        font-size: var(--font-sm);
        padding: 1px var(--space-2);
        border-radius: var(--radius-sm);
        background: color-mix(in srgb, #d4762a 22%, transparent);
        text-transform: capitalize;
    }
    .block {
        margin-bottom: var(--space-3);
    }
    .block h3 {
        font-size: var(--font-sm);
        color: var(--text-muted);
        text-transform: uppercase;
        letter-spacing: 0.04em;
        margin: 0 0 var(--space-1);
    }
    .tag-row {
        display: flex;
        flex-wrap: wrap;
        gap: var(--space-1);
    }
    .tag {
        font-size: var(--font-sm);
        padding: 1px var(--space-2);
        border-radius: var(--radius-pill);
        background: var(--surface-elevated);
    }
    .ocr {
        list-style: none;
        margin: 0;
        padding: 0;
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
    }
    .ocr-kind {
        display: inline-block;
        min-width: 5.5rem;
        font-size: var(--font-xs);
        color: var(--text-muted);
        text-transform: uppercase;
    }
</style>
