Просмотр исходного кода

[Docs] Note landing emerge UX, MoreInfoModal, portal pattern

CLAUDE.md:
- Landing roll snap-to-gap, settled-until-re-roll, emerge animation
- Modal portal rule: descendants of animate-emerge (or any
  transformed ancestor) must createPortal to document.body

PROJECT_SCOPE.md (via PM):
- 5.3 [~] -> [x] with dated note (commits dea71d9, 0061375)
- Section 4 landing row + Section 5 Feature Flow steps 4c/4d describe
  emerge-in-carousel UX and the new landing MoreInfoModal
- 9.9 QA criterion updated; Section 9 success criterion replaced
- Explicit callout that the landing MoreInfoModal is distinct from
  U5 (in-app expanded-panel More Info, still TODO)

.gitignore:
- Ignore screenshot-*.png debugging artifacts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User 2 месяцев назад
Родитель
Сommit
550af057bb
3 измененных файлов с 50 добавлено и 31 удалено
  1. 3 0
      .gitignore
  2. 2 0
      CLAUDE.md
  3. 45 31
      PROJECT_SCOPE.md

+ 3 - 0
.gitignore

@@ -46,3 +46,6 @@ next-env.d.ts
 
 
 # docker
 # docker
 docker-compose.override.yml
 docker-compose.override.yml
+
+# local debugging artifacts
+screenshot-*.png

+ 2 - 0
CLAUDE.md

@@ -48,6 +48,8 @@ Tables: `users`, `groups`, `group_members`, `movies`, `landing_reel_posters`
 - Home roll teaser renders in place — do not router.push() from a home-page roll.
 - Home roll teaser renders in place — do not router.push() from a home-page roll.
 - Landing has two distinct movie pools: carousel reads `landing_reel_posters` via `/api/tmdb/reel-posters`; "Roll the Dice" hits `/api/tmdb/popular?page=N` (random 1–50, ~1000-movie pool). Don't conflate.
 - Landing has two distinct movie pools: carousel reads `landing_reel_posters` via `/api/tmdb/reel-posters`; "Roll the Dice" hits `/api/tmdb/popular?page=N` (random 1–50, ~1000-movie pool). Don't conflate.
 - `PINNED_REEL_POSTERS` in `src/app/api/tmdb/reel-posters/route.ts` always-include list (deduped by `tmdb_id`); edit there to add/remove pins.
 - `PINNED_REEL_POSTERS` in `src/app/api/tmdb/reel-posters/route.ts` always-include list (deduped by `tmdb_id`); edit there to add/remove pins.
+- Landing roll result emerges in carousel center: snap math lands a poster gap exactly at viewport center, posters spread ±`SPREAD_AMOUNT`, card pops in. Card stays settled until next roll (no auto-resume).
+- Modals descended from `animate-emerge` (or any transformed ancestor) MUST `createPortal` to `document.body` — `transform: scale(1)` from `fill-mode: both` establishes a containing block and clamps `fixed inset-0` to the ancestor's box.
 
 
 ## Auth
 ## Auth
 
 

+ 45 - 31
PROJECT_SCOPE.md

@@ -28,30 +28,30 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 
 
 ### In Scope (MVP)
 ### In Scope (MVP)
 
 
