Pārlūkot izejas kodu

[Scope] Close 1.6 (recovery verified), mark 3.7 info-only, unblock 9.4

Task 1.6 browser-verified 2026-05-06 — both GoTrue escalation triggers
cleared. Task 3.7 reframed as a deliberate scope reduction: Genre Roll is
the sole filter entry point; tags elsewhere are informational only.
Task 9.4 unblocked by 1.6 closure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User 1 mēnesi atpakaļ
vecāks
revīzija
7d991d4684
1 mainītis faili ar 209 papildinājumiem un 130 dzēšanām
  1. 209 130
      PROJECT_SCOPE.md

+ 209 - 130
PROJECT_SCOPE.md

@@ -4,7 +4,7 @@
 
 MovieDice is a mobile-first web app that helps friend groups collaboratively build a shared movie watchlist and — when nobody can agree on what to watch — randomly select one using an animated "Roll the Dice" mechanic. Groups join via a short invite code, add movies from a live TMDB search, and mark films as watched together. The app removes the friction of the "what should we watch?" problem by making both curation and random selection fast and fun.
 
-A public landing page lets visitors try the dice mechanic against the TMDB database before signing up, lowering the barrier to entry. The landing page features a slot-machine-style reel animation on roll — distinct from the in-app scatter/eliminate animation — to create a visually striking first impression.
+A public landing page lets visitors try the dice mechanic against the TMDB database before signing up, lowering the barrier to entry. Both the landing page and in-app rolls use the same horizontal poster-carousel mechanic: a strip of posters spins and snaps so a gap lands at viewport center, and the result card emerges with an animate-emerge pop-in (scale 0 → 1.08 → 1, ~500ms, posters spread ±110px on settle).
 
 ## 2. Target Users
 
@@ -28,30 +28,30 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 
 ### 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. 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   |
-| 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   |
+| 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 (`PosterCard`, now a full-card button) shows movie poster (full bleed, TMDB native sized URLs) with title below and meaningful `alt` text; added-by avatar overlaid top-right; green-circle checkmark overlaid top-left when watched; tapping a card opens `ListMoreInfoModal`; infinite scroll loading 12 movies initially                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | Must Have   |
+| Movie info modal                  | Tapping a poster opens `ListMoreInfoModal` (portal to document.body). Contents: poster (w500 with alt text) + title + "Added by [username]" + genre tags + Watched It button + Trailer button + Delete button (only when caller supplies `onDelete` prop — roll-result modal omits delete). Watched It: three-state idle→confirming→committed with shake-to-arm and `localWatched` display; reverse path mirrors with gray "Unwatch Movie" intermediate. Delete: red idle → "Click to confirm" → mutation + close; both armed states apply animate-shake with 4s auto-disarm; arming one disarms the other. Trailer opens stored URL in new tab. Modal uses createPortal to escape transformed ancestors; ESC closes, click-outside closes, focus trap, focus restoration on close. TMDB overview fetched per-open (no DB column; TanStack Query caches within session). | Must Have   |
+| Genre filter                      | Genre tags render in `ListMoreInfoModal` and on result cards — informational only, not tappable filters. **Genre Roll is the sole filter entry point.** Deliberate scope decision (2026-05-06): tap-tag filtering was removed when ExpandedPanel was replaced by the modal; `selectedGenre` wiring on `PosterGrid` is dormant and being removed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | Must Have   |
+| Roll the Dice                     | Button pinned above the movie grid (list view) or in RollSection (home); triggers the carousel roll animation; announce result via `aria-live="polite"` region; result card shows gold glow (`ring-2 ring-yellow-400/70 shadow-[0_0_32px_rgba(250,204,21,0.55)]`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | 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. Green-circle checkmark 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 a single-column centered grid and replaces Login with Create List; Roll the Dice, Genre Roll, and Create List buttons grouped in RollSection (stacked full-width on mobile, side-by-side centered on desktop); Roll the Dice and Genre Roll roll across all user lists combined and display the result using `ListRollCarousel` 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)
 
