<script lang="ts">
	import { onMount, untrack } from 'svelte';
	import { goto, invalidateAll } from '$app/navigation';
	import type { PageData } from './$types';
	import CommentsSection from '$lib/components/CommentsSection.svelte';
	import StarRating from '$lib/components/StarRating.svelte';
	import * as m from '$lib/paraglide/messages.js';
	import type { ShelfName } from '$lib/server/pocketbase';
	import { audioStore } from '$lib/audio.svelte';

	let { data }: { data: PageData } = $props();

	// ── Resume audio progress ──────────────────────────────────────────────────
	/** audioTime > 5 means we have a saved position worth resuming */
	let resumeAudioTime = $state<number | null>(null);

	onMount(() => {
		if (data.book?.slug) {
			window.umami?.track('book_view', { slug: data.book.slug });
		}

		// Check if there's saved audio progress for the last chapter
		if (data.book?.slug && data.lastChapter) {
			fetch(`/api/progress/audio-time?slug=${encodeURIComponent(data.book.slug)}&chapter=${data.lastChapter}`)
				.then((r) => r.ok ? r.json() : null)
				.then((body) => {
					if (body?.audioTime != null && body.audioTime > 5) {
						resumeAudioTime = body.audioTime;
					}
				})
				.catch(() => {});
		}
	});

	function resumeAudio() {
		if (!data.book?.slug || !data.lastChapter) return;
		audioStore.autoStartChapter = data.lastChapter;
		goto(`/books/${data.book.slug}/chapters/${data.lastChapter}`);
	}

	// ── Save / unsave ─────────────────────────────────────────────────────────
	let saved = $state(untrack(() => data.saved));
	let saving = $state(false);

	// ── Ratings ───────────────────────────────────────────────────────────────
	// svelte-ignore state_referenced_locally
	let userRating = $state(data.userRating ?? 0);
	// svelte-ignore state_referenced_locally
	let ratingAvg = $state(data.ratingAvg ?? { avg: 0, count: 0 });

	async function rate(r: number) {
		userRating = r;
		try {
			const res = await fetch(`/api/ratings/${encodeURIComponent(data.book?.slug ?? '')}`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ rating: r })
			});
			if (res.ok) {
				const body = await res.json();
				ratingAvg = { avg: body.avg, count: body.count };
			}
		} catch { /* ignore */ }
	}

	// ── Shelf ─────────────────────────────────────────────────────────────────
	// svelte-ignore state_referenced_locally
	let currentShelf = $state<ShelfName>((data.currentShelf as ShelfName) ?? '');

	async function setShelf(shelf: ShelfName) {
		currentShelf = shelf;
		await fetch(`/api/library/${encodeURIComponent(data.book?.slug ?? '')}`, {
			method: 'PATCH',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ shelf })
		});
	}

	async function toggleSave() {
		if (saving || !data.book) return;
		saving = true;
		try {
			const method = saved ? 'DELETE' : 'POST';
			const res = await fetch(`/api/library/${encodeURIComponent(data.book?.slug ?? '')}`, { method });
			if (res.ok) saved = !saved;
		} finally {
			saving = false;
		}
	}

	function parseGenres(genres: string[] | string): string[] {
		if (Array.isArray(genres)) return genres;
		try {
			return JSON.parse(genres);
		} catch {
			return [];
		}
	}

	const genres = $derived(parseGenres(data.book?.genres ?? []));
	const chapterList = $derived(data.chapters ?? []);

	// ── Admin: rescrape ───────────────────────────────────────────────────────
	let scraping = $state(false);
	let scrapeResult = $state<'queued' | 'busy' | 'error' | ''>('');

	async function rescrape() {
		if (scraping || !data.book?.source_url) return;
		scraping = true;
		scrapeResult = '';
		try {
			const res = await fetch('/api/scrape', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ url: data.book?.source_url })
			});
			if (res.ok) scrapeResult = 'queued';
			else if (res.status === 409) scrapeResult = 'busy';
			else scrapeResult = 'error';
		} catch {
			scrapeResult = 'error';
		} finally {
			scraping = false;
		}
	}

	// ── Admin: scrape range ───────────────────────────────────────────────────
	let rangeFrom = $state('');
	let rangeTo = $state('');
	let rangeScraping = $state(false);
	let rangeResult = $state<'queued' | 'busy' | 'error' | ''>('');

	async function scrapeRange() {
		if (rangeScraping || !data.book?.source_url) return;
		const from = parseInt(rangeFrom, 10);
		const to = parseInt(rangeTo, 10);
		if (!from || from < 1) return;
		rangeScraping = true;
		rangeResult = '';
		try {
			const res = await fetch('/api/scrape/range', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ url: data.book?.source_url, from, to: to || undefined })
			});
			if (res.ok) rangeResult = 'queued';
			else if (res.status === 409) rangeResult = 'busy';
			else rangeResult = 'error';
		} catch {
			rangeResult = 'error';
		} finally {
			rangeScraping = false;
		}
	}

	// ── Summary expand/collapse ───────────────────────────────────────────────
	let summaryExpanded = $state(false);

	// ── Admin panel expand/collapse ───────────────────────────────────────────
	let adminOpen = $state(false);

	// ── Admin: book cover generation ──────────────────────────────────────────
	let coverGenerating = $state(false);
	let coverPreview = $state<string | null>(null);
	let coverSaving = $state(false);
	let coverResult = $state<'saved' | 'error' | ''>('');
	let coverErrorMsg = $state('');
	let coverPromptOpen = $state(false);

	function buildCoverPrompt(): string {
		const title = data.book?.title ?? '';
		const summary = data.book?.summary ?? '';
		const excerpt = summary.length > 200 ? summary.slice(0, 200) + '…' : summary;
		return `Book cover art for "${title}"${excerpt ? ` — ${excerpt}` : ''}. Epic scene with dramatic lighting, professional book cover, highly detailed, 4K.`;
	}

	let coverPrompt = $state('');
	let coverUseAsRef = $state(false);

	$effect(() => {
		if (coverPromptOpen && !coverPrompt) coverPrompt = buildCoverPrompt();
	});

	async function generateCover() {
		const slug = data.book?.slug;
		if (coverGenerating || !slug) return;
		coverGenerating = true;
		coverPreview = null;
		coverResult = '';
		coverErrorMsg = '';
		const promptToUse = coverPrompt.trim() || buildCoverPrompt();
		try {
			let res: Response;
			if (coverUseAsRef && data.book?.cover) {
				try {
					const imgRes = await fetch(data.book.cover);
					const blob = await imgRes.blob();
					const ext = blob.type === 'image/jpeg' ? 'jpg' : blob.type === 'image/webp' ? 'webp' : 'png';
					const file = new File([blob], `${slug}-cover-ref.${ext}`, { type: blob.type });
					const fd = new FormData();
					fd.append('json', JSON.stringify({ slug, type: 'cover', prompt: promptToUse, strength: 0.65 }));
					fd.append('reference', file);
					res = await fetch('/api/admin/image-gen', { method: 'POST', body: fd });
				} catch {
					// Fall back to text-only if cover fetch fails
					res = await fetch('/api/admin/image-gen', {
						method: 'POST',
						headers: { 'Content-Type': 'application/json' },
						body: JSON.stringify({ slug, type: 'cover', prompt: promptToUse })
					});
				}
			} else {
				res = await fetch('/api/admin/image-gen', {
					method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, type: 'cover', prompt: promptToUse })
			});
		}
		if (res.ok) {
			const d = await res.json();
			coverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null;
		} else {
			const d = await res.json().catch(() => ({}));
			coverErrorMsg = (d as any).error ?? '';
			coverResult = 'error';
		}
	} catch (e: any) {
		coverErrorMsg = e?.message ?? '';
		coverResult = 'error';
	} finally {
		coverGenerating = false;
	}
}

	async function saveCover() {
		const slug = data.book?.slug;
		if (coverSaving || !coverPreview || !slug) return;
		coverSaving = true;
		coverResult = '';
		try {
			const b64 = coverPreview.replace(/^data:[^;]+;base64,/, '');
			const res = await fetch('/api/admin/image-gen/save-cover', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, image_b64: b64 })
			});
			if (res.ok) {
				coverResult = 'saved';
				coverPreview = null;
				await invalidateAll();
			} else {
				coverResult = 'error';
			}
		} catch {
			coverResult = 'error';
		} finally {
			coverSaving = false;
		}
	}

	// ── Admin: chapter cover generation ───────────────────────────────────────
	let chapterCoverN = $state('1');
	let chapterCoverGenerating = $state(false);
	let chapterCoverPreview = $state<string | null>(null);
	let chapterCoverResult = $state<'saved' | 'error' | ''>('');
	let chapterCoverErrorMsg = $state('');
	let chapterCoverSaving = $state(false);
	let chapterCoverPrompt = $state('');

	async function generateChapterCover() {
		const slug = data.book?.slug;
		if (chapterCoverGenerating || !slug) return;
		const n = parseInt(chapterCoverN, 10);
		if (!n || n < 1) return;
		chapterCoverGenerating = true;
		chapterCoverPreview = null;
		chapterCoverResult = '';
		chapterCoverErrorMsg = '';
		const promptToUse = chapterCoverPrompt.trim() || `Chapter ${n} illustration for "${data.book?.title ?? slug}". Dramatic scene, vivid colors, detailed art, cinematic lighting.`;
		try {
			const res = await fetch('/api/admin/image-gen', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, type: 'chapter', chapter: n, prompt: promptToUse })
			});
			if (res.ok) {
				const d = await res.json();
				chapterCoverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null;
			} else {
				const d = await res.json().catch(() => ({}));
				chapterCoverErrorMsg = (d as any).error ?? '';
				chapterCoverResult = 'error';
			}
		} catch (e: any) {
			chapterCoverErrorMsg = e?.message ?? '';
			chapterCoverResult = 'error';
		} finally {
			chapterCoverGenerating = false;
		}
	}

	async function saveChapterCover() {
		const slug = data.book?.slug;
		if (chapterCoverSaving || !chapterCoverPreview || !slug) return;
		const n = parseInt(chapterCoverN, 10);
		if (!n || n < 1) return;
		chapterCoverSaving = true;
		chapterCoverResult = '';
		try {
			const b64 = chapterCoverPreview.replace(/^data:[^;]+;base64,/, '');
			const res = await fetch('/api/admin/image-gen/save-cover', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, chapter: n, image_b64: b64 })
			});
			if (res.ok) {
				chapterCoverResult = 'saved';
				chapterCoverPreview = null;
			} else {
				chapterCoverResult = 'error';
			}
		} catch {
			chapterCoverResult = 'error';
		} finally {
			chapterCoverSaving = false;
		}
	}

	// ── Admin: description generation ─────────────────────────────────────────
	let descGenerating = $state(false);
	let descPreview = $state('');
	let descApplying = $state(false);
	let descResult = $state<'applied' | 'error' | ''>('');
	let descInstructions = $state('');

	async function generateDesc() {
		const slug = data.book?.slug;
		if (descGenerating || !slug) return;
		descGenerating = true;
		descPreview = '';
		descResult = '';
		try {
			const res = await fetch('/api/admin/text-gen/description', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, instructions: descInstructions.trim() || undefined })
			});
			if (res.ok) {
				const d = await res.json();
				descPreview = d.new_description ?? '';
			} else {
				descResult = 'error';
			}
		} catch {
			descResult = 'error';
		} finally {
			descGenerating = false;
		}
	}

	async function applyDesc() {
		const slug = data.book?.slug;
		if (descApplying || !descPreview || !slug) return;
		descApplying = true;
		descResult = '';
		try {
			const res = await fetch('/api/admin/text-gen/description/apply', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, description: descPreview })
			});
			if (res.ok) {
				descResult = 'applied';
				descPreview = '';
				await invalidateAll();
			} else {
				descResult = 'error';
			}
		} catch {
			descResult = 'error';
		} finally {
			descApplying = false;
		}
	}

	// ── Admin: chapter names generation ───────────────────────────────────────
	let chapNamesGenerating = $state(false);
	let chapNamesPreview = $state<{ number: number; old_title: string; new_title: string; edited: string }[]>([]);
	let chapNamesApplying = $state(false);
	let chapNamesResult = $state<'applied' | 'error' | ''>('');
	let chapNamesPattern = $state('{scene}');
	let chapNamesBatchProgress = $state('');
	let chapNamesBatchWarnings = $state<string[]>([]);

	async function generateChapNames() {
		const slug = data.book?.slug;
		if (chapNamesGenerating || !slug) return;
		chapNamesGenerating = true;
		chapNamesPreview = [];
		chapNamesResult = '';
		chapNamesBatchProgress = '';
		chapNamesBatchWarnings = [];
		try {
			const res = await fetch('/api/admin/text-gen/chapter-names', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, pattern: chapNamesPattern.trim() || '{scene}' })
			});
			if (!res.ok) {
				chapNamesResult = 'error';
				return;
			}
			// SSE streaming response
			const reader = res.body!.getReader();
			const decoder = new TextDecoder();
			let buffer = '';
			outer: while (true) {
				const { value, done } = await reader.read();
				if (done) break;
				buffer += decoder.decode(value, { stream: true });
				const lines = buffer.split('\n');
				buffer = lines.pop() ?? '';
				for (const line of lines) {
					if (!line.startsWith('data: ')) continue;
					const payload = line.slice(6).trim();
					if (!payload) continue;
					let evt: { batch?: number; total_batches?: number; chapters_done?: number; total_chapters?: number; chapters?: { number: number; old_title: string; new_title: string }[]; error?: string; done?: boolean };
					try { evt = JSON.parse(payload); } catch { continue; }
					if (evt.done) { chapNamesBatchProgress = `Done — ${evt.total_chapters ?? chapNamesPreview.length} chapters`; chapNamesGenerating = false; break outer; }
					if (evt.error) {
						chapNamesBatchWarnings = [...chapNamesBatchWarnings, `Batch ${evt.batch}/${evt.total_batches}: ${evt.error}`];
					} else if (evt.chapters) {
						chapNamesPreview = [...chapNamesPreview, ...evt.chapters.map((c) => ({ ...c, edited: c.new_title }))];
					}
					if (evt.batch != null && evt.total_batches != null) {
						chapNamesBatchProgress = `Batch ${evt.batch}/${evt.total_batches} · ${evt.chapters_done ?? chapNamesPreview.length}/${evt.total_chapters ?? '?'}`;
					}
				}
			}
			if (chapNamesPreview.length === 0 && chapNamesBatchWarnings.length === 0) chapNamesResult = 'error';
		} catch {
			chapNamesResult = 'error';
		} finally {
			chapNamesGenerating = false;
		}
	}

	async function applyChapNames() {
		const slug = data.book?.slug;
		if (chapNamesApplying || chapNamesPreview.length === 0 || !slug) return;
		chapNamesApplying = true;
		chapNamesResult = '';
		try {
			const chapters = chapNamesPreview.map((c) => ({ number: c.number, title: c.edited?.trim() || c.new_title }));
			const res = await fetch('/api/admin/text-gen/chapter-names/apply', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, chapters })
			});
			if (res.ok) {
				chapNamesResult = 'applied';
				chapNamesPreview = [];
				await invalidateAll();
			} else {
				chapNamesResult = 'error';
			}
		} catch {
			chapNamesResult = 'error';
		} finally {
			chapNamesApplying = false;
		}
	}

	// ── Admin: audio TTS bulk enqueue ─────────────────────────────────────────
	interface Voice { id: string; engine: string; lang: string; gender: string }
	let audioVoices = $state<Voice[]>([]);
	let audioVoicesLoaded = $state(false);
	let audioVoice = $state('af_bella');
	let audioFrom = $state('1');
	let audioTo = $state('');
	let audioEnqueuing = $state(false);
	let audioResult = $state<{ enqueued: number; skipped: number } | null>(null);
	let audioError = $state('');

	// Load voices lazily when admin panel opens
	$effect(() => {
		if (!adminOpen || audioVoicesLoaded) return;
		fetch('/api/voices')
			.then((r) => r.json())
			.then((d: { voices: Voice[] }) => {
				audioVoices = d.voices ?? [];
				audioVoicesLoaded = true;
			})
			.catch(() => { audioVoicesLoaded = true; });
	});

	function voiceLabel(v: Voice): string {
		if (v.engine === 'cfai') {
			const speaker = v.id.startsWith('cfai:') ? v.id.slice(5) : v.id;
			return speaker.replace(/\b\w/g, (c) => c.toUpperCase()) + ' (CF AI)';
		}
		if (v.engine === 'pocket-tts') {
			return v.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + ' (Pocket)';
		}
		// Kokoro
		const langMap: Record<string, string> = {
			af: 'US', am: 'US', bf: 'UK', bm: 'UK',
			ef: 'ES', em: 'ES', ff: 'FR', hf: 'IN', hm: 'IN',
			'if': 'IT', im: 'IT', jf: 'JP', jm: 'JP', pf: 'PT', pm: 'PT', zf: 'ZH', zm: 'ZH',
		};
		const prefix = v.id.slice(0, 2);
		const name = v.id.slice(3).replace(/^v0/, '').replace(/^([a-z])/, (c) => c.toUpperCase());
		const lang = langMap[prefix] ?? prefix.toUpperCase();
		return `${name} (${lang})`;
	}

	async function enqueueAudio() {
		const slug = data.book?.slug;
		if (audioEnqueuing || !slug) return;
		const from = parseInt(audioFrom, 10);
		const to = audioTo ? parseInt(audioTo, 10) : (data.book?.total_chapters ?? 1);
		if (!from || from < 1) return;
		audioEnqueuing = true;
		audioResult = null;
		audioError = '';
		try {
			const res = await fetch('/api/admin/audio/bulk', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, voice: audioVoice, from, to })
			});
			if (res.ok || res.status === 202) {
				const d = await res.json();
				audioResult = { enqueued: d.enqueued ?? 0, skipped: d.skipped ?? 0 };
			} else {
				const d = await res.json().catch(() => ({}));
				audioError = d.error ?? 'Failed to enqueue';
			}
		} catch {
			audioError = 'Network error';
		} finally {
			audioEnqueuing = false;
		}
	}

	async function cancelAudio() {
		const slug = data.book?.slug;
		if (!slug) return;
		audioResult = null;
		audioError = '';
		try {
			const res = await fetch('/api/admin/audio/cancel-bulk', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug })
			});
			if (res.ok) {
				const d = await res.json();
				audioError = `Cancelled ${d.cancelled ?? 0} task(s).`;
			}
		} catch {
			audioError = 'Cancel failed';
		}
	}

	// ── "More like this" ─────────────────────────────────────────────────────
	interface SimilarBook { slug: string; title: string; cover: string | null; author: string | null }
	let similarBooks = $state<SimilarBook[]>([]);

	$effect(() => {
		const firstGenre = genres[0];
		if (!firstGenre) return;
		const currentSlug = data.book?.slug ?? '';
		fetch(`/api/catalogue-page?genre=${encodeURIComponent(firstGenre)}&sort=popular&page=1`)
			.then((r) => r.json())
			.then((d: { novels?: { slug: string; title: string; cover: string | null; author: string | null }[] }) => {
				similarBooks = (d.novels ?? [])
					.filter((b) => b.slug !== currentSlug)
					.slice(0, 10);
			})
			.catch(() => { /* non-critical */ });
	});

	// ── Auto-poll when scrape task is in flight ───────────────────────────────
	$effect(() => {
		if (!data.scraping || !data.taskId) return;

		const INTERVAL_MS = 3000;
		const id = data.taskId;

		const timer = setInterval(async () => {
			try {
				const res = await fetch(`/api/scrape/task/${encodeURIComponent(id)}`);
				if (!res.ok) return;
				const body: { id: string; status: string } = await res.json();
				if (body.status === 'done' || body.status === 'failed') {
					clearInterval(timer);
					await invalidateAll();
				}
			} catch {
				// network blip — try again next tick
			}
		}, INTERVAL_MS);

		return () => clearInterval(timer);
	});
