<script lang="ts">
    import { browser } from '$app/environment';
    import { goto, preloadData } from '$app/navigation';
    import { fileUrl, ApiError } from '$lib/api/client';
    import { createBookmark, deleteBookmark, type Bookmark } from '$lib/api/bookmarks';
    import {
        attachTag,
        detachTag,
        type AuthorRef,
        type GenreRef,
        type MangaDetail,
        type TagRef
    } from '$lib/api/mangas';
    import { resyncManga } from '$lib/api/admin';
    import { chapterLabel } from '$lib/api/chapters';
    import { isChapterRead } from '$lib/chapterProgress';
    import { formatBytes } from '$lib/upload-validation';
    import { listTags, type Tag } from '$lib/api/tags';
    import type { ContentWarning } from '$lib/api/page_tags';
    import type { DetailData } from './types';
    import { session } from '$lib/session.svelte';
    import { toast } from '$lib/toast.svelte';
    import Chip from '$lib/components/Chip.svelte';
    import MangaCard from '$lib/components/MangaCard.svelte';
    import ReactionButtons from '$lib/components/ReactionButtons.svelte';
    import Sheet from '$lib/components/Sheet.svelte';
    import AddToCollectionModal from '$lib/components/AddToCollectionModal.svelte';
    import Plus from '@lucide/svelte/icons/plus';
    import FolderPlus from '@lucide/svelte/icons/folder-plus';
    import Pencil from '@lucide/svelte/icons/pencil';
    import UploadCloud from '@lucide/svelte/icons/upload-cloud';
    import RefreshCw from '@lucide/svelte/icons/refresh-cw';
    import ChevronLeft from '@lucide/svelte/icons/chevron-left';
    import Check from '@lucide/svelte/icons/check';
    import MoreHorizontal from '@lucide/svelte/icons/more-horizontal';
    import BookmarkIcon from '@lucide/svelte/icons/bookmark';
    import BookmarkCheck from '@lucide/svelte/icons/bookmark-check';

    let { data }: { data: DetailData } = $props();
    // `manga` is locally overridable so a successful force resync can
    // swap in the refreshed detail (new cover URL, refreshed status,
    // etc.) without a router reload. Falls back to the server-loaded
    // data otherwise.
    let mangaOverride = $state<MangaDetail | null>(null);
    const manga = $derived<MangaDetail>(mangaOverride ?? data.manga);
    const chapters = $derived(data.chapters);
    const readProgress = $derived(data.readProgress);
    /** Total stored bytes of this manga's chapter content (cover excluded).
     *  Comes from the backend, which sums over ALL chapters (the loaded
     *  chapter list is paginated, so summing it client-side would
     *  undercount mangas with many chapters). `null` → unknown (a page is
     *  un-backfilled) → show an em-dash; `0` → no content → hide. */
    const contentBytes = $derived(manga.chapter_storage_bytes);
    /** Chapter row from the local chapters list when present (so we
     *  can also surface the chapter title). Falls back below to the
     *  server-supplied `chapter_number` when the chapter sits past
     *  the first page of `chapters` (large mangas with >50 chapters). */
    const continueChapter = $derived(
        readProgress?.chapter_id
            ? chapters.find((c) => c.id === readProgress.chapter_id) ?? null
            : null
    );
    /** Reader link target — always the chapter id when we have one,
     *  even for chapters past the loaded `chapters` list page. */
    const continueChapterId = $derived(readProgress?.chapter_id ?? null);
    const continueChapterNumber = $derived(
        continueChapter?.number ?? readProgress?.chapter_number ?? null
    );
    const continueChapterTitle = $derived(continueChapter?.title ?? null);
    const continueLabel = $derived(
        continueChapterNumber != null
            ? chapterLabel({ number: continueChapterNumber, title: continueChapterTitle })
            : null
    );

    /** The reader's last-read chapter number, used to mark chapters read.
     *  `null` for guests / never-read. Read-state is by chapter *number*
     *  (all we persist), so re-reading one scanlation marks same-numbered
     *  scanlations read too — intended, since "chapter N" is read. */
    const lastReadNumber = $derived(readProgress?.chapter_number ?? null);
    /** New chapters since last read. Authoritative backend count over ALL
     *  chapters (distinct numbers) — not recomputed over the paginated
     *  `chapters` list, which would under-count long series and disagree
     *  with the homepage shelf. `0` for guests / never-read. */
    const newChapterCount = $derived(readProgress?.new_chapters_count ?? 0);

    const authors = $derived<AuthorRef[]>(manga.authors);
    const genres = $derived<GenreRef[]>(manga.genres);
    /** Deduped content warnings across the manga's pages (analysis worker). */
    const contentWarnings = $derived<ContentWarning[]>(manga.content_warnings ?? []);

    /** Up to 5 mangas with the most similar tags; empty hides the section. */
    const similar = $derived(data.similar);

    // svelte-ignore state_referenced_locally
    let tags = $state<TagRef[]>([...manga.tags]);
    // svelte-ignore state_referenced_locally
    let bookmarks = $state<Bookmark[]>([...data.bookmarks]);

    const mangaBookmark = $derived(
        bookmarks.find((b) => b.manga_id === manga.id && b.chapter_id === null) ?? null
    );

    let busy = $state(false);
    let altTitlesOpen = $state(false);
    let overflowOpen = $state(false);
    let descExpanded = $state(false);

    // Cheap heuristic — descriptions short enough to fit in ~3 lines
    // don't need a Read more toggle. Avoids dangling buttons that
    // would do nothing visible on tap.
    const descNeedsClamp = $derived((manga.description?.length ?? 0) > 200);

    /**
     * Sticky-CTA target on the mobile detail. Three states:
     * - Has read progress → "Continue {chapterLabel}"
     * - No progress, has chapters → "Read first chapter" (oldest = end of
     *   the list since the server returns chapters newest-first)
     * - No chapters → null (CTA bar hides)
     */
    const ctaTarget = $derived.by(() => {
        if (continueChapterId && continueLabel) {
            return {
                href: `/manga/${manga.id}/chapter/${continueChapterId}`,
                label: `Continue ${continueLabel}`
            };
        }
        if (chapters.length > 0) {
            const first = chapters[chapters.length - 1];
            return {
                href: `/manga/${manga.id}/chapter/${first.id}`,
                label: 'Read first chapter'
            };
        }
        return null;
    });

    function onBack() {
        if (!browser) return;
        if (window.history.length > 1) {
            window.history.back();
        } else {
            goto('/');
        }
    }

    // Warm the reader (its load + first page images) for the highest-intent
    // link on the page — tapping Continue / Read then feels instant. Re-runs
    // only when the target chapter changes.
    $effect(() => {
        const href = ctaTarget?.href;
        if (!browser || !href) return;
        preloadData(href).catch(() => {});
    });

    // Tell the root layout to drop its top padding so the hero can sit
    // at the viewport top under the page-specific app bar. Scoped to
    // <html> so the rule survives sibling re-renders; cleaned up on
    // navigation away from the detail.
    $effect(() => {
        if (!browser) return;
        document.documentElement.dataset.mobileFullBleed = 'true';
        return () => {
            delete document.documentElement.dataset.mobileFullBleed;
        };
    });

    async function toggleBookmark() {
        if (!session.user || busy) return;
        const existing = mangaBookmark;
        // Snapshot for rollback — the update below is applied optimistically
        // so the button flips instantly, then reconciled/reverted.
        const snapshot = bookmarks;
        busy = true;
        try {
            if (existing) {
                bookmarks = bookmarks.filter((b) => b.id !== existing.id);
                await deleteBookmark(existing.id);
            } else {
                // Temporary placeholder so the derived `mangaBookmark` flips
                // immediately; swapped for the server row (with its real id)
                // once the POST returns.
                const tempId = `optimistic-${crypto.randomUUID()}`;
                const placeholder: Bookmark = {
                    id: tempId,
                    user_id: session.user.id,
                    manga_id: manga.id,
                    chapter_id: null,
                    page: null,
                    created_at: new Date().toISOString()
                };
                bookmarks = [placeholder, ...bookmarks];
                const b = await createBookmark({ manga_id: manga.id });
                bookmarks = [b, ...bookmarks.filter((x) => x.id !== tempId)];
            }
        } catch {
            bookmarks = snapshot;
            toast.error(existing ? 'Could not remove bookmark.' : 'Could not add bookmark.');
        } finally {
            busy = false;
        }
    }

    // ---- Tag UI ----
    let tagDraft = $state('');
    let tagAddBusy = $state(false);
    let tagError = $state<string | null>(null);
    let suggestions = $state<Tag[]>([]);
    let suggestHighlight = $state(-1);
    let suggestTimer: ReturnType<typeof setTimeout> | null = null;
    // Monotonic counter — late-returning fetches with a stale seq are
    // discarded so a fast typist can't see results from a previous
    // query overwrite the current one.
    let suggestSeq = 0;
    const suggestListId = 'tag-suggest-list';

    function onTagDraftInput() {
        tagError = null;
        suggestHighlight = -1;
        if (suggestTimer) clearTimeout(suggestTimer);
        const q = tagDraft.trim();
        if (q.length === 0) {
            suggestions = [];
            suggestSeq++;
            return;
        }
        const seq = ++suggestSeq;
        suggestTimer = setTimeout(async () => {
            try {
                const matched = await listTags({ search: q, limit: 6 });
                if (seq !== suggestSeq) return;
                // Hide tags already attached so the menu only suggests new picks.
                const attached = new Set(tags.map((t) => t.id));
                suggestions = matched.filter((m) => !attached.has(m.id));
            } catch {
                if (seq === suggestSeq) suggestions = [];
            }
        }, 180);
    }

    function onTagKeydown(e: KeyboardEvent) {
        if (e.key === 'ArrowDown' && suggestions.length > 0) {
            e.preventDefault();
            suggestHighlight = (suggestHighlight + 1) % suggestions.length;
        } else if (e.key === 'ArrowUp' && suggestions.length > 0) {
            e.preventDefault();
            suggestHighlight =
                suggestHighlight <= 0 ? suggestions.length - 1 : suggestHighlight - 1;
        } else if (e.key === 'Escape') {
            suggestions = [];
            suggestHighlight = -1;
        }
        // Enter is handled by the form's onsubmit — if a suggestion is
        // highlighted we submit that name, otherwise we submit the
        // raw draft so a brand-new tag can still be created inline.
    }

    async function submitTag(name: string) {
        const trimmed = name.trim();
        if (!trimmed || !session.user || tagAddBusy) return;
        const snapshot = tags;
        // Show the chip immediately with the typed name; reconcile with the
        // server ref (which may normalize the name/id) on success. Drop any
        // existing same-name chip so a re-add doesn't duplicate.
        const tempId = `optimistic-${crypto.randomUUID()}`;
        tags = [
            ...tags.filter((t) => t.name.toLowerCase() !== trimmed.toLowerCase()),
            { id: tempId, name: trimmed, added_by: session.user.id }
        ];
        tagDraft = '';
        suggestions = [];
        suggestHighlight = -1;
        tagAddBusy = true;
        tagError = null;
        try {
            const attached = await attachTag(manga.id, trimmed);
            // Replace the placeholder (and any pre-existing entry the server
            // may have matched) with the authoritative ref.
            tags = [...tags.filter((t) => t.id !== tempId && t.id !== attached.id), attached];
        } catch (e) {
            tags = snapshot;
            tagError = (e as Error).message;
        } finally {
            tagAddBusy = false;
        }
    }

    async function removeTag(tag: TagRef) {
        if (!session.user || tag.added_by !== session.user.id) return;
        const snapshot = tags;
        tags = tags.filter((t) => t.id !== tag.id);
        try {
            await detachTag(manga.id, tag.id);
        } catch (e) {
            tags = snapshot;
            tagError = (e as Error).message;
        }
    }

    function onTagFormSubmit(e: SubmitEvent) {
        e.preventDefault();
        // If the user arrowed down to a suggestion, pick it; otherwise
        // submit whatever's in the input (allows creating a new tag).
        const target =
            suggestHighlight >= 0 && suggestions[suggestHighlight]
                ? suggestions[suggestHighlight].name
                : tagDraft;
        submitTag(target);
    }

    const statusLabel = $derived(manga.status === 'completed' ? 'Completed' : 'Ongoing');

    let collectionModalOpen = $state(false);

    // ---- Admin force resync ----
    let resyncBusy = $state(false);
    let resyncMessage = $state<{ kind: 'ok' | 'err'; text: string } | null>(null);
    async function forceResync() {
        if (!session.user?.is_admin || resyncBusy) return;
        resyncBusy = true;
        resyncMessage = null;
        try {
            const r = await resyncManga(manga.id);
            mangaOverride = r.manga;
            const coverNote = r.cover_fetched
                ? ' Cover re-downloaded.'
                : ' Cover unchanged.';
            resyncMessage = {
                kind: 'ok',
                text: `Metadata ${r.metadata_status}.${coverNote}`
            };
        } catch (e) {
            const msg = e instanceof ApiError ? e.message : (e as Error).message;
            resyncMessage = { kind: 'err', text: msg };
        } finally {
            resyncBusy = false;
        }
    }
