<script lang="ts">
	import '../app.css';
	import { page, navigating } from '$app/state';
	import { goto } from '$app/navigation';
	import { setContext, untrack } from 'svelte';
	import type { Snippet } from 'svelte';
	import type { LayoutData } from './$types';
	import { audioStore } from '$lib/audio.svelte';
	import { env } from '$env/dynamic/public';
	import { Button } from '$lib/components/ui/button';
	import { cn } from '$lib/utils';
	import * as m from '$lib/paraglide/messages.js';
	import { locales, getLocale } from '$lib/paraglide/runtime.js';
	import ListeningMode from '$lib/components/ListeningMode.svelte';
	import SearchModal from '$lib/components/SearchModal.svelte';
	import { fly, fade } from 'svelte/transition';

	let { children, data }: { children: Snippet; data: LayoutData } = $props();

	// Mobile nav drawer state
	let menuOpen = $state(false);

	// Universal search
	let searchOpen = $state(false);

	// Notifications
	let notificationsOpen = $state(false);
	let notifications = $state<{id: string; title: string; message: string; link: string; read: boolean}[]>([]);
	let notifFilter = $state<'all' | 'unread'>('all');
	async function loadNotifications() {
		if (!data.user) return;
		try {
			const res = await fetch('/api/notifications?user_id=' + data.user.id);
			if (res.ok) {
				const d = await res.json();
				notifications = d.notifications || [];
			}
		} catch (e) { console.error('load notifications:', e); }
	}
	async function markRead(id: string) {
		try {
			await fetch('/api/notifications/' + id, { method: 'PATCH' });
			notifications = notifications.map(n => n.id === id ? {...n, read: true} : n);
		} catch (e) { console.error('mark read:', e); }
	}
	async function markAllRead() {
		if (!data.user) return;
		try {
			await fetch('/api/notifications?user_id=' + data.user.id, { method: 'PATCH' });
			notifications = notifications.map(n => ({ ...n, read: true }));
		} catch (e) { console.error('mark all read:', e); }
	}
	async function dismissNotification(id: string) {
		try {
			await fetch('/api/notifications/' + id, { method: 'DELETE' });
			notifications = notifications.filter(n => n.id !== id);
		} catch (e) { console.error('dismiss notification:', e); }
	}
	async function clearAllNotifications() {
		if (!data.user) return;
		try {
			await fetch('/api/notifications?user_id=' + data.user.id, { method: 'DELETE' });
			notifications = [];
		} catch (e) { console.error('clear notifications:', e); }
	}
	$effect(() => { if (data.user) loadNotifications(); });
	const unreadCount = $derived(notifications.filter(n => !n.read).length);
	const filteredNotifications = $derived(
		notifFilter === 'unread' ? notifications.filter(n => !n.read) : notifications
	);

	// Close search on navigation
	$effect(() => {
		void page.url.pathname;
		searchOpen = false;
	});

	// Desktop dropdown menus
	let userMenuOpen = $state(false);
	let langMenuOpen = $state(false);
	let themeMenuOpen = $state(false);

	const THEMES = [
		{ id: 'amber',       color: '#f59e0b' },
		{ id: 'slate',       color: '#818cf8' },
		{ id: 'rose',        color: '#fb7185' },
		{ id: 'forest',      color: '#4ade80' },
		{ id: 'mono',        color: '#f4f4f5' },
		{ id: 'cyber',       color: '#22d3ee' },
		{ id: 'light',       color: '#d97706', light: true },
		{ id: 'light-slate', color: '#4f46e5', light: true },
		{ id: 'light-rose',  color: '#e11d48', light: true },
	];

	// Chapter list drawer state for the mini-player
	let listeningModeOpen = $state(false);
	let listeningModeChapters = $state(false);

	// Build time formatted in the user's local timezone (populated on mount so
	// SSR and CSR don't produce a mismatch — SSR renders nothing, hydration fills it in).
	let buildTimeLocal = $state('');

	$effect(() => {
		if (env.PUBLIC_BUILD_TIME && env.PUBLIC_BUILD_TIME !== 'unknown') {
			buildTimeLocal = new Date(env.PUBLIC_BUILD_TIME).toLocaleString();
		}
	});

	// The single <audio> element that persists across navigations.
	// AudioPlayer components in chapter pages control it via audioStore.
	let audioEl = $state<HTMLAudioElement | null>(null);

	// ── Theme ──────────────────────────────────────────────────────────────
	// svelte-ignore state_referenced_locally
	let currentTheme = $state(data.settings?.theme ?? 'amber');
	// svelte-ignore state_referenced_locally
	let currentFontFamily = $state(data.settings?.fontFamily ?? 'system');
	// svelte-ignore state_referenced_locally
	let currentFontSize = $state(data.settings?.fontSize ?? 1.0);

	// Expose theme + font state to child pages (e.g. profile picker)
	setContext('theme', {
		get current() { return currentTheme; },
		set current(v: string) { currentTheme = v; },
		get fontFamily() { return currentFontFamily; },
		set fontFamily(v: string) { currentFontFamily = v; },
		get fontSize() { return currentFontSize; },
		set fontSize(v: number) { currentFontSize = v; }
	});

	$effect(() => {
		if (typeof document !== 'undefined') {
			document.documentElement.setAttribute('data-theme', currentTheme);
		}
	});

	$effect(() => {
		if (typeof document === 'undefined') return;
		const fontMap: Record<string, string> = {
			system: 'system-ui, -apple-system, sans-serif',
			serif: "Georgia, 'Times New Roman', serif",
			mono: "'Courier New', monospace",
		};
		document.documentElement.style.setProperty('--reading-font', fontMap[currentFontFamily] ?? fontMap.system);
		document.documentElement.style.setProperty('--reading-size', `${currentFontSize}rem`);
	});

	// Apply persisted settings once on mount (server-loaded data).
	// Use a derived to react to future invalidateAll() re-loads too.
	let settingsApplied = false;
	let settingsDirty = false; // true only after the first apply completes
	$effect(() => {
		if (data.settings) {
			if (!settingsApplied) {
				settingsApplied = true;
				audioStore.autoNext = data.settings.autoNext;
				audioStore.voice = data.settings.voice;
				audioStore.speed = data.settings.speed;
				audioStore.announceChapter = data.settings.announceChapter ?? false;
			}
			// Always sync theme + font (profile page calls invalidateAll after saving)
			currentTheme = data.settings.theme ?? 'amber';
			currentFontFamily = data.settings.fontFamily ?? 'system';
			currentFontSize = data.settings.fontSize || 1.0;
			// Mark dirty only after the synchronous apply is done so the save
			// effect doesn't fire for this initial load.
			setTimeout(() => { settingsDirty = true; }, 0);
		}
	});

	// ── Persist settings changes (debounced 800ms) ──────────────────────────
	let settingsSaveTimer = 0;
	$effect(() => {
		// Subscribe to settings fields
		const autoNext = audioStore.autoNext;
		const voice = audioStore.voice;
		const speed = audioStore.speed;
		const theme = currentTheme;
		const fontFamily = currentFontFamily;
		const fontSize = currentFontSize;
		const announceChapter = audioStore.announceChapter;

		// Skip saving until settings have been applied from the server AND
		// at least one user-driven change has occurred after that.
		if (!settingsDirty) return;

		clearTimeout(settingsSaveTimer);
		settingsSaveTimer = setTimeout(() => {
			fetch('/api/settings', {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ autoNext, voice, speed, theme, fontFamily, fontSize, announceChapter })
			}).catch(() => {});
		}, 800) as unknown as number;
	});

	// Keep the audio element's playback rate in sync with the store speed.
	$effect(() => {
		if (audioEl) audioEl.playbackRate = audioStore.speed;
	});

	// When audioUrl changes, load the new source.
	// Use a local variable to track which URL is currently loaded so we never
	// compare against audioEl.src (browsers normalise it, causing false mismatches).
	let loadedUrl = '';
	$effect(() => {
		if (!audioEl) return;
		const url = audioStore.audioUrl;
		if (url && url !== loadedUrl) {
			loadedUrl = url;
			audioEl.src = url;
			audioEl.load();
			audioEl.playbackRate = audioStore.speed;
			audioEl.play().catch(() => {});
		} else if (!url) {
			loadedUrl = '';
		}
	});

	// Handle toggle requests from AudioPlayer controller.
	// IMPORTANT: isPlaying must be read inside untrack() so the effect only
	// re-runs when toggleRequest increments, not every time isPlaying changes.
	// Without untrack the effect subscribes to both toggleRequest AND isPlaying,
	// causing an infinite play/pause loop: play() fires onplay → isPlaying=true
	// → effect re-runs → sees isPlaying=true → calls pause() → onpause fires
	// → isPlaying=false → effect re-runs → calls play() → …
	$effect(() => {
		// Read toggleRequest to subscribe; ignore value 0 (initial).
		const _req = audioStore.toggleRequest;
		if (!audioEl || _req === 0) return;
		untrack(() => {
			if (audioStore.isPlaying) {
				audioEl!.pause();
			} else {
				audioEl!.play().catch(() => {});
			}
		});
	});

	// Handle seek requests from AudioPlayer controller.
	$effect(() => {
		const t = audioStore.seekRequest;
		if (!audioEl || t === null) return;
		audioEl.currentTime = t;
		audioStore.seekRequest = null;
	});

	// Sleep timer — fires once when time is up
	$effect(() => {
		const until = audioStore.sleepUntil;
		if (!until) return;
		const ms = until - Date.now();
		if (ms <= 0) {
			audioStore.sleepUntil = 0;
			if (audioStore.isPlaying) audioStore.toggleRequest++;
			return;
		}
		const id = setTimeout(() => {
			audioStore.sleepUntil = 0;
			if (audioStore.isPlaying) audioStore.toggleRequest++;
		}, ms);
		return () => clearTimeout(id);
	});

	// ── MediaSession action handlers ────────────────────────────────────────────
	// Without explicit handlers, iOS Safari loses lock-screen resume ability after
	// ~1 minute of pause because it falls back to its own default which doesn't
	// satisfy the user-gesture requirement for <audio>.play().
	// Handlers registered here call audioEl directly so iOS trusts the gesture.
	$effect(() => {
		if (typeof navigator === 'undefined' || !('mediaSession' in navigator) || !audioEl) return;
		const el = audioEl; // capture for closure

		navigator.mediaSession.setActionHandler('play', () => {
			el.play().catch(() => {});
		});
		navigator.mediaSession.setActionHandler('pause', () => {
			el.pause();
		});
		navigator.mediaSession.setActionHandler('seekbackward', (d) => {
			el.currentTime = Math.max(0, el.currentTime - (d.seekOffset ?? 15));
		});
		navigator.mediaSession.setActionHandler('seekforward', (d) => {
			el.currentTime = Math.min(el.duration || 0, el.currentTime + (d.seekOffset ?? 30));
		});
		// previoustrack / nexttrack fall back to skip ±30s if no chapter nav available
		try {
			navigator.mediaSession.setActionHandler('previoustrack', () => {
				el.currentTime = Math.max(0, el.currentTime - 30);
			});
			navigator.mediaSession.setActionHandler('nexttrack', () => {
				el.currentTime = Math.min(el.duration || 0, el.currentTime + 30);
			});
		} catch { /* some browsers don't support these */ }

		return () => {
			(['play', 'pause', 'seekbackward', 'seekforward'] as MediaSessionAction[]).forEach((a) => {
				try { navigator.mediaSession.setActionHandler(a, null); } catch { /* ignore */ }
			});
		};
	});

	// Keep playbackState in sync so iOS lock screen shows the right button state
	$effect(() => {
		if (typeof navigator === 'undefined' || !('mediaSession' in navigator)) return;
		navigator.mediaSession.playbackState = audioStore.isPlaying ? 'playing' : 'paused';
	});

	// ── Announce-chapter safety timeout ──────────────────────────────────────
	// Module-level so the onended handler can clear it if the clip completes
	// before the timeout fires.
	let announceTimeout = 0;

	// ── Save audio time on pause/end (debounced 2s) ─────────────────────────
	let audioTimeSaveTimer = 0;
	function saveAudioTime() {
		if (!audioStore.slug || !audioStore.chapter) return;
		const slug = audioStore.slug;
		const chapter = audioStore.chapter;
		const currentTime = audioStore.currentTime;
		clearTimeout(audioTimeSaveTimer);
		audioTimeSaveTimer = setTimeout(() => {
			fetch('/api/progress/audio-time', {
				method: 'PATCH',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, chapter, audioTime: currentTime })
			}).catch(() => {});
		}, 2000) as unknown as number;
	}

	function formatDuration(s: number): string {
		if (!isFinite(s) || s <= 0) return '--:--';
		return formatTime(s);
	}

	function formatTime(s: number): string {
		if (!isFinite(s) || s < 0) return '0:00';
		const m = Math.floor(s / 60);
		const sec = Math.floor(s % 60);
		return `${m}:${sec.toString().padStart(2, '0')}`;
	}

	function togglePlay() {
		if (!audioEl) return;
		if (audioStore.isPlaying) {
			audioEl.pause();
		} else {
			audioEl.play().catch(() => {});
		}
	}

	function seek(e: Event) {
		if (!audioEl) return;
		audioEl.currentTime = parseFloat((e.target as HTMLInputElement).value);
	}

	function skipBack() {
		if (!audioEl) return;
		audioEl.currentTime = Math.max(0, audioEl.currentTime - 15);
	}

	function skipForward() {
		if (!audioEl) return;
		audioEl.currentTime = Math.min(audioEl.duration || 0, audioEl.currentTime + 30);
	}

	const speedSteps = [0.75, 1.0, 1.25, 1.5, 2.0];

	function cycleSpeed() {
		const idx = speedSteps.indexOf(audioStore.speed);
		audioStore.speed = speedSteps[(idx + 1) % speedSteps.length];
	}

	function dismiss() {
		if (audioEl) {
			audioEl.pause();
			audioEl.src = '';
		}
		audioStore.status = 'idle';
		audioStore.audioUrl = '';
		audioStore.slug = '';
		audioStore.chapter = 0;
		audioStore.isPlaying = false;
		audioStore.currentTime = 0;
		audioStore.duration = 0;
	}