-| Feature                            | Description                                                                                                                                                                                                                                                                                                                                                                                                           | Priority    |
-| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
-| Landing page                       | Centered logo, splash text, slot-machine reel animation on Roll the Dice (3 reels spinning through ~20 automatically fetched posters from TMDB popular/top-rated, replaced on each periodic refresh), Genre Roll against TMDB (no login required), Login button, scrolling About section, 3-step how-it-works demo with alternating left-right-left alignment, TMDB attribution footer, privacy policy link           | Must Have   |
-| Anonymous auth via Supabase        | User picks a display name and optional avatar color; account created via `supabase.auth.signInAnonymously()` which issues a JWT for RLS-compatible sessions; persisted on device via `@supabase/ssr` cookie-based session handling                                                                                                                                                                                    | Must Have   |
-| Recovery code                      | A 24-character alphanumeric code (128-bit entropy) shown once after account creation that lets users reclaim their identity on a new device; hashed with Argon2id (memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes) before storage; single-use (invalidated after successful claim); claim endpoint rate-limited (5 failed attempts per IP per 15-minute window)                                       | Must Have   |
-| Group creation with invite code    | Creator gets a short human-readable code in WORD-WORD format (e.g., WOLF-MOON; word list: 2,000+ words, 3-8 characters each, offensive/confusing terms filtered, uppercase display, case-insensitive comparison, collision check on generation) to share; creator becomes List Admin                                                                                                                                  | Must Have   |
-| Group join via invite code         | Enter code to join a group and access its shared list; regular member role assigned; join endpoint rate-limited (5-10 failed attempts per IP per 15-minute window); group join is a server-side operation via service role key (not client-side INSERT)                                                                                                                                                               | Must Have   |
-| List Admin permissions             | Creator can rename the list, initiate list deletion or ownership transfer (on self-removal), remove members, and regenerate the invite code                                                                                                                                                                                                                                                                           | Must Have   |
-| Regular user permissions           | Members can add/remove movies, mark movies as watched, and leave the list                                                                                                                                                                                                                                                                                                                                             | Must Have   |
-| Movie search (TMDB integration)    | Search bar queries TMDB via server-side API proxy (`/api/tmdb/*`) with debounce (~300ms); all calls set `include_adult=false`; results show below a separator from in-list results                                                                                                                                                                                                                                    | Must Have   |
-| Add/remove movie                   | Tap a TMDB result to add it; poster, genres, title, year, and trailer URL auto-populate (trailer URL stored in DB and refreshed periodically); added-by attribution stored                                                                                                                                                                                                                                            | Must Have   |
-| Poster-forward grid view           | 2-column evenly-scaling grid; each card shows movie poster (full bleed, using TMDB native sized URLs) with title below and meaningful `alt` text; added-by avatar overlaid top-right; binoculars emoji overlaid top-left when watched; infinite scroll loading 12 movies initially                                                                                                                                    | Must Have   |
+| Feature                            | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Priority    |
+| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| Landing page                       | Centered logo, splash text, slot-machine reel animation on Roll the Dice (3 reels spinning through ~20 automatically fetched posters from TMDB popular/top-rated, replaced on each periodic refresh), Genre Roll against TMDB (no login required), Login button, scrolling About section, 3-step how-it-works demo with alternating left-right-left alignment, TMDB attribution footer, privacy policy link. Roll result card emerges in the carousel center and stays settled until next user action. Result card has an "i" info button that opens a MoreInfoModal (plot, Add to list → /login, Watch Trailer). | Must Have   |
+| Anonymous auth via Supabase        | User picks a display name and optional avatar color; account created via `supabase.auth.signInAnonymously()` which issues a JWT for RLS-compatible sessions; persisted on device via `@supabase/ssr` cookie-based session handling                                                                                                                                                                                                                                                                                                    | Must Have   |
+| Recovery code                      | A 24-character alphanumeric code (128-bit entropy) shown once after account creation that lets users reclaim their identity on a new device; hashed with Argon2id (memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes) before storage; single-use (invalidated after successful claim); claim endpoint rate-limited (5 failed attempts per IP per 15-minute window)                                                                                                                                                       | Must Have   |
+| Group creation with invite code    | Creator gets a short human-readable code in WORD-WORD format (e.g., WOLF-MOON; word list: 2,000+ words, 3-8 characters each, offensive/confusing terms filtered, uppercase display, case-insensitive comparison, collision check on generation) to share; creator becomes List Admin                                                                                                                                                                                                                                                  | Must Have   |
+| Group join via invite code         | Enter code to join a group and access its shared list; regular member role assigned; join endpoint rate-limited (5-10 failed attempts per IP per 15-minute window); group join is a server-side operation via service role key (not client-side INSERT)                                                                                                                                                                                                                                                                               | Must Have   |
+| List Admin permissions             | Creator can rename the list, initiate list deletion or ownership transfer (on self-removal), remove members, and regenerate the invite code                                                                                                                                                                                                                                                                                                                                                                                           | Must Have   |
+| Regular user permissions           | Members can add/remove movies, mark movies as watched, and leave the list                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Must Have   |
+| Movie search (TMDB integration)    | Search bar queries TMDB via server-side API proxy (`/api/tmdb/*`) with debounce (~300ms); all calls set `include_adult=false`; results show below a separator from in-list results                                                                                                                                                                                                                                                                                                                                                    | Must Have   |
+| Add/remove movie                   | Tap a TMDB result to add it; poster, genres, title, year, and trailer URL auto-populate (trailer URL stored in DB and refreshed periodically); added-by attribution stored                                                                                                                                                                                                                                                                                                                                                            | Must Have   |
+| Poster-forward grid view           | 2-column evenly-scaling grid; each card shows movie poster (full bleed, using TMDB native sized URLs) with title below and meaningful `alt` text; added-by avatar overlaid top-right; binoculars emoji overlaid top-left when watched; infinite scroll loading 12 movies initially                                                                                                                                                                                                                                                    | Must Have   |
 | Expanded movie card (inline panel) | Tapping a poster expands a full-page-width panel downward, inserted below that row in the grid — not a modal or popup. Panel order (top to bottom): full-size poster → title → "Added by [username]" → genre tags → Watched It + Trailer (side by side) → Delete (centered below). Delete uses two-tap shake-and-confirm. Watched It toggles watched state. Trailer opens in new tab. Panel collapses on tap outside. | Must Have   |
 | Expanded movie card (inline panel) | Tapping a poster expands a full-page-width panel downward, inserted below that row in the grid — not a modal or popup. Panel order (top to bottom): full-size poster → title → "Added by [username]" → genre tags → Watched It + Trailer (side by side) → Delete (centered below). Delete uses two-tap shake-and-confirm. Watched It toggles watched state. Trailer opens in new tab. Panel collapses on tap outside. | Must Have   |