</script>

<svelte:head>
    <title>Mangalord | {manga.title}</title>
</svelte:head>

<article>
    <header class="mobile-hero" data-testid="mobile-hero">
        {#if manga.cover_image_path}
            <div
                class="hero-bg"
                style="background-image: url('{fileUrl(manga.cover_image_path)}');"
                aria-hidden="true"
            ></div>
        {/if}
        <div class="hero-scrim" aria-hidden="true"></div>

        <div class="hero-appbar">
            <button
                type="button"
                class="hero-icon-btn"
                onclick={onBack}
                aria-label="Back"
                data-testid="detail-back"
            >
                <ChevronLeft size={22} aria-hidden="true" />
            </button>
            <div class="hero-spacer"></div>
            {#if session.user}
                <button
                    type="button"
                    class="hero-icon-btn"
                    onclick={toggleBookmark}
                    disabled={busy}
                    aria-label={mangaBookmark ? 'Remove bookmark' : 'Add bookmark'}
                    aria-pressed={mangaBookmark ? 'true' : 'false'}
                    data-testid="detail-bookmark-icon"
                >
                    {#if mangaBookmark}
                        <BookmarkCheck size={22} aria-hidden="true" />
                    {:else}
                        <BookmarkIcon size={22} aria-hidden="true" />
                    {/if}
                </button>
            {/if}
            <button
                type="button"
                class="hero-icon-btn"
                onclick={() => (overflowOpen = true)}
                aria-label="More actions"
                data-testid="detail-overflow"
            >
                <MoreHorizontal size={22} aria-hidden="true" />
            </button>
        </div>

        <div class="hero-content">
            {#if manga.cover_image_path}
                <img
                    class="hero-cover"
                    src={fileUrl(manga.cover_image_path)}
                    alt=""
                    loading="eager"
                    decoding="async"
                />
            {/if}
            <div class="hero-text">
                <h1 class="hero-title" data-testid="mobile-manga-title">{manga.title}</h1>
                {#if authors.length > 0}
                    <p class="hero-authors" data-testid="mobile-manga-authors">
                        by {authors.map((a) => a.name).join(', ')}
                    </p>
                {/if}
                <span class="hero-status status-{manga.status}">{statusLabel}</span>
            </div>
        </div>
    </header>

    <header class="overview">
        {#if manga.cover_image_path}
            <img
                src={fileUrl(manga.cover_image_path)}
                alt="{manga.title} cover"
                class="cover"
                loading="eager"
                decoding="async"
                data-testid="manga-cover"
            />
        {/if}
        <div class="meta">
            <div class="title-row">
                <h1 data-testid="manga-title">{manga.title}</h1>
                <span
                    class="status-badge status-{manga.status}"
                    data-testid="manga-status"
                >
                    {statusLabel}
                </span>
            </div>

            {#if authors.length > 0}
                <div class="chip-row" data-testid="manga-authors">
                    <span class="chip-row-label">by</span>
                    {#each authors as a (a.id)}
                        <Chip label={a.name} href={`/authors/${a.id}`} variant="primary" />
                    {/each}
                </div>
            {/if}

            {#if manga.alt_titles.length > 0}
                <details
                    class="alt-titles"
                    bind:open={altTitlesOpen}
                    data-testid="manga-alt-titles"
                >
                    <summary>Also known as ({manga.alt_titles.length})</summary>
                    <ul>
                        {#each manga.alt_titles as alt}
                            <li>{alt}</li>
                        {/each}
                    </ul>
                </details>
            {/if}

            {#if contentWarnings.length > 0}
                <div class="content-warnings" data-testid="manga-content-warnings" role="note">
                    <span class="cw-label">⚠ Content warning</span>
                    {#each contentWarnings as w (w)}
                        <span class="cw-chip">{w}</span>
                    {/each}
                </div>
            {/if}

            {#if genres.length > 0}
                <div class="chip-row" data-testid="manga-genres">
                    <span class="chip-row-label">Genres</span>
                    {#each genres as g (g.id)}
                        <Chip label={g.name} href={`/?genres=${g.id}`} testid={`genre-chip-${g.id}`} />
                    {/each}
                </div>
            {/if}

            <div class="chip-row tag-row" data-testid="manga-tags">
                <span class="chip-row-label">Tags</span>
                {#each tags as t (t.id)}
                    <Chip
                        label={t.name}
                        href={`/?tags=${t.id}`}
                        testid={`tag-chip-${t.id}`}
                        variant="soft"
                        onRemove={session.user && t.added_by === session.user.id
                            ? () => removeTag(t)
                            : undefined}
                        removeLabel="Remove tag"
                    />
                {/each}
                {#if session.user}
                    <form class="tag-form" onsubmit={onTagFormSubmit}>
                        <input
                            type="text"
                            role="combobox"
                            bind:value={tagDraft}
                            oninput={onTagDraftInput}
                            onkeydown={onTagKeydown}
                            placeholder="Add tag"
                            maxlength="64"
                            aria-label="Add tag"
                            aria-controls={suggestListId}
                            aria-expanded={suggestions.length > 0}
                            aria-autocomplete="list"
                            aria-activedescendant={suggestHighlight >= 0
                                ? `${suggestListId}-opt-${suggestHighlight}`
                                : undefined}
                            class="tag-input"
                            data-testid="tag-input"
                        />
                        <button
                            type="submit"
                            class="tag-add-btn"
                            disabled={!tagDraft.trim() || tagAddBusy}
                            aria-label="Add tag"
                            title="Add tag"
                        >
                            <Plus size={14} aria-hidden="true" />
                        </button>
                        {#if suggestions.length > 0}
                            <ul class="tag-suggestions" role="listbox" id={suggestListId}>
                                {#each suggestions as s, i (s.id)}
                                    <li
                                        id={`${suggestListId}-opt-${i}`}
                                        role="option"
                                        aria-selected={i === suggestHighlight}
                                        class:active={i === suggestHighlight}
                                    >
                                        <button
                                            type="button"
                                            tabindex="-1"
                                            onmouseenter={() => (suggestHighlight = i)}
                                            onclick={() => submitTag(s.name)}
                                            data-testid="tag-suggestion"
                                        >
                                            {s.name}
                                        </button>
                                    </li>
                                {/each}
                            </ul>
                        {/if}
                    </form>
                {/if}
            </div>
            {#if tagError}
                <p class="tag-error" role="alert">{tagError}</p>
            {/if}

            {#if manga.description}
                <p
                    class="description"
                    class:clamped={descNeedsClamp && !descExpanded}
                    data-testid="manga-description"
                >
                    {manga.description}
                </p>
                {#if descNeedsClamp}
                    <button
                        type="button"
                        class="read-more"
                        onclick={() => (descExpanded = !descExpanded)}
                        data-testid="read-more-toggle"
                    >
                        {descExpanded ? 'Read less' : 'Read more'}
                    </button>
                {/if}
            {/if}

            {#if session.user}
                <div class="action-row">
                    <button
                        type="button"
                        class="action"
                        class:active={mangaBookmark}
                        onclick={toggleBookmark}
                        disabled={busy}
                        aria-pressed={mangaBookmark ? 'true' : 'false'}
                        data-testid="bookmark-toggle"
                    >
                        {mangaBookmark ? '★ Bookmarked' : '☆ Bookmark'}
                    </button>
                    <ReactionButtons mangaId={manga.id} initial={data.reaction} />
                    <button
                        type="button"
                        class="action"
                        onclick={() => (collectionModalOpen = true)}
                        data-testid="add-to-collection-open"
                    >
                        <FolderPlus size={16} aria-hidden="true" />
                        <span>Add to collection</span>
                    </button>
                    <a
                        class="action"
                        href="/manga/{manga.id}/edit"
                        data-testid="edit-manga-link"
                    >
                        <Pencil size={16} aria-hidden="true" />
                        <span>Edit</span>
                    </a>
                    <a
                        class="action"
                        href="/manga/{manga.id}/upload-chapter"
                        data-testid="upload-chapter-link"
                    >
                        <UploadCloud size={16} aria-hidden="true" />
                        <span>Upload chapter</span>
                    </a>
                    {#if session.user.is_admin}
                        <button
                            type="button"
                            class="action"
                            onclick={forceResync}
                            disabled={resyncBusy}
                            title="Refetch metadata + cover from the crawler source"
                            data-testid="force-resync-manga"
                        >
                            <RefreshCw
                                size={16}
                                aria-hidden="true"
                                class={resyncBusy ? 'spin' : ''}
                            />
                            <span>{resyncBusy ? 'Resyncing…' : 'Force resync'}</span>
                        </button>
                    {/if}
                </div>
                {#if resyncMessage}
                    <p
                        class="resync-msg"
                        class:err={resyncMessage.kind === 'err'}
                        role="status"
                        data-testid="force-resync-message"
                    >
                        {resyncMessage.text}
                    </p>
                {/if}
            {:else}
                <a class="action" href="/login" data-testid="bookmark-signin">
                    Sign in to bookmark or collect
                </a>
            {/if}
        </div>
    </header>

    {#if session.user}
        <AddToCollectionModal
            open={collectionModalOpen}
            target={{ kind: 'manga', id: manga.id }}
            onClose={() => (collectionModalOpen = false)}
        />
    {/if}

    <section aria-label="chapters">
        <div class="chapters-head">
            <div class="chapters-head-left">
                <h2>Chapters</h2>
                {#if newChapterCount > 0}
                    <span class="new-chapters" data-testid="new-chapters-summary">
                        {newChapterCount} new since last read
                    </span>
                {/if}
            </div>
            {#if contentBytes === null || contentBytes > 0}
                <span class="content-size" data-testid="manga-content-size">
                    Content: {contentBytes === null ? '—' : formatBytes(contentBytes)}
                </span>
            {/if}
        </div>
        {#if continueChapterId != null && continueChapterNumber != null}
            <a
                class="continue"
                href="/manga/{manga.id}/chapter/{continueChapterId}"
                data-testid="continue-reading"
            >
                <span class="continue-label">Continue reading</span>
                <span class="continue-target">
                    {continueLabel}
                    {#if readProgress && readProgress.page > 1}
                        — page {readProgress.page}
                    {/if}
                </span>
            </a>
        {/if}
        {#if chapters.length === 0}
            <p data-testid="chapters-empty">No chapters yet.</p>
        {:else}
            <ol class="chapter-list" data-testid="chapter-list">
                {#each chapters as c (c.id)}
                    {@const read = isChapterRead(c.number, lastReadNumber)}
                    <li class:read data-testid={read ? `chapter-read-${c.id}` : undefined}>
                        {#if read}
                            <Check size={15} class="read-check" aria-hidden="true" />
                            <span class="sr-only">Read.</span>
                        {/if}
                        <a href="/manga/{manga.id}/chapter/{c.id}">
                            {chapterLabel(c)}
                        </a>
                        <span class="pages">({c.page_count} pages)</span>
                        <span class="size" data-testid="chapter-size">
                            {c.page_count === 0 || c.size_bytes === null
                                ? '—'
                                : formatBytes(c.size_bytes)}
                        </span>
                    </li>
                {/each}
            </ol>
        {/if}
    </section>

    {#if similar.length > 0}
        <section aria-label="similar mangas" class="similar" data-testid="similar-section">
            <h2>Similar</h2>
            <ul class="manga-grid" data-testid="similar-list">
                {#each similar as m (m.id)}
                    <MangaCard manga={m} authors={m.authors} genres={m.genres} />
                {/each}
            </ul>
        </section>
    {/if}

    <Sheet
        open={overflowOpen}
        title="Actions"
        onClose={() => (overflowOpen = false)}
        testid="detail-overflow-sheet"
    >
        <div class="overflow-list">
            {#if session.user}
                <button
                    type="button"
                    class="overflow-item"
                    onclick={() => {
                        overflowOpen = false;
                        collectionModalOpen = true;
                    }}
                    data-testid="overflow-add-to-collection"
                >
                    <FolderPlus size={18} aria-hidden="true" />
                    <span>Add to collection</span>
                </button>
                <a
                    class="overflow-item"
                    href="/manga/{manga.id}/edit"
                    data-testid="overflow-edit"
                >
                    <Pencil size={18} aria-hidden="true" />
                    <span>Edit manga</span>
                </a>
                <a
                    class="overflow-item"
                    href="/manga/{manga.id}/upload-chapter"
                    data-testid="overflow-upload-chapter"
                >
                    <UploadCloud size={18} aria-hidden="true" />
                    <span>Upload chapter</span>
                </a>
                {#if session.user.is_admin}
                    <button
                        type="button"
                        class="overflow-item"
                        onclick={() => {
                            overflowOpen = false;
                            forceResync();
                        }}
                        disabled={resyncBusy}
                        data-testid="overflow-force-resync"
                    >
                        <RefreshCw size={18} aria-hidden="true" />
                        <span>{resyncBusy ? 'Resyncing…' : 'Force resync'}</span>
                    </button>
                {/if}
            {:else}
                <a class="overflow-item" href="/login" data-testid="overflow-signin">
                    Sign in to bookmark or collect
                </a>
            {/if}
        </div>
    </Sheet>

    {#if ctaTarget}
        <div class="cta-bar" data-testid="cta-bar">
            <a class="cta" href={ctaTarget.href} data-testid="continue-cta">
                {ctaTarget.label}
            </a>
        </div>
    {/if}
</article>

<style>
    .overview {
        display: grid;
        grid-template-columns: minmax(0, 200px) 1fr;
        gap: var(--space-4);
        align-items: start;
        margin-bottom: var(--space-6);
    }

    @media (max-width: 640px) {
        .overview {
            grid-template-columns: 1fr;
        }
    }

    .similar {
        margin-top: var(--space-6);
    }

    /* `.manga-grid` is a shared global (see lib/styles/tokens.css). */

    .cover {
        width: 100%;
        /* Reserve the 2/3 box so the meta column doesn't reflow when the
           cover decodes (mirrors MangaCard's cover). */
        aspect-ratio: 2 / 3;
        object-fit: cover;
        border-radius: var(--radius-md);
        background: var(--surface);
    }

    .title-row {
        display: flex;
        align-items: center;
        gap: var(--space-3);
        flex-wrap: wrap;
    }

    .title-row h1 {
        margin: 0;
    }

    .status-badge {
        display: inline-flex;
        align-items: center;
        padding: 2px var(--space-2);
        border-radius: var(--radius-pill);
        font-size: var(--font-xs);
        font-weight: var(--weight-semibold);
        background: var(--surface-elevated);
        border: 1px solid var(--border-strong);
        color: var(--text-muted);
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }

    .status-badge.status-completed {
        background: var(--success-soft-bg, var(--surface-elevated));
        color: var(--success);
        border-color: var(--success);
    }

    .chip-row {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
        margin: var(--space-2) 0;
    }

    .chip-row-label {
        color: var(--text-muted);
        font-size: var(--font-sm);
    }

    .alt-titles {
        margin: var(--space-2) 0;
        color: var(--text-muted);
        font-size: var(--font-sm);
    }

    .alt-titles ul {
        margin: var(--space-1) 0 0;
        padding-left: var(--space-5);
    }

    .description {
        white-space: pre-wrap;
        color: var(--text);
        margin: var(--space-3) 0;
        /* Long URLs / romanized titles in the description shouldn't
           push the article past the screen — `anywhere` breaks them
           before they overflow. */
        overflow-wrap: anywhere;
    }

    .tag-row {
        position: relative;
    }

    .content-warnings {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--space-2);
        margin: var(--space-3) 0;
        padding: var(--space-2) var(--space-3);
        border: 1px solid color-mix(in srgb, #d4762a 50%, transparent);
        border-radius: var(--radius-md, 8px);
        background: color-mix(in srgb, #d4762a 12%, transparent);
    }

    .cw-label {
        font-size: var(--font-sm);
        font-weight: 600;
        color: #b85e1a;
    }

    .cw-chip {
        font-size: var(--font-sm);
        padding: 2px var(--space-2);
        border-radius: var(--radius-sm, 4px);
        background: color-mix(in srgb, #d4762a 22%, transparent);
        color: var(--text);
        text-transform: capitalize;
    }

    .tag-form {
        display: inline-flex;
        align-items: center;
        gap: var(--space-1);
        position: relative;
    }

    .tag-input {
        height: 28px;
        padding: 0 var(--space-2);
        font-size: var(--font-xs);
        width: 8rem;
    }

    .tag-add-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 28px;
        height: 28px;
        padding: 0;
        background: var(--primary);
        color: var(--primary-contrast);
        border: 1px solid var(--primary);
    }

    .tag-add-btn:hover:not(:disabled) {
        background: var(--primary-hover);
    }

    .tag-suggestions {
        position: absolute;
        top: 100%;
        left: 0;
        margin: var(--space-1) 0 0;
        padding: var(--space-1) 0;
        list-style: none;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
        z-index: var(--z-dropdown);
        min-width: 10rem;
    }

    .tag-suggestions button {
        width: 100%;
        background: transparent;
        border: 0;
        text-align: left;
        padding: var(--space-1) var(--space-3);
        color: var(--text);
        cursor: pointer;
        font-size: var(--font-sm);
    }

    .tag-suggestions li.active button,
    .tag-suggestions button:hover {
        background: var(--primary-soft-bg);
    }

    .tag-error {
        color: var(--danger);
        font-size: var(--font-sm);
    }

    .action-row {
        display: flex;
        gap: var(--space-2);
        margin-top: var(--space-2);
        flex-wrap: wrap;
    }

    .action {
        display: inline-flex;
        align-items: center;
        gap: var(--space-2);
        padding: 0 var(--space-3);
        height: 36px;
        border: 1px solid var(--border-strong);
        border-radius: var(--radius-md);
        background: var(--surface);
        color: var(--text);
        text-decoration: none;
        cursor: pointer;
        font-size: var(--font-sm);
        font-weight: var(--weight-medium);
        transition:
            background var(--transition),
            border-color var(--transition),
            color var(--transition);
    }

    .action:hover {
        background: var(--surface-elevated);
        text-decoration: none;
    }

    .action.active {
        background: var(--warning-soft-bg);
        border-color: var(--warning-border);
        color: var(--text);
    }

    .resync-msg {
        margin-top: var(--space-2);
        color: var(--text-muted);
        font-size: var(--font-sm);
    }

    .resync-msg.err {
        color: var(--danger);
    }

    :global(.spin) {
        animation: spin 0.9s linear infinite;
    }

    @keyframes spin {
        from {
            transform: rotate(0deg);
        }
        to {
            transform: rotate(360deg);
        }
    }

    .continue {
        display: flex;
        flex-direction: column;
        gap: var(--space-1);
        margin: var(--space-3) 0;
        padding: var(--space-3);
        background: var(--primary-soft-bg);
        border: 1px solid var(--primary);
        border-radius: var(--radius-md);
        color: var(--text);
        text-decoration: none;
    }

    .continue:hover {
        background: var(--surface-elevated);
        text-decoration: none;
    }

    .continue-label {
        font-size: var(--font-xs);
        color: var(--primary);
        font-weight: var(--weight-semibold);
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }

    .continue-target {
        font-weight: var(--weight-medium);
    }

    .chapter-list {
        padding-left: var(--space-6);
        color: var(--text);
    }

    .chapter-list li {
        padding: var(--space-1) 0;
    }

    /* Already-read chapters (at or before the reader's last-read chapter)
       dim so the eye is drawn to what's still unread; the check keeps its
       accent colour as a positive "done" marker. */
    .chapter-list li.read {
        color: var(--text-muted);
    }

    .chapter-list li.read :global(svg) {
        vertical-align: -2px;
        margin-right: var(--space-1);
        color: var(--primary);
    }

    .new-chapters {
        color: var(--primary);
        background: var(--primary-soft-bg);
        border-radius: var(--radius-pill);
        padding: 0 var(--space-2);
        font-size: var(--font-sm);
        font-weight: var(--weight-semibold);
    }

    .sr-only {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }

    .pages {
        color: var(--text-muted);
        margin-left: var(--space-2);
        font-size: var(--font-sm);
    }

    .chapters-head {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: var(--space-3);
    }

    .chapters-head-left {
        display: flex;
        align-items: baseline;
        gap: var(--space-2);
        min-width: 0;
    }

    .content-size {
        color: var(--text-muted);
        font-size: var(--font-sm);
        font-family: var(--font-mono, monospace);
    }

    .size {
        color: var(--text-muted);
        margin-left: var(--space-2);
        font-size: var(--font-sm);
        font-family: var(--font-mono, monospace);
    }

    /* Mobile hero + chrome — hidden above 640px so the existing
       desktop layout stays untouched. */
    .mobile-hero {
        display: none;
    }

    .read-more {
        display: none;
    }

    .cta-bar {
        display: none;
    }

    @media (max-width: 640px) {
        /* article gets explicit horizontal clip so the hero's
           negative margins (which bleed edge-to-edge by extending
           past main's `--space-3` gutters) don't show up as touch-
           pannable internal scroll. Fixed-position descendants like
           .cta-bar escape this clip so the sticky CTA still spans
           the full viewport. */
        article {
            overflow-x: hidden;
        }

        .mobile-hero {
            position: relative;
            display: block;
            /* Negative margin cancels main's 16px mobile gutter so the
               blurred backdrop runs edge-to-edge — keep this number in
               sync with main's padding-left/right in +layout.svelte. */
            margin: 0 calc(-1 * var(--space-4)) var(--space-4);
            overflow: hidden;
            color: #fff;
            min-height: 220px;
        }

        .hero-bg {
            position: absolute;
            inset: -20px;
            background-size: cover;
            background-position: center;
            filter: blur(28px);
            transform: scale(1.1);
            z-index: 0;
        }

        .hero-scrim {
            position: absolute;
            inset: 0;
            background: linear-gradient(
                to bottom,
                rgba(0, 0, 0, 0.45),
                rgba(0, 0, 0, 0.75)
            );
            z-index: 0;
        }

        .hero-appbar {
            position: relative;
            z-index: 1;
            display: flex;
            align-items: center;
            gap: var(--space-1);
            /* 16px horizontal gutter + 16px above the buttons so they
               aren't kissing the screen top or hero edges. iOS / Material
               both ship app-bar buttons with ~16dp inset; tighter than
               that reads as "stuck to the border". */
            padding: var(--space-4) var(--space-4);
            padding-top: calc(var(--space-4) + var(--safe-top));
            min-height: 48px;
        }

        .hero-spacer {
            flex: 1;
        }

        .hero-icon-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 40px;
            height: 40px;
            padding: 0;
            background: rgba(0, 0, 0, 0.25);
            color: #fff;
            border: 1px solid rgba(255, 255, 255, 0.15);
            border-radius: var(--radius-pill);
            cursor: pointer;
        }

        .hero-icon-btn:hover:not(:disabled),
        .hero-icon-btn:focus-visible {
            background: rgba(0, 0, 0, 0.45);
        }

        .hero-content {
            position: relative;
            z-index: 1;
            display: flex;
            gap: var(--space-3);
            /* 20px side padding gives the cover thumb + title visible
               separation from the hero's edge (vs. main's 16px gutter,
               which on a tinted hero reads as flush). */
            padding: var(--space-3) var(--space-5) var(--space-5);
        }

        .hero-cover {
            width: 110px;
            aspect-ratio: 2 / 3;
            object-fit: cover;
            border-radius: var(--radius-md);
            box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
            background: var(--surface);
            flex-shrink: 0;
        }

        .hero-text {
            display: flex;
            flex-direction: column;
            gap: var(--space-1);
            justify-content: flex-end;
            min-width: 0;
        }

        .hero-title {
            margin: 0;
            font-size: var(--font-xl);
            line-height: var(--leading-tight);
            color: #fff;
            /* Clamp to two lines — long titles ellipsize rather than
               push the status badge off the cover. */
            display: -webkit-box;
            -webkit-line-clamp: 2;
            line-clamp: 2;
            -webkit-box-orient: vertical;
            overflow: hidden;
            overflow-wrap: anywhere;
        }

        .hero-authors {
            margin: 0;
            font-size: var(--font-sm);
            color: rgba(255, 255, 255, 0.85);
        }

        .hero-status {
            align-self: flex-start;
            display: inline-flex;
            align-items: center;
            padding: 2px var(--space-2);
            border-radius: var(--radius-pill);
            font-size: var(--font-xs);
            font-weight: var(--weight-semibold);
            background: rgba(255, 255, 255, 0.15);
            color: #fff;
            border: 1px solid rgba(255, 255, 255, 0.25);
            text-transform: uppercase;
            letter-spacing: 0.04em;
        }

        .hero-status.status-completed {
            background: rgba(74, 222, 128, 0.2);
            border-color: rgba(74, 222, 128, 0.6);
        }

        /* Hide the desktop hero pieces (cover img + title-row) on mobile
           — the new hero block above replaces them. The rest of .meta
           (genres, alt-titles, tags, description) is preserved. */
        .overview > .cover {
            display: none;
        }

        .overview .title-row {
            display: none;
        }

        .overview {
            /* `1fr` resolves to `minmax(auto, 1fr)` and the `auto`
               minimum is the intrinsic content size — a single long
               unbroken tag or URL in .meta then pushes the whole
               column past the article's gutter. `minmax(0, 1fr)`
               clamps it so the grid item respects parent width and
               its children's `overflow-wrap: anywhere` actually
               takes effect. */
            grid-template-columns: minmax(0, 1fr);
            margin-bottom: var(--space-4);
        }

        .overview .meta {
            /* Defensive — every flex/grid container down this tree
               needs a min-width:0 escape so unbreakable tokens can't
               re-expand. */
            min-width: 0;
        }

        .overview .chip-row {
            min-width: 0;
            max-width: 100%;
        }

        /* Inline action-row is replaced by the AppBar bookmark icon +
           overflow sheet on mobile; the inline Continue link is
           replaced by the sticky bottom CTA. */
        .action-row {
            display: none;
        }

        /* `.resync-msg` is a sibling of `.action-row`, not a child, so it
           survives the row being hidden. Keep it visible on mobile: admins
           trigger Force resync from the overflow sheet and need the result. */

        .continue {
            display: none;
        }

        .description.clamped {
            display: -webkit-box;
            -webkit-line-clamp: 3;
            line-clamp: 3;
            -webkit-box-orient: vertical;
            overflow: hidden;
            white-space: normal;
        }

        .read-more {
            display: inline-block;
            background: transparent;
            border: 0;
            padding: 0;
            color: var(--primary);
            font-size: var(--font-sm);
            font-weight: var(--weight-medium);
            cursor: pointer;
            margin: var(--space-1) 0 var(--space-3);
        }

        .cta-bar {
            position: fixed;
            left: 0;
            right: 0;
            bottom: 0;
            z-index: var(--z-sticky);
            display: block;
            padding: var(--space-3);
            padding-left: calc(var(--space-3) + var(--safe-left));
            padding-right: calc(var(--space-3) + var(--safe-right));
            padding-bottom: calc(var(--space-3) + var(--safe-bottom));
            background: var(--bg);
            border-top: 1px solid var(--border);
        }

        .cta {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
            min-height: 48px;
            padding: 0 var(--space-4);
            background: var(--primary);
            color: var(--primary-contrast);
            border-radius: var(--radius-md);
            text-decoration: none;
            text-align: center;
            font-weight: var(--weight-semibold);
            box-shadow: var(--shadow-md);
            /* "Continue {chapterLabel}" can carry an unbroken slug like
               a hash or romanized title; `anywhere` keeps the button
               from pushing past the .cta-bar's padded gutter. */
            overflow-wrap: anywhere;
        }

        .cta:hover {
            background: var(--primary-hover);
            text-decoration: none;
        }

        .overflow-list {
            display: flex;
            flex-direction: column;
            gap: 0;
        }

        .overflow-item {
            display: flex;
            align-items: center;
            gap: var(--space-3);
            padding: var(--space-3) var(--space-1);
            background: transparent;
            border: 0;
            border-bottom: 1px solid var(--border);
            color: var(--text);
            text-align: left;
            text-decoration: none;
            font-size: var(--font-base);
            min-height: 48px;
            cursor: pointer;
        }

        .overflow-item:last-child {
            border-bottom: 0;
        }

        .overflow-item:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
    }
</style>