@@ -93,6 +93,7 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
       (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.
+      Gold glow applied to result card: ring-2 ring-yellow-400/70 shadow-[0_0_32px_rgba(250,204,21,0.55)].
       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
@@ -101,13 +102,11 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
       - "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;
    result displayed as a static teaser card (poster with alt text, title, genres).
    No link, no tap action.
+   On mobile: Roll + Genre buttons stack full-width (max-w-xs flex-col); desktop unchanged.
 6. "Login / Get Started" button below the roll buttons
 7. User scrolls down to reveal:
    a. About section — fuller description of how MovieDice works
@@ -129,8 +128,9 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
    cookie-based handling (createBrowserClient / createServerClient)
 4. Recovery code (24 alphanumeric characters, 128-bit entropy) shown once — user prompted to save it
 5. User selects: "Create a Group" or "Join with a Code"
-   A. Create → enter group name → group created, invite code shown (e.g., WOLF-MOON)
-      → creator assigned List Admin role → lands on home page (with their new list card shown)
+   A. Create → enter group name → group created → creator assigned List Admin role
+      → router.push("/list/{id}") directly into the new list view (no in-form invite-code
+        panel after creation; join code is visible in the list header under the list name)
    B. Join → enter invite code → validated (rate-limited, server-side via service role key)
       → member role assigned → lands on home page (with the joined list card shown)
 ```
@@ -142,18 +142,23 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 2. If valid session found → navigate directly to the home page (skip landing page)
 3. If no session → show landing page (pre-login)
 4. Home page layout mirrors the landing page layout, with these differences:
-   a. The Login button is replaced by a "Create List" button
+   a. The Login button is replaced — Roll, Genre Roll, and Create List are grouped in one
+      RollSection component (stacked full-width on mobile via flex-col items-stretch w-full;
+      side-by-side centered on desktop via sm:flex-row sm:justify-center). Create List button
+      is bg-blue-700 hover:bg-blue-600. Standalone Create List button in home/page.tsx removed.
    b. The About / how-it-works section is replaced by the user's list cards
-5. Each list card shows:
+5. List grid: mx-auto flex max-w-md flex-col gap-4 — single column always, centered
+6. Each list card shows:
    - List name (left-aligned)
    - Number of movies with a film emoji (right-aligned)
    - Below: "Created by: [username]"
-6. Tapping a list card navigates into that list view
-7. "Roll the Dice" and "Genre Roll" buttons at the top roll across ALL unwatched movies
-   from ALL of the user's lists combined (cross-list roll)
-8. The roll result is displayed as a standalone teaser card directly on the home page.
-   The result does NOT navigate the user into any specific list.
-9. If no session (new device) → prompt for recovery code or start fresh
+7. Tapping a list card navigates into that list view
+8. "Roll the Dice" and "Genre Roll" buttons roll across ALL unwatched movies from ALL of the
+   user's lists combined (cross-list roll); home roll uses ListRollCarousel (same component as
+   list view); cross-list watched toggle done via inline fetch + manual queryClient.invalidateQueries
+   (per-movie group_id)
+9. Page header text is centered on mobile
+10. If no session (new device) → prompt for recovery code or start fresh
 ```
 
 ### Adding a Movie
@@ -176,70 +181,107 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 
 ```
 1. Default view: 2-column evenly-scaling poster grid (3-4 columns on tablet/desktop)
-2. Each card shows:
-   - Movie poster (full bleed, using TMDB native sized URL: w342 for mobile grid)
-     with meaningful alt text (e.g., "Movie Title (Year) poster")
+2. Each card (PosterCard — full-card button, no inline ExpandedPanel):
+   - Movie poster (full bleed, TMDB native sized URL: w342) with meaningful alt text
    - Movie title below the poster
    - Added-by user avatar overlaid top-right corner
-   - Binoculars emoji overlaid top-left corner — only when movie is watched
+   - Green-circle checkmark overlaid top-left — only when movie is watched (replaces binoculars emoji)
+   - Decorative "i" glyph bottom-right (pointer-events-none, opens nothing — info is in modal)
 3. Grid loads 12 movies initially; additional movies load automatically on scroll to bottom
-4. No action buttons on collapsed grid cards — cards are tap-only
+4. Tapping a card opens ListMoreInfoModal (portal)
 5. All poster images use native loading="lazy" attribute
 ```
 
-### Expanded Movie Card (Inline Panel)
+### Movie Info Modal
 
 ```
-1. User taps any movie poster in the grid
-2. A full-page-width panel expands downward, inserted inline below that row in the grid
-   (mirrors Google Image Search inline expansion — not a modal, popup, or slide-up sheet)
-3. Panel contents, top to bottom:
-   a. Full-size movie poster (TMDB native sized URL: w500) with alt text
-   b. Movie title
-   c. "Added by [username]"
-   d. Genre tags — tappable; each filters the grid to that genre
-   e. "Watched It" + "Trailer" buttons — displayed side by side
-   f. Delete button — centered below, separated to prevent accidental taps
-4. Delete behavior:
-   - Tap 1: button shakes, text changes to "Click to confirm delete"
-   - Tap 2: movie removed from list for all group members in real time
-   - Tapping elsewhere after Tap 1 resets button to default state
-5. Watched It (toggle):
-   - If unwatched: button color changes to watched state, binoculars overlay appears on grid
-     poster (top-left), movie moves to "Watched" section
-   - If watched: button color reverts, binoculars overlay removed, movie returns to main list
-   - Both indicators update simultaneously on all group members' screens
-6. Trailer: opens stored trailer URL in a new tab (with rel="noopener noreferrer")
-7. Tapping outside the panel or a close affordance collapses it
+SHIPPED (2026-05-06): src/components/dice/list-more-info-modal.tsx
+Replaces the former ExpandedPanel inline expansion. Always renders as a portal to document.body.
+
+1. User taps any movie poster in the grid (or taps the result card after a list/home roll)
+2. ListMoreInfoModal opens:
+   a. Poster (w500 with alt text)
+   b. Title
+   c. "Added by [username]" (when addedByName prop supplied)
+   d. Genre tags
+   e. TMDB overview fetched per-open via /api/tmdb/movie/[id] (no DB column; cached in TanStack Query
+      within session only — no persistent caching)
+   f. "Watched It" button — three-state with shake-to-arm:
+      - Idle (unwatched): gray "Watched It"
+      - Armed: shaking green "Click to confirm"
+      - Committed: static green "Watched" (uses localWatched for display correctness mid-mutation)
+      - Reverse path: idle (watched) → gray "Unwatch Movie" armed → back to unwatched
+   g. "Trailer" button — opens stored trailer_url in new tab (rel="noopener noreferrer")
+   h. "Delete Movie" button (only when onDelete prop supplied; list view supplies it; roll-result
+      view omits it to prevent accidental deletes):
+      - Idle: red "Delete Movie"
+      - Armed: shaking "Click to confirm"
+      - Confirmed: mutation fires + modal closes
+   i. Arming one action (watched/delete) auto-disarms the other. 4s auto-disarm timer on both.
+3. a11y: ESC closes, click-outside closes, focus trap, focus restoration on close
+4. NOTE: movies.overview is NOT stored in the DB. Modal fetches from TMDB on each open.
+   Follow-up: consider caching in TanStack Query with a reasonable staleTime.
 ```
 
 ### Rolling the Dice (In-App)
 
 ```
-1. User taps "Roll the Dice!" (pinned above the movie grid or on the home page)
-2. Animated randomizer plays — scatter/flip/spin elimination sequence, 2-3 seconds
-   (this is distinct from the landing page slot-machine reel animation)
-   Respects prefers-reduced-motion: if enabled, use a simple fade-in on the winner instead
-3. Animation settles on one random unwatched movie from the eligible pool
-   - On a list page: pool is that list's unwatched movies
-   - On the home page: pool is all unwatched movies across all of the user's lists combined
-4. On the home page, the result is shown as a standalone teaser card in place on the home page.
-   The user is NOT navigated into any specific list.
+SHIPPED (2026-05-06): src/components/dice/list-roll-carousel.tsx
+Used on both the list page (via RollBar above the search bar) and the home page (via RollSection).
+
+1. User taps "Roll the Dice!" button
+2. ListRollCarousel plays:
+   - Pool tiles to ≥10 posters by duplicating when list is short
+   - ~500ms dice-emerge pop-in entrance
+   - 100ms pause
+   - 2750ms easeOutCubic spin
+   - Snap to gap-at-center
+   - Posters spread ±110px
+   - Result card emerges with animate-emerge (gold glow: ring-2 ring-yellow-400/70
+     shadow-[0_0_32px_rgba(250,204,21,0.55)])
+   - Respects prefers-reduced-motion
+3. Pool:
+   - List page: that list's unwatched movies
+   - Home page: all unwatched movies across all user lists combined
+4. On the home page, result stays in place — user is NOT navigated into any list
 5. Result announced via aria-live="polite" region for screen readers
-6. Tapping Roll again re-rolls from the same eligible pool
-7. Tapping "Genre Roll!" opens a text input
-8. User enters genres and/or emotions (e.g., "action, excited")
-9. App normalizes input, maps emotion keywords to TMDB genre IDs, filters the unwatched pool
-10. Same scatter/eliminate animation plays on filtered results
-11. If no matches found: "No matches — showing full list" shown, roll proceeds unfiltered
-12. On a list page, the result movie is displayed prominently; tapping it opens the inline
-    expanded panel
+   NOTE: RollAnnouncer fires "complete" at useRoll's 2500ms mark while carousel spins
+   until ~3350ms — minor a11y timing mismatch, not blocking
+6. Tapping the result card opens ListMoreInfoModal
+7. Tapping Roll again re-rolls from the same eligible pool
+8. Tapping "Genre Roll!" opens a text input
+9. User enters genres and/or emotions (e.g., "action, excited")
+10. App normalizes input, maps emotion keywords to TMDB genre IDs, filters the unwatched pool
+11. Same carousel animation plays on filtered results
+12. If no matches found: "No matches — showing full list" shown, roll proceeds unfiltered
+
+Button layout:
+- List view: RollBar moved ABOVE search bar; Roll + Genre buttons are flex-col items-stretch
+  w-full on mobile, sm:flex-row sm:flex-1 on desktop
+- Home: Roll + Genre + Create List grouped in RollSection; flex-col items-stretch w-full on
+  mobile, sm:flex-row sm:justify-center on desktop
+```
+
+### List Page Header
+
+```
+SHIPPED (2026-05-06): src/app/(app)/list/[id]/page.tsx
+- Centered vertical stack (both mobile and desktop)
+- List name: text-2xl sm:text-3xl font-bold
+- Uppercase tracked "JOIN CODE" eyebrow with mono chip (bg-foreground/10) showing WORD-WORD code
+- Settings cog below the join code
 ```
 
 ### List Admin Actions
 
+NOTE (2026-05-06): Settings page at `src/app/(app)/list/[id]/settings/page.tsx` is now live (gear
+icon was previously broken/404). The page mounts `src/components/groups/settings-panel.tsx`.
+Join code is displayed in the list header (`src/app/(app)/list/[id]/page.tsx`) under the list
+name as "Join code: WORD-WORD" — visible to all members on the list view, no need to navigate
+to settings to find it.
+
 ```
-1. List Admin opens group settings (gear icon or settings menu)
+1. List Admin taps the gear icon → navigates to /list/[id]/settings (settings page now exists)
 2. Available actions:
    - Rename the list
    - Delete the list (see deletion flow below)
@@ -247,27 +289,31 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
    - Regenerate invite code (revokes old code)
    - Display current invite code with copy-to-clipboard
 3. Regular members see a settings menu with only: "Leave this list" option
+   (same shake-to-arm confirmation pattern as admin terminal actions)
 ```
 
 Admin Self-Removal / Ownership Transfer flow:
 
 ```
    a. List Admin taps "Leave this list" (or equivalent self-removal action)
-   b. If other members exist:
-      - "Transfer Ownership" popup appears before the admin can leave
-      - Popup shows the current member list; admin selects one member to become the new admin
-      - "Cancel" button at the bottom dismisses the popup with no changes
-      - Once a new admin is selected and confirmed, ownership transfers and the original admin
-        is removed from the list (they leave; the list is NOT deleted)
-   c. If the admin is the last remaining member (no one else to transfer to):
-      - The list is deleted automatically (with a standard confirmation prompt)
-      - Confirmed deletion removes the list and all its movies permanently
+   b. Shake-to-arm pattern (uses animate-shake utility, globals.css):
+      - First click: button shakes and enters armed state; 4-second auto-disarm timer starts
+      - Second click within 4s: confirms the action
+      - Clicking elsewhere or waiting >4s: disarms with no changes
+   c. If other members exist — second click reveals an inline successor picker:
+      - Shows non-admin members only
+      - Selecting a member runs transfer+leave atomically
+      - On success: router.push("/")
+   d. If the admin is the last remaining member (no one else to transfer to):
+      - Second click permanently deletes the list and all its movies
+      - On success: router.push("/")
 
 List Deletion flow (separate from self-removal):
    a. List Admin taps "Delete the list" in settings
-   b. Standard delete confirmation prompt shown
+   b. Shake-to-arm pattern: first click arms + shakes (4s auto-disarm), second click confirms
    c. Confirmed deletion removes the list and all its movies permanently
-   d. This action does NOT trigger the Transfer Ownership popup
+   d. On success: router.push("/")
+   e. This action does NOT trigger the successor picker / ownership transfer
 ```
 
 ### Master Admin Flow
@@ -292,11 +338,10 @@ List Deletion flow (separate from self-removal):
 
 - **Mobile-first**: All layouts designed for 375px+ screens first; desktop is a bonus. Grid expands to 3-4 columns on tablet/desktop.
 - **PWA**: App should be installable to home screen and behave like a native app in browser.
-- **Inline expansion**: The expanded movie panel is a full-page-width inline expansion inserted below the tapped card's row — never a modal, popup, slide-up sheet, or centered dialogue. Mirrors Google Image Search inline expansion behavior.
-- **Inline panel keyboard navigation**: Panel must be operable via keyboard: Enter to open, Escape to close, focus moves into panel on open and returns to trigger on close. Use `aria-expanded` on trigger and `role="region"` on panel.
-- **Two distinct roll animations**: The landing page uses a slot-machine reel animation (3 spinning reels of poster images). The in-app roll uses a scatter/flip/spin elimination animation. These must feel visually distinct — the landing animation is attention-grabbing and cinematic; the in-app animation is fast and satisfying but not distracting from repeat use.
-- **Reduced motion**: Both animations must respect `prefers-reduced-motion`. Landing page reel: instant reveal or simple fade-in. In-app roll: fade to winner instead of scatter animation. Full animation remains the default.
-- **Roll animation (in-app)**: Target 2-3 seconds max — visually impactful but not so long it frustrates repeat use.
+- **Movie info modal**: Tapping a poster card opens `ListMoreInfoModal` (portal to document.body). The former inline ExpandedPanel has been replaced. Modal must use createPortal — any transformed ancestor (e.g., `animate-emerge fill-mode: both`) establishes a containing block that clamps `fixed inset-0` to the ancestor's box.
+- **Modal keyboard navigation**: Modal must be operable via keyboard: ESC to close, focus trap on open, focus restoration on close.
+- **Shared roll animation**: Both the landing page and in-app rolls use the same horizontal poster-carousel mechanic (`ListRollCarousel` for in-app; landing carousel for landing). Animation is user-triggered only and completes within 5 seconds (WCAG 2.2.2). Target ~3.35 seconds total (500ms entrance + 100ms pause + 2750ms spin).
+- **Reduced motion**: All animations must respect `prefers-reduced-motion`. Landing page reel: instant reveal or simple fade-in. In-app roll (`ListRollCarousel`): animation skipped, result card appears instantly. Full animation remains the default.
 - **Search responsiveness**: Debounce TMDB queries at ~300ms; show loading state during fetch.
 - **Real-time updates**: New movies and watched status changes appear without a page refresh via Supabase subscriptions.
 - **Offline tolerance**: If connection drops, the app should degrade gracefully — show cached list (via TanStack Query persistence), disable write actions with an explanatory message. Do not queue offline writes.
@@ -332,7 +377,7 @@ TMDB posters are served directly from TMDB's CDN using native sized URLs — not
 
 - `w342` — grid thumbnails (mobile)
 - `w185` — reel animation posters
-- `w500` — expanded inline panel
+- `w500` — movie info modal (ListMoreInfoModal)
 - Add `loading="lazy"` attribute on all poster `<img>` tags
 - Add meaningful `alt` text on all poster images (e.g., "Movie Title (Year) poster"); spinning reel posters use `aria-hidden` (decorative)
 - Reserve `next/image` for locally-served assets only (logo, icons)
@@ -388,6 +433,7 @@ All TMDB API calls must be routed through Next.js API Route Handlers (`/api/tmdb
 - watched (boolean, default false)
 - watched_at (timestamp, nullable)
 - added_at
+- NOTE: `overview` (plot/description) is NOT stored. `ListMoreInfoModal` fetches it from TMDB per-open.
 
 **landing_reel_posters**
 
@@ -486,7 +532,10 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - [ ] 1.3 — Configure Supabase client in Next.js using `@supabase/ssr` (`createBrowserClient` for browser, `createServerClient` for server-side with `SUPABASE_INTERNAL_URL`); implement env var validation at startup via t3-env (`@t3-oss/env-nextjs`) with zod — server vars in `server` block, client vars in `client` block; create TMDB API proxy route (`/api/tmdb/*`) to keep API key server-side; set `include_adult=false` on all TMDB calls
 - [ ] 1.4 — Implement anonymous auth via `supabase.auth.signInAnonymously()`; display name input and optional avatar color picker; session managed by Supabase GoTrue via `@supabase/ssr` cookie-based handling (JWT issued automatically); verify GoTrue returns 200 (not 400) — confirm `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` is set
 - [ ] 1.5 — Implement recovery code generation (24 alphanumeric characters, 128-bit entropy), Argon2id hashing (explicit OWASP parameters: memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes — do not use library defaults), and show-once display screen. NOTE (2026-05-02, commits f71cb76 + a54a5ca): Fixed a post-signup race on the /recovery page where React 19 StrictMode double-mount caused the page to hang on "Generating your recovery code..." indefinitely (reload was required to see the code). Root cause: the original useMutation + useRef guard fired on the first mount instance; the second instance never observed the response. Fixed by replacing useMutation with useQuery (key: `recovery-code-generate`, staleTime/gcTime: Infinity, refetchOnMount: false) — TanStack Query dedupes the request across mount/remount/StrictMode and persists the result in cache. Server route (POST /api/auth/recovery/generate) unchanged.
-- [ ] 1.6 — Implement recovery code claim flow (enter code → verify against Argon2id hash → restore identity on new device); rate-limit claim endpoint (5 failed attempts per IP per 15-minute window); invalidate code after successful claim
+- [x] 1.6 — Implement recovery code claim flow (enter code → verify against Argon2id hash → restore identity on new device); rate-limit claim endpoint (5 failed attempts per IP per 15-minute window); invalidate code after successful claim. VERIFIED (2026-05-06): Manual end-to-end browser test passed. Both escalation triggers cleared: (A) `auth.identities` row present and correct — no issue; (B) GoTrue #2013 residual — not triggered with pinned v2.170.0. This was the highest-risk open item in the recovery system: GoTrue accepting our minted JWTs via HS256 signature alone (without a matching `auth.sessions` row) was the critical architectural unknown. Browser verification clears it — the synthetic-identity-at-generate architecture is sound. Architectural risk for the recovery system is resolved.
+
+  ARCHITECTURE — Synthetic-identity-at-generate (replaces broken claim-only-verifies-no-session flow): At code-generation time, `/api/auth/recovery/generate` assigns `<uid>@moviedice.invalid` + HKDF-SHA256 derived password (label `"moviedice-recovery-v1"`, salt=uid) to the GoTrue user via admin API. Returns 409 if a code already exists (idempotent — no auto-rotate). Atomic rollback on failure (`admin.deleteUser` + 500). At claim time, `/api/auth/recovery/claim` runs Argon2 scan → `signInWithPassword` with derived synthetic creds → cookies set via `createServerClient` bound to response → `{ ok: true }`. `/recover` page calls `window.location.assign("/")` post-claim (hard nav for cookie propagation). New files: `src/lib/auth/synthetic.ts` (`syntheticEmail()`, `derivePassword()`), `supabase/migrations/00003_synthetic_email_constraint.sql` (CHECK on `auth.users.email` rejecting `@moviedice.invalid` on non-promoted rows), `src/lib/supabase/admin.ts` updated with module-load assertion on `GOTRUE_EXTERNAL_EMAIL_ENABLED`. Tests: 15 passing (synthetic.test.ts ×6, recovery-generate.test.ts ×5, recovery-claim.test.ts ×4) + 1 todo regression scaffold for GoTrue #2013 canary.
+
 - [ ] 1.7 — Build Docker infrastructure: multi-stage Dockerfile (node:22-slim, non-root user, tini; builder stage installs `python3 make g++` for argon2 native build), docker-compose.yml orchestrating Next.js app + self-hosted Supabase stack + Caddy reverse proxy + Node.js cron container + pg_dump backup container (daily, 7-day retention), .dockerignore, /api/health endpoint; network security: Kong/Postgres ports internal only, Studio restricted to 127.0.0.1; Caddy persistent volumes for `/data` and `/config` (TLS certificates); Docker log rotation on all containers (max-size: 10m, max-file: 5); use Let's Encrypt staging for initial testing; deploy and confirm Supabase connection works in production
 - [ ] 1.8 — Add Sentry error monitoring (free tier); configure `beforeSend` to strip UUID path segments from error events; do not call `Sentry.setUser()` with user identifiers
 
@@ -495,38 +544,50 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - [ ] 2.1 — Build "Create a Group" flow: name input (validated: 1-50 chars), invite code generation (WORD-WORD format; word list: 2,000+ words, 3-8 chars, offensive terms filtered, collision check), store in DB, assign creator as List Admin; document API routes in markdown
 - [ ] 2.2 — Build "Join with a Code" flow: code entry, validation via server-side route handler using service role key (not client-side INSERT), group_members record with role: 'member'; rate-limit join endpoint (5-10 failed attempts per IP per 15-minute window); document API routes in markdown
 - [ ] 2.3 — Build logged-in home page: mirrors landing page layout; replaces Login with "Create List" button; shows user's list cards (list name left, movie count + film emoji right, "Created by: [username]" below); tapping a card navigates to that list; Roll the Dice and Genre Roll on the home page roll across all lists combined and display the result as a standalone teaser card on the home page (no navigation into a specific list); use polling (not real-time subscriptions) for home page movie counts
-- [ ] 2.4 — Implement List Admin settings: rename list, regenerate invite code, display + copy invite code, view + remove members; implement "Leave this list" self-removal for admin — if other members exist, show "Transfer Ownership" popup (member list + Cancel button); on confirm, transfer ownership and remove original admin from the list; if admin is last member, delete the list with confirmation
-- [ ] 2.5 — Implement List Admin direct deletion: "Delete the list" action in settings shows a standard confirmation prompt and permanently deletes the list; does NOT trigger the Transfer Ownership popup
-- [ ] 2.6 — Implement regular user settings: leave list option only
+- [x] 2.4 — Implement List Admin settings. SHIPPED (2026-05-06): Settings page `src/app/(app)/list/[id]/settings/page.tsx` now exists (gear icon was previously broken/404); mounts `src/components/groups/settings-panel.tsx`. Join code displayed in list header under list name (`src/app/(app)/list/[id]/page.tsx` selects `invite_code`, renders "Join code: WORD-WORD"). Delete/leave/transfer actions use shake-to-arm pattern (animate-shake, 4s auto-disarm) replacing all `window.confirm()` popups. Admin with other members: second click reveals inline non-admin successor picker; selecting a member runs transfer+leave atomically → router.push("/"). Admin last member: second click permanently deletes → router.push("/"). Settings also supports: rename list, regenerate invite code (copy-to-clipboard), view + remove members.
+- [x] 2.5 — Implement List Admin direct deletion. SHIPPED (2026-05-06): "Delete the list" in SettingsPanel uses shake-to-arm (first click arms + shakes, 4s auto-disarm, second click confirms permanent deletion) → router.push("/"). Does NOT trigger the successor/ownership picker.
+- [x] 2.6 — Implement regular user settings. SHIPPED (2026-05-06): "Leave this list" for non-admin members uses same shake-to-arm pattern for consistency → router.push("/") on confirm.
 
 ### Phase 3: Movie List Core (April 14-20, 2026) — MVP
 
-- [ ] 3.1 — Integrate TMDB API via server-side proxy (`/api/tmdb/*`): search endpoint with `include_adult=false` and server-side `adult` field filtering, poster URL construction using TMDB native sizes (w342 grid, w185 reel, w500 expanded), trailer URL fetch at add-time; implement TanStack Query caching with explicit `staleTime` configuration; document API routes in markdown
+- [ ] 3.1 — Integrate TMDB API via server-side proxy (`/api/tmdb/*`): search endpoint with `include_adult=false` and server-side `adult` field filtering, poster URL construction using TMDB native sizes (w342 grid, w185 reel, w500 modal), trailer URL fetch at add-time; implement TanStack Query caching with explicit `staleTime` configuration; document API routes in markdown
 - [ ] 3.2 — Build search bar with ~300ms debounce, loading state, and two-section results ("In Your List" above separator, TMDB results below)
 - [ ] 3.3 — Implement add-movie flow: tap result → insert into movies table with TMDB metadata, stored trailer_url (fetched via server proxy at add-time; validated against domain allowlist: youtube.com, themoviedb.org, imdb.com), trailer_url_refreshed_at, and added_by attribution; document API routes in markdown
-- [ ] 3.4 — Build 2-column poster grid: full-bleed poster using TMDB native sized URLs (w342) with `loading="lazy"` and meaningful `alt` text (e.g., "Movie Title (Year) poster"), title below, added-by avatar overlaid top-right, binoculars emoji overlaid top-left (watched only), no action buttons on cards, tap-only interaction
+
+  BUG FIX SHIPPED (2026-05-06, master — uncommitted): Clicking "+ Add" on a search result flashed the button but silently failed. Root cause: `src/app/api/movies/route.ts` was calling TMDB with `?api_key=${TMDB_API_KEY}` (v3 query-string auth), but `TMDB_API_KEY` is a v4 read-access JWT that TMDB only accepts via `Authorization: Bearer`. Every other TMDB call in the repo (`src/lib/tmdb-fetch.ts`, `src/lib/tmdb/client.ts`) already used Bearer — only this route was wrong. TMDB returned 401 → route returned 404 "Movie not found on TMDB" → `useAddMovie` threw silently (no `onError` handler) → invisible failure to the user. Fix: switched both TMDB fetches in that route to `Authorization: Bearer ${TMDB_API_KEY}` headers; added `console.error` with TMDB response body+status on details-fetch failure. UX fix in the same pass (`src/components/movies/movie-list-client.tsx`): clicking a search result now immediately hides the results panel (`resultsHidden` state, reset on next keystroke); search input text is preserved.
+
+  Known follow-ups not addressed in this fix: (1) `useAddMovie` hook still has no `onError` handler — add-movie failures remain silent in the UI; (2) Turbopack did not recompile the API route after edits during this session — required manual `next dev` restart + `.next` cache wipe. Note if it recurs.
+
+  DEAD CODE (2026-05-06 UI batch — uncommitted): The following files are no longer imported anywhere and should be deleted:
+  - `src/components/movies/expanded-panel.tsx` — replaced by `ListMoreInfoModal`
+  - `src/components/movies/movie-search-panel.tsx` — never wired up
+  - `src/components/home/home-roll-teaser-card.tsx` — home now uses `ListRollCarousel`
+  - `src/components/dice/roll-animation.tsx` — home and list both now use `ListRollCarousel`
+  - `src/components/dice/roll-result-card.tsx` — list used it before; list now uses `ListRollCarousel`
+
+- [x] 3.4 — Build 2-column poster grid. SHIPPED (2026-05-06): `PosterCard` is now a full-card button (no ExpandedPanel). Tapping opens `ListMoreInfoModal`. "i" glyph bottom-right is decorative `pointer-events-none`. Watched overlay changed from binoculars emoji to green-circle checkmark top-left. Avatar dot remains top-right. Grid layout unchanged (w342 posters, `loading="lazy"`, meaningful `alt` text, added-by avatar overlaid top-right).
 - [ ] 3.5 — Implement infinite scroll: load 12 movies initially, fetch and append next batch automatically on scroll to bottom
-- [ ] 3.6 — Build expanded movie inline panel: full-page-width expansion inserted below the tapped row (not a modal); fixed element order (poster at w500 with alt text → title → Added by → genre tags → Watched It + Trailer side-by-side → Delete centered below); delete two-tap shake-and-confirm; Watched It toggle; Trailer opens stored trailer_url in new tab with rel="noopener noreferrer"; collapse on tap outside
-- [ ] 3.7 — Implement genre filter: tapping a genre tag in the inline panel filters the grid to that genre; announce filter state change via `aria-live="polite"` region
-- [ ] 3.8 — Implement Watched state: toggle marks/unmarks movie as watched for the group; watched movies move to collapsed "Watched" section; binoculars overlay and button color update simultaneously across all members; announce state change via `aria-live="polite"` region
+- [x] 3.6 — Build movie info modal. SHIPPED (2026-05-06): `src/components/dice/list-more-info-modal.tsx`. Replaces the planned inline ExpandedPanel entirely. See "Movie Info Modal" flow and feature table entry for full spec. Portal to document.body; TMDB overview fetched per-open; Watched + Delete both use shake-to-arm three-state pattern.
+- [x] 3.7 — Genre filter (scope clarified 2026-05-06). DELIBERATE SCOPE REDUCTION — not a regression. Genre tags are display-only everywhere except Genre Roll. Genre Roll (`RollBar` / `RollSection`) is the sole filter entry point — tappable tag-filters have been explicitly removed from the product design when ExpandedPanel was replaced by `ListMoreInfoModal`. Cleanup in flight: dormant `selectedGenre` prop being removed from `PosterGrid`.
+- [ ] 3.8 — Implement Watched state: toggle marks/unmarks movie as watched for the group; watched movies move to collapsed "Watched" section; green-circle checkmark overlay and button color update simultaneously across all members; announce state change via `aria-live="polite"` region
 - [ ] 3.9 — Enable Supabase real-time subscriptions on movies table for live add, remove, and watched-status updates; subscribe only to the currently-viewed list (subscribe on mount, unsubscribe on unmount via useEffect cleanup); implement exponential backoff on reconnection
 
 ### Phase 4: Randomizer (April 20-23, 2026) — MVP
 
-- [~] 4.1 — Build "Roll the Dice!" and "Genre Roll!" button layout pinned above the movie grid
-- [~] 4.2 — Implement randomizer logic: select a random unwatched movie from the eligible pool (single list when in a list view; all user lists combined when on the home page); on the home page, render result as a standalone teaser card in place — do not navigate into any list; announce result via `aria-live="polite"` region
-- [~] 4.3 — Build in-app roll animation: scatter/flip/spin elimination sequence landing on winner (target 2-3 seconds); test performance on low-end mobile devices; respect `prefers-reduced-motion` (use simple fade-in on winner when enabled)
-- [~] 4.4 — Implement re-roll on second tap of Roll button
-- [~] 4.5 — Build Genre Roll text input UI
-- [~] 4.6 — Implement emotion-to-genre mapping as a static TypeScript constant (see Section 10 reference table); translate genre labels to TMDB numeric genre IDs
-- [~] 4.7 — Implement genre + emotion filter logic: normalize input (lowercase, tokenize on spaces/commas), map emotions to TMDB genre IDs, filter unwatched pool; if no matches, show "No matches — showing full list" and proceed unfiltered. NOTE: home-view Genre Roll swapped to `filterByGenresAndEmotionsStructured` (2026-04-14) — fixes genre-only rolls silently discarding the filter when `movies.genres` stores TMDB names, not numeric IDs.
-- [~] 4.8 — Apply roll animation to genre-filtered results; document API routes in markdown
+- [x] 4.1 — Build "Roll the Dice!" and "Genre Roll!" button layout. SHIPPED (2026-05-06): List view — `RollBar` moved ABOVE search bar; Roll + Genre stack full-width on mobile (`flex-col items-stretch w-full`), side-by-side on desktop (`sm:flex-row sm:flex-1`). Home — Roll + Genre + Create List in `RollSection`; same mobile/desktop pattern with `sm:justify-center`. Create List button `bg-blue-700 hover:bg-blue-600`. Landing — Roll + Genre stack full-width on mobile (`max-w-xs flex-col`), desktop unchanged.
+- [x] 4.2 — Implement randomizer logic. SHIPPED: `useRoll` hook selects a random unwatched movie from the eligible pool (single list on list page; all user lists combined on home page). Home page result displayed via `ListRollCarousel` in place — no navigation. `aria-live="polite"` region announces result (minor timing mismatch noted — `RollAnnouncer` fires at 2500ms, carousel settles at ~3350ms).
+- [x] 4.3 — Build in-app roll animation. SHIPPED (2026-05-06): `src/components/dice/list-roll-carousel.tsx`. Horizontal poster strip (tiles to ≥10 by duplication when list is short); 500ms dice-emerge entrance → 100ms pause → 2750ms easeOutCubic spin → snap to gap-at-center → ±110px spread → animate-emerge result card with gold glow. Replaces scatter/flip/spin design. Respects `prefers-reduced-motion`.
+- [x] 4.4 — Implement re-roll. SHIPPED: tapping Roll again re-rolls from the same eligible pool.
+- [x] 4.5 — Build Genre Roll text input UI. SHIPPED: Genre Roll modal/input wired on both list and home pages.
+- [x] 4.6 — Implement emotion-to-genre mapping. SHIPPED: static TypeScript constant; genre labels translated to TMDB numeric genre IDs.
+- [x] 4.7 — Implement genre + emotion filter logic. SHIPPED: normalize input (lowercase, tokenize on spaces/commas), map emotions to TMDB genre IDs, filter unwatched pool; if no matches, show "No matches — showing full list" and proceed unfiltered. NOTE: home-view Genre Roll uses `filterByGenresAndEmotionsStructured` (2026-04-14) — fixes genre-only rolls silently discarding the filter when `movies.genres` stores TMDB names, not numeric IDs.
+- [x] 4.8 — Apply roll animation to genre-filtered results. SHIPPED: `ListRollCarousel` receives the filtered pool directly.
 
 ### Phase 5: Landing Page and MVP Polish (April 23-26, 2026) — MVP CUTOFF
 
 - [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.
-- [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.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. Gold glow added to TeaserCard in same UI batch as in-app roll (2026-05-06). NOTE: landing MoreInfoModal is distinct from the in-app `ListMoreInfoModal` (src/components/dice/list-more-info-modal.tsx).
 - [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.
 - [ ] 5.5 — Loading and empty states for all major views (empty list, no search results, no genre matches, empty home page for new users)
@@ -561,7 +622,7 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 
 - [ ] 8.1 — Add PWA manifest and service worker via `@serwist/next` for home screen installation; requires authoring `app/sw.ts`, `tsconfig.worker.json` (WebWorker lib), and `public/manifest.json` with required fields and icon sizes (192x192, 512x512, maskable variants); budget a half-day
 - [ ] 8.2 — Implement offline graceful degradation: cached list data via TanStack Query `persistQueryClient` (IndexedDB; requires `@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister`, `idb-keyval`), disable write actions with message; do not queue offline writes; budget 2-4 hours including serialization testing
-- [ ] 8.3 — Accessibility pass: contrast ratios, tap target sizes (min 44x44px), aria labels on icon buttons; inline panel keyboard navigation and focus management (Enter to open, Escape to close, focus trap on open, focus return on close, `aria-expanded` on trigger, `role="region"` on panel); verify alt text and aria-live regions added in Phases 3-5 are correct
+- [ ] 8.3 — Accessibility pass: contrast ratios, tap target sizes (min 44x44px), aria labels on icon buttons; modal keyboard navigation and focus management (ESC to close, focus trap on open, focus return on close); verify alt text and aria-live regions added in Phases 3-5 are correct; verify `RollAnnouncer` timing aligns with carousel settle (~3350ms)
 - [ ] 8.4 — Performance tuning: poster lazy loading verification, infinite scroll performance, search debounce, animation on low-end devices; consider virtualized scrolling (`@tanstack/react-virtual`) if grid performance degrades with large lists
 
 ### Phase 9: QA and Cross-Device Testing (June 1-21, 2026) — Post-MVP
@@ -569,14 +630,14 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - [ ] 9.1 — Cross-device testing: iOS Safari, Android Chrome, desktop Chrome/Firefox
 - [ ] 9.2 — Group flow end-to-end: create group, join from second device, add movies, roll, mark watched
 - [ ] 9.3 — Real-time sync test: verify live updates across two active sessions; verify subscription cleanup on navigation
-- [ ] 9.4 — Recovery code test: create account, simulate new device, recover identity; verify rate limiting on claim endpoint; verify code invalidation after use
+- [ ] 9.4 — Recovery code test: create account, simulate new device, recover identity; verify rate limiting on claim endpoint; verify code invalidation after use. NOTE: Task 1.6 browser verification completed 2026-05-06 — both escalation triggers (auth.identities row, GoTrue #2013) cleared. This task is unblocked.
 - [ ] 9.5 — List Admin permissions test: confirm admin-only actions are blocked for regular members via RLS
-- [ ] 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 shake-to-arm arms on first click; confirm inline successor picker appears on second click; confirm transfer+leave runs atomically and router.push("/") fires; confirm list is NOT deleted; confirm "Delete the list" action does NOT show the successor picker
 - [ ] 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.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.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.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 with gold glow, 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 in single-column centered grid; RollSection buttons stack correctly on mobile and sit side-by-side on desktop; cross-list roll plays ListRollCarousel and result appears on the home page (no navigation into a list); new user with no lists sees empty state
+- [ ] 9.11 — Movie info modal test: tapping a poster opens ListMoreInfoModal; modal renders poster, title, added-by, genre tags, TMDB overview (fetched live), Watched It (three-state shake-to-arm), Trailer (opens URL), Delete (shake-to-arm, only when onDelete supplied); ESC and click-outside close; focus trap and restoration work; arming Watched disarms Delete and vice versa; 4s auto-disarm fires; roll-result modal does NOT show Delete button; modal renders correctly above carousel (portal escape works)
 - [ ] 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.13 — Security headers test: verify CSP, HSTS, X-Frame-Options are correctly applied on all routes; verify CSP uses self-hosted URLs (not \*.supabase.co)
 - [ ] 9.14 — RLS test: verify unauthorized access attempts are blocked at the database level (attempt direct Supabase queries outside group membership); verify WITH CHECK prevents added_by spoofing and role escalation
@@ -608,17 +669,17 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - Returning users land on their home page (not the landing page) when a valid Supabase session exists
 - Logged-in home page correctly shows all of the user's lists as cards with accurate movie counts
 - Cross-list roll on the home page draws from all user lists combined and displays the result as a standalone teaser card on the home page without navigating into any list
-- Admin "Leave this list" with other members present triggers the Transfer Ownership popup; original admin is removed after transfer; list is not deleted; direct "Delete the list" action does not trigger the transfer popup
+- Admin "Leave this list" with other members present uses shake-to-arm; second click reveals inline successor picker; transfer+leave runs atomically; original admin is removed after transfer; list is not deleted; direct "Delete the list" action does not trigger the successor picker
 - 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)
 - Monthly metadata refresh job keeps cached TMDB data current (TMDB ToS compliance)
-- 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 result TeaserCard emerges in the carousel center with gold glow, 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
 - 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 is fully operable via keyboard (Enter/Escape, focus management)
-- Delete two-tap confirmation does not trigger on a single tap
-- Watched state toggle (binoculars overlay + button color) updates in real time across all group members' screens
+- Tapping a movie poster opens ListMoreInfoModal; modal shows TMDB overview, Watched It (three-state shake-to-arm), Trailer, and Delete (when applicable); portal renders correctly above carousel
+- ListMoreInfoModal is fully operable via keyboard (ESC to close, focus trap, focus restoration)
+- Delete two-tap confirmation does not trigger on a single tap; arming watched disarms delete and vice versa
+- Watched state toggle (green-circle checkmark overlay + button color) updates in real time across all group members' screens
 - No personal data beyond display name is stored or transmitted
 - TMDB API key is not exposed in any client-side code or network request
 - TMDB attribution is visible on all pages
@@ -634,6 +695,24 @@ Configure HTTP security headers at the Caddy reverse proxy level (not in next.co
 - Sentry error events do not contain user UUIDs
 - Privacy policy contains all required GDPR/CCPA sections
 - User-facing sign-out clears the session cookie and redirects to the landing page; signed-out users cannot access authenticated routes
+- Create-group flow redirects directly to /list/{id} on success; join code visible in list header under list name; no in-form invite-code panel
+
+## 9a. Scope Clarifications — Recovery Flow Refactor (2026-05-03)
+
+Decisions made during the Task 1.6 synthetic-identity-at-generate implementation. Recorded here to prevent re-litigation.
+
+**Decided NOT to do — rationale:**
+
+| Decision                                             | Rationale                                                                                                                                                                                             |
+| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Caddy rate-limit on `/auth/v1/token`                 | Unnecessary — Kong/GoTrue are not externally exposed; existing claim 5/15min per-IP rate limit on `/api/auth/recovery/claim` is sufficient                                                            |
+| Backfill synthetic identity onto existing anon users | Pre-launch only: all users will be dropped (`TRUNCATE auth.users CASCADE` + public tables) before first real user. No backfill needed.                                                                |
+| Recovery-code rotation endpoint                      | Out of scope (deferred). Reload of `/recovery` regenerates the code server-side (server overwrites `users.recovery_code`). A dedicated rotation API is not planned for MVP.                           |
+| Post-claim `is_anonymous` re-anonymization           | Rejected — would re-introduce the runtime GoTrue conversion bug that the synthetic-identity architecture was designed to sidestep. Claimed users remain non-anonymous in GoTrue; this is intentional. |
+
+**Known GoTrue constraint pinned in docker-compose.yml:**
+
+GoTrue v2.170.0 is pinned in docker-compose.yml (comment references `supabase/auth#2013`). This version is required to avoid a residual bug where `signInWithPassword` with a synthetic email may not create an `auth.identities` row correctly. Do not upgrade GoTrue without verifying the #2013 fix is included and running the recovery flow end-to-end.
 
 ## 10. Reference: Emotion-to-Genre Mapping