</script>

<svelte:head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<title>libnovel</title>
	<!-- Apply theme before first paint to avoid flash -->
	<!-- eslint-disable-next-line svelte/no-at-html-tags -->
	{@html `<script>document.documentElement.setAttribute('data-theme','${data.settings?.theme ?? 'amber'}')</script>`}
	<!-- Umami analytics — no-op when PUBLIC_UMAMI_WEBSITE_ID is unset -->
	{#if env.PUBLIC_UMAMI_WEBSITE_ID && env.PUBLIC_UMAMI_SCRIPT_URL}
		<script
			defer
			src={env.PUBLIC_UMAMI_SCRIPT_URL}
			data-website-id={env.PUBLIC_UMAMI_WEBSITE_ID}
		></script>
	{/if}
</svelte:head>

<!-- Persistent audio element — always in the DOM, never conditionally unmounted.
     Conditional rendering ({#if}) would destroy/recreate it when reactive state
     changes (e.g. currentTime ticking), triggering onpause and restarting audio. -->
<audio
	bind:this={audioEl}
	bind:currentTime={audioStore.currentTime}
	bind:duration={audioStore.duration}
	onplay={() => (audioStore.isPlaying = true)}
	onpause={() => {
		audioStore.isPlaying = false;
		saveAudioTime();
	}}
	onended={() => {
		audioStore.isPlaying = false;

		// ── If we just finished playing an announcement clip, navigate now ──
		if (audioStore.announceNavigatePending) {
			audioStore.announceNavigatePending = false;
			clearTimeout(announceTimeout);
			announceTimeout = 0;
			const slug    = audioStore.announcePendingSlug;
			const chapter = audioStore.announcePendingChapter;
			audioStore.announcePendingSlug    = '';
			audioStore.announcePendingChapter = 0;
			goto(`/books/${slug}/chapters/${chapter}`).catch(() => {
				audioStore.autoStartChapter = null;
			});
			return;
		}

		// Cancel any pending debounced save and reset the position to 0 for
		// the chapter that just finished. Without this, the 2s debounce fires
		// after navigation and saves currentTime≈duration, causing resume to
		// start at the very end next time the user returns to this chapter.
		clearTimeout(audioTimeSaveTimer);
		if (audioStore.slug && audioStore.chapter) {
			const slug = audioStore.slug;
			const chapter = audioStore.chapter;
			fetch('/api/progress/audio-time', {
				method: 'PATCH',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ slug, chapter, audioTime: 0 })
			}).catch(() => {});
		}
		// If sleep-after-chapter is set, just pause instead of navigating
		if (audioStore.sleepAfterChapter) {
			audioStore.sleepAfterChapter = false;
			// Don't navigate — just let it end. Audio is already stopped (ended).
			return;
		}
		if (audioStore.autoNext && audioStore.nextChapter !== null && audioStore.slug) {
			// Capture values synchronously before any async work — the AudioPlayer
			// component will unmount during navigation, but we've already read what
			// we need.
			const targetSlug    = audioStore.slug;
			const targetChapter = audioStore.nextChapter;
			// Store the target chapter number so only the newly-mounted AudioPlayer
			// for that chapter reacts — not the outgoing chapter's component.
			audioStore.autoStartChapter = targetChapter;

			const doNavigate = () => {
				goto(`/books/${targetSlug}/chapters/${targetChapter}`).catch(() => {
					audioStore.autoStartChapter = null;
				});
			};

			// Announce via a real audio clip so the audio session stays alive on
			// iOS Safari / Chrome Android (speechSynthesis is silently muted after
			// onended because the audio session has been released).
			if (audioStore.announceChapter) {
				const nextInfo  = audioStore.chapters.find((c) => c.number === targetChapter);
				const titlePart = nextInfo?.title ? ` — ${nextInfo.title}` : '';
				const text      = `Chapter ${targetChapter}${titlePart}`;

				// Always request MP3 — universally supported and the backend
				// auto-selects the right TTS engine from the voice ID.
				const qs       = new URLSearchParams({ text, voice: audioStore.voice, format: 'mp3' });
				const announceUrl = `/api/announce?${qs}`;

				// Store pending navigation target so the next onended (from the
				// announcement clip) knows where to go.
				audioStore.announcePendingSlug    = targetSlug;
				audioStore.announcePendingChapter = targetChapter;
				audioStore.announceNavigatePending = true;

				// Safety timeout: if the clip never loads/ends (network issue,
				// browser policy, unsupported codec), navigate anyway after 10s.
				clearTimeout(announceTimeout);
				announceTimeout = setTimeout(() => {
					if (audioStore.announceNavigatePending) {
						audioStore.announceNavigatePending = false;
						audioStore.announcePendingSlug    = '';
						audioStore.announcePendingChapter = 0;
						doNavigate();
					}
				}, 10_000) as unknown as number;

				// Point the persistent <audio> element at the announcement clip.
				// The $effect in the layout that watches audioStore.audioUrl will
				// pick this up, set audioEl.src, and call play().
				audioStore.audioUrl = announceUrl;
			} else {
				doNavigate();
			}
		}
	}}
	preload="metadata"
	style="display:none"
></audio>

<div class="min-h-screen flex flex-col" class:pb-24={audioStore.active}>
	<!-- Navigation progress bar — shown while SSR is running for any page transition -->
	{#if navigating}
		<div class="fixed top-0 left-0 right-0 z-[100] h-1 bg-(--color-surface-2)">
			<div class="h-full bg-(--color-brand) animate-progress-bar"></div>
		</div>
	{/if}
	<header class="border-b border-(--color-border) bg-(--color-surface) sticky top-0 z-50">
		<nav class="max-w-6xl mx-auto px-4 h-14 flex items-center gap-6">
			<a href="/" class="text-(--color-brand) font-bold text-lg tracking-tight hover:text-(--color-brand-dim) shrink-0">
				libnovel
			</a>

			{#if page.data.book?.title && /\/books\/[^/]+\/chapters\//.test(page.url.pathname)}
				<a
					href="/books/{page.data.book.slug}"
					class="text-(--color-muted) hover:text-(--color-text) text-sm truncate min-w-0 flex-1 sm:flex-none sm:max-w-xs transition-colors"
				>
					{page.data.book.title}
				</a>
			{/if}

			{#if data.user}
				<!-- Desktop nav links (hidden on mobile) -->
				<a
			href="/books"
				class="hidden sm:block text-sm transition-colors {page.url.pathname.startsWith('/books') ? 'text-(--color-text) font-medium' : 'text-(--color-muted) hover:text-(--color-text)'}"
				>
					{m.nav_library()}
				</a>
		<a
			href="/discover"
			class="hidden sm:block text-sm transition-colors {page.url.pathname.startsWith('/discover') ? 'text-(--color-text) font-medium' : 'text-(--color-muted) hover:text-(--color-text)'}"
		>
			Discover
		</a>
		<a
			href="/feed"
			class="hidden sm:block text-sm transition-colors {page.url.pathname.startsWith('/feed') ? 'text-(--color-text) font-medium' : 'text-(--color-muted) hover:text-(--color-text)'}"
		>
			{m.nav_feed()}
		</a>
		<a
			href="/catalogue"
			class="hidden sm:block text-sm transition-colors {page.url.pathname.startsWith('/catalogue') ? 'text-(--color-text) font-medium' : 'text-(--color-muted) hover:text-(--color-text)'}"
		>
			{m.nav_catalogue()}
		</a>
		{#if !data.isPro}
		<a
			href="/subscribe"
			class="hidden sm:inline-flex items-center gap-1 text-sm font-semibold transition-colors {page.url.pathname.startsWith('/subscribe') ? 'text-(--color-brand)' : 'text-(--color-brand) opacity-70 hover:opacity-100'}"
		>
			<svg class="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/></svg>
			Pro
		</a>
		{/if}
			<div class="ml-auto flex items-center gap-2">
				<!-- Universal search button (hidden on chapter/reader pages) -->
				{#if !/\/books\/[^/]+\/chapters\//.test(page.url.pathname)}
					<button
						type="button"
						onclick={() => { searchOpen = true; userMenuOpen = false; langMenuOpen = false; themeMenuOpen = false; menuOpen = false; notificationsOpen = false; }}
						title="Search (/ or ⌘K)"
						aria-label="Search books"
						class="flex items-center justify-center w-8 h-8 rounded transition-colors text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-2)"
					>
						<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
						</svg>
					</button>
				{/if}

				<!-- Notifications bell -->
				{#if data.user?.role === 'admin'}
					<div class="relative">
						<button
							type="button"
							onclick={() => { notificationsOpen = !notificationsOpen; searchOpen = false; userMenuOpen = false; langMenuOpen = false; themeMenuOpen = false; }}
							title="Notifications"
							class="flex items-center justify-center w-8 h-8 rounded transition-colors {notificationsOpen ? 'bg-(--color-surface-2)' : 'hover:bg-(--color-surface-2)'} relative"
						>
							<svg class="w-4 h-4 text-(--color-muted)" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
							</svg>
							{#if unreadCount > 0}
								<span class="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
							{/if}
						</button>
						{#if notificationsOpen}
							<div class="absolute right-0 top-full mt-1 w-80 bg-(--color-surface-2) border border-(--color-border) rounded-lg shadow-xl z-50 flex flex-col max-h-[28rem]">
								<!-- Header -->
								<div class="flex items-center justify-between px-3 pt-3 pb-2 shrink-0">
									<span class="text-sm font-semibold">Notifications</span>
									<div class="flex items-center gap-1">
										{#if unreadCount > 0}
											<button
												type="button"
												onclick={markAllRead}
												class="text-xs text-(--color-muted) hover:text-(--color-text) transition-colors px-1.5 py-0.5 rounded hover:bg-(--color-surface-3)"
											>Mark all read</button>
										{/if}
										{#if notifications.length > 0}
											<button
												type="button"
												onclick={clearAllNotifications}
												class="text-xs text-(--color-muted) hover:text-red-400 transition-colors px-1.5 py-0.5 rounded hover:bg-(--color-surface-3)"
											>Clear all</button>
										{/if}
									</div>
								</div>
								<!-- Filter tabs -->
								<div class="flex gap-0 px-3 pb-2 shrink-0">
									<button
										type="button"
										onclick={() => notifFilter = 'all'}
										class="text-xs px-2.5 py-1 rounded-l border border-(--color-border) transition-colors {notifFilter === 'all' ? 'bg-(--color-brand) text-black border-(--color-brand)' : 'text-(--color-muted) hover:text-(--color-text)'}"
									>All ({notifications.length})</button>
									<button
										type="button"
										onclick={() => notifFilter = 'unread'}
										class="text-xs px-2.5 py-1 rounded-r border border-l-0 border-(--color-border) transition-colors {notifFilter === 'unread' ? 'bg-(--color-brand) text-black border-(--color-brand)' : 'text-(--color-muted) hover:text-(--color-text)'}"
									>Unread ({unreadCount})</button>
								</div>
								<!-- List -->
								<div class="overflow-y-auto flex-1 min-h-0">
									{#if filteredNotifications.length === 0}
										<div class="p-4 text-center text-(--color-muted) text-sm">
											{notifFilter === 'unread' ? 'No unread notifications' : 'No notifications'}
										</div>
									{:else}
										{#each filteredNotifications as n (n.id)}
											<div class="flex items-start gap-1 border-b border-(--color-border)/40 hover:bg-(--color-surface-3) group {n.read ? 'opacity-60' : ''}">
												<a
													href={n.link || '/admin'}
													onclick={() => { markRead(n.id); notificationsOpen = false; }}
													class="flex-1 p-3 min-w-0"
												>
													<div class="flex items-center gap-1.5">
														{#if !n.read}
															<span class="w-1.5 h-1.5 rounded-full bg-(--color-brand) shrink-0"></span>
														{/if}
														<span class="text-sm font-medium truncate">{n.title}</span>
													</div>
													<div class="text-xs text-(--color-muted) mt-0.5 line-clamp-2">{n.message}</div>
												</a>
												<button
													type="button"
													onclick={() => dismissNotification(n.id)}
													class="shrink-0 p-2.5 text-(--color-muted) hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all"
													title="Dismiss"
												>
													<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="M6 18L18 6M6 6l12 12"/>
													</svg>
												</button>
											</div>
										{/each}
									{/if}
								</div>
								<!-- Footer -->
								<div class="px-3 py-2 border-t border-(--color-border)/40 shrink-0">
									<a
										href="/admin/notifications"
										onclick={() => notificationsOpen = false}
										class="block text-center text-xs text-(--color-muted) hover:text-(--color-brand) transition-colors"
									>View all notifications</a>
								</div>
							</div>
						{/if}
					</div>
				{/if}
				<!-- Theme dropdown (desktop) -->
				<div class="hidden sm:block relative">
					<button
						type="button"
						onclick={() => { themeMenuOpen = !themeMenuOpen; langMenuOpen = false; userMenuOpen = false; }}
						title="Theme"
						class="flex items-center justify-center w-7 h-7 rounded transition-colors {themeMenuOpen ? 'bg-(--color-surface-2)' : 'hover:bg-(--color-surface-2)'}"
					>
						<span
							class="w-3.5 h-3.5 rounded-full border-2 border-(--color-text) shrink-0"
							style="background: {THEMES.find(t => t.id === currentTheme)?.color ?? '#f59e0b'};"
						></span>
					</button>
					{#if themeMenuOpen}
						<div class="absolute right-0 top-full mt-1 bg-(--color-surface-2) border border-(--color-border) rounded-lg shadow-xl py-2 px-2.5 z-50">
							<div class="flex items-center gap-1.5">
								{#each THEMES as t, i}
									{#if i === 3}
										<span class="w-px h-3 bg-(--color-border) mx-0.5"></span>
									{/if}
									<button
										type="button"
										onclick={() => { currentTheme = t.id; themeMenuOpen = false; }}
										title={t.id}
										class="w-4 h-4 rounded-full border-2 transition-all {currentTheme === t.id ? 'border-(--color-text) scale-110' : t.light ? 'border-(--color-border) opacity-70 hover:opacity-100' : 'border-transparent opacity-50 hover:opacity-100'}"
										style="background: {t.color};"
									></button>
								{/each}
							</div>
						</div>
					{/if}
				</div>

				<!-- Language dropdown (desktop) -->
				<div class="hidden sm:block relative">
					<button
						type="button"
						onclick={() => { langMenuOpen = !langMenuOpen; userMenuOpen = false; themeMenuOpen = false; }}
						class="flex items-center gap-1 px-2 py-1 rounded text-xs font-mono transition-colors {langMenuOpen ? 'bg-(--color-surface-2) text-(--color-text)' : 'text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text)'}"
					>
						<svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
							<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
						</svg>
						{getLocale().toUpperCase()}
					</button>
						{#if langMenuOpen}
							<div class="absolute right-0 top-full mt-1 bg-(--color-surface-2) border border-(--color-border) rounded-lg shadow-xl py-1 z-50 min-w-[80px]">
								{#each locales as locale}
									<button
										type="button"
										onclick={async () => {
											langMenuOpen = false;
											await fetch('/api/settings', {
												method: 'PUT',
												headers: { 'Content-Type': 'application/json' },
												body: JSON.stringify({ autoNext: audioStore.autoNext, voice: audioStore.voice, speed: audioStore.speed, theme: currentTheme, fontFamily: currentFontFamily, fontSize: currentFontSize, locale })
											}).catch(() => {});
											const { setLocale } = await import('$lib/paraglide/runtime.js');
											setLocale(locale as any, { reload: true });
										}}
										class="w-full text-left px-3 py-1.5 text-xs font-mono transition-colors {getLocale() === locale ? 'text-(--color-brand) bg-(--color-brand)/10' : 'text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3)'}"
									>
										{locale.toUpperCase()}
									</button>
								{/each}
							</div>
						{/if}
					</div>

				<!-- User menu dropdown (desktop) -->
				<div class="hidden sm:block relative">
					<button
						type="button"
						onclick={() => { userMenuOpen = !userMenuOpen; langMenuOpen = false; themeMenuOpen = false; }}
						class="flex items-center gap-1.5 pl-1.5 pr-1.5 py-1 rounded transition-colors {userMenuOpen ? 'bg-(--color-surface-2)' : 'hover:bg-(--color-surface-2)'}"
					>
						<span class="w-6 h-6 rounded-full bg-(--color-brand)/20 text-(--color-brand) text-xs font-bold flex items-center justify-center shrink-0">
							{data.user.username[0].toUpperCase()}
						</span>
					</button>
						{#if userMenuOpen}
							<div class="absolute right-0 top-full mt-1 bg-(--color-surface-2) border border-(--color-border) rounded-lg shadow-xl py-1 z-50 min-w-[170px]">
								<a
									href="/profile"
									onclick={() => { userMenuOpen = false; }}
									class="flex items-center justify-between gap-2 px-3 py-2 text-sm transition-colors {page.url.pathname === '/profile' ? 'text-(--color-text)' : 'text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3)'}"
								>
									{m.nav_profile()}
									<span class="text-xs opacity-40 truncate max-w-[80px]">{data.user.username}</span>
								</a>
								{#if data.user?.role === 'admin'}
									<a
										href="/admin/scrape"
										onclick={() => { userMenuOpen = false; }}
										class="flex items-center gap-2 px-3 py-2 text-sm transition-colors {page.url.pathname.startsWith('/admin') ? 'text-(--color-text)' : 'text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3)'}"
									>
										{m.nav_admin_panel()}
									</a>
								{/if}
								<a
									href="https://feedback.libnovel.cc"
									target="_blank"
									rel="noopener noreferrer"
									onclick={() => { userMenuOpen = false; }}
									class="flex items-center justify-between gap-2 px-3 py-2 text-sm text-(--color-muted) hover:text-(--color-text) hover:bg-(--color-surface-3) transition-colors"
								>
									{m.nav_feedback()}
									<svg class="w-3 h-3 shrink-0 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
										<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
									</svg>
								</a>
								<div class="my-1 border-t border-(--color-border)/60"></div>
								<form method="POST" action="/logout">
									<button type="submit" class="w-full text-left px-3 py-2 text-sm text-(--color-danger) hover:bg-(--color-surface-3) transition-colors">
										{m.nav_sign_out()}
									</button>
								</form>
							</div>
						{/if}
					</div>

					<!-- Mobile: hamburger button -->
					<Button
						variant="ghost"
						size="icon"
						onclick={() => (menuOpen = !menuOpen)}
						aria-label={m.nav_toggle_menu()}
						aria-expanded={menuOpen}
						class="sm:hidden -mr-1"
					>
						{#if menuOpen}
							<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
							</svg>
						{:else}
							<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
							</svg>
						{/if}
					</Button>
				</div>

				<!-- Click-outside overlay for dropdowns -->
				{#if langMenuOpen || userMenuOpen}
					<div
						class="fixed inset-0 z-40"
						onpointerdown={() => { langMenuOpen = false; userMenuOpen = false; }}
						aria-hidden="true"
					></div>
				{/if}
			{:else}
				<div class="ml-auto">
					<a
						href="/login"
						class="text-sm px-3 py-1.5 rounded bg-(--color-brand) text-(--color-surface) font-semibold hover:bg-(--color-brand-dim) transition-colors"
					>
						{m.nav_sign_in()}
					</a>
				</div>
			{/if}
		</nav>

		<!-- Mobile drawer (full-width, below the bar) -->
		{#if data.user && menuOpen}
			<div class="sm:hidden border-t border-(--color-border) bg-(--color-surface) px-4 py-3 flex flex-col gap-1">
				<a
					href="/books"
					onclick={() => (menuOpen = false)}
					class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname.startsWith('/books') ? 'bg-(--color-surface-2) text-(--color-text)' : 'text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text)'}"
				>
					{m.nav_library()}
				</a>
		<a
			href="/discover"
			onclick={() => (menuOpen = false)}
			class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname.startsWith('/discover') ? 'bg-(--color-surface-2) text-(--color-text)' : 'text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text)'}"
		>
			Discover
		</a>
		<a
			href="/feed"
			onclick={() => (menuOpen = false)}
			class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname.startsWith('/feed') ? 'bg-(--color-surface-2) text-(--color-text)' : 'text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text)'}"
		>
			{m.nav_feed()}
		</a>
		<a
			href="/catalogue"
			onclick={() => (menuOpen = false)}
			class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname.startsWith('/catalogue') ? 'bg-(--color-surface-2) text-(--color-text)' : 'text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text)'}"
		>
			{m.nav_catalogue()}
		</a>
			<a
				href="https://feedback.libnovel.cc"
				target="_blank"
				rel="noopener noreferrer"
				onclick={() => (menuOpen = false)}
				class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text) flex items-center justify-between"
			>
				{m.nav_feedback()}
				<svg class="w-3.5 h-3.5 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
					<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
				</svg>
			</a>
				<a
					href="/profile"
					onclick={() => (menuOpen = false)}
					class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname === '/profile' ? 'bg-(--color-surface-2) text-(--color-text)' : 'text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text)'}"
				>
					{m.nav_profile()} <span class="text-(--color-muted) font-normal opacity-60">({data.user.username})</span>
				</a>
			{#if data.user?.role === 'admin'}
				<div class="my-1 border-t border-(--color-border)/60"></div>
				<p class="px-3 pt-1 pb-0.5 text-xs text-(--color-muted) opacity-50 uppercase tracking-widest">{m.nav_admin()}</p>
				<a
					href="/admin/scrape"
					onclick={() => (menuOpen = false)}
					class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname.startsWith('/admin') ? 'bg-(--color-surface-2) text-(--color-text)' : 'text-(--color-muted) hover:bg-(--color-surface-2) hover:text-(--color-text)'}"
				>
					{m.nav_admin_panel()}
				</a>
			{/if}
			<!-- Theme switcher -->
			<div class="my-1 border-t border-(--color-border)/60"></div>
			<div class="px-3 py-2.5 flex items-center justify-between">
				<span class="text-xs text-(--color-muted) uppercase tracking-widest">{m.profile_theme_label()}</span>
				<div class="flex items-center gap-1.5">
					{#each THEMES as t, i}
						{#if i === 3}
							<span class="w-px h-4 bg-(--color-border) mx-0.5"></span>
						{/if}
						<button
							type="button"
							onclick={() => { currentTheme = t.id; }}
							title={t.id}
							class="w-5 h-5 rounded-full border-2 transition-all {currentTheme === t.id ? 'border-(--color-text) scale-110' : t.light ? 'border-(--color-border) opacity-70 hover:opacity-100' : 'border-transparent opacity-50 hover:opacity-100'}"
							style="background: {t.color};"
						></button>
					{/each}
				</div>
			</div>

			<!-- Language switcher -->
			<div class="px-3 py-2.5 flex items-center justify-between">
				<span class="text-xs text-(--color-muted) uppercase tracking-widest">{m.locale_switcher_label()}</span>
				<div class="flex items-center gap-0.5">
					{#each locales as locale}
						<button
							type="button"
							onclick={async () => {
								menuOpen = false;
								await fetch('/api/settings', {
									method: 'PUT',
									headers: { 'Content-Type': 'application/json' },
									body: JSON.stringify({ autoNext: audioStore.autoNext, voice: audioStore.voice, speed: audioStore.speed, theme: currentTheme, fontFamily: currentFontFamily, fontSize: currentFontSize, locale })
								}).catch(() => {});
								const { setLocale } = await import('$lib/paraglide/runtime.js');
								setLocale(locale as any, { reload: true });
							}}
							class="px-1.5 py-0.5 rounded text-xs font-mono transition-colors {getLocale() === locale ? 'text-(--color-brand) bg-(--color-brand)/10' : 'text-(--color-muted) hover:text-(--color-text)'}"
						>
							{locale.toUpperCase()}
						</button>
					{/each}
				</div>
			</div>

			<div class="my-1 border-t border-(--color-border)/60"></div>
			<form method="POST" action="/logout">
				<Button
					type="submit"
					variant="ghost"
					class="w-full justify-start px-3 py-2.5 h-auto text-sm font-medium text-(--color-danger) hover:bg-(--color-surface-2) hover:text-(--color-danger)"
				>
					{m.nav_sign_out()}
				</Button>
			</form>
			</div>
		{/if}
	</header>

	<main class="flex-1 max-w-6xl mx-auto w-full px-4 py-8">
		{#key page.url.pathname + page.url.search}
			<div in:fade={{ duration: 180, delay: 60 }} out:fade={{ duration: 100 }}>
				{@render children()}
			</div>
		{/key}
	</main>

	<footer class="border-t border-(--color-border) mt-auto"
		class:hidden={/\/books\/[^/]+\/chapters\//.test(page.url.pathname)}
	>
		<div class="max-w-6xl mx-auto px-4 py-6 flex flex-col items-center gap-4 text-xs text-(--color-muted)">
			<!-- Top row: site links -->
			<nav class="flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
			<a href="/books" class="hover:text-(--color-text) transition-colors">{m.footer_library()}</a>
			<a href="/catalogue" class="hover:text-(--color-text) transition-colors">{m.footer_catalogue()}</a>
			<a
				href="https://feedback.libnovel.cc"
				target="_blank"
				rel="noopener noreferrer"
				class="hover:text-(--color-text) transition-colors flex items-center gap-1"
			>
				{m.footer_feedback()}
				<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
				</svg>
			</a>
			<a
				href="https://novelfire.net"
					target="_blank"
					rel="noopener noreferrer"
					class="hover:text-(--color-text) transition-colors flex items-center gap-1"
				>
					novelfire.net
					<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
					</svg>
				</a>
		</nav>
		<!-- Bottom row: legal links + copyright -->
		<div class="flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-(--color-muted)">
			<a href="/disclaimer" class="hover:text-(--color-text) transition-colors">{m.footer_disclaimer()}</a>
			<a href="/privacy" class="hover:text-(--color-text) transition-colors">{m.footer_privacy()}</a>
			<a href="/dmca" class="hover:text-(--color-text) transition-colors">{m.footer_dmca()}</a>
			<span>{m.footer_copyright({ year: String(new Date().getFullYear()) })}</span>
		</div>
		<!-- Build version / commit SHA / build time -->
		{#snippet buildTime()}
			{#if buildTimeLocal}
				<span class="text-(--color-muted)" title="Build time">
					· {buildTimeLocal}
				</span>
			{/if}
		{/snippet}
		<div class="text-xs tabular-nums font-mono px-2 py-0.5 rounded bg-(--color-surface-2) border border-(--color-border)">
			{#if env.PUBLIC_BUILD_VERSION && env.PUBLIC_BUILD_VERSION !== 'dev'}
				<span class="text-(--color-text)" title="Build version">{env.PUBLIC_BUILD_VERSION}</span>
				{#if env.PUBLIC_BUILD_COMMIT && env.PUBLIC_BUILD_COMMIT !== 'unknown'}
					<span class="text-(--color-muted) select-all" title="Commit SHA"
						>+{env.PUBLIC_BUILD_COMMIT.slice(0, 7)}</span
					>
				{/if}
				{@render buildTime()}
			{:else}
				<span class="text-(--color-muted)">dev</span>
			{/if}
		</div>
		</div>
	</footer>
</div>

<!-- ── Persistent mini-player bar ─────────────────────────────────────────── -->
{#if audioStore.active}
	<div class="fixed bottom-0 left-0 right-0 z-50 bg-(--color-surface) border-t border-(--color-border) shadow-2xl">

		<!-- Generation progress bar (sits at very top of the bar) -->
		{#if audioStore.status === 'generating' || audioStore.status === 'loading'}
			<div class="h-0.5 bg-(--color-surface-2)">
				<div
					class="h-full bg-(--color-brand) transition-none"
					style="width: {audioStore.progress}%"
				></div>
			</div>
		{:else if audioStore.status === 'ready'}
			<!-- Seek bar flush at top — tappable on mobile -->
			<div class="px-0">
				<input
					type="range"
					aria-label={m.player_seek_label()}
					min="0"
					max={audioStore.duration || 0}
					value={audioStore.currentTime}
					oninput={seek}
					class="w-full h-1 accent-[--color-brand] cursor-pointer block"
					style="margin: 0; border-radius: 0; accent-color: var(--color-brand);"
				/>
			</div>
		{/if}

		<div class="max-w-6xl mx-auto px-4 py-2 flex items-center gap-3">

		<!-- Track info (click to open chapter list in listening mode) -->
			<button
				class="flex-1 min-w-0 text-left rounded px-1 -ml-1 hover:bg-(--color-surface-2) transition-colors"
				onclick={() => { if (audioStore.chapters.length > 0) { listeningModeChapters = true; listeningModeOpen = true; } }}
				aria-label={audioStore.chapters.length > 0 ? m.player_toggle_chapter_list() : undefined}
				title={audioStore.chapters.length > 0 ? m.player_chapter_list_label() : undefined}
			>
				{#if audioStore.status === 'generating'}
					<p class="text-xs text-(--color-brand) leading-tight">
						{m.player_generating({ percent: String(Math.round(audioStore.progress)) })}
					</p>
				{:else if audioStore.status === 'ready'}
					<p class="text-xs text-(--color-muted) tabular-nums leading-tight flex items-center gap-1.5">
						{formatTime(audioStore.currentTime)} / {formatDuration(audioStore.duration)}
						{#if audioStore.isPreview}
							<span class="px-1 py-0.5 rounded text-[10px] font-medium bg-(--color-brand)/15 text-(--color-brand) leading-none">preview</span>
						{/if}
					</p>
				{:else if audioStore.status === 'loading'}
					<p class="text-xs text-(--color-muted) leading-tight">{m.player_loading()}</p>
				{/if}
			</button>

			{#if audioStore.status === 'ready'}
				<!-- Skip back 15s -->
				<Button
					variant="ghost"
					size="icon"
					onclick={skipBack}
					title={m.player_back_15()}
					aria-label={m.player_rewind_15()}
				>
					<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
						<path d="M11.99 5V1l-5 5 5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6h-2c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
						<text x="8.5" y="14.5" font-size="5" font-family="sans-serif" font-weight="bold" fill="currentColor">15</text>
					</svg>
				</Button>

				<!-- Play / Pause — custom circular brand style, kept as raw button -->
				<button
					onclick={togglePlay}
					class="w-10 h-10 rounded-full bg-(--color-brand) text-(--color-surface) flex items-center justify-center hover:bg-(--color-brand-dim) transition-colors flex-shrink-0"
					aria-label={audioStore.isPlaying ? m.player_pause() : m.player_play()}
				>
					{#if audioStore.isPlaying}
						<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
							<path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/>
						</svg>
					{:else}
						<svg class="w-4 h-4 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
							<path d="M8 5v14l11-7z"/>
						</svg>
					{/if}
				</button>

				<!-- Skip forward 30s -->
				<Button
					variant="ghost"
					size="icon"
					onclick={skipForward}
					title={m.player_forward_30()}
					aria-label={m.player_skip_30()}
				>
					<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
						<path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"/>
						<text x="8.5" y="14.5" font-size="5" font-family="sans-serif" font-weight="bold" fill="currentColor">30</text>
					</svg>
				</Button>

			{:else if audioStore.status === 'generating'}
				<!-- Spinner during generation -->
				<svg class="w-6 h-6 text-(--color-brand) animate-spin flex-shrink-0" 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>
			{/if}

			<!-- Cover thumbnail / go-to-chapter link -->
			{#if audioStore.slug && audioStore.chapter > 0}
				<a
					href="/books/{audioStore.slug}/chapters/{audioStore.chapter}"
					class="shrink-0 rounded overflow-hidden hover:opacity-80 transition-opacity"
					title={m.player_go_to_chapter()}
					aria-label={m.player_go_to_chapter()}
				>
					{#if audioStore.cover}
						<img
							src={audioStore.cover}
							alt=""
							class="w-8 h-11 object-cover rounded"
						/>
					{:else}
						<!-- Fallback book icon -->
						<div class="w-8 h-11 flex items-center justify-center bg-(--color-surface-2) rounded border border-(--color-border)">
							<svg class="w-4 h-4 text-(--color-muted)" fill="currentColor" viewBox="0 0 24 24">
								<path d="M18 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 14H8v-2h8v2zm0-4H8v-2h8v2zm0-4H8V6h8v2z"/>
							</svg>
						</div>
					{/if}
				</a>
			{/if}

			<!-- Headphones: open listening mode -->
			<Button
				variant="ghost"
				size="icon"
				onclick={() => { listeningModeChapters = false; listeningModeOpen = true; }}
				title="Listening mode"
				aria-label="Open listening mode"
				class="text-(--color-muted) hover:text-(--color-text) 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="M3 18v-6a9 9 0 0118 0v6"/>
					<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 19a2 2 0 01-2 2h-1a2 2 0 01-2-2v-3a2 2 0 012-2h3zM3 19a2 2 0 002 2h1a2 2 0 002-2v-3a2 2 0 00-2-2H3z"/>
				</svg>
			</Button>

			<!-- Dismiss -->
			<Button
				variant="ghost"
				size="icon"
				onclick={dismiss}
				title={m.player_close()}
				aria-label={m.player_close()}
				class="text-(--color-muted) hover:text-(--color-text) 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="M6 18L18 6M6 6l12 12"/>
				</svg>
			</Button>
		</div>
	</div>
{/if}

<!-- Listening mode — mounted at root level, independent of audioStore.active,
     so closing/pausing audio never tears it down and loses context. -->
{#if listeningModeOpen}
	<div transition:fly={{ y: '100%', duration: 320, opacity: 1 }} style="pointer-events: none;">
		<ListeningMode
			onclose={() => { listeningModeOpen = false; listeningModeChapters = false; }}
			openChapters={listeningModeChapters}
		/>
	</div>
{/if}

<!-- Universal search modal — shown from anywhere except focus mode / listening mode -->
{#if searchOpen && !listeningModeOpen}
	<SearchModal onclose={() => { searchOpen = false; }} />
{/if}

<svelte:window onkeydown={(e) => {
	// Don't intercept when typing in an input/textarea
	const tag = (e.target as HTMLElement).tagName;
	if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement).isContentEditable) return;
	// Don't open on chapter reader pages
	if (/\/books\/[^/]+\/chapters\//.test(page.url.pathname)) return;
	if (searchOpen) return;
	// `/` key or Cmd/Ctrl+K
	if (e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key === 'k')) {
		e.preventDefault();
		searchOpen = true;
	}
}} />
