Pagination (regression): the IntersectionObserver sentinel only fires on
intersection *changes*, so a short first page kept it on-screen and it never
re-fired — stuck at 20 — and I'd hidden the Load more button whenever IO exists,
leaving no fallback. Now:
- Load more button is always shown while there's more (the reliable control).
- Auto-load is self-correcting: track sentinel visibility and, after each load,
pull again via rAF if it's still on-screen (rAF lets the observer update first
so a cached category doesn't load every page at once).
- page increment moved inside loadGrid so the button + sentinel can't double-bump.
Caching: cachedFetch() memoizes /discover/list responses per URL for the session
(rails + grid pages) — revisits, paging and reopening a category are instant and
don't re-hit TMDB.
Responsive: small-screen pass for the Browse panel, hero (height/title/actions
full-width), grid header wrap, and the trailer close button (kept on-screen).
Also fixed the ambient layer's 130% width that could cause horizontal scroll.
Colors now carry meaning instead of cycling by position:
- segments (Movies/TV, sort) → the app accent (they're modes, not categories)
- genre chips → a thematic colour per genre (Horror red, Comedy gold, Sci-Fi
cyan, Romance pink, …) via a name→colour map; unmapped fall back to neutral
- provider chips → each service's brand colour (Netflix red, Disney+ blue, Max
purple, Hulu green, …)
- era chips → a single warm amber; 'All/Any …' reset chips → neutral grey
Edge-fade: removed the mask-image from .vdsc-rail and .vdsc-chips. On short
filter rows only the left fade landed (dimming the first chip) while the right
fell on empty space — reading as a one-sided fade over everything.
The fully-round accent pills + sliding-thumb segments didn't fit. Now every
filter control (kind/sort segments + genre/provider/era chips) shares the
album-detail action-button look: rounded-rect (9px), each tinted from a vibey
8-colour palette that cycles across the row (green/purple/blue/amber/pink/cyan/
coral/violet) — soft fill + border + coloured text, brightening on hover, the
selected one filling with its own colour. Dropped the sliding-thumb segmented
control + its now-dead JS (moveSeg/positionSegs).
Provider filter (#4):
- client.discover() + engine.discover_filter() take a TMDB provider id and pass
with_watch_providers + watch_region (engine._region) + flatrate. Browse gets a
streaming-service chip row (Netflix/Prime/Disney+/Max/Apple TV+/Hulu/Paramount+/
Peacock); the grid title reflects 'on <service>'.
Infinite scroll (#6):
- Grid paginates via an IntersectionObserver sentinel (600px lookahead) with a
bottom spinner; the Load more button stays only as a no-IO fallback.
Polish (#7):
- Hero keyboard nav (←/→ when Discover is the visible view, ignoring inputs and
while the trailer is open); focus-visible rings on chips/segments/cards/arrows.
Note: 'complete-the-franchise' rail (#5) needs a collection_id per movie, which
the schema doesn't store yet — deferred (would need an enrichment pass).
Tests: +1 (provider watch_region params); updated the discover_filter fake for
the new kwargs. Enrichment + API suites: 115 passed.
- Segmented controls (Kind/Sort) now have a highlight 'thumb' that springs
between options (JS measures the active button → CSS var slide); repositions
on click, page-show, and resize.
- Chips: brighter active gradient with a glow ring + subtle lift, smoother hover.
- No-TMDB empty state: genres are a static TMDB endpoint, so when they come back
empty the page shows a 'Discover needs TMDB' card instead of a bare shell.
- Persist 'Hide owned' across sessions (localStorage); async image decoding on
cards.
Browse panel (was boring native dropdowns):
- Kind + Sort are now segmented pill controls; Genre + Era are horizontally
scrollable, edge-faded chip rows with an accent-glow active state; primary
'Browse all →' CTA. Genre chips rebuild when kind flips. Selection lives in
state.sel (no <select> reads).
More data per rail (so 'Hide owned' doesn't gut a shelf):
- /discover/list gains a 'pages' param (1–3): fetches that many consecutive
TMDB pages and concatenates them deduped in one response. Rails request
pages=2 (~40 items); trending is a fixed list so extra pages are skipped.
Cleanup: removed dead .vdsc-filterbar/.vdsc-select CSS.
Tests: +3 (discover routes registered; multi-page concat+dedup; trending
fetched once despite pages). API suite: 22 passed.
Performance:
- Batched ownership: new db.library_ids_for_tmdb() resolves a whole rail in one
query per kind. _stamp_owned (now also used by search + trending) groups by
kind, so a full Discover page drops from ~500 connections to a couple per rail.
Function/data:
- 'See all' on every rail opens it as a paged grid (Load more); the filter bar's
Browse routes through the same generic category grid with a back button + title.
- Personalized 'Because you like <Genre>' rails seeded from your most-owned
genres (new db.top_owned_genres + /discover/taste endpoint).
- 'Hide owned' toggle drops in-library titles from every rail/grid (CSS class,
instant).
Visual vibes:
- Ambient page-top color bleed that follows the current hero slide's hue.
- Rail edge-fade mask, gentle fade-in on load, per-title hue glow on card hover.
Tests: +4 (batched id map, server scoping, one-query-per-kind stamp, top genres).
Full video enrichment + database suites: 145 passed.