</script>

<svelte:head>
	<title>{data.scraping ? m.book_detail_scraping() : (data.book?.title ?? 'Book')} — libnovel</title>
</svelte:head>

{#if data.scraping}
<!-- ═══════════════════════════════════════════ Scraping in progress ══ -->
<div class="flex flex-col items-center justify-center py-24 gap-5 text-center">
	<svg class="w-10 h-10 text-(--color-brand) animate-spin" fill="none" viewBox="0 0 24 24">
		<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
		<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
	</svg>
	<div>
		<p class="text-(--color-text) font-semibold text-lg">{m.book_detail_scraping()}</p>
		<p class="text-(--color-muted) text-sm mt-1">
			{m.book_detail_scraping_progress()}
		</p>
		{#if data.taskId && data.user?.role === 'admin'}
			<p class="text-(--color-muted) text-xs mt-2 font-mono">task: {data.taskId}</p>
		{/if}
	</div>
	<a href="/" class="mt-2 text-sm text-(--color-brand) hover:text-(--color-brand-dim) transition-colors">{m.book_detail_scraping_home()}</a>
</div>

{:else}
{@const book = data.book!}

<!-- ═══════════════════════════════════════════════════════════════ Hero ══ -->
<div class="relative rounded-xl overflow-hidden mb-8">
	<!-- Blurred cover background -->
	{#if book.cover}
		<div
			class="absolute inset-0 bg-cover bg-center scale-110"
			style="background-image: url('{book.cover}'); filter: blur(24px); opacity: 0.18;"
			aria-hidden="true"
		></div>
	{/if}
	<div class="absolute inset-0 bg-gradient-to-b from-(--color-surface)/60 to-(--color-surface)/95 pointer-events-none" aria-hidden="true"></div>

	<div class="relative flex flex-col p-5 sm:p-7 gap-4">
		<!-- Cover + meta row -->
		<div class="flex gap-5 sm:gap-8">
			<!-- Cover image -->
			{#if book.cover}
				<img
					src={book.cover}
					alt={book.title}
					class="w-28 sm:w-48 rounded-lg object-cover flex-shrink-0 border border-(--color-border) shadow-xl self-start"
				/>
			{/if}

			<!-- Meta -->
			<div class="flex flex-col gap-2 min-w-0 flex-1">
				<!-- Title + "not in library" badge -->
				<div class="flex items-start gap-2 flex-wrap">
					<h1 class="text-xl sm:text-3xl font-bold text-(--color-text) leading-tight">{book.title}</h1>
					{#if !data.inLib}
						<span
							class="mt-1 text-xs px-2 py-0.5 rounded-full bg-(--color-surface-3) text-(--color-muted) border border-(--color-border) shrink-0"
							title="This book was fetched live from the source and is not yet in your library"
						>
							{m.book_detail_not_in_library()}
						</span>
					{/if}
				</div>

				<!-- Author -->
				{#if book.author}
					<p class="text-(--color-muted) text-sm">{book.author}</p>
				{/if}

				<!-- Status + genres -->
				<div class="flex flex-wrap gap-1.5 mt-0.5">
					{#if book.status}
						<span class="text-xs px-2 py-0.5 rounded bg-(--color-surface-3) text-(--color-text) border border-(--color-border)">{book.status}</span>
					{/if}
					{#each genres as genre}
						<a
							href="/catalogue?genre={encodeURIComponent(genre)}"
							class="text-xs px-2 py-0.5 rounded bg-(--color-surface-2) text-(--color-muted) border border-(--color-border) hover:border-(--color-brand)/50 hover:text-(--color-text) transition-colors"
						>{genre}</a>
					{/each}
					{#if data.readersThisWeek && data.readersThisWeek > 0}
						<span class="text-xs px-2 py-0.5 rounded bg-(--color-surface-2) text-(--color-muted) border border-(--color-border) flex items-center gap-1">
							<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>
							{data.readersThisWeek} reading this week
						</span>
					{/if}
				</div>


				<!-- CTA buttons — desktop only -->
				<div class="hidden sm:flex gap-2 mt-3 items-center flex-wrap">
					{#if data.lastChapter}
						<a
							href="/books/{book.slug}/chapters/{data.lastChapter}"
							class="px-5 py-2 bg-(--color-brand) text-(--color-surface) font-semibold rounded-lg text-sm hover:bg-(--color-brand-dim) transition-colors shadow"
						>
							{m.book_detail_continue_ch({ n: String(data.lastChapter) })}
						</a>
					{/if}
					{#if resumeAudioTime != null && data.lastChapter}
						<button
							type="button"
							onclick={resumeAudio}
							class="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-semibold bg-(--color-surface-3) text-(--color-text) border border-(--color-border) hover:border-(--color-brand)/50 hover:text-(--color-brand) transition-colors"
							title="Resume audio from where you left off"
						>
							<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
							Resume Ch. {data.lastChapter}
						</button>
					{/if}
					{#if chapterList.length > 0}
						<a
							href="/books/{book.slug}/chapters/1"
							class="px-4 py-2 rounded-lg text-sm font-semibold transition-colors
								{data.lastChapter
					? 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-3)'
								: 'bg-(--color-brand) text-(--color-surface) hover:bg-(--color-brand-dim) shadow'}"
						>
							{data.inLib ? m.book_detail_start_ch1() : m.book_detail_preview_ch1()}
						</a>
					{/if}
					{#if !data.isLoggedIn}
						<a
							href="/login"
							title={m.book_detail_signin_to_save()}
							class="flex items-center justify-center w-9 h-9 rounded-lg border border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:text-(--color-text) transition-colors"
						>
							<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
							</svg>
						</a>
					{:else if data.inLib}
						<button
							onclick={toggleSave}
							disabled={saving}
							title={saved ? m.book_detail_remove_from_library() : m.book_detail_add_to_library()}
							class="flex items-center justify-center w-9 h-9 rounded-lg border transition-colors disabled:opacity-50
								{saved
									? 'bg-(--color-brand)/20 text-(--color-brand-dim) border-(--color-brand)/30 hover:bg-red-500/20 hover:text-red-300 hover:border-red-400/30'
									: 'bg-(--color-surface-3) text-(--color-muted) border-(--color-border) hover:bg-(--color-surface-3) hover:text-(--color-text)'}"
						>
							{#if saving}
								<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
									<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
									<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
								</svg>
							{:else if saved}
								<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
									<path d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
								</svg>
							{:else}
								<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
									<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
								</svg>
							{/if}
						</button>
					{/if}
				</div>

				<!-- Ratings + shelf — desktop -->
				<div class="hidden sm:flex items-center gap-3 flex-wrap mt-1">
					<StarRating
						rating={userRating}
						avg={ratingAvg.avg}
						count={ratingAvg.count}
						onrate={rate}
						size="md"
					/>
					{#if saved}
						<div class="relative">
							<select
								value={currentShelf}
								onchange={(e) => setShelf((e.currentTarget as HTMLSelectElement).value as ShelfName)}
								class="bg-(--color-surface-2) border border-(--color-border) rounded-lg px-3 py-1.5 text-sm text-(--color-muted) focus:outline-none focus:ring-2 focus:ring-(--color-brand) cursor-pointer"
							>
								<option value="">Reading</option>
								<option value="plan_to_read">Plan to Read</option>
								<option value="completed">Completed</option>
								<option value="dropped">Dropped</option>
							</select>
						</div>
					{/if}
				</div>
			</div>
		</div>

		<!-- CTA buttons — mobile only -->
		<div class="flex sm:hidden flex-col gap-2 mt-3">
			<!-- Row 1: primary read button(s) -->
			<div class="flex gap-2">
				{#if data.lastChapter}
					<a
						href="/books/{book.slug}/chapters/{data.lastChapter}"
						class="flex-1 text-center px-4 py-2.5 bg-(--color-brand) text-(--color-surface) font-semibold rounded-lg text-sm hover:bg-(--color-brand-dim) transition-colors shadow"
					>
						{m.book_detail_continue_ch({ n: String(data.lastChapter) })}
					</a>
				{/if}
				{#if chapterList.length > 0}
					<a
						href="/books/{book.slug}/chapters/1"
						class="text-center px-4 py-2.5 rounded-lg text-sm font-semibold transition-colors
							{data.lastChapter
								? 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-3)'
								: 'flex-1 bg-(--color-brand) text-(--color-surface) hover:bg-(--color-brand-dim) shadow'}"
					>
						{data.inLib ? m.book_detail_start_ch1() : m.book_detail_preview_ch1()}
					</a>
				{/if}
			</div>

			<!-- Resume audio row (mobile) -->
			{#if resumeAudioTime != null && data.lastChapter}
				<button
					type="button"
					onclick={resumeAudio}
					class="flex items-center justify-center gap-1.5 w-full px-4 py-2.5 rounded-lg text-sm font-semibold bg-(--color-surface-3) text-(--color-text) border border-(--color-border) hover:border-(--color-brand)/50 hover:text-(--color-brand) transition-colors"
					title="Resume audio from where you left off"
				>
					<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
					Resume Ch. {data.lastChapter}
				</button>
			{/if}

			<!-- Row 2: bookmark + shelf + stars -->
			<div class="flex items-center gap-2 flex-wrap">
				{#if !data.isLoggedIn}
					<a
						href="/login"
						title={m.book_detail_signin_to_save()}
						class="flex items-center justify-center w-9 h-9 rounded-lg border border-(--color-border) bg-(--color-surface-3) text-(--color-muted) hover:text-(--color-text) transition-colors flex-shrink-0"
					>
						<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
							<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
						</svg>
					</a>
				{:else if data.inLib}
					<button
						onclick={toggleSave}
						disabled={saving}
						title={saved ? m.book_detail_remove_from_library() : m.book_detail_add_to_library()}
						class="flex items-center justify-center w-9 h-9 flex-shrink-0 rounded-lg border transition-colors disabled:opacity-50
							{saved
								? 'bg-(--color-brand)/20 text-(--color-brand-dim) border-(--color-brand)/30 hover:bg-red-500/20 hover:text-red-300 hover:border-red-400/30'
								: 'bg-(--color-surface-3) text-(--color-muted) border-(--color-border) hover:bg-(--color-surface-3) hover:text-(--color-text)'}"
					>
						{#if saving}
							<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
								<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
								<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
							</svg>
						{:else if saved}
							<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
								<path d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
							</svg>
						{:else}
							<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 4a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 20V4z"/>
							</svg>
						{/if}
					</button>
				{/if}

				{#if saved}
					<select
						value={currentShelf}
						onchange={(e) => setShelf((e.currentTarget as HTMLSelectElement).value as ShelfName)}
						class="bg-(--color-surface-2) border border-(--color-border) rounded-lg px-3 py-1.5 text-sm text-(--color-muted) focus:outline-none focus:ring-2 focus:ring-(--color-brand) cursor-pointer flex-shrink-0"
					>
						<option value="">Reading</option>
						<option value="plan_to_read">Plan to Read</option>
						<option value="completed">Completed</option>
						<option value="dropped">Dropped</option>
					</select>
				{/if}

				<StarRating
					rating={userRating}
					avg={ratingAvg.avg}
					count={ratingAvg.count}
					onrate={rate}
					size="sm"
				/>
			</div>
		</div>
	</div>
</div>

<!-- ── Book description ──────────────────────────────────────────────────────── -->
{#if book.summary}
<div class="mb-6">
	<div class="relative">
		<p
			class="text-(--color-muted) text-sm leading-7 break-words whitespace-pre-line {summaryExpanded ? '' : 'line-clamp-5'}"
		>
			{book.summary}
		</p>
		{#if !summaryExpanded && book.summary.length > 300}
			<!-- gradient fade over the last line when collapsed -->
			<div class="absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-(--color-surface) to-transparent pointer-events-none"></div>
		{/if}
	</div>
	{#if book.summary.length > 300}
		<button
			onclick={() => (summaryExpanded = !summaryExpanded)}
			class="mt-2 text-xs text-(--color-brand)/70 hover:text-(--color-brand) transition-colors inline-flex items-center gap-1"
		>
			{summaryExpanded ? m.book_detail_less() : m.book_detail_more()}
			<svg class="w-3 h-3 transition-transform {summaryExpanded ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
				<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
			</svg>
		</button>
	{/if}
</div>
{/if}

<!-- ══════════════════════════════════════════════════ Download row ══ -->
{#if data.inLib && chapterList.length > 0}
<div class="flex items-center gap-3 border border-(--color-border) rounded-xl px-4 py-3 mb-4">
	<svg class="w-4 h-4 text-(--color-muted) flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
		<path stroke-linecap="round" stroke-linejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"/>
	</svg>
	<div class="flex-1 min-w-0">
		<p class="text-sm font-medium text-(--color-text)">Download</p>
		<p class="text-xs text-(--color-muted)">All {chapterList.length} chapters as EPUB</p>
	</div>
	{#if data.isPro}
		<a
			href="/api/export/{book.slug}"
			download="{book.slug}.epub"
			class="px-3 py-1.5 rounded-lg bg-(--color-surface-2) border border-(--color-border) text-sm font-medium text-(--color-muted) hover:text-(--color-text) hover:border-zinc-500 transition-colors flex-shrink-0"
		>
			.epub
		</a>
	{:else}
		<a
			href="/profile"
			class="px-3 py-1.5 rounded-lg bg-(--color-brand)/15 border border-(--color-brand)/30 text-sm font-medium text-(--color-brand) hover:bg-(--color-brand)/25 transition-colors flex-shrink-0"
		>
			Pro
		</a>
	{/if}
</div>
{/if}

<!-- ══════════════════════════════════════════════════ Chapters row ══ -->
<div class="flex flex-col divide-y divide-(--color-border) border border-(--color-border) rounded-xl overflow-hidden mb-6">
	<a
		href="/books/{book.slug}/chapters"
		class="flex items-center gap-3 px-4 py-3.5 hover:bg-(--color-surface-2)/60 transition-colors group"
	>
		<svg class="w-4 h-4 text-(--color-brand) flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
			<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h10"/>
		</svg>
		<div class="flex flex-col min-w-0 flex-1">
			<span class="text-sm font-semibold text-(--color-text)">{m.chapters_heading()}</span>
			{#if chapterList.length > 0}
				<span class="text-xs text-(--color-muted)">
					{#if data.lastChapter && data.lastChapter > 0}
						{m.book_detail_reading_ch({ n: String(data.lastChapter), total: String(chapterList.length) })}
					{:else}
						{m.book_detail_n_chapters({ n: String(chapterList.length) })}
					{/if}
				</span>
			{/if}
		</div>
		<svg class="w-4 h-4 text-(--color-muted) group-hover:text-(--color-muted) transition-colors flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
			<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
		</svg>
	</a>

	<!-- Admin panel (collapsed by default, admin only) -->
	{#if data.isAdmin && book.source_url}
		<div>
			<button
				onclick={() => (adminOpen = !adminOpen)}
				class="w-full flex items-center gap-2 px-4 py-2.5 text-xs font-medium text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2)/50 transition-colors text-left"
			>
				<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
					<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
					<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
				</svg>
				{m.book_detail_admin()}
				<svg class="w-3 h-3 ml-auto transition-transform {adminOpen ? 'rotate-180' : ''}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
					<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
				</svg>
			</button>

		{#if adminOpen}
			<div class="px-4 py-3 border-t border-(--color-border) flex flex-col gap-5">
				<!-- Rescrape -->
				<div class="flex items-center gap-3 flex-wrap">
					<button
						onclick={rescrape}
						disabled={scraping}
						class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
							{scraping ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-3)'}"
					>
						{#if scraping}
							<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24">
								<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
								<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
							</svg>
							{m.book_detail_rescraping()}
						{:else}
							<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
							</svg>
							{m.book_detail_rescrape_book()}
						{/if}
					</button>
					{#if scrapeResult}
						<span class="text-xs {scrapeResult === 'queued' ? 'text-green-400' : scrapeResult === 'busy' ? 'text-(--color-brand)' : 'text-(--color-danger)'}">
							{scrapeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : scrapeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'}
						</span>
					{/if}
				</div>

				<!-- Range scrape -->
				<div class="flex flex-wrap items-end gap-3">
					<div class="flex flex-col gap-1">
						<label for="range-from" class="text-xs text-(--color-muted)">{m.book_detail_from_chapter()}</label>
						<input
							id="range-from"
							type="number"
							min="1"
							bind:value={rangeFrom}
							placeholder="1"
							class="w-24 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
						/>
					</div>
					<div class="flex flex-col gap-1">
						<label for="range-to" class="text-xs text-(--color-muted)">{m.book_detail_to_chapter()}</label>
						<input
							id="range-to"
							type="number"
							min="1"
							bind:value={rangeTo}
							placeholder="end"
							class="w-24 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
						/>
					</div>
					<button
						onclick={scrapeRange}
						disabled={rangeScraping || !rangeFrom}
						class="px-3 py-1.5 rounded text-xs font-medium transition-colors
							{rangeScraping || !rangeFrom
								? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed'
								: 'bg-(--color-brand)/20 text-(--color-brand-dim) hover:bg-(--color-brand)/40 border border-(--color-brand)/30'}"
					>
						{rangeScraping ? m.book_detail_range_queuing() : m.book_detail_scrape_range()}
					</button>
					{#if rangeResult}
						<span class="text-xs {rangeResult === 'queued' ? 'text-green-400' : rangeResult === 'busy' ? 'text-(--color-brand)' : 'text-(--color-danger)'}">
							{rangeResult === 'queued' ? m.catalogue_scrape_queued_badge() + '.' : rangeResult === 'busy' ? m.catalogue_scrape_busy_badge() + '.' : m.common_error() + '.'}
						</span>
					{/if}
				</div>

				<hr class="border-(--color-border)" />

				<!-- Book cover generation -->
				<div class="flex flex-col gap-2">
					<div class="flex items-center justify-between">
						<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_book_cover()}</p>
						<div class="flex items-center gap-2">
							<button
								onclick={() => (coverPromptOpen = !coverPromptOpen)}
								class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors"
							>
								{coverPromptOpen ? 'Hide prompt' : 'Edit prompt'}
							</button>
							<a href="/admin/image-gen" class="text-xs text-(--color-brand)/70 hover:text-(--color-brand) transition-colors">Full editor ↗</a>
						</div>
					</div>
					{#if coverPromptOpen}
						<textarea
							bind:value={coverPrompt}
							rows="3"
							placeholder={buildCoverPrompt()}
							class="w-full px-2 py-1.5 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand) resize-y"
						></textarea>
						{#if book.cover}
							<label class="flex items-center gap-2 cursor-pointer select-none w-fit">
								<input type="checkbox" bind:checked={coverUseAsRef} class="accent-(--color-brand)" />
								<span class="text-xs text-(--color-muted)">Use current cover as reference (img2img)</span>
							</label>
						{/if}
					{/if}
					<div class="flex items-center gap-3 flex-wrap">
						<button
							onclick={generateCover}
							disabled={coverGenerating}
							class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
								{coverGenerating ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
						>
							{#if coverGenerating}
								<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
							{:else}
								<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
							{/if}
							{m.book_detail_admin_generate()}{coverUseAsRef ? ' (img2img)' : ''}
						</button>
						{#if coverResult === 'error'}
							<span class="text-xs text-(--color-danger)">{coverErrorMsg || m.common_error()}</span>
						{:else if coverResult === 'saved'}
							<span class="text-xs text-green-400">{m.book_detail_admin_saved()}</span>
						{/if}
					</div>
					{#if coverPreview}
						<div class="flex items-start gap-3 mt-1">
							<img src={coverPreview} alt="Cover preview" class="w-24 rounded border border-(--color-border)" />
							<div class="flex flex-col gap-2 pt-1">
								<button
									onclick={saveCover}
									disabled={coverSaving}
									class="px-3 py-1.5 rounded text-xs font-medium transition-colors
										{coverSaving ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30 border border-green-600/30'}"
								>
									{coverSaving ? m.book_detail_admin_saving() : m.book_detail_admin_save_cover()}
								</button>
								<button onclick={() => (coverPreview = null)} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors">{m.book_detail_admin_discard()}</button>
							</div>
						</div>
					{/if}
				</div>

				<!-- Chapter cover generation -->
				<div class="flex flex-col gap-2">
					<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_chapter_cover()}</p>
					<textarea
						bind:value={chapterCoverPrompt}
						rows="2"
						placeholder="Chapter {chapterCoverN} illustration for &quot;{data.book?.title}&quot;. Dramatic scene, vivid colors…"
						class="w-full px-2 py-1.5 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand) resize-y"
					></textarea>
					<div class="flex items-end gap-3 flex-wrap">
						<div class="flex flex-col gap-1">
							<label for="ch-cover-n" class="text-xs text-(--color-muted)">{m.book_detail_admin_chapter_n()}</label>
							<input
								id="ch-cover-n"
								type="number"
								min="1"
								bind:value={chapterCoverN}
								placeholder="1"
								class="w-20 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
							/>
						</div>
						<button
							onclick={generateChapterCover}
							disabled={chapterCoverGenerating || !chapterCoverN}
							class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
								{chapterCoverGenerating || !chapterCoverN ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
						>
							{#if chapterCoverGenerating}
								<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
							{:else}
								<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
							{/if}
							{m.book_detail_admin_generate()}
						</button>
						{#if chapterCoverResult === 'error'}
							<span class="text-xs text-(--color-danger)">{chapterCoverErrorMsg || m.common_error()}</span>
						{:else if chapterCoverResult === 'saved'}
							<span class="text-xs text-green-400">{m.book_detail_admin_saved()}</span>
						{/if}
					</div>
					{#if chapterCoverPreview}
						<div class="flex items-start gap-3 mt-1">
							<img src={chapterCoverPreview} alt="Chapter cover preview" class="w-24 rounded border border-(--color-border)" />
							<div class="flex flex-col gap-2 pt-1">
								<button
									onclick={saveChapterCover}
									disabled={chapterCoverSaving}
									class="px-3 py-1.5 rounded text-xs font-medium transition-colors
										{chapterCoverSaving ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30 border border-green-600/30'}"
								>
									{chapterCoverSaving ? m.book_detail_admin_saving() : 'Save Ch. Cover'}
								</button>
								<button onclick={() => (chapterCoverPreview = null)} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors">{m.book_detail_admin_discard()}</button>
							</div>
						</div>
					{/if}
				</div>

				<hr class="border-(--color-border)" />

				<!-- Description generation -->
				<div class="flex flex-col gap-2">
					<div class="flex items-center justify-between">
						<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_description()}</p>
						<a href="/admin/text-gen" class="text-xs text-(--color-brand)/70 hover:text-(--color-brand) transition-colors">Full editor ↗</a>
					</div>
					<input
						type="text"
						bind:value={descInstructions}
						placeholder="e.g. 3-sentence blurb, avoid spoilers, dramatic tone"
						class="w-full px-2 py-1.5 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
					/>
					<div class="flex items-center gap-3 flex-wrap">
						<button
							onclick={generateDesc}
							disabled={descGenerating}
							class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
								{descGenerating ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
						>
							{#if descGenerating}
								<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
							{:else}
								<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
							{/if}
							{m.book_detail_admin_generate()}
						</button>
						{#if descResult === 'error'}
							<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
						{:else if descResult === 'applied'}
							<span class="text-xs text-green-400">{m.book_detail_admin_applied()}</span>
						{/if}
					</div>
					{#if descPreview}
						<div class="flex flex-col gap-2">
							<textarea
								bind:value={descPreview}
								rows="5"
								class="w-full px-2 py-1.5 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand) resize-y"
							></textarea>
							<div class="flex items-center gap-2">
								<button
									onclick={applyDesc}
									disabled={descApplying}
									class="px-3 py-1.5 rounded text-xs font-medium transition-colors
										{descApplying ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30 border border-green-600/30'}"
								>
									{descApplying ? m.book_detail_admin_applying() : m.book_detail_admin_apply()}
								</button>
								<button onclick={() => (descPreview = '')} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors">{m.book_detail_admin_discard()}</button>
							</div>
						</div>
					{/if}
				</div>

				<!-- Chapter names generation -->
				<div class="flex flex-col gap-2">
					<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_chapter_names()}</p>
					<input
						type="text"
						bind:value={chapNamesPattern}
						placeholder="{'{scene}'}"
						class="w-full px-2 py-1.5 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
					/>
					<div class="flex items-center gap-3 flex-wrap">
						<button
							onclick={generateChapNames}
							disabled={chapNamesGenerating}
							class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
								{chapNamesGenerating ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-surface-3) text-(--color-text) hover:bg-(--color-surface-2)'}"
						>
							{#if chapNamesGenerating}
								<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
							{:else}
								<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h10"/></svg>
							{/if}
							{m.book_detail_admin_generate()}
						</button>
						{#if chapNamesBatchProgress && chapNamesGenerating}
							<span class="text-xs text-(--color-muted)">{chapNamesBatchProgress}</span>
						{/if}
						{#if chapNamesResult === 'error'}
							<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
						{:else if chapNamesResult === 'applied'}
							<span class="text-xs text-green-400">{m.book_detail_admin_applied()}</span>
						{/if}
					</div>
					{#if chapNamesBatchWarnings.length > 0}
						{#each chapNamesBatchWarnings as w}
							<p class="text-xs text-amber-400">{w}</p>
						{/each}
					{/if}
					{#if chapNamesPreview.length > 0}
						<div class="flex flex-col gap-1.5 max-h-48 overflow-y-auto rounded border border-(--color-border) p-2 bg-(--color-surface-3)">
							{#each chapNamesPreview as ch}
								<div class="flex gap-2 text-xs items-center">
									<span class="text-(--color-muted) flex-shrink-0 w-6 text-right">{ch.number}.</span>
									<input
										type="text"
										bind:value={ch.edited}
										class="flex-1 min-w-0 bg-transparent border-b border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand) py-0.5"
									/>
								</div>
							{/each}
						</div>
						<div class="flex items-center gap-2">
							<button
								onclick={applyChapNames}
								disabled={chapNamesApplying}
								class="px-3 py-1.5 rounded text-xs font-medium transition-colors
									{chapNamesApplying ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-green-600/20 text-green-400 hover:bg-green-600/30 border border-green-600/30'}"
							>
								{chapNamesApplying ? m.book_detail_admin_applying() : m.book_detail_admin_apply()} ({chapNamesPreview.length})
							</button>
							<button onclick={() => { chapNamesPreview = []; chapNamesBatchProgress = ''; chapNamesBatchWarnings = []; }} class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors">{m.book_detail_admin_discard()}</button>
						</div>
					{/if}
				</div>

				<hr class="border-(--color-border)" />

				<!-- Audio TTS bulk enqueue -->
				<div class="flex flex-col gap-2">
					<div class="flex items-center justify-between">
						<p class="text-xs font-medium text-(--color-muted) uppercase tracking-wide">{m.book_detail_admin_audio_tts()}</p>
						<a href="/admin/audio" class="text-xs text-(--color-brand)/70 hover:text-(--color-brand) transition-colors">Monitor jobs ↗</a>
					</div>
					<div class="flex flex-col gap-3">
						<!-- Voice selector -->
						<div class="flex flex-col gap-1">
							<label for="audio-voice" class="text-xs text-(--color-muted)">{m.book_detail_admin_voice()}</label>
							<select
								id="audio-voice"
								bind:value={audioVoice}
								class="px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
							>
								{#if !audioVoicesLoaded}
									<option value="af_bella">af_bella (loading…)</option>
								{:else}
									{#each audioVoices.filter(v => v.engine === 'kokoro') as v}
										<option value={v.id}>{voiceLabel(v)}</option>
									{/each}
									{#each audioVoices.filter(v => v.engine === 'pocket-tts') as v}
										<option value={v.id}>{voiceLabel(v)}</option>
									{/each}
									{#each audioVoices.filter(v => v.engine === 'cfai') as v}
										<option value={v.id}>{voiceLabel(v)}</option>
									{/each}
								{/if}
							</select>
						</div>
						<!-- Chapter range -->
						<div class="flex items-end gap-3 flex-wrap">
							<div class="flex flex-col gap-1">
								<label for="audio-from" class="text-xs text-(--color-muted)">{m.book_detail_from_chapter()}</label>
								<input
									id="audio-from"
									type="number"
									min="1"
									bind:value={audioFrom}
									placeholder="1"
									class="w-20 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
								/>
							</div>
							<div class="flex flex-col gap-1">
								<label for="audio-to" class="text-xs text-(--color-muted)">{m.book_detail_to_chapter()}</label>
								<input
									id="audio-to"
									type="number"
									min="1"
									bind:value={audioTo}
									placeholder="end"
									class="w-20 px-2 py-1 rounded bg-(--color-surface-3) border border-(--color-border) text-(--color-text) text-xs focus:outline-none focus:border-(--color-brand)"
								/>
							</div>
							<button
								onclick={enqueueAudio}
								disabled={audioEnqueuing || !audioFrom}
								class="flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-colors
									{audioEnqueuing || !audioFrom ? 'bg-(--color-surface-3) text-(--color-muted) cursor-not-allowed' : 'bg-(--color-brand)/20 text-(--color-brand-dim) hover:bg-(--color-brand)/40 border border-(--color-brand)/30'}"
							>
								{#if audioEnqueuing}
									<svg class="w-3 h-3 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
								{:else}
									<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M12 6v6m0 0l-2-2m2 2l2-2M6.343 17.657a8 8 0 010-11.314"/></svg>
								{/if}
								{m.book_detail_admin_enqueue_audio()}
							</button>
							<button
								onclick={cancelAudio}
								class="px-3 py-1.5 rounded text-xs font-medium text-(--color-muted) hover:text-(--color-danger) transition-colors border border-(--color-border)"
							>
								{m.book_detail_admin_cancel_audio()}
							</button>
						</div>
						{#if audioResult}
							<span class="text-xs text-green-400">{m.book_detail_admin_enqueued({ enqueued: audioResult.enqueued, skipped: audioResult.skipped })}</span>
						{/if}
						{#if audioError}
							<span class="text-xs text-(--color-muted)">{audioError}</span>
						{/if}
					</div>
				</div>
			</div>
		{/if}
		</div>
	{/if}
</div>

<!-- ══════════════════════════════════════════════════ Comments ══ -->
<CommentsSection slug={book.slug} isLoggedIn={data.isLoggedIn} currentUserId={data.currentUserId} />

<!-- ══════════════════════════════════════════════ More like this ══ -->
{#if similarBooks.length > 0}
<div class="mt-12">
	<h2 class="text-sm font-semibold text-(--color-muted) uppercase tracking-wider mb-3">More like this</h2>
	<div class="flex gap-3 overflow-x-auto pb-2 -mx-1 px-1 scrollbar-none">
		{#each similarBooks as b}
			<a
				href="/books/{b.slug}"
				class="flex-shrink-0 w-24 sm:w-28 group"
			>
				<div class="aspect-[2/3] rounded-lg overflow-hidden bg-(--color-surface-2) border border-(--color-border) group-hover:border-zinc-500 transition-colors mb-1.5">
					{#if b.cover}
						<img src={b.cover} alt={b.title} class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" loading="lazy" />
					{:else}
						<div class="w-full h-full flex items-center justify-center text-(--color-muted)">
							<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
							</svg>
						</div>
					{/if}
				</div>
				<p class="text-xs font-medium text-(--color-text) line-clamp-2 leading-snug">{b.title}</p>
				{#if b.author}
					<p class="text-xs text-(--color-muted) truncate mt-0.5">{b.author}</p>
				{/if}
			</a>
		{/each}
	</div>
</div>
{/if}

{/if}