-| Genre filter                       | Tapping a genre tag in the expanded panel filters the grid to that genre; announce filter state change via `aria-live="polite"` region                                                                                                                                                                                                                                                                                | Must Have   |
-| Roll the Dice                      | Large button pinned above the list; triggers an animated randomizer that lands on one unwatched movie from the group list; announce result via `aria-live="polite"` region                                                                                                                                                                                                                                            | Must Have   |
-| Re-roll                            | Tapping Roll again re-rolls from the same eligible pool                                                                                                                                                                                                                                                                                                                                                               | Must Have   |
-| Genre + Emotion Roll               | Secondary button accepting comma-separated genres and/or emotion keywords; maps emotions to genre IDs, filters pool, then rolls                                                                                                                                                                                                                                                                                       | Must Have   |
-| Watched state (per group, toggle)  | Marking a movie watched moves it to a collapsed "Watched" section; marking again moves it back. Binoculars overlay and button color update in real time across all members. Announce state change via `aria-live="polite"` region.                                                                                                                                                                                    | Must Have   |
-| Real-time list sync                | Add, remove, and watched-status changes appear live on all connected group members' screens (Supabase real-time); subscribe only to the currently-viewed list, unsubscribe on navigation away                                                                                                                                                                                                                         | Must Have   |
-| Logged-in home page                | Upon login or return visit (stored user ID detected), user lands on a home page that mirrors the landing page layout but shows their lists as cards and replaces Login with Create List; Roll the Dice and Genre Roll roll across all user lists combined and display the result as a standalone teaser card on the home page (no navigation into a specific list)                                                    | Must Have   |
-| Multi-group support                | A user can belong to more than one group; all their lists appear as cards on the home page                                                                                                                                                                                                                                                                                                                            | Should Have |
-| Invite code rotation               | List Admin can regenerate the invite code to revoke access for anyone with the old code                                                                                                                                                                                                                                                                                                                               | Should Have |
-| Trailer URL periodic refresh       | Background job via Node.js cron container on a bi-weekly cadence refreshes stored trailer URLs only for movies where trailer_url is currently null. Note: this behavior should be reassessed post-launch to also refresh stale URLs after a certain age.                                                                                                                                                              | Should Have |
-| Master Admin panel                 | Site-owner-only admin page with TOTP 2FA; can search and delete any list or user (deletion must remove both `public.users` row and `auth.users` record via `supabase.auth.admin.deleteUser()`); credentials set via environment variables; session managed via iron-session v8 (HttpOnly, Secure, SameSite=Strict cookie, 8-hour expiry)                                                                              | Must Have   |
+| Genre filter                       | Tapping a genre tag in the expanded panel filters the grid to that genre; announce filter state change via `aria-live="polite"` region                                                                                                                                                                                                                                                                                                                                                                                                | Must Have   |
+| Roll the Dice                      | Large button pinned above the list; triggers an animated randomizer that lands on one unwatched movie from the group list; announce result via `aria-live="polite"` region                                                                                                                                                                                                                                                                                                                                                            | Must Have   |
+| Re-roll                            | Tapping Roll again re-rolls from the same eligible pool                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | Must Have   |
+| Genre + Emotion Roll               | Secondary button accepting comma-separated genres and/or emotion keywords; maps emotions to genre IDs, filters pool, then rolls                                                                                                                                                                                                                                                                                                                                                                                                       | Must Have   |
+| Watched state (per group, toggle)  | Marking a movie watched moves it to a collapsed "Watched" section; marking again moves it back. Binoculars overlay and button color update in real time across all members. Announce state change via `aria-live="polite"` region.                                                                                                                                                                                                                                                                                                    | Must Have   |
+| Real-time list sync                | Add, remove, and watched-status changes appear live on all connected group members' screens (Supabase real-time); subscribe only to the currently-viewed list, unsubscribe on navigation away                                                                                                                                                                                                                                                                                                                                         | Must Have   |
+| Logged-in home page                | Upon login or return visit (stored user ID detected), user lands on a home page that mirrors the landing page layout but shows their lists as cards and replaces Login with Create List; Roll the Dice and Genre Roll roll across all user lists combined and display the result as a standalone teaser card on the home page (no navigation into a specific list)                                                                                                                                                                    | Must Have   |
+| Multi-group support                | A user can belong to more than one group; all their lists appear as cards on the home page                                                                                                                                                                                                                                                                                                                                                                                                                                            | Should Have |
+| Invite code rotation               | List Admin can regenerate the invite code to revoke access for anyone with the old code                                                                                                                                                                                                                                                                                                                                                                                                                                               | Should Have |
+| Trailer URL periodic refresh       | Background job via Node.js cron container on a bi-weekly cadence refreshes stored trailer URLs only for movies where trailer_url is currently null. Note: this behavior should be reassessed post-launch to also refresh stale URLs after a certain age.                                                                                                                                                                                                                                                                              | Should Have |
+| Master Admin panel                 | Site-owner-only admin page with TOTP 2FA; can search and delete any list or user (deletion must remove both `public.users` row and `auth.users` record via `supabase.auth.admin.deleteUser()`); credentials set via environment variables; session managed via iron-session v8 (HttpOnly, Secure, SameSite=Strict cookie, 8-hour expiry)                                                                                                                                                                                              | Must Have   |
 
 
 ### Out of Scope (Future)
 ### Out of Scope (Future)
 
 
