Chore: Revamped Lib Files for Svelte 5 Rewrite

This commit is contained in:
Youwes09
2026-03-19 23:36:26 -05:00
parent 43630ef72d
commit 94b92d000f
12 changed files with 1127 additions and 2004 deletions
+119 -431
View File
@@ -1,22 +1,13 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import {
Play, ArrowRight, ArrowLeft, BookOpen, Clock, Fire, TrendUp,
CalendarBlank, CheckCircle, PushPin, X as XIcon,
MagnifyingGlass, ListBullets,
} from "phosphor-svelte";
import { onMount } from "svelte";
import { Play, ArrowRight, ArrowLeft, BookOpen, Clock, Fire, TrendUp, CalendarBlank, CheckCircle, PushPin, X as XIcon, MagnifyingGlass, ListBullets } from "phosphor-svelte";
import { gql, thumbUrl } from "../../lib/client";
import { GET_LIBRARY, GET_CHAPTERS } from "../../lib/queries";
import { cache, CACHE_KEYS } from "../../lib/cache";
import {
history, readingStats, settings, activeManga, navPage,
previewManga, openReader, activeChapterList,
COMPLETED_FOLDER_ID, setHeroSlot,
} from "../../store";
import { history, readingStats, settings, activeManga, navPage, previewManga, openReader, COMPLETED_FOLDER_ID, setHeroSlot } from "../../store";
import type { HistoryEntry } from "../../store";
import type { Manga, Chapter } from "../../lib/types";
// ── Helpers ───────────────────────────────────────────────────────────────────
function timeAgo(ts: number): string {
const diff = Date.now() - ts, m = Math.floor(diff / 60000);
if (m < 1) return "Just now";
@@ -27,6 +18,7 @@
if (d < 7) return `${d}d ago`;
return new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
function formatReadTime(mins: number): string {
if (mins < 1) return `${Math.round(mins * 60)}s`;
if (mins < 60) return `${Math.round(mins)}m`;
@@ -35,11 +27,11 @@
const d = Math.floor(h / 24), rh = h % 24;
return rh === 0 ? `${d}d` : `${d}d ${rh}h`;
}
function focusEl(node: HTMLElement) { node.focus(); }
// ── Library ───────────────────────────────────────────────────────────────────
let libraryManga: Manga[] = [];
let loadingLibrary = true;
let libraryManga: Manga[] = $state([]);
let loadingLibrary: boolean = $state(true);
onMount(() => {
cache.get(CACHE_KEYS.LIBRARY, () =>
@@ -49,35 +41,26 @@
.finally(() => loadingLibrary = false);
});
// ── Continue reading (deduped) ────────────────────────────────────────────────
$: continueReading = (() => {
const continueReading = $derived((() => {
const seen = new Set<number>();
const out: HistoryEntry[] = [];
for (const e of $history) {
for (const e of history) {
if (seen.has(e.mangaId)) continue;
seen.add(e.mangaId);
out.push(e);
if (out.length >= 10) break;
}
return out;
})();
})());
// ── Hero slots ────────────────────────────────────────────────────────────────
const TOTAL_SLOTS = 4;
interface HeroSlot {
kind: "continue" | "pinned" | "empty";
entry?: HistoryEntry;
manga?: Manga;
slotIndex: number;
}
interface HeroSlot { kind: "continue" | "pinned" | "empty"; entry?: HistoryEntry; manga?: Manga; slotIndex: number; }
$: resolvedSlots = (() => {
const pins = $settings.heroSlots ?? [null, null, null, null];
const resolvedSlots = $derived((() => {
const pins = settings.heroSlots ?? [null, null, null, null];
const slots: HeroSlot[] = [];
const first = continueReading[0];
slots.push(first
? { kind: "continue", entry: first, slotIndex: 0 }
: { kind: "empty", slotIndex: 0 });
slots.push(first ? { kind: "continue", entry: first, slotIndex: 0 } : { kind: "empty", slotIndex: 0 });
let hi = 1;
for (let i = 1; i < TOTAL_SLOTS; i++) {
const pinId = pins[i];
@@ -86,23 +69,18 @@
if (manga) { slots.push({ kind: "pinned", manga, slotIndex: i }); continue; }
}
const entry = continueReading[hi++];
slots.push(entry
? { kind: "continue", entry, slotIndex: i }
: { kind: "empty", slotIndex: i });
slots.push(entry ? { kind: "continue", entry, slotIndex: i } : { kind: "empty", slotIndex: i });
}
return slots;
})();
})());
let activeIdx = 0;
$: activeSlot = resolvedSlots[activeIdx];
$: heroThumb = activeSlot?.kind === "pinned" ? thumbUrl(activeSlot.manga?.thumbnailUrl ?? "")
: activeSlot?.kind === "continue" ? thumbUrl(activeSlot.entry?.thumbnailUrl ?? "") : "";
$: heroTitle = activeSlot?.kind === "pinned" ? (activeSlot.manga?.title ?? "")
: activeSlot?.kind === "continue" ? (activeSlot.entry?.mangaTitle ?? "") : "";
$: heroManga = activeSlot?.kind === "pinned" ? activeSlot.manga
: activeSlot?.kind === "continue" ? libraryManga.find(m => m.id === activeSlot.entry?.mangaId) : null;
$: heroEntry = activeSlot?.kind === "continue" ? activeSlot.entry : null;
$: heroMangaId = heroEntry?.mangaId ?? heroManga?.id ?? null;
let activeIdx = $state(0);
const activeSlot = $derived(resolvedSlots[activeIdx]);
const heroThumb = $derived(activeSlot?.kind === "pinned" ? thumbUrl(activeSlot.manga?.thumbnailUrl ?? "") : activeSlot?.kind === "continue" ? thumbUrl(activeSlot.entry?.thumbnailUrl ?? "") : "");
const heroTitle = $derived(activeSlot?.kind === "pinned" ? (activeSlot.manga?.title ?? "") : activeSlot?.kind === "continue" ? (activeSlot.entry?.mangaTitle ?? "") : "");
const heroManga = $derived(activeSlot?.kind === "pinned" ? activeSlot.manga : activeSlot?.kind === "continue" ? libraryManga.find(m => m.id === activeSlot.entry?.mangaId) : null);
const heroEntry = $derived(activeSlot?.kind === "continue" ? activeSlot.entry : null);
const heroMangaId = $derived(heroEntry?.mangaId ?? heroManga?.id ?? null);
function cycleNext() { activeIdx = (activeIdx + 1) % TOTAL_SLOTS; heroChapters = []; }
function cyclePrev() { activeIdx = (activeIdx - 1 + TOTAL_SLOTS) % TOTAL_SLOTS; heroChapters = []; }
@@ -113,18 +91,18 @@
if (e.key === "ArrowRight") cycleNext();
if (e.key === "ArrowLeft") cyclePrev();
}
onMount(() => window.addEventListener("keydown", onKey));
onDestroy(() => window.removeEventListener("keydown", onKey));
onMount(() => {
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
});
// ── Hero chapter panel ────────────────────────────────────────────────────────
// Load chapters for the active slot's manga, show 3-5 starting at where user left off
let heroChapters: Chapter[] = [];
let loadingHeroChapters = false;
let heroChapters: Chapter[] = $state([]);
let loadingHeroChapters = $state(false);
let heroChaptersFor: number | null = null;
$: if (heroMangaId && heroMangaId !== heroChaptersFor) {
loadHeroChapters(heroMangaId);
}
$effect(() => {
if (heroMangaId && heroMangaId !== heroChaptersFor) loadHeroChapters(heroMangaId);
});
async function loadHeroChapters(mangaId: number) {
heroChaptersFor = mangaId;
@@ -132,20 +110,16 @@
heroChapters = [];
try {
const d = await gql<{ chapters: { nodes: Chapter[] } }>(GET_CHAPTERS, { mangaId });
if (heroChaptersFor !== mangaId) return; // stale
if (heroChaptersFor !== mangaId) return;
const all = [...d.chapters.nodes].sort((a, b) => a.sourceOrder - b.sourceOrder);
// Find the chapter user left off on, show from one before it
const lastReadIdx = heroEntry
? all.findIndex(c => c.id === heroEntry!.chapterId)
: all.findLastIndex(c => c.isRead);
const lastReadIdx = heroEntry ? all.findIndex(c => c.id === heroEntry!.chapterId) : all.findLastIndex(c => c.isRead);
const startIdx = Math.max(0, lastReadIdx);
heroChapters = all.slice(startIdx, startIdx + 5);
} catch { heroChapters = []; }
finally { loadingHeroChapters = false; }
}
// ── Resume helpers ────────────────────────────────────────────────────────────
let resuming = false;
let resuming = $state(false);
async function openChapter(chapter: Chapter) {
if (!heroMangaId) return;
@@ -157,28 +131,24 @@
all = [...d.chapters.nodes].sort((a, b) => a.sourceOrder - b.sourceOrder);
}
openReader(chapter, all);
} catch {
activeManga.set({ id: heroMangaId, title: heroTitle, thumbnailUrl: (heroManga?.thumbnailUrl ?? "") } as any);
} finally { resuming = false; }
} catch { activeManga = { id: heroMangaId, title: heroTitle, thumbnailUrl: heroManga?.thumbnailUrl ?? "" } as any; }
finally { resuming = false; }
}
async function resumeActive() {
if (!heroEntry && heroManga) { activeManga.set(heroManga); return; }
if (!heroEntry && heroManga) { activeManga = heroManga; return; }
if (!heroEntry) return;
// Use hero chapter panel data if available (already fetched)
const target = heroChapters.find(c => c.id === heroEntry!.chapterId) ?? heroChapters[0];
if (target && heroChapters.length) { await openChapter(target); return; }
// Fallback — fetch
resuming = true;
try {
const d = await gql<{ chapters: { nodes: Chapter[] } }>(GET_CHAPTERS, { mangaId: heroEntry.mangaId });
const chapters = [...d.chapters.nodes].sort((a, b) => a.sourceOrder - b.sourceOrder);
const ch = chapters.find(c => c.id === heroEntry!.chapterId) ?? chapters[0];
if (ch) openReader(ch, chapters);
else activeManga.set({ id: heroEntry.mangaId, title: heroEntry.mangaTitle, thumbnailUrl: heroEntry.thumbnailUrl } as any);
} catch {
activeManga.set({ id: heroEntry.mangaId, title: heroEntry.mangaTitle, thumbnailUrl: heroEntry.thumbnailUrl } as any);
} finally { resuming = false; }
else activeManga = { id: heroEntry.mangaId, title: heroEntry.mangaTitle, thumbnailUrl: heroEntry.thumbnailUrl } as any;
} catch { activeManga = { id: heroEntry.mangaId, title: heroEntry.mangaTitle, thumbnailUrl: heroEntry.thumbnailUrl } as any; }
finally { resuming = false; }
}
async function resumeEntry(entry: HistoryEntry) {
@@ -187,32 +157,27 @@
const chapters = [...d.chapters.nodes].sort((a, b) => a.sourceOrder - b.sourceOrder);
const ch = chapters.find(c => c.id === entry.chapterId) ?? chapters[0];
if (ch) openReader(ch, chapters);
else activeManga.set({ id: entry.mangaId, title: entry.mangaTitle, thumbnailUrl: entry.thumbnailUrl } as any);
} catch {
activeManga.set({ id: entry.mangaId, title: entry.mangaTitle, thumbnailUrl: entry.thumbnailUrl } as any);
}
else activeManga = { id: entry.mangaId, title: entry.mangaTitle, thumbnailUrl: entry.thumbnailUrl } as any;
} catch { activeManga = { id: entry.mangaId, title: entry.mangaTitle, thumbnailUrl: entry.thumbnailUrl } as any; }
}
// ── Slot picker ───────────────────────────────────────────────────────────────
let pickerOpen = false;
let pickerSlotIndex: 1|2|3|null = null;
let pickerSearch = "";
$: pickerResults = pickerSearch.trim()
let pickerOpen = $state(false);
let pickerSlotIndex: 1|2|3|null = $state(null);
let pickerSearch = $state("");
const pickerResults = $derived(pickerSearch.trim()
? libraryManga.filter(m => m.title.toLowerCase().includes(pickerSearch.toLowerCase())).slice(0, 20)
: libraryManga.slice(0, 20);
: libraryManga.slice(0, 20));
function openPicker(i: 1|2|3) { pickerSlotIndex = i; pickerOpen = true; pickerSearch = ""; }
function closePicker() { pickerOpen = false; pickerSlotIndex = null; }
function pinManga(m: Manga) { if (pickerSlotIndex !== null) { setHeroSlot(pickerSlotIndex, m.id); closePicker(); } }
function unpinSlot(i: 1|2|3) { setHeroSlot(i, null); }
// ── Completed, activity, stats ────────────────────────────────────────────────
$: completedIds = $settings.folders.find(f => f.id === COMPLETED_FOLDER_ID)?.mangaIds ?? [];
$: completedManga = completedIds.length > 0
? libraryManga.filter(m => completedIds.includes(m.id)).slice(0, 10)
: [];
$: recentHistory = $history.slice(0, 8);
$: stats = $readingStats;
$: hasStats = true;
const completedIds = $derived(settings.folders.find(f => f.id === COMPLETED_FOLDER_ID)?.mangaIds ?? []);
const completedManga = $derived(completedIds.length > 0 ? libraryManga.filter(m => completedIds.includes(m.id)).slice(0, 10) : []);
const recentHistory = $derived(history.slice(0, 8));
const stats = $derived(readingStats);
function handleRowWheel(e: WheelEvent) {
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
@@ -224,11 +189,9 @@
<div class="root">
<div class="body">
<!-- ══ HERO ════════════════════════════════════════════════════════════════ -->
<div class="hero-section">
<div class="hero-stage">
<!-- Blurred backdrop -->
{#if heroThumb}
<div class="hero-backdrop" style="background-image:url({heroThumb})"></div>
{:else}
@@ -236,37 +199,23 @@
{/if}
<div class="hero-scrim"></div>
<!-- ── Col 1: Cover (clickable → resume) ─────────────────────────── -->
<button
class="hero-cover-col"
on:click={resumeActive}
disabled={resuming || activeSlot?.kind === "empty"}
title={heroTitle ? `Resume ${heroTitle}` : undefined}
aria-label={heroTitle ? `Resume ${heroTitle}` : "No manga selected"}
>
<button class="hero-cover-col" onclick={resumeActive} disabled={resuming || activeSlot?.kind === "empty"} title={heroTitle ? `Resume ${heroTitle}` : undefined} aria-label={heroTitle ? `Resume ${heroTitle}` : "No manga selected"}>
{#if heroThumb}
<img src={heroThumb} alt={heroTitle} class="hero-cover" loading="eager" decoding="async" />
{#if activeSlot?.kind === "continue"}
<div class="cover-resume-hint">
<Play size={18} weight="fill" />
</div>
<div class="cover-resume-hint"><Play size={18} weight="fill" /></div>
{/if}
{:else}
<div class="hero-cover-empty"><BookOpen size={28} weight="light" /></div>
{/if}
</button>
<!-- ── Col 2: Details ────────────────────────────────────────────── -->
<div class="hero-details">
{#if activeSlot?.kind === "empty"}
<p class="hero-empty-title">Nothing here yet</p>
<p class="hero-empty-sub">
{activeSlot.slotIndex === 0
? "Read a manga to see it here"
: "Pin a manga or keep reading to fill this slot"}
</p>
<p class="hero-empty-sub">{activeSlot.slotIndex === 0 ? "Read a manga to see it here" : "Pin a manga or keep reading to fill this slot"}</p>
{#if activeSlot.slotIndex !== 0}
<button class="hero-cta" on:click={() => openPicker(activeSlot.slotIndex as 1|2|3)}>
<button class="hero-cta" onclick={() => openPicker(activeSlot.slotIndex as 1|2|3)}>
<PushPin size={11} weight="fill" /> Pin manga
</button>
{/if}
@@ -283,10 +232,7 @@
</div>
<h2 class="hero-title">{heroTitle}</h2>
{#if heroManga?.author}
<p class="hero-author">{heroManga.author}</p>
{/if}
{#if heroManga?.author}<p class="hero-author">{heroManga.author}</p>{/if}
{#if heroEntry}
<p class="hero-progress">
@@ -297,28 +243,25 @@
</p>
{/if}
{#if heroManga?.description}
<p class="hero-desc">{heroManga.description}</p>
{/if}
{#if heroManga?.description}<p class="hero-desc">{heroManga.description}</p>{/if}
<div class="hero-actions">
{#if activeSlot?.kind === "continue"}
<button class="hero-cta" on:click={resumeActive} disabled={resuming}>
<Play size={11} weight="fill" />
{resuming ? "Loading…" : "Resume"}
<button class="hero-cta" onclick={resumeActive} disabled={resuming}>
<Play size={11} weight="fill" />{resuming ? "Loading…" : "Resume"}
</button>
{:else if heroManga}
<button class="hero-cta" on:click={() => previewManga.set(heroManga!)}>
<button class="hero-cta" onclick={() => previewManga = heroManga!}>
<BookOpen size={11} weight="light" /> View manga
</button>
{/if}
{#if activeSlot?.slotIndex !== 0}
{#if activeSlot?.kind === "pinned"}
<button class="hero-cta-ghost" on:click={() => unpinSlot(activeSlot.slotIndex as 1|2|3)}>
<button class="hero-cta-ghost" onclick={() => unpinSlot(activeSlot.slotIndex as 1|2|3)}>
<XIcon size={10} weight="bold" /> Unpin
</button>
{:else}
<button class="hero-cta-ghost" on:click={() => openPicker(activeSlot!.slotIndex as 1|2|3)}>
<button class="hero-cta-ghost" onclick={() => openPicker(activeSlot!.slotIndex as 1|2|3)}>
<PushPin size={10} weight="light" /> Pin
</button>
{/if}
@@ -326,35 +269,20 @@
</div>
{/if}
<!-- Slot dots and arrows — inside details col, at the bottom -->
<div class="hero-nav-row">
<button class="hero-nav-btn" on:click={cyclePrev} aria-label="Previous">
<ArrowLeft size={12} weight="bold" />
</button>
<button class="hero-nav-btn" onclick={cyclePrev} aria-label="Previous"><ArrowLeft size={12} weight="bold" /></button>
<div class="hero-dots">
{#each resolvedSlots as slot, i}
<button
class="hero-dot"
class:active={activeIdx === i}
class:pinned={slot.kind === "pinned"}
on:click={() => goToSlot(i)}
aria-label="Slot {i + 1}"
></button>
<button class="hero-dot" class:active={activeIdx === i} class:pinned={slot.kind === "pinned"} onclick={() => goToSlot(i)} aria-label="Slot {i + 1}"></button>
{/each}
</div>
<button class="hero-nav-btn" on:click={cycleNext} aria-label="Next">
<ArrowRight size={12} weight="bold" />
</button>
<button class="hero-nav-btn" onclick={cycleNext} aria-label="Next"><ArrowRight size={12} weight="bold" /></button>
<span class="hero-counter">{activeIdx + 1}/{TOTAL_SLOTS}</span>
</div>
</div>
<!-- ── Col 3: Chapters panel ──────────────────────────────────────── -->
<div class="hero-chapters">
<div class="hero-chapters-header">
<ListBullets size={11} weight="bold" />
<span>Up Next</span>
</div>
<div class="hero-chapters-header"><ListBullets size={11} weight="bold" /><span>Up Next</span></div>
{#if activeSlot?.kind === "empty"}
<p class="hero-chapters-empty">No chapters to show</p>
@@ -362,10 +290,7 @@
{#each Array(4) as _}
<div class="chapter-row-sk">
<div class="sk sk-num"></div>
<div class="sk-info">
<div class="sk sk-name"></div>
<div class="sk sk-meta"></div>
</div>
<div class="sk-info"><div class="sk sk-name"></div><div class="sk sk-meta"></div></div>
</div>
{/each}
{:else if heroChapters.length === 0}
@@ -373,12 +298,7 @@
{:else}
{#each heroChapters as ch (ch.id)}
{@const isCurrent = heroEntry?.chapterId === ch.id}
<button
class="chapter-row"
class:chapter-row-current={isCurrent}
class:chapter-row-read={ch.isRead && !isCurrent}
on:click={() => openChapter(ch)}
>
<button class="chapter-row" class:chapter-row-current={isCurrent} class:chapter-row-read={ch.isRead && !isCurrent} onclick={() => openChapter(ch)}>
<span class="ch-num">Ch.{ch.chapterNumber % 1 === 0 ? Math.floor(ch.chapterNumber) : ch.chapterNumber}</span>
<div class="ch-info">
<span class="ch-name">{ch.name}</span>
@@ -390,13 +310,11 @@
<span class="ch-meta">{new Date(Number(ch.uploadDate) > 1e10 ? Number(ch.uploadDate) : Number(ch.uploadDate)*1000).toLocaleDateString("en-US",{month:"short",day:"numeric"})}</span>
{/if}
</div>
{#if isCurrent}
<Play size={10} weight="fill" class="ch-play-icon" />
{/if}
{#if isCurrent}<Play size={10} weight="fill" class="ch-play-icon" />{/if}
</button>
{/each}
{#if heroManga}
<button class="ch-view-all" on:click={() => { if (heroManga) activeManga.set(heroManga); }}>
<button class="ch-view-all" onclick={() => { if (heroManga) activeManga = heroManga; }}>
All chapters <ArrowRight size={9} weight="bold" />
</button>
{/if}
@@ -406,24 +324,19 @@
</div>
</div>
<!-- ══ RECENT ACTIVITY ═════════════════════════════════════════════════════ -->
{#if recentHistory.length > 0}
<div class="section">
<div class="section-header">
<span class="section-title"><Clock size={10} weight="bold" /> Recent Activity</span>
<button class="see-all" on:click={() => navPage.set("history")}>
Full history <ArrowRight size={9} weight="bold" />
</button>
<button class="see-all" onclick={() => navPage = "history"}>Full history <ArrowRight size={9} weight="bold" /></button>
</div>
<div class="activity-list">
{#each recentHistory as entry (entry.chapterId)}
<button class="activity-row" on:click={() => resumeEntry(entry)}>
<button class="activity-row" onclick={() => resumeEntry(entry)}>
<img src={thumbUrl(entry.thumbnailUrl)} alt={entry.mangaTitle} class="activity-thumb" loading="lazy" decoding="async" />
<div class="activity-info">
<span class="activity-title">{entry.mangaTitle}</span>
<span class="activity-sub">
{entry.chapterName}{entry.pageNumber > 1 ? ` · p.${entry.pageNumber}` : ""}
</span>
<span class="activity-sub">{entry.chapterName}{entry.pageNumber > 1 ? ` · p.${entry.pageNumber}` : ""}</span>
</div>
<span class="activity-time">{timeAgo(entry.readAt)}</span>
<span class="activity-play"><Play size={10} weight="fill" /></span>
@@ -434,27 +347,22 @@
{:else}
<div class="empty-state">
<p class="empty-text">Start reading to build your activity feed</p>
<button class="empty-cta" on:click={() => navPage.set("library")}>
Open Library <ArrowRight size={11} weight="bold" />
</button>
<button class="empty-cta" onclick={() => navPage = "library"}>Open Library <ArrowRight size={11} weight="bold" /></button>
</div>
{/if}
<!-- ══ BOTTOM ROW ══════════════════════════════════════════════════════════ -->
<div class="bottom-row">
<!-- Left: Completed -->
<div class="bottom-col">
<div class="bottom-section-hd">
<span class="section-title"><CheckCircle size={10} weight="bold" /> Completed</span>
{#if completedManga.length > 0}
<button class="see-all" on:click={() => navPage.set("library")}>View all <ArrowRight size={9} weight="bold" /></button>
<button class="see-all" onclick={() => navPage = "library"}>View all <ArrowRight size={9} weight="bold" /></button>
{/if}
</div>
{#if completedManga.length > 0}
<div class="mini-row" on:wheel|preventDefault={handleRowWheel}>
<div class="mini-row" onwheel={(e) => { e.preventDefault(); handleRowWheel(e); }}>
{#each completedManga as m (m.id)}
<button class="mini-card" on:click={() => previewManga.set(m)}>
<button class="mini-card" onclick={() => previewManga = m}>
<div class="mini-cover-wrap">
<img src={thumbUrl(m.thumbnailUrl)} alt={m.title} class="mini-cover" loading="lazy" decoding="async" />
<div class="mini-gradient"></div>
@@ -473,54 +381,17 @@
<div class="bottom-divider"></div>
<!-- Right: Stats -->
<div class="bottom-col">
<div class="bottom-section-hd">
<span class="section-title"><TrendUp size={10} weight="bold" /> Your Stats</span>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon-wrap stat-fire"><Fire size={16} weight="fill" /></div>
<div class="stat-body">
<span class="stat-val">{stats.currentStreakDays}</span>
<span class="stat-label">Day streak</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon-wrap stat-accent"><BookOpen size={16} weight="light" /></div>
<div class="stat-body">
<span class="stat-val">{stats.totalChaptersRead}</span>
<span class="stat-label">Chapters read</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon-wrap stat-neutral"><Clock size={16} weight="light" /></div>
<div class="stat-body">
<span class="stat-val">{formatReadTime(stats.totalMinutesRead)}</span>
<span class="stat-label">Read time</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon-wrap stat-neutral"><TrendUp size={16} weight="light" /></div>
<div class="stat-body">
<span class="stat-val">{stats.totalMangaRead}</span>
<span class="stat-label">Series started</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon-wrap stat-green"><CheckCircle size={16} weight="light" /></div>
<div class="stat-body">
<span class="stat-val">{completedIds.length}</span>
<span class="stat-label">Completed</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon-wrap stat-neutral"><CalendarBlank size={16} weight="light" /></div>
<div class="stat-body">
<span class="stat-val">{stats.longestStreakDays}d</span>
<span class="stat-label">Best streak</span>
</div>
</div>
<div class="stat-card"><div class="stat-icon-wrap stat-fire"><Fire size={16} weight="fill" /></div><div class="stat-body"><span class="stat-val">{stats.currentStreakDays}</span><span class="stat-label">Day streak</span></div></div>
<div class="stat-card"><div class="stat-icon-wrap stat-accent"><BookOpen size={16} weight="light" /></div><div class="stat-body"><span class="stat-val">{stats.totalChaptersRead}</span><span class="stat-label">Chapters read</span></div></div>
<div class="stat-card"><div class="stat-icon-wrap stat-neutral"><Clock size={16} weight="light" /></div><div class="stat-body"><span class="stat-val">{formatReadTime(stats.totalMinutesRead)}</span><span class="stat-label">Read time</span></div></div>
<div class="stat-card"><div class="stat-icon-wrap stat-neutral"><TrendUp size={16} weight="light" /></div><div class="stat-body"><span class="stat-val">{stats.totalMangaRead}</span><span class="stat-label">Series started</span></div></div>
<div class="stat-card"><div class="stat-icon-wrap stat-green"><CheckCircle size={16} weight="light" /></div><div class="stat-body"><span class="stat-val">{completedIds.length}</span><span class="stat-label">Completed</span></div></div>
<div class="stat-card"><div class="stat-icon-wrap stat-neutral"><CalendarBlank size={16} weight="light" /></div><div class="stat-body"><span class="stat-val">{stats.longestStreakDays}d</span><span class="stat-label">Best streak</span></div></div>
</div>
</div>
</div>
@@ -528,15 +399,12 @@
</div>
</div>
<!-- ── Slot picker ────────────────────────────────────────────────────────────── -->
{#if pickerOpen}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="picker-backdrop" on:click|self={closePicker}>
<div class="picker-backdrop" onclick={(e) => { if (e.target === e.currentTarget) closePicker(); }}>
<div class="picker-modal">
<div class="picker-header">
<span class="picker-title">Pin manga — slot {(pickerSlotIndex ?? 0) + 1}</span>
<button class="picker-close" on:click={closePicker}><XIcon size={13} weight="light" /></button>
<button class="picker-close" onclick={closePicker}><XIcon size={13} weight="light" /></button>
</div>
<div class="picker-search-wrap">
<MagnifyingGlass size={12} weight="light" style="color:var(--text-faint);flex-shrink:0" />
@@ -549,7 +417,7 @@
<p class="picker-empty">No results</p>
{:else}
{#each pickerResults as m (m.id)}
<button class="picker-row" on:click={() => pinManga(m)}>
<button class="picker-row" onclick={() => pinManga(m)}>
<img src={thumbUrl(m.thumbnailUrl)} alt={m.title} class="picker-thumb" loading="lazy" />
<div class="picker-info">
<span class="picker-manga-title">{m.title}</span>
@@ -567,228 +435,75 @@
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; animation: fadeIn 0.14s ease both; }
.body { flex: 1; overflow-y: auto; scrollbar-width: none; padding-bottom: var(--sp-8); }
.body::-webkit-scrollbar { display: none; }
/* ══ HERO ════════════════════════════════════════════════════════════════════ */
.hero-section { padding: var(--sp-4) var(--sp-5) 0; }
.hero-stage {
position: relative; display: flex; align-items: stretch;
height: 340px; border-radius: var(--radius-xl); overflow: hidden;
background: var(--bg-raised); border: 1px solid var(--border-dim);
box-shadow: 0 6px 28px rgba(0,0,0,0.28);
}
/* Backdrop */
.hero-backdrop {
position: absolute; inset: -14px;
background-size: cover; background-position: center 25%;
filter: blur(24px) saturate(1.4) brightness(0.32);
transform: scale(1.07); pointer-events: none; z-index: 0;
}
.hero-stage { position: relative; display: flex; align-items: stretch; height: 340px; border-radius: var(--radius-xl); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); box-shadow: 0 6px 28px rgba(0,0,0,0.28); }
.hero-backdrop { position: absolute; inset: -14px; background-size: cover; background-position: center 25%; filter: blur(24px) saturate(1.4) brightness(0.32); transform: scale(1.07); pointer-events: none; z-index: 0; }
.hero-bd-empty { background: var(--bg-void); filter: none; }
.hero-scrim {
position: absolute; inset: 0; z-index: 1; pointer-events: none;
background: linear-gradient(100deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.4) 100%);
}
/* ── Cover column ─────────────────────────────────────────────────────────── */
.hero-cover-col {
position: relative; z-index: 2;
width: clamp(150px, 30%, 195px); flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
padding: var(--sp-5); background: none; border: none;
cursor: pointer;
/* Subtle inner separator */
border-right: 1px solid rgba(255,255,255,0.06);
}
.hero-scrim { position: absolute; inset: 0; z-index: 1; pointer-events: none; background: linear-gradient(100deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.4) 100%); }
.hero-cover-col { position: relative; z-index: 2; width: clamp(150px, 30%, 195px); flex-shrink: 0; display: flex; align-items: center; justify-content: center; padding: var(--sp-5); background: none; border: none; cursor: pointer; border-right: 1px solid rgba(255,255,255,0.06); }
.hero-cover-col:hover .hero-cover { filter: brightness(1.1); }
.hero-cover-col:hover .cover-resume-hint { opacity: 1; }
.hero-cover-col:disabled { cursor: default; }
.hero-cover {
width: 100%; aspect-ratio: 2/3; object-fit: cover;
border-radius: var(--radius-lg);
box-shadow: 0 10px 36px rgba(0,0,0,0.75), 0 2px 8px rgba(0,0,0,0.4);
display: block; transition: filter 0.18s ease;
}
.hero-cover-empty {
width: 100%; aspect-ratio: 2/3;
display: flex; align-items: center; justify-content: center;
background: var(--bg-overlay); border-radius: var(--radius-lg);
color: var(--text-faint);
}
/* Play hint overlay on cover hover */
.cover-resume-hint {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 32px;
background: rgba(0,0,0,0.35); border-radius: var(--radius-lg);
opacity: 0; transition: opacity 0.18s ease;
pointer-events: none;
}
/* ── Details column ───────────────────────────────────────────────────────── */
.hero-details {
position: relative; z-index: 2; flex: 1; min-width: 0;
padding: var(--sp-5) var(--sp-4) var(--sp-4);
display: flex; flex-direction: column; gap: var(--sp-2); overflow: hidden;
border-right: 1px solid rgba(255,255,255,0.06);
}
.hero-cover { width: 100%; aspect-ratio: 2/3; object-fit: cover; border-radius: var(--radius-lg); box-shadow: 0 10px 36px rgba(0,0,0,0.75), 0 2px 8px rgba(0,0,0,0.4); display: block; transition: filter 0.18s ease; }
.hero-cover-empty { width: 100%; aspect-ratio: 2/3; display: flex; align-items: center; justify-content: center; background: var(--bg-overlay); border-radius: var(--radius-lg); color: var(--text-faint); }
.cover-resume-hint { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 32px; background: rgba(0,0,0,0.35); border-radius: var(--radius-lg); opacity: 0; transition: opacity 0.18s ease; pointer-events: none; }
.hero-details { position: relative; z-index: 2; flex: 1; min-width: 0; padding: var(--sp-5) var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-2); overflow: hidden; border-right: 1px solid rgba(255,255,255,0.06); }
.hero-tags { display: flex; flex-wrap: wrap; gap: 5px; flex-shrink: 0; }
.hero-tag {
font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide);
text-transform: uppercase; padding: 2px 7px; border-radius: var(--radius-full);
background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.62);
border: 1px solid rgba(255,255,255,0.14);
}
.hero-tag { font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 2px 7px; border-radius: var(--radius-full); background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.62); border: 1px solid rgba(255,255,255,0.14); }
.hero-tag-reading { background: var(--accent-muted); color: var(--accent-fg); border-color: var(--accent-dim); }
.hero-tag-pinned { background: rgba(168,132,232,0.18); color: #c4a8f0; border-color: rgba(168,132,232,0.28); }
.hero-title {
font-size: var(--text-xl); font-weight: var(--weight-medium); color: #fff;
line-height: var(--leading-tight); margin: 0; flex-shrink: 0;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
text-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.hero-title { font-size: var(--text-xl); font-weight: var(--weight-medium); color: #fff; line-height: var(--leading-tight); margin: 0; flex-shrink: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-shadow: 0 2px 10px rgba(0,0,0,0.5); }
.hero-author { font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.48); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
.hero-progress {
display: flex; align-items: center; gap: 5px; flex-shrink: 0;
font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.58);
letter-spacing: var(--tracking-wide);
}
.hero-progress { display: flex; align-items: center; gap: 5px; flex-shrink: 0; font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.58); letter-spacing: var(--tracking-wide); }
.hero-prog-page { color: rgba(255,255,255,0.38); }
.hero-prog-time { margin-left: auto; color: rgba(255,255,255,0.32); }
.hero-desc {
font-size: var(--text-xs); color: rgba(255,255,255,0.42); line-height: 1.55;
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
flex: 1; min-height: 0;
}
.hero-desc { font-size: var(--text-xs); color: rgba(255,255,255,0.42); line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; flex: 1; min-height: 0; }
.hero-empty-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: rgba(255,255,255,0.5); flex-shrink: 0; }
.hero-empty-sub { font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.28); letter-spacing: var(--tracking-wide); line-height: var(--leading-snug); }
.hero-actions { display: flex; gap: var(--sp-2); flex-shrink: 0; flex-wrap: wrap; }
.hero-cta {
display: inline-flex; align-items: center; gap: 6px;
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
padding: 7px 16px; border-radius: var(--radius-full);
background: var(--accent-muted); border: 1px solid var(--accent-dim); color: var(--accent-fg);
cursor: pointer; transition: filter var(--t-base); white-space: nowrap;
}
.hero-cta { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 16px; border-radius: var(--radius-full); background: var(--accent-muted); border: 1px solid var(--accent-dim); color: var(--accent-fg); cursor: pointer; transition: filter var(--t-base); white-space: nowrap; }
.hero-cta:hover:not(:disabled) { filter: brightness(1.15); }
.hero-cta:disabled { opacity: 0.55; cursor: default; }
.hero-cta-ghost {
display: inline-flex; align-items: center; gap: 6px;
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
padding: 7px 14px; border-radius: var(--radius-full);
background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.13);
color: rgba(255,255,255,0.52); cursor: pointer;
transition: background var(--t-base), color var(--t-base); white-space: nowrap;
}
.hero-cta-ghost { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 14px; border-radius: var(--radius-full); background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.13); color: rgba(255,255,255,0.52); cursor: pointer; transition: background var(--t-base), color var(--t-base); white-space: nowrap; }
.hero-cta-ghost:hover { background: rgba(255,255,255,0.13); color: rgba(255,255,255,0.82); }
/* Nav row — arrows + dots in one line at bottom of details col */
.hero-nav-row {
display: flex; align-items: center; gap: var(--sp-2);
flex-shrink: 0; margin-top: auto; padding-top: var(--sp-2);
border-top: 1px solid rgba(255,255,255,0.08);
}
.hero-nav-btn {
display: flex; align-items: center; justify-content: center;
width: 22px; height: 22px; border-radius: 50%;
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.12);
color: rgba(255,255,255,0.6); cursor: pointer; flex-shrink: 0;
transition: background var(--t-base), color var(--t-base);
}
.hero-nav-row { display: flex; align-items: center; gap: var(--sp-2); flex-shrink: 0; margin-top: auto; padding-top: var(--sp-2); border-top: 1px solid rgba(255,255,255,0.08); }
.hero-nav-btn { display: flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.12); color: rgba(255,255,255,0.6); cursor: pointer; flex-shrink: 0; transition: background var(--t-base), color var(--t-base); }
.hero-nav-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
.hero-dots { display: flex; gap: 5px; align-items: center; }
.hero-dot {
width: 5px; height: 5px; border-radius: 50%;
background: rgba(255,255,255,0.22); border: none; cursor: pointer; padding: 0;
transition: background var(--t-base), transform var(--t-base);
}
.hero-dot { width: 5px; height: 5px; border-radius: 50%; background: rgba(255,255,255,0.22); border: none; cursor: pointer; padding: 0; transition: background var(--t-base), transform var(--t-base); }
.hero-dot:hover { background: rgba(255,255,255,0.5); }
.hero-dot.active { background: #fff; transform: scale(1.35); }
.hero-dot.pinned { background: rgba(168,132,232,0.55); }
.hero-dot.pinned.active { background: #c4a8f0; }
.hero-counter {
font-family: var(--font-ui); font-size: 10px; color: rgba(255,255,255,0.3);
letter-spacing: var(--tracking-wide); margin-left: auto;
}
/* ── Chapters panel ───────────────────────────────────────────────────────── */
.hero-chapters {
position: relative; z-index: 2;
width: clamp(180px, 32%, 240px); flex-shrink: 0;
display: flex; flex-direction: column;
padding: var(--sp-4) var(--sp-3);
gap: 1px; overflow: hidden;
}
.hero-chapters-header {
display: flex; align-items: center; gap: var(--sp-2);
font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.4);
letter-spacing: var(--tracking-wider); text-transform: uppercase;
padding-bottom: var(--sp-2); margin-bottom: var(--sp-1);
border-bottom: 1px solid rgba(255,255,255,0.08); flex-shrink: 0;
}
.hero-chapters-empty {
font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.25);
letter-spacing: var(--tracking-wide); padding: var(--sp-3) 0;
}
/* Chapter rows */
.chapter-row {
display: flex; align-items: center; gap: var(--sp-2);
width: 100%; padding: 7px var(--sp-2); border-radius: var(--radius-sm);
background: none; border: none; text-align: left; cursor: pointer;
transition: background var(--t-fast);
}
.hero-counter { font-family: var(--font-ui); font-size: 10px; color: rgba(255,255,255,0.3); letter-spacing: var(--tracking-wide); margin-left: auto; }
.hero-chapters { position: relative; z-index: 2; width: clamp(180px, 32%, 240px); flex-shrink: 0; display: flex; flex-direction: column; padding: var(--sp-4) var(--sp-3); gap: 1px; overflow: hidden; }
.hero-chapters-header { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.4); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding-bottom: var(--sp-2); margin-bottom: var(--sp-1); border-bottom: 1px solid rgba(255,255,255,0.08); flex-shrink: 0; }
.hero-chapters-empty { font-family: var(--font-ui); font-size: var(--text-xs); color: rgba(255,255,255,0.25); letter-spacing: var(--tracking-wide); padding: var(--sp-3) 0; }
.chapter-row { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: 7px var(--sp-2); border-radius: var(--radius-sm); background: none; border: none; text-align: left; cursor: pointer; transition: background var(--t-fast); }
.chapter-row:hover { background: rgba(255,255,255,0.07); }
.chapter-row-current { background: rgba(255,255,255,0.1) !important; }
.ch-num {
font-family: var(--font-ui); font-size: var(--text-2xs); color: rgba(255,255,255,0.35);
letter-spacing: var(--tracking-wide); flex-shrink: 0; min-width: 36px;
}
.ch-num { font-family: var(--font-ui); font-size: var(--text-2xs); color: rgba(255,255,255,0.35); letter-spacing: var(--tracking-wide); flex-shrink: 0; min-width: 36px; }
.chapter-row-current .ch-num { color: var(--accent-fg); }
.ch-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; min-width: 0; }
.ch-name {
font-size: var(--text-xs); color: rgba(255,255,255,0.75);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ch-name { font-size: var(--text-xs); color: rgba(255,255,255,0.75); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chapter-row-read .ch-name { color: rgba(255,255,255,0.35); }
.chapter-row-current .ch-name { color: #fff; font-weight: var(--weight-medium); }
.ch-meta {
font-family: var(--font-ui); font-size: 9px; color: rgba(255,255,255,0.28);
letter-spacing: var(--tracking-wide);
}
.ch-meta { font-family: var(--font-ui); font-size: 9px; color: rgba(255,255,255,0.28); letter-spacing: var(--tracking-wide); }
.ch-read { color: rgba(255,255,255,0.2); }
:global(.ch-play-icon) { color: var(--accent-fg); flex-shrink: 0; }
/* Skeleton rows */
.chapter-row-sk { display: flex; gap: var(--sp-2); padding: 7px var(--sp-2); align-items: center; }
.sk-info { flex: 1; display: flex; flex-direction: column; gap: 4px; }
.sk { background: rgba(255,255,255,0.08); border-radius: var(--radius-sm); animation: pulse 1.4s ease infinite; }
.sk-num { width: 32px; height: 10px; flex-shrink: 0; }
.sk-name { height: 11px; width: 85%; }
.sk-meta { height: 9px; width: 50%; }
/* View all link */
.ch-view-all {
display: flex; align-items: center; gap: 4px; margin-top: auto;
font-family: var(--font-ui); font-size: var(--text-2xs);
color: rgba(255,255,255,0.3); letter-spacing: var(--tracking-wide);
background: none; border: none; cursor: pointer; padding: var(--sp-2) var(--sp-2) 0;
transition: color var(--t-base);
}
.ch-view-all { display: flex; align-items: center; gap: 4px; margin-top: auto; font-family: var(--font-ui); font-size: var(--text-2xs); color: rgba(255,255,255,0.3); letter-spacing: var(--tracking-wide); background: none; border: none; cursor: pointer; padding: var(--sp-2) var(--sp-2) 0; transition: color var(--t-base); }
.ch-view-all:hover { color: var(--accent-fg); }
/* ══ SECTIONS ════════════════════════════════════════════════════════════════ */
.section { border-top: 1px solid var(--border-dim); margin-top: var(--sp-4); }
.section-header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-5) var(--sp-2); }
.section-title { display: inline-flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
.see-all { display: flex; align-items: center; gap: 4px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-faint); background: none; border: none; cursor: pointer; padding: 0; transition: color var(--t-base); }
.see-all:hover { color: var(--accent-fg); }
/* ── Activity ─────────────────────────────────────────────────────────────── */
.activity-list { display: flex; flex-direction: column; padding: 0 var(--sp-3); }
.activity-row { display: flex; align-items: center; gap: var(--sp-3); padding: 9px var(--sp-2); border-radius: var(--radius-md); border: 1px solid transparent; background: none; text-align: left; cursor: pointer; width: 100%; transition: background var(--t-fast), border-color var(--t-fast); }
.activity-row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
@@ -799,49 +514,26 @@
.activity-sub { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.activity-time { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
.activity-play { color: var(--accent-fg); flex-shrink: 0; opacity: 0; transition: opacity var(--t-base); }
/* ── Bottom row ───────────────────────────────────────────────────────────── */
.bottom-row {
display: grid; grid-template-columns: 1fr 1px 1fr;
padding: 0 var(--sp-5) 0; margin-top: var(--sp-4);
border-top: 1px solid var(--border-dim); align-items: start;
}
.bottom-row { display: grid; grid-template-columns: 1fr 1px 1fr; padding: 0 var(--sp-5) 0; margin-top: var(--sp-4); border-top: 1px solid var(--border-dim); align-items: start; }
.bottom-divider { background: var(--border-dim); align-self: stretch; min-height: 100%; }
.bottom-col { display: flex; flex-direction: column; min-width: 0; padding-top: var(--sp-3); }
.bottom-col:first-child { padding-right: var(--sp-5); }
.bottom-col:last-child { padding-left: var(--sp-5); }
.bottom-section-hd { display: flex; align-items: center; justify-content: space-between; padding-bottom: var(--sp-2); }
.bottom-empty { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: var(--sp-2) 0; }
/* Completed cards — Discover format */
.mini-row { display: flex; gap: var(--sp-3); overflow-x: auto; scrollbar-width: none; padding-bottom: var(--sp-2); }
.mini-row::-webkit-scrollbar { display: none; }
.mini-card { flex-shrink: 0; width: 120px; background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
.mini-card:hover .mini-cover { filter: brightness(1.08) saturate(1.05); transform: scale(1.02); }
.mini-card:hover { will-change: transform; }
.mini-cover-wrap {
position: relative; aspect-ratio: 2/3; overflow: hidden;
border-radius: var(--radius-md); background: var(--bg-raised);
border: 1px solid var(--border-dim); box-shadow: 0 2px 12px rgba(0,0,0,0.35);
}
.mini-cover-wrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); box-shadow: 0 2px 12px rgba(0,0,0,0.35); }
.mini-cover { width: 100%; height: 100%; object-fit: cover; display: block; transition: filter 0.15s ease, transform 0.15s ease; }
.mini-gradient { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.1) 55%, transparent 75%); pointer-events: none; }
.mini-footer { position: absolute; bottom: 0; left: 0; right: 0; padding: var(--sp-2); pointer-events: none; }
.mini-card-title {
font-size: var(--text-xs); font-weight: var(--weight-medium);
color: rgba(255,255,255,0.92); line-height: var(--leading-snug);
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
text-shadow: 0 1px 4px rgba(0,0,0,0.7);
}
.mini-card-title { font-size: var(--text-xs); font-weight: var(--weight-medium); color: rgba(255,255,255,0.92); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-shadow: 0 1px 4px rgba(0,0,0,0.7); }
.mini-card-source { font-family: var(--font-ui); font-size: 9px; color: rgba(255,255,255,0.45); letter-spacing: var(--tracking-wide); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Stats grid */
.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.stat-card {
display: flex; align-items: center; gap: var(--sp-2);
background: var(--bg-raised); border: 1px solid var(--border-dim);
border-radius: var(--radius-md); padding: var(--sp-2) var(--sp-3);
}
.stat-card { display: flex; align-items: center; gap: var(--sp-2); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: var(--sp-2) var(--sp-3); }
.stat-icon-wrap { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); flex-shrink: 0; }
.stat-fire { background: rgba(251,146,60,0.15); color: #fb923c; }
.stat-accent { background: var(--accent-muted); color: var(--accent-fg); }
@@ -850,13 +542,10 @@
.stat-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.stat-val { font-family: var(--font-ui); font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); line-height: 1; }
.stat-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap; }
/* ── Empty state / Picker ─────────────────────────────────────────────────── */
.empty-state { display: flex; flex-direction: column; align-items: center; gap: var(--sp-3); padding: var(--sp-7) var(--sp-6); }
.empty-text { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.empty-cta { display: flex; align-items: center; gap: var(--sp-2); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 7px 16px; border-radius: var(--radius-full); background: var(--accent-muted); border: 1px solid var(--accent-dim); color: var(--accent-fg); cursor: pointer; transition: filter var(--t-base); }
.empty-cta:hover { filter: brightness(1.1); }
.picker-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: var(--z-settings); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
.picker-modal { width: min(460px, calc(100vw - 48px)); max-height: 68vh; display: flex; flex-direction: column; background: var(--bg-surface); border: 1px solid var(--border-base); border-radius: var(--radius-xl); overflow: hidden; box-shadow: 0 24px 64px rgba(0,0,0,0.6); animation: scaleIn 0.14s ease both; }
.picker-header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
@@ -875,7 +564,6 @@
.picker-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; min-width: 0; }
.picker-manga-title { font-size: var(--text-sm); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.picker-source { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
@keyframes pulse { 0%,100% { opacity: 0.4 } 50% { opacity: 0.7 } }