Replaces the gold experiment with the actual brand.
- Palette: brand charcoal (#303030, sampled from the logo's own background)
with the logo's glow as the primary accent (#6d7cff) and its purple
(#c084fc) as a secondary. Text is the palette's white / light gray. Light
mode re-tuned to the same hues.
- Logo: the source mark keyed off its charcoal background to RGBA, cropped to
its content bounds, and added as web/public/logo.png. It now appears in the
sidebar and on the login screen; a square favicon is derived from it and
wired into index.html.
- The featured kicker uses the purple secondary, so both brand hues are present
without adding a second competing accent.
Verified: logo loads in the app, accent resolves to #6d7cff, secondary to
#c084fc, surface to #303030.
Not new features — motion, focus, and depth, kept restrained.
- Motion tokens: one easing curve and three durations, used everywhere, so the
app moves as a single thing rather than component by component. Honours
prefers-reduced-motion by disabling all of it.
- View transitions: navigating fades and rises the content instead of swapping
it, and each view starts scrolled to the top.
- Topbar frosts once the content scrolls, so the header separates from the page
without a permanent bar of chrome.
- Featured area on Home: the item you are most likely to act on (something in
progress, else the newest), with the cover as an ambient blurred wash. The
colour comes from the artwork, not a decorative gradient, so it belongs to
whatever is on screen.
- Cards: lift and scale on hover, a scrim so the action glyph always has
contrast, a kind-appropriate glyph (play / read / expand), press feedback,
and a visible keyboard focus ring.
- Shelves: edge fades and arrow buttons that appear only when the row actually
continues that way; the scrollbar is hidden and the row snaps.
- Item page: ambient wash from its own cover.
- Global :focus-visible ring.
Verified in the built bundle: featured renders the in-progress item, hover
scales a 192px card to 197.8px with the glyph visible, and the item page tints
from its artwork.
- All 50 font-size declarations converted from px to rem, so a single root
value scales headings, labels and body together. The root is now 15px, a
deliberate ~7% increase over the previous 14px.
- Card width and grid column width are now --card-w / --grid-min variables,
driven by --card-scale independently of text. Defaults bumped too: shelf
cards 150 -> 160px, grid columns 148 -> 158px.
- Appearance panel (nav item, visible to everyone): Text size and Card size
as segmented controls, a live preview, and a reset. Settings are stored in
localStorage, per device rather than per account, because a TV across the
room wants bigger text than a laptop and the same person uses both.
- nav: 'settings' was missing from the View union after the home rewrite;
added back along with the Appearance entry.
Verified in the built bundle: root font 15px by default; "Larger" scales it to
19.2px; "Large" cards scale 160 to 192px; both persist across a reload and are
independent of each other.
The app looked like an admin panel. This is the product people actually see
when they open their library.
- Home: a landing screen with a Continue shelf (progress bars, straight from
the progress table) and per-type shelves of recent additions. One request
feeds it, so it does not pop in piece by piece.
- Browse: cover grid per media type with type chips, plus browse-all. Powered
by a new GET /items that lists across every visible library, filtered by
kind, through the same access chokepoint.
- Item page: cover, kind, title, authors, progress, a primary action that says
Continue when you have started, description, and the file list for
multi-file items. Cards now open the item page rather than jumping straight
into a reader.
- Search: results grouped by media type, cover-forward.
- Navigation moved from "libraries" to "media types" — you browse Ebooks, not
"the Ebooks folder". Libraries became an admin concern.
- Design system: dark by default, cover-forward, one accent, tighter type
scale, horizontal shelves, skeleton loaders.
- media: GET /home, GET /items, and authors on item detail.
Verified by screenshot in a live instance: home shelves, browse grid, and item
page all render correctly.
- migration 0003: an FTS5 index over media_items as an external-content table,
so the index stores no copy of the text. Triggers keep it in sync for every
write path, including the scanner's bulk inserts.
- migrate: splitStatements now tracks BEGIN...END depth, so CREATE TRIGGER
bodies survive as one statement. Without this, the trigger's internal
semicolons would tear it apart.
- search: every whitespace term is quoted before it reaches FTS5. Unquoted
input like a bare quote, a hyphen, or the word OR is valid FTS5 syntax and
would either error or silently change the query's meaning. Tested against a
list of hostile inputs, including a DROP TABLE attempt.
- scoped through the same VisibleLibraryIDs chokepoint; an empty library set
returns nothing, never everything.
- web: search box in the topbar and a results view.
Verified live: finds items across libraries, hostile input returns 200 with no
results, index stays in sync on insert/update/delete.
The frontend the app needs to be usable at all.
- web/: Svelte 5 + Vite + TypeScript SPA. Login, app shell with per-type nav,
library list/create/scan/delete, item grid with covers, admin user
management, and an epub.js reader that syncs progress.
- Reader resumes by spine index — an integer — which is why the polymorphic
progress table needs no text column.
- Session state uses classic stores in a plain .ts module rather than runes in
a .svelte.ts module, avoiding special module resolution.
- Embedding: web/embed.go embeds web/dist. A committed .gitkeep keeps the
directive valid before the frontend is built, so `go build ./...` never
fails. Unknown non-API paths serve index.html for client routes; /api/* and
/healthz always return JSON and never fall back to HTML.
- Dockerfile builds the UI in a node stage and overlays dist into the Go stage.
- pnpm 12 needs allowBuilds in pnpm-workspace.yaml; esbuild approved, the two
unused transitive scripts explicitly denied so install exits 0.
svelte-check: 0 errors, 0 warnings. Verified served: SPA shell at /, route
fallback, hashed asset, and JSON 404 on /api/*.