@@ -84,13 +84,27 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
       TWO DISTINCT POOLS — do not conflate:
       TWO DISTINCT POOLS — do not conflate:
         - Carousel/reel posters: landing_reel_posters table + PINNED_REEL_POSTERS fallback (curated)
         - Carousel/reel posters: landing_reel_posters table + PINNED_REEL_POSTERS fallback (curated)
         - Roll the Dice result: live /api/tmdb/popular?page=N, random page 1–50 (~1000-movie pool)
         - Roll the Dice result: live /api/tmdb/popular?page=N, random page 1–50 (~1000-movie pool)
-   b. Reels decelerate and land on a single movie result drawn from /api/tmdb/popular?page=N
-      (random page 1–50; pool of ~1000 movies). The result is NOT constrained to the reel poster set.
+   b. Reels decelerate and snap so the midpoint of a gap between two posters lands at viewport center.
+      Result is a single movie drawn from /api/tmdb/popular?page=N (random page 1–50; pool ~1000 movies).
+      The result is NOT constrained to the reel poster set.
       KNOWN BUG (U9): modulo wrap missing in spinning branch causes scroll offset to drift on
       KNOWN BUG (U9): modulo wrap missing in spinning branch causes scroll offset to drift on
       repeated rolls; fix tracked in research/PHASE5_UI_FIXES_PLAN.md U9.
       repeated rolls; fix tracked in research/PHASE5_UI_FIXES_PLAN.md U9.
-   c. Result is displayed as a static teaser card showing the movie poster (with alt text),
-      title, and genres. No link, no tap action.
-   d. Animation is user-triggered only and completes within 5 seconds (WCAG 2.2.2)
+   c. Result TeaserCard (~1.3x reel poster size) emerges with an animate-emerge keyframe
+      (scale 0 → 1.08 → 1, ~500ms, bouncy easing) in the carousel center — posters spread ±110px
+      at viewport center on settle. Card remains in settled state until the user re-rolls or refreshes
+      (no auto-resume timer). Card shows poster (with alt text), title, genres, and an "i" info button.
+      Respects prefers-reduced-motion: animation skipped, card appears instantly.
+   d. Tapping "i" opens <MoreInfoModal> (src/components/landing/more-info-modal.tsx):
+      - Side-by-side layout at sm+ (poster left, content right), stacks on mobile, max-w-5xl/lg:max-w-6xl
+      - Shows: title + year, genres, plot (movie.overview)
+      - "Add to list" button → navigates to /login (no save-intent flow; user adds manually post-auth)
+      - "Watch Trailer" button → lazy-fetches /api/tmdb/movie/[id]/videos, opens trailer
+      - a11y: ESC closes, click-outside closes, focus trap, focus restoration on close
+      - Uses createPortal to document.body to escape the animate-emerge transformed ancestor
+      NOTE: this is a LANDING-ONLY modal for unauthenticated visitors. It is separate from the
+      in-app "More Info" button (U5 in research/PHASE5_UI_FIXES_PLAN.md) which targets
+      src/components/movies/expanded-panel.tsx and is still TODO.
+   e. Animation is user-triggered only and completes within 5 seconds (WCAG 2.2.2)
 5. "Genre Roll" button visible — accepts comma-separated genres/emotions, no reel animation;
 5. "Genre Roll" button visible — accepts comma-separated genres/emotions, no reel animation;
    result displayed as a static teaser card (poster with alt text, title, genres).
    result displayed as a static teaser card (poster with alt text, title, genres).
    No link, no tap action.
    No link, no tap action.
@@ -509,7 +523,7 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 
 
 - [x] 5.1 — Build landing page structure: centered logo, splash text, Roll + Genre Roll buttons, Login button, scrolling About section, 3-step how-it-works demo (Step 1 left, Step 2 right, Step 3 left); add site-wide footer with TMDB attribution (logo + link + disclaimer) and privacy policy link; build privacy policy page with required sections (controller identity, lawful basis, data inventory, third-party recipients including Sentry, international transfers, user rights, children's disclaimer, cookie/localStorage disclosure, change notification procedure). NOTE (2026-05-02): Hero, Carousel, About, HowItWorks, Login link, and TMDBFooter all present in `src/app/(public)/page.tsx` + `(public)/layout.tsx`. Duplicate stub `src/app/page.tsx` removed so (public)/page.tsx is no longer shadowed.
 - [x] 5.1 — Build landing page structure: centered logo, splash text, Roll + Genre Roll buttons, Login button, scrolling About section, 3-step how-it-works demo (Step 1 left, Step 2 right, Step 3 left); add site-wide footer with TMDB attribution (logo + link + disclaimer) and privacy policy link; build privacy policy page with required sections (controller identity, lawful basis, data inventory, third-party recipients including Sentry, international transfers, user rights, children's disclaimer, cookie/localStorage disclosure, change notification procedure). NOTE (2026-05-02): Hero, Carousel, About, HowItWorks, Login link, and TMDBFooter all present in `src/app/(public)/page.tsx` + `(public)/layout.tsx`. Duplicate stub `src/app/page.tsx` removed so (public)/page.tsx is no longer shadowed.
 - [~] 5.2 — Build and seed landing_reel_posters table; implement periodic refresh job in the Node.js cron container on bi-weekly cadence that automatically fetches ~20 posters from TMDB popular or top-rated endpoints (with `include_adult=false`) and replaces the full set in the DB on each run. NOTE (2026-05-02): Table exists and is populated by seed.sql. `PINNED_REEL_POSTERS` constant in the API route always prepends hard-coded entries (e.g., tmdb_id 615 "The Passion of the Christ"). OUTSTANDING: `cron/index.ts` reel-refresh handler is still a TODO — the `/api/tmdb/reel-posters` route currently fetches TMDB live on each request and ignores the DB table. Cron wiring is the remaining gap for this task.
 - [~] 5.2 — Build and seed landing_reel_posters table; implement periodic refresh job in the Node.js cron container on bi-weekly cadence that automatically fetches ~20 posters from TMDB popular or top-rated endpoints (with `include_adult=false`) and replaces the full set in the DB on each run. NOTE (2026-05-02): Table exists and is populated by seed.sql. `PINNED_REEL_POSTERS` constant in the API route always prepends hard-coded entries (e.g., tmdb_id 615 "The Passion of the Christ"). OUTSTANDING: `cron/index.ts` reel-refresh handler is still a TODO — the `/api/tmdb/reel-posters` route currently fetches TMDB live on each request and ignores the DB table. Cron wiring is the remaining gap for this task.
-- [~] 5.3 — Build slot-machine reel animation for the landing page Roll the Dice button: 3 side-by-side reels that spin through the automatically fetched poster set (using w185 size, `aria-hidden` on spinning posters), decelerate, and land on a single TMDB movie result with alt text (the final result is fetched live from TMDB via server proxy — not constrained to the reel poster set); animation is user-triggered only and completes within 5 seconds (WCAG 2.2.2); respect `prefers-reduced-motion` (instant reveal or simple fade-in when enabled). NOTE (2026-05-02): Continuous carousel and slot-machine spin on roll are implemented. KNOWN BUG (U9): modulo wrap missing in spinning branch causes scroll offset to drift on repeated rolls; fix tracked in `research/PHASE5_UI_FIXES_PLAN.md`.
+- [x] 5.3 — Build slot-machine reel animation for the landing page Roll the Dice button: 3 side-by-side reels that spin through the automatically fetched poster set (using w185 size, `aria-hidden` on spinning posters), decelerate, and land on a single TMDB movie result with alt text (the final result is fetched live from TMDB via server proxy — not constrained to the reel poster set); animation is user-triggered only and completes within 5 seconds (WCAG 2.2.2); respect `prefers-reduced-motion` (instant reveal or simple fade-in when enabled). NOTE (2026-05-02): Continuous carousel and slot-machine spin on roll are implemented. KNOWN BUG (U9): modulo wrap missing in spinning branch causes scroll offset to drift on repeated rolls; fix tracked in `research/PHASE5_UI_FIXES_PLAN.md`. NOTE (2026-05-02 dea71d9/0061375): Roll result card now emerges inside the carousel center via animate-emerge keyframe (scale 0→1.08→1, ~500ms, bouncy, skipped under prefers-reduced-motion); snap math positions midpoint of a gap at viewport center so card sits in a symmetric slot; posters spread ±110px on settle. Card stays settled until user re-rolls or refreshes (RESUME_DELAY auto-resume timer removed). TeaserCard reduced to ~1.3x reel poster size. "i" info button on TeaserCard opens <MoreInfoModal> (src/components/landing/more-info-modal.tsx) with title+year, genres, plot, Add to list (→ /login, no save-intent), Watch Trailer (lazy-fetches /api/tmdb/movie/[id]/videos); mirrors GenreRollModal a11y pattern; uses createPortal to escape the transformed ancestor. IMPORTANT: this landing MoreInfoModal is distinct from the in-app U5 "More Info" button targeting src/components/movies/expanded-panel.tsx — U5 is still TODO.
 - [x] 5.3a — Wire landing page Roll the Dice to TMDB via `/api/tmdb/popular?page=N` (random page 1–50, pool ~1000 movies). Result displayed as a static teaser card (poster with alt text, title, genres) — no link, no tap action. NOTE: this is a distinct pool from the carousel reel — see Feature Flow section 5 for the two-pool distinction.
 - [x] 5.3a — Wire landing page Roll the Dice to TMDB via `/api/tmdb/popular?page=N` (random page 1–50, pool ~1000 movies). Result displayed as a static teaser card (poster with alt text, title, genres) — no link, no tap action. NOTE: this is a distinct pool from the carousel reel — see Feature Flow section 5 for the two-pool distinction.
 - [x] 5.4 — Wire landing page Genre Roll to TMDB via server proxy (no login required); display result as a static teaser card showing poster (with alt text), title, and genres — no link, no tap action.
 - [x] 5.4 — Wire landing page Genre Roll to TMDB via server proxy (no login required); display result as a static teaser card showing poster (with alt text), title, and genres — no link, no tap action.
 - [ ] 5.5 — Loading and empty states for all major views (empty list, no search results, no genre matches, empty home page for new users)
 - [ ] 5.5 — Loading and empty states for all major views (empty list, no search results, no genre matches, empty home page for new users)
@@ -555,7 +569,7 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - [ ] 9.6 — Admin self-removal test: trigger "Leave this list" as admin with other members present; confirm Transfer Ownership popup appears with member list and Cancel; confirm original admin is removed (not just demoted) after transfer; confirm list is NOT deleted; confirm that "Delete the list" action does NOT show the Transfer Ownership popup
 - [ ] 9.6 — Admin self-removal test: trigger "Leave this list" as admin with other members present; confirm Transfer Ownership popup appears with member list and Cancel; confirm original admin is removed (not just demoted) after transfer; confirm list is NOT deleted; confirm that "Delete the list" action does NOT show the Transfer Ownership popup
 - [ ] 9.7 — Master Admin test: TOTP login, iron-session v8 cookie validation, list/user search and deletion (verify both public.users and auth.users records removed), session protection and expiry
 - [ ] 9.7 — Master Admin test: TOTP login, iron-session v8 cookie validation, list/user search and deletion (verify both public.users and auth.users records removed), session protection and expiry
 - [ ] 9.8 — TMDB rate limit check: confirm debounce and TanStack Query caching stay within rate limits; verify TMDB API key is not exposed in client-side code or network requests; verify include_adult=false on all calls
 - [ ] 9.8 — TMDB rate limit check: confirm debounce and TanStack Query caching stay within rate limits; verify TMDB API key is not exposed in client-side code or network requests; verify include_adult=false on all calls
-- [ ] 9.9 — Landing page test: slot-machine reel animation plays using automatically fetched TMDB posters and lands on a valid movie result; result displays as a static teaser card (poster with alt text, title, genres) with no tap/link action; roll buttons work without login; 3-step demo alignment renders correctly on all screen sizes; TMDB attribution footer visible; privacy policy accessible and contains all required sections
+- [ ] 9.9 — Landing page test: slot-machine reel animation plays using automatically fetched TMDB posters and lands on a valid movie result; result TeaserCard emerges in carousel center and stays settled until re-roll; "i" button opens MoreInfoModal with plot, Add to list (→ /login), and Watch Trailer; roll buttons work without login; 3-step demo alignment renders correctly on all screen sizes; TMDB attribution footer visible; privacy policy accessible and contains all required sections
 - [ ] 9.10 — Home page test: returning user lands on home page (not landing page); list cards display correctly; cross-list roll works and result appears as a standalone teaser card on the home page (no navigation into a list); new user with no lists sees empty state
 - [ ] 9.10 — Home page test: returning user lands on home page (not landing page); list cards display correctly; cross-list roll works and result appears as a standalone teaser card on the home page (no navigation into a list); new user with no lists sees empty state
 - [ ] 9.11 — Inline panel test: expands below correct row, collapses cleanly, delete two-tap flow works, binoculars overlay and Watched It button update in real time; keyboard navigation works (Enter/Escape, focus management)
 - [ ] 9.11 — Inline panel test: expands below correct row, collapses cleanly, delete two-tap flow works, binoculars overlay and Watched It button update in real time; keyboard navigation works (Enter/Escape, focus management)
 - [ ] 9.12 — Background job test: confirm Node.js cron container runs trailer URL refresh (null-only) and metadata refresh correctly; verify URL domain validation; verify no adult content in reel posters
 - [ ] 9.12 — Background job test: confirm Node.js cron container runs trailer URL refresh (null-only) and metadata refresh correctly; verify URL domain validation; verify no adult content in reel posters
@@ -593,7 +607,7 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - Master Admin can log in with TOTP (credentials from environment variables) and delete any list or user (both public.users and auth.users records); session expires after 8 hours
 - Master Admin can log in with TOTP (credentials from environment variables) and delete any list or user (both public.users and auth.users records); session expires after 8 hours
 - Trailer URL refresh job processes only movies where trailer_url is null (bi-weekly cadence)
 - Trailer URL refresh job processes only movies where trailer_url is null (bi-weekly cadence)
 - Monthly metadata refresh job keeps cached TMDB data current (TMDB ToS compliance)
 - Monthly metadata refresh job keeps cached TMDB data current (TMDB ToS compliance)
-- Landing page slot-machine reel animation uses automatically fetched TMDB posters (no adult content) and lands on a valid movie result displayed as a static teaser card (poster with alt text, title, genres) with no tap/link action
+- Landing page roll result TeaserCard emerges in the carousel center, stays settled until re-roll, and the "i" info button opens a MoreInfoModal with plot, Add to list (→ /login), and Watch Trailer
 - Landing page roll buttons work without any login or account creation
 - Landing page roll buttons work without any login or account creation
 - App installs to mobile home screen and functions as a PWA
 - App installs to mobile home screen and functions as a PWA
 - Inline panel expands below the correct grid row with no layout shift
 - Inline panel expands below the correct grid row with no layout shift