Pārlūkot izejas kodu

feat: initialize Next.js 15 foundation scaffold with all MVP dependencies

Sets up the complete project skeleton that all parallel feature units will
branch from: Next.js 15 App Router with standalone output, TypeScript strict,
Tailwind CSS v4, TanStack Query, Supabase SSR clients, t3-env validation,
shared types (database + TMDB), constants (emotion-to-genre mapping, image
sizes), husky + lint-staged, ESLint, Prettier, and Vitest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
User 2 mēneši atpakaļ
vecāks
revīzija
a805344837

+ 23 - 0
.env.example

@@ -0,0 +1,23 @@
+# TMDB API (server-side only — never NEXT_PUBLIC_)
+TMDB_API_KEY=your_tmdb_api_key_here
+
+# Supabase (public — browser client)
+NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key_here
+
+# Supabase (server-side — internal Docker network)
+SUPABASE_INTERNAL_URL=http://supabase_kong:8000
+SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
+
+# Master Admin
+MASTER_ADMIN_USERNAME=admin
+MASTER_ADMIN_TOTP_SECRET=your_base32_totp_secret_here
+
+# Session encryption (32+ characters)
+IRON_SESSION_SECRET=this_must_be_at_least_32_characters_long
+
+# Sentry (optional)
+NEXT_PUBLIC_SENTRY_DSN=
+
+# GoTrue (set in docker-compose, not here)
+# GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true

+ 44 - 0
.gitignore

@@ -0,0 +1,44 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files
+.env
+.env.local
+.env.production
+.env.staging
+
+# supabase
+supabase/.temp/
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts

+ 1 - 0
.husky/pre-commit

@@ -0,0 +1 @@
+npx lint-staged

+ 6 - 0
.prettierignore

@@ -0,0 +1,6 @@
+.next
+node_modules
+coverage
+*.min.js
+pnpm-lock.yaml
+package-lock.json

+ 7 - 0
.prettierrc

@@ -0,0 +1,7 @@
+{
+  "semi": true,
+  "singleQuote": false,
+  "tabWidth": 2,
+  "trailingComma": "all",
+  "printWidth": 100
+}

+ 82 - 0
CLAUDE.md

@@ -0,0 +1,82 @@
+# MovieDice
+
+**Keep this file under 100 lines.** Prefer terse descriptions. Don't duplicate what's in PROJECT_SCOPE.md or derivable from code. Stack versions in `research/TECHFILE.md`.
+
+## Development
+
+```bash
+npm install && npm run build && npm run dev
+```
+
+Docker: `docker compose up --build`. Supabase Studio at `localhost:3000` (dev only — restricted to 127.0.0.1 in production).
+
+**Supabase first-run:** Replace ALL default secrets before first `docker compose up` (JWT_SECRET, POSTGRES_PASSWORD, ANON_KEY, SERVICE_ROLE_KEY, DASHBOARD_USERNAME, DASHBOARD_PASSWORD). ANON_KEY + SERVICE_ROLE_KEY must be regenerated from JWT_SECRET as a lockstep set.
+
+## API Routes
+
+```
+TMDB Proxy:  /api/tmdb/search, /api/tmdb/* (server-side only — TMDB_API_KEY never NEXT_PUBLIC_)
+Auth:        Supabase GoTrue (signInAnonymously) — no custom auth routes
+Groups:      /api/groups, /api/groups/join (rate-limited, server-side via service role key)
+Health:      /api/health
+Admin:       /admin (TOTP login, iron-session v8)
+```
+
+All TMDB calls set `include_adult=false` and server-side filter by `adult` field.
+
+## Database (Supabase self-hosted Postgres)
+
+Tables: `users`, `groups`, `group_members`, `movies`, `landing_reel_posters`
+
+- `users.id` = Supabase Auth UID from `signInAnonymously()`. Recovery codes hashed with Argon2id (memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes).
+- `users.last_active_at` — updated on writes, throttled to 1x/24h. 12-month inactivity = auto-delete.
+- `movies.added_by` — FK with `ON DELETE SET NULL`. `trailer_url` validated against allowlist (youtube.com, themoviedb.org, imdb.com).
+- `movies.metadata_refreshed_at` — for monthly TMDB metadata refresh (post-MVP).
+- Invite codes: WORD-WORD format (2,000+ words, 3-8 chars, offensive terms filtered, case-insensitive).
+- `admin_sessions` — iron-session v8 encrypted cookies, no DB table.
+- **RLS on all tables** with `WITH CHECK` clauses. `movies` INSERT enforces `added_by = auth.uid()`. `group_members` UPDATE prevents role escalation. Group join is server-side (service role key).
+- **Migrations:** `supabase migration new` only. No ad-hoc SQL in production.
+
+## Frontend (Next.js App Router)
+
+- `@supabase/ssr` — `createBrowserClient` (browser) / `createServerClient` with `SUPABASE_INTERNAL_URL` (server)
+- TanStack Query with explicit `staleTime`. Offline: `persistQueryClient` + IndexedDB (3 packages: `react-query-persist-client`, `query-async-storage-persister`, `idb-keyval`).
+- TMDB posters: native sized URLs from TMDB CDN (w342 grid, w185 reel, w500 panel). No `next/image` for posters. `loading="lazy"` + meaningful `alt` text on all images. Reel posters `aria-hidden`.
+- Real-time: subscribe on mount, unsubscribe on unmount (one list at a time). Home page counts via polling.
+- Accessibility: `aria-live="polite"` for roll results, filter changes, watched toggles. `prefers-reduced-motion` on both animations.
+
+## Auth
+
+- Users: Supabase Anonymous Sign-In → JWT via GoTrue → cookie-based sessions via `@supabase/ssr`
+- Recovery: 24-char alphanumeric (128-bit entropy), Argon2id hashed, single-use, claim rate-limited (5/15min per IP)
+- Admin: username + TOTP (otplib), iron-session v8 (HttpOnly, Secure, SameSite=Strict, 8h expiry)
+- GoTrue config: `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true`, all other auth methods disabled
+
+## Security
+
+- **RLS** — authorization layer; anon key is public by design. WITH CHECK on INSERT/UPDATE.
+- **CSP** — `img-src image.tmdb.org`, `connect-src 'self' wss://[domain]` (self-hosted, not \*.supabase.co)
+- **HSTS** — in Caddyfile only (not next.config.js). Short max-age during testing, 2yr for production.
+- **Network** — Kong (8000/8443) and Postgres (5432) internal to Docker only. Studio 127.0.0.1 only. Only Caddy exposed.
+- **Env validation** — t3-env (`@t3-oss/env-nextjs`) enforces server/client split at build time
+- **Sentry** — `beforeSend` strips UUID path segments. Never call `Sentry.setUser()`.
+- **Rate limiting** — join endpoint (5-10/15min per IP), recovery claim (5/15min per IP)
+- **Trailer URLs** — domain allowlist (youtube.com, themoviedb.org, imdb.com), `rel="noopener noreferrer"`
+- **Argon2** — Dockerfile builder stage needs `python3 make g++` (or use `@node-rs/argon2` for pre-compiled binaries)
+
+## Docker
+
+docker-compose orchestrates: Next.js app (node:22-slim, standalone, non-root, tini), self-hosted Supabase stack (Postgres, GoTrue, Realtime, PostgREST, Kong, Studio), Caddy (HTTPS, persistent volumes for /data and /config), Node.js cron container (node:22-alpine + node-cron for reel/trailer/metadata refresh), pg_dump backup container (daily, 7-day retention). Log rotation on all containers (max-size: 10m, max-file: 5). Disk encryption recommended on host (LUKS or cloud equivalent).
+
+## Env Vars
+
+```
+TMDB_API_KEY                              # server-side only
+NEXT_PUBLIC_SUPABASE_URL                  # public Supabase URL (browser client)
+NEXT_PUBLIC_SUPABASE_ANON_KEY             # public anon key (browser client)
+SUPABASE_INTERNAL_URL                     # Docker internal Kong URL (server-side)
+SUPABASE_SERVICE_ROLE_KEY                 # server-side admin ops
+MASTER_ADMIN_USERNAME / _TOTP_SECRET      # admin auth
+IRON_SESSION_SECRET                       # 32+ chars
+GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED   # must be true
+```

+ 206 - 144
PROJECT_SCOPE.md

@@ -13,6 +13,7 @@ A public landing page lets visitors try the dice mechanic against the TMDB datab
 **Secondary:** Remote groups (long-distance friends, online communities) who watch movies synchronously or asynchronously and want a shared queue.
 
 **Key traits:**
+
 - Mobile-first users — majority of interaction happens on phones
 - Low tolerance for signup friction — will abandon if auth is annoying
 - Motivated by the social/playful aspect as much as the utility
@@ -27,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 | 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 | 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 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) 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 window) | 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); 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; 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 | 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 | 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. | 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 Supabase pg_cron 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; credentials set via environment variables; session managed via iron-session (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           | 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   |
 
 ### Out of Scope (Future)
 
@@ -67,6 +68,7 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 ## 5. Feature Flows
 
 ### Landing Page (Pre-Login)
+
 ```
 1. Visitor lands on the root URL — no login required
 2. Centered "Movie Dice" header/logo displayed
@@ -74,12 +76,15 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 4. "Roll the Dice" button is visible — tapping it triggers the slot-machine reel animation:
    a. Three side-by-side reels spin through ~20 movie poster images pulled automatically
       from TMDB popular/top-rated endpoints (no manual curation; replaced on each periodic refresh)
+      Reel posters use aria-hidden (decorative during animation)
    b. Reels decelerate and land on a single movie result
       (the final result can be any TMDB movie, not constrained to the reel poster set)
-   c. Result is displayed as a static teaser card showing the movie poster, title, and genres.
-      No link, no tap action.
+   c. Result is displayed as a static teaser card showing the movie poster (with alt text),
+      title, and genres. No link, no tap action.
+   d. Animation is user-triggered only and completes within 5 seconds (WCAG 2.2.2)
 5. "Genre Roll" button visible — accepts comma-separated genres/emotions, no reel animation;
-   result displayed as a static teaser card (poster, title, genres). No link, no tap action.
+   result displayed as a static teaser card (poster with alt text, title, genres).
+   No link, no tap action.
 6. "Login / Get Started" button below the roll buttons
 7. User scrolls down to reveal:
    a. About section — fuller description of how MovieDice works
@@ -91,20 +96,23 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 ```
 
 ### Onboarding (New User)
+
 ```
 1. User taps "Login / Get Started" on landing page
 2. User enters display name, optionally picks an avatar color
 3. Account created via Supabase Anonymous Sign-In (supabase.auth.signInAnonymously());
-   JWT issued and managed by Supabase GoTrue; session persisted automatically
+   JWT issued and managed by Supabase GoTrue; session persisted via @supabase/ssr
+   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)
-   B. Join → enter invite code → validated (rate-limited) → member role assigned → lands on home page
-         (with the joined list card shown)
+   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)
 ```
 
 ### Logged-In Home Page (Returning or Newly Onboarded User)
+
 ```
 1. App checks for valid Supabase Auth session
 2. If valid session found → navigate directly to the home page (skip landing page)
@@ -125,13 +133,14 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 ```
 
 ### Adding a Movie
+
 ```
 1. User taps search bar at top of list view
 2. User types a movie title; TMDB is queried via server-side proxy (/api/tmdb/search)
-   with ~300ms debounce
+   with ~300ms debounce; include_adult=false on all calls
 3. Results appear in two sections:
    - Top: movies already in the group's list (labeled "In Your List")
-   - Below separator: TMDB search results
+   - Below separator: TMDB search results (server-side filtered by adult field)
 4. User taps a TMDB result → movie inserted into DB with poster, genres, title, year,
    trailer URL (fetched from TMDB via server proxy and stored at add-time;
    validated against allowlist: youtube.com, themoviedb.org, imdb.com),
@@ -140,10 +149,12 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 ```
 
 ### Movie Card Grid View
+
 ```
 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")
    - Movie title below the poster
    - Added-by user avatar overlaid top-right corner
    - Binoculars emoji overlaid top-left corner — only when movie is watched
@@ -153,12 +164,13 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 ```
 
 ### Expanded Movie Card (Inline Panel)
+
 ```
 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)
+   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
@@ -178,6 +190,7 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
 ```
 
 ### 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
@@ -188,17 +201,19 @@ MovieDice solves group decision paralysis by combining collaborative curation (e
    - 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.
-5. Tapping Roll again re-rolls from the same eligible pool
-6. Tapping "Genre Roll!" opens a text input
-7. User enters genres and/or emotions (e.g., "action, excited")
-8. App normalizes input, maps emotion keywords to TMDB genre IDs, filters the unwatched pool
-9. Same scatter/eliminate animation plays on filtered results
-10. If no matches found: "No matches — showing full list" shown, roll proceeds unfiltered
-11. On a list page, the result movie is displayed prominently; tapping it opens the inline
+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
 ```
 
 ### List Admin Actions
+
 ```
 1. List Admin opens group settings (gear icon or settings menu)
 2. Available actions:
@@ -229,15 +244,18 @@ List Deletion flow (separate from self-removal):
 ```
 
 ### Master Admin Flow
+
 ```
 1. Master Admin navigates to /admin
 2. Login prompt: username + TOTP authenticator code (no password-only fallback)
 3. Credentials (username and TOTP secret) are set via environment variables — no first-run UI
-4. On successful auth → iron-session issues encrypted HttpOnly cookie (8-hour expiry)
+4. On successful auth → iron-session v8 issues encrypted HttpOnly cookie (8-hour expiry)
    → Master Admin dashboard
 5. Available tools:
    - Search any list by name or ID → view details → delete (with confirmation)
    - Search any user by display name or ID → view details → delete (with confirmation)
+     Deletion must remove both public.users row AND auth.users record
+     (via supabase.auth.admin.deleteUser() using service role key)
 6. Master Admin session is separate from regular user sessions
 7. All /admin routes redirect to login if no valid admin session
 8. TOTP secret rotation requires redeployment (documented operational constraint)
@@ -255,39 +273,41 @@ List Deletion flow (separate from self-removal):
 - **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.
-- **Accessibility**: Sufficient color contrast on poster overlays; tap targets minimum 44x44px; screen reader labels on icon buttons.
-- **TMDB API**: All TMDB calls routed through server-side proxy (`/api/tmdb/*`). Cache recent search results via TanStack Query with explicit `staleTime` configuration.
+- **Accessibility**: Sufficient color contrast on poster overlays; tap targets minimum 44x44px; screen reader labels on icon buttons; meaningful `alt` text on all poster images; `aria-live="polite"` regions for dynamic status messages (roll results, filter changes, watched state toggles, action confirmations).
+- **TMDB API**: All TMDB calls routed through server-side proxy (`/api/tmdb/*`) with `include_adult=false`. Cache recent search results via TanStack Query with explicit `staleTime` configuration.
 
 ## 7. Technical Considerations
 
 ### Tech Stack
 
-| Layer | Choice | Notes |
-|-------|--------|-------|
-| Frontend | Next.js (React, App Router) | PWA support; `output: 'standalone'` for Docker |
-| Styling | Tailwind CSS | Mobile-first, fast iteration |
-| Backend / Database | Supabase (self-hosted) | Postgres + real-time subscriptions + GoTrue auth; full Docker stack |
-| Auth | Supabase Anonymous Sign-In | `signInAnonymously()` — no email, instant account, JWT for RLS |
-| Movie Data | TMDB API | Posters, genres, metadata, trailer URLs; all calls via server-side proxy |
-| State Management | TanStack Query (React Query) | Server state sync, caching with explicit `staleTime`, loading states |
-| Admin Sessions | iron-session | Encrypted HttpOnly cookie for Master Admin TOTP sessions |
-| 2FA (Master Admin) | TOTP via otplib (or equivalent) | Authenticator-app compatible; TOTP secret never exposed client-side |
-| PWA | @serwist/next | App Router compatible, Workbox 7, actively maintained |
-| Image Optimization | sharp (local assets only) | TMDB posters use native sized URLs from TMDB CDN directly (not next/image) |
-| Background Jobs | Supabase pg_cron + Edge Functions | Runs on self-hosted Postgres; replaces Vercel Cron |
-| Reverse Proxy | Caddy | HTTPS termination (required for PWA, wss://, secure cookies) |
-| Linting / Formatting | ESLint + Prettier + TypeScript strict | next/core-web-vitals + next/typescript presets; husky + lint-staged pre-commit |
-| Testing | Vitest (unit) + Playwright (E2E) | Unit tests for pure logic; E2E for critical paths |
-| Env Validation | zod (or t3-env) | Validate all required env vars at startup |
-| Runtime | Node.js 22 LTS | node:22-slim Docker base image |
+| Layer                | Choice                                                  | Notes                                                                                                                               |
+| -------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| Frontend             | Next.js (React, App Router)                             | PWA support; `output: 'standalone'` for Docker                                                                                      |
+| Styling              | Tailwind CSS                                            | Mobile-first, fast iteration                                                                                                        |
+| Backend / Database   | Supabase (self-hosted)                                  | Postgres + real-time subscriptions + GoTrue auth; full Docker stack                                                                 |
+| Auth                 | Supabase Anonymous Sign-In via `@supabase/ssr`          | `signInAnonymously()` — no email, instant account, JWT for RLS; cookie-based session via `createBrowserClient`/`createServerClient` |
+| Movie Data           | TMDB API                                                | Posters, genres, metadata, trailer URLs; all calls via server-side proxy with `include_adult=false`                                 |
+| State Management     | TanStack Query (React Query)                            | Server state sync, caching with explicit `staleTime`, loading states                                                                |
+| Admin Sessions       | iron-session v8                                         | Encrypted HttpOnly cookie for Master Admin TOTP sessions (use v8 README directly — v7 patterns are incompatible)                    |
+| 2FA (Master Admin)   | TOTP via otplib (or equivalent)                         | Authenticator-app compatible; TOTP secret never exposed client-side                                                                 |
+| PWA                  | @serwist/next                                           | App Router compatible, Workbox 7; requires authoring `app/sw.ts` and `tsconfig.worker.json`                                         |
+| Image Optimization   | sharp (local assets only)                               | TMDB posters use native sized URLs from TMDB CDN directly (not next/image)                                                          |
+| Background Jobs      | Node.js cron container (`node:22-alpine` + `node-cron`) | Runs alongside app in docker-compose; writes to Postgres via service role key                                                       |
+| Reverse Proxy        | Caddy                                                   | HTTPS termination (required for PWA, wss://, secure cookies); persistent volume for TLS certificates                                |
+| Linting / Formatting | ESLint + Prettier + TypeScript strict                   | next/core-web-vitals + next/typescript presets; husky + lint-staged pre-commit                                                      |
+| Testing              | Vitest (unit) + Playwright (E2E)                        | Unit tests for pure logic and Client Components only (Vitest cannot render RSC); Playwright runs against Docker production stack    |
+| Env Validation       | t3-env (`@t3-oss/env-nextjs`) with zod                  | Structural enforcement of server/client env var split at build time                                                                 |
+| Runtime              | Node.js 22 LTS                                          | node:22-slim Docker base image                                                                                                      |
 
 ### TMDB Image Strategy
 
 TMDB posters are served directly from TMDB's CDN using native sized URLs — not processed through `next/image` optimization (which would run `sharp` in-container and create CPU/memory pressure in Docker). Use these TMDB size variants:
+
 - `w342` — grid thumbnails (mobile)
 - `w185` — reel animation posters
 - `w500` — expanded inline panel
 - 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)
 - Install `sharp` as an explicit production dependency for local asset optimization
 
@@ -297,32 +317,36 @@ TMDB Terms of Service require visible attribution on every page: the TMDB logo,
 
 ### TMDB API Proxy
 
-All TMDB API calls must be routed through Next.js API Route Handlers (`/api/tmdb/*`). The `TMDB_API_KEY` environment variable must NEVER use the `NEXT_PUBLIC_` prefix — it must remain server-side only. This is a TMDB Terms of Service requirement. The proxy also enables server-side response caching via `Cache-Control` headers.
+All TMDB API calls must be routed through Next.js API Route Handlers (`/api/tmdb/*`). The `TMDB_API_KEY` environment variable must NEVER use the `NEXT_PUBLIC_` prefix — it must remain server-side only. This is a TMDB Terms of Service requirement. The proxy also enables server-side response caching via `Cache-Control` headers. All calls must set `include_adult=false` and server-side filter results by the `adult` field as defense in depth.
 
 ### Data Model
 
 **users**
+
 - id (UUID, primary key — maps to Supabase Auth UID from `signInAnonymously()`)
 - display_name (text, CHECK: 1-30 characters, no HTML angle brackets or control characters, Unicode letters allowed)
 - avatar_color (text, hex)
-- recovery_code (text, hashed with Argon2id; 24 alphanumeric characters / 128-bit entropy; single-use)
-- last_active_at (timestamp — updated on login/activity; used for 12-month retention policy)
+- recovery_code (text, hashed with Argon2id — memory=19456 KiB, iterations=2, parallelism=1, output=32 bytes; 24 alphanumeric characters / 128-bit entropy; single-use)
+- last_active_at (timestamp — updated on write operations, throttled to once per 24 hours per user; used for 12-month retention policy)
 - created_at
 
 **groups**
+
 - id (UUID, primary key)
 - name (text, CHECK: 1-50 characters, no HTML angle brackets or control characters, Unicode letters allowed)
-- invite_code (text, unique, WORD-WORD human-readable format)
+- invite_code (text, unique, WORD-WORD human-readable format; word list: 2,000+ words, 3-8 chars each, offensive/confusing terms filtered, uppercase display, case-insensitive comparison, collision check on generation)
 - created_by (FK → users.id)
 - created_at
 
 **group_members**
+
 - group_id (FK → groups.id)
 - user_id (FK → users.id)
 - role (text: 'admin' | 'member')
 - joined_at
 
 **movies**
+
 - id (UUID, primary key)
 - group_id (FK → groups.id)
 - tmdb_id (integer)
@@ -332,30 +356,34 @@ All TMDB API calls must be routed through Next.js API Route Handlers (`/api/tmdb
 - genres (text[], TMDB genre labels)
 - trailer_url (text, nullable — fetched from TMDB at add-time via server proxy and stored; validated against domain allowlist: youtube.com, themoviedb.org, imdb.com; background job refreshes only null entries on a bi-weekly cadence)
 - trailer_url_refreshed_at (timestamp — tracks when the trailer URL was last fetched, used by the refresh job)
-- added_by (FK → users.id)
+- metadata_refreshed_at (timestamp, nullable — tracks when title/poster/genres/year were last refreshed from TMDB; used by the monthly metadata refresh job post-MVP)
+- added_by (FK → users.id, ON DELETE SET NULL)
 - watched (boolean, default false)
 - watched_at (timestamp, nullable)
 - added_at
 
 **landing_reel_posters**
+
 - id (integer, primary key)
 - tmdb_id (integer)
 - poster_path (text)
 - title (text)
-- refreshed_at (timestamp — set by the periodic reel refresh job via pg_cron)
+- refreshed_at (timestamp — set by the periodic reel refresh job)
 - (Table holds ~20 rows; entire set replaced on each refresh from TMDB popular/top-rated)
 
 **admin_sessions**
-- Managed via iron-session encrypted HttpOnly cookies; no database table required. Session expiry: 8 hours. Cookies set with Secure and SameSite=Strict flags.
+
+- Managed via iron-session v8 encrypted HttpOnly cookies; no database table required. Session expiry: 8 hours. Cookies set with Secure and SameSite=Strict flags.
 
 ### Row Level Security (RLS)
 
 RLS must be enabled on ALL tables with explicit policies — no permissive catch-all. Supabase's anon key is public by design; RLS is the authorization mechanism. Policies use `auth.uid()` from the JWT issued by Supabase Anonymous Sign-In. Define policies alongside the schema in Phase 1.2. Key rules:
+
 - **users**: Users can read/update only their own row
 - **groups**: Readable by members of the group only
-- **group_members**: Readable by members of the same group; insertable via valid invite code flow; deletable by admins or self (leave)
-- **movies**: Full CRUD for members of the owning group only
-- **landing_reel_posters**: Readable by anyone (public); writable only by service role (pg_cron job)
+- **group_members**: Readable by members of the same group; joining is a server-side operation via service role key (not client INSERT); deletable by admins or self (leave); UPDATE must prevent role escalation (member cannot set own role to admin)
+- **movies**: Full CRUD for members of the owning group only; INSERT `WITH CHECK` must enforce `added_by = auth.uid()` (prevent attribution spoofing); UPDATE must prevent changing `added_by`
+- **landing_reel_posters**: Readable by anyone (public); writable only by service role (cron job)
 
 Supabase Realtime also respects RLS — subscriptions are authorized by the same policies.
 
@@ -364,43 +392,56 @@ Supabase Realtime also respects RLS — subscriptions are authorized by the same
 Use `supabase migration new` via the Supabase CLI. All migrations stored in version control. Migrations must be the sole mechanism for schema changes — no ad-hoc SQL in production.
 
 ### Privacy
+
 - No email addresses collected in MVP
 - Display names only — no real identity data
 - Recovery codes hashed with Argon2id before storage
 - Invite codes are the primary access control mechanism for regular users; join endpoint rate-limited
 - Master Admin credentials (username) and TOTP secret are stored as environment variables server-side only; never exposed client-side
 - TMDB data is public — no privacy concern
-- Privacy policy page required: factual description of what data is stored (anonymous UUID, display name, group membership, movie preferences, server logs with IPs), how it is used, and user rights
-- **Data retention**: Inactive accounts (no activity for 12 months) are automatically deleted. Users cannot be notified before deletion due to anonymous auth (no email). This is documented in the privacy policy.
+- **Privacy policy page required** with the following sections: controller identity, lawful basis per processing activity, data inventory with retention periods (anonymous UUID, display name, group membership, movie preferences, server/container logs with IPs), third-party recipients (TMDB API, Sentry), international transfer basis (Sentry — US servers), full user rights with exercise instructions (access, erasure, portability, objection), children's disclaimer (under-13/under-16), cookie/localStorage disclosure, change notification procedure
+- **Data retention**: Inactive accounts (no activity for 12 months) are automatically deleted. Auto-deletion must handle orphaned groups: auto-transfer admin to longest-tenured member; cascade-delete group if last member. `added_by` FK uses `ON DELETE SET NULL` to prevent FK violations. Deletion wrapped in a transaction per user. Account deletion must also call `supabase.auth.admin.deleteUser(userId)` to remove the `auth.users` record. Users cannot be notified before deletion due to anonymous auth (no email). This is documented in the privacy policy.
 - TMDB attribution displayed on all pages per Terms of Service
+- **Sentry data sanitization**: Configure `beforeSend` callback to strip UUID path segments from error events; do not call `Sentry.setUser()` with user identifiers. Disclose Sentry as a third-party processor in the privacy policy.
+- **Container logs**: Supabase containers (Kong, GoTrue, PostgREST, Realtime) produce logs containing IP addresses and JWTs. Docker log rotation configured on all containers (max-size: 10m, max-file: 5) to bound retention to ~30 days.
 
 ### Security Headers
 
-Configure HTTP security headers in `next.config.js` or at the Caddy reverse proxy level:
-- `Content-Security-Policy` — restrict `img-src` to `image.tmdb.org`, `connect-src` to Supabase `wss://`, etc.
+Configure HTTP security headers at the Caddy reverse proxy level (not in next.config.js):
+
+- `Content-Security-Policy` — restrict `img-src` to `image.tmdb.org`, `connect-src` to `'self'` for API calls and `wss://[deployment-domain]` for Supabase Realtime WebSocket (not `*.supabase.co` — self-hosted routes through own domain)
 - `X-Frame-Options: DENY`
 - `X-Content-Type-Options: nosniff`
 - `Referrer-Policy: strict-origin-when-cross-origin`
-- `Strict-Transport-Security` (HSTS)
+- `Strict-Transport-Security` (HSTS) — configured in Caddyfile; start with short `max-age` (86400) during testing, increase to 2-year production value before launch; do not submit to HSTS preload list until confident
 - `Permissions-Policy`
 - Use `Content-Security-Policy-Report-Only` during development to identify violations without blocking
 
 ### Deployment
+
 - **Self-hosted Docker deployment** orchestrated via docker-compose
-- **Next.js app container**: multi-stage Dockerfile with `node:22-slim`, `output: 'standalone'`, non-root user, `tini` for PID 1 signal handling, `.dockerignore`
+- **Next.js app container**: multi-stage Dockerfile with `node:22-slim`, `output: 'standalone'`, non-root user, `tini` for PID 1 signal handling, `.dockerignore`; builder stage must install `python3 make g++` for argon2 native compilation (alternative: `@node-rs/argon2` with pre-compiled NAPI binaries eliminates node-gyp requirement)
 - **Supabase self-hosted**: full Docker stack (Postgres, GoTrue, Realtime, PostgREST, Kong, Studio) using Supabase's official docker-compose configuration adapted for this project
-- **Reverse proxy**: Caddy for HTTPS termination (required for PWA service workers, `wss://` Supabase Realtime, and secure cookies)
+- **Supabase secret replacement (MANDATORY before first deployment)**: ALL default secrets must be replaced before the first `docker compose up`. Defaults are published on GitHub — a default JWT_SECRET allows forging JWTs that bypass all RLS. Replace these as a lockstep set: `JWT_SECRET` → regenerate both `ANON_KEY` and `SERVICE_ROLE_KEY` (they derive from JWT_SECRET); also replace `POSTGRES_PASSWORD`, `DASHBOARD_USERNAME`, `DASHBOARD_PASSWORD`. Consider adding a startup check that refuses to start if default values are detected.
+- **GoTrue configuration**: Set `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` (disabled by default — without it, `signInAnonymously()` returns 400); disable all other auth methods: `GOTRUE_EXTERNAL_EMAIL_ENABLED=false`, `GOTRUE_EXTERNAL_PHONE_ENABLED=false`, all OAuth providers disabled
+- **Network security**: Kong ports (8000, 8443) and Postgres port (5432) must be internal to the Docker network only — no host port mapping. Supabase Studio must not be publicly accessible — restrict to `127.0.0.1:3000` or remove from production docker-compose; access via SSH tunnel only. Only Caddy is exposed to the internet.
+- **Reverse proxy**: Caddy for HTTPS termination (required for PWA service workers, `wss://` Supabase Realtime, and secure cookies); Caddy `/data` and `/config` directories must be mounted as persistent named Docker volumes (certificate loss + Let's Encrypt rate limits = up to 1 week downtime); use Let's Encrypt staging endpoint for initial testing
 - **Health check**: `/api/health` endpoint checking Supabase connectivity; used by Docker `HEALTHCHECK`
-- **Background jobs**: Supabase pg_cron triggering Edge Functions (local Deno runtime in Docker) for landing reel refresh and trailer URL refresh
+- **Background jobs**: Node.js cron container (`node:22-alpine` + `node-cron`) running alongside the app in docker-compose; connects to Postgres via `SUPABASE_SERVICE_ROLE_KEY`; handles landing reel refresh and trailer URL refresh
+- **Database backups**: `pg_dump` backup container in docker-compose running daily with 7-day retention; document restore procedure; test restore before launch
+- **Docker log rotation**: All containers must configure Docker logging with `max-size: 10m, max-file: 5` to prevent disk exhaustion and bound GDPR log retention
+- **Disk encryption recommendation**: Enable full-disk encryption on the Docker host (LUKS or cloud provider equivalent) to protect Postgres volume data at rest
 - Environment variables required:
   - `TMDB_API_KEY` (server-side only — never `NEXT_PUBLIC_`)
-  - `SUPABASE_URL`
-  - `SUPABASE_ANON_KEY`
-  - `SUPABASE_SERVICE_ROLE_KEY` (for server-side admin operations)
+  - `NEXT_PUBLIC_SUPABASE_URL` — the public Supabase URL (browser client uses this)
+  - `NEXT_PUBLIC_SUPABASE_ANON_KEY` — the public anon key (browser client uses this; public by design in Supabase's security model)
+  - `SUPABASE_INTERNAL_URL` — Docker internal Kong URL (e.g., `http://supabase_kong:8000`; server-side Next.js code uses this to avoid routing through Caddy)
+  - `SUPABASE_SERVICE_ROLE_KEY` (for server-side admin operations; never exposed client-side)
   - `MASTER_ADMIN_USERNAME` — the master admin login username
   - `MASTER_ADMIN_TOTP_SECRET` — the TOTP secret (base32); configure this in your authenticator app (e.g., Google Authenticator, Authy) before first use
   - `IRON_SESSION_SECRET` — 32+ character secret for iron-session cookie encryption
-- All environment variables validated at startup via zod (or t3-env); missing or malformed variables produce a clear startup failure
+  - `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` — required for anonymous sign-in
+- All environment variables validated at startup via t3-env (`@t3-oss/env-nextjs`) with zod; server vars in `server` block, public vars in `client` block; missing or malformed variables produce a clear build/startup failure
 - No first-run setup UI exists; the master admin account is fully configured via environment variables before deployment
 - **CI**: Linting, type-checking, and build validation enforced via husky pre-push hook (no external CI platform required)
 
@@ -412,37 +453,41 @@ Configure HTTP security headers in `next.config.js` or at the Caddy reverse prox
 ---
 
 ### Phase 1: Foundation (April 6-10, 2026) — MVP
-- [ ] 1.1 — Initialize Next.js project with Tailwind CSS, App Router, and `output: 'standalone'` in `next.config.ts`; configure TypeScript strict mode, ESLint (`next/core-web-vitals` + `next/typescript`), Prettier; set up husky + lint-staged (pre-commit: lint/format, pre-push: lint + typecheck + build); install sharp as production dependency; add Vitest for unit testing
-- [ ] 1.2 — Set up self-hosted Supabase Docker stack; create schema (users, groups, group_members, movies, landing_reel_posters tables) with CHECK constraints on display_name and group name; define and enable RLS policies on all tables (see RLS section); initialize Supabase CLI migrations workflow — all schema changes via `supabase migration new`
-- [ ] 1.3 — Configure Supabase client in Next.js with environment variables; implement env var validation at startup via zod (or t3-env); create TMDB API proxy route (`/api/tmdb/*`) to keep API key server-side
-- [ ] 1.4 — Implement anonymous auth via `supabase.auth.signInAnonymously()`; display name input and optional avatar color picker; session managed by Supabase GoTrue (JWT issued automatically)
-- [ ] 1.5 — Implement recovery code generation (24 alphanumeric characters, 128-bit entropy), Argon2id hashing, and show-once display screen
+
+- [ ] 1.1 — Initialize Next.js project with Tailwind CSS, App Router, and `output: 'standalone'` in `next.config.ts`; configure TypeScript strict mode, ESLint (`next/core-web-vitals` + `next/typescript`), Prettier; set up husky + lint-staged (pre-commit: lint/format, pre-push: lint + typecheck + build); install sharp as production dependency; add Vitest for unit testing (scope: pure logic and Client Components only — Vitest cannot render RSC)
+- [ ] 1.2 — Set up self-hosted Supabase Docker stack; replace ALL default secrets before first `docker compose up` (JWT_SECRET → regenerate ANON_KEY + SERVICE_ROLE_KEY together; replace POSTGRES_PASSWORD, DASHBOARD_USERNAME, DASHBOARD_PASSWORD); configure GoTrue: set `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true`, disable email/phone/OAuth auth methods; create schema (users, groups, group_members, movies, landing_reel_posters tables) with CHECK constraints on display_name and group name; define and enable RLS policies on all tables with `WITH CHECK` clauses (see RLS section); initialize Supabase CLI migrations workflow — all schema changes via `supabase migration new`
+- [ ] 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
 - [ ] 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
-- [ ] 1.7 — Build Docker infrastructure: multi-stage Dockerfile (node:22-slim, non-root user, tini), docker-compose.yml orchestrating Next.js app + self-hosted Supabase stack + Caddy reverse proxy, .dockerignore, /api/health endpoint; deploy and confirm Supabase connection works in production
-- [ ] 1.8 — Add Sentry error monitoring (free tier) for production error tracking from day one
+- [ ] 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
 
 ### Phase 2: Groups and Permissions (April 10-14, 2026) — MVP
-- [ ] 2.1 — Build "Create a Group" flow: name input (validated: 1-50 chars), invite code generation (WORD-WORD format), store in DB, assign creator as List Admin; document API routes in markdown
-- [ ] 2.2 — Build "Join with a Code" flow: code entry, validation, group_members record with role: 'member'; rate-limit join endpoint (5-10 failed attempts per IP per window); document API routes in markdown
+
+- [ ] 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
 
 ### Phase 3: Movie List Core (April 14-20, 2026) — MVP
-- [ ] 3.1 — Integrate TMDB API via server-side proxy (`/api/tmdb/*`): search endpoint, 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 expanded), 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"`, title below, added-by avatar overlaid top-right, binoculars emoji overlaid top-left (watched only), no action buttons on cards, tap-only interaction
+- [ ] 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
 - [ ] 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 → 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
-- [ ] 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
+- [ ] 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
 - [ ] 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
+- [ ] 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
@@ -451,64 +496,74 @@ Configure HTTP security headers in `next.config.js` or at the Caddy reverse prox
 - [ ] 4.8 — Apply roll animation to genre-filtered results; document API routes in markdown
 
 ### Phase 5: Landing Page and MVP Polish (April 23-26, 2026) — MVP CUTOFF
-- [ ] 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
-- [ ] 5.2 — Build and seed landing_reel_posters table: implement periodic refresh job via Supabase pg_cron + Edge Function (local Deno runtime) on bi-weekly cadence that automatically fetches ~20 posters from TMDB popular or top-rated endpoints (no manual curation) and replaces the full set in the DB on each run
-- [ ] 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), decelerate, and land on a single TMDB movie result (the final result is fetched live from TMDB via server proxy — not constrained to the reel poster set); respect `prefers-reduced-motion` (instant reveal or simple fade-in when enabled)
-- [ ] 5.4 — Wire landing page Roll the Dice and Genre Roll to TMDB via server proxy (no login required); display result as a static teaser card showing poster, title, and genres — no link, no tap action
+
+- [ ] 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)
+- [ ] 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
+- [ ] 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)
+- [ ] 5.4 — Wire landing page Roll the Dice and Genre Roll to TMDB via server proxy (no login required); display result as a static teaser card showing poster (with alt text), title, and genres — no link, no tap action
 - [ ] 5.5 — Loading and empty states for all major views (empty list, no search results, no genre matches, empty home page for new users)
 - [ ] 5.6 — Error handling: invalid invite code, TMDB API failure, network errors
-- [ ] 5.7 — Configure HTTP security headers (CSP, X-Frame-Options, HSTS, etc.) in next.config.js or Caddy; use Report-Only mode during testing
+- [ ] 5.7 — Configure HTTP security headers in Caddyfile: CSP (with self-hosted URLs, not \*.supabase.co), X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS (start with max-age=86400, increase before launch), Permissions-Policy; use Report-Only mode during testing
 - [ ] 5.8 — Final MVP smoke test and Docker production deployment
 
 ---
 
-### Phase 6: Trailer URL Refresh (April 27 - May 3, 2026) — Post-MVP
-- [ ] 6.1 — Implement background trailer URL refresh job: query movies where trailer_url IS NULL (bi-weekly cadence); fetch trailer URL from TMDB via server proxy for each; validate against domain allowlist; update trailer_url and trailer_url_refreshed_at in DB. NOTE: this scope (null-only) should be reassessed post-launch to also refresh stale/aging URLs after a certain age threshold.
-- [ ] 6.2 — Set up job scheduling via Supabase pg_cron + Edge Function (local Deno runtime); confirm job runs reliably in production
+### Phase 6: Trailer URL and Metadata Refresh (April 27 - May 3, 2026) — Post-MVP
+
+- [ ] 6.1 — Implement background trailer URL refresh job in Node.js cron container: query movies where trailer_url IS NULL (bi-weekly cadence); fetch trailer URL from TMDB via server proxy for each; validate against domain allowlist; update trailer_url and trailer_url_refreshed_at in DB. NOTE: this scope (null-only) should be reassessed post-launch to also refresh stale/aging URLs after a certain age threshold.
+- [ ] 6.2 — Configure Node.js cron container scheduling; confirm jobs run reliably in production
 - [ ] 6.3 — Add monitoring/logging for refresh failures (e.g., TMDB returned no trailer) so missing URLs can be investigated
+- [ ] 6.4 — Implement monthly TMDB metadata refresh job: query movies where metadata_refreshed_at is older than 30 days (or NULL); refresh title, poster_path, genres, year from TMDB; update metadata_refreshed_at. Ensures TMDB ToS compliance for cached data freshness.
 
 ### Phase 7: Master Admin (May 4-17, 2026) — Post-MVP
+
 - [ ] 7.1 — Document Master Admin setup: MASTER_ADMIN_USERNAME and MASTER_ADMIN_TOTP_SECRET must be set as environment variables before first use; provide instructions for generating and enrolling the TOTP secret in an authenticator app; document that TOTP secret rotation requires redeployment
 - [ ] 7.2 — Build /admin login route: username + TOTP code input, no password-only fallback; reads credentials from environment variables
-- [ ] 7.3 — Implement server-side TOTP verification (otplib); issue admin session via iron-session (encrypted HttpOnly cookie, Secure, SameSite=Strict, 8-hour expiry)
+- [ ] 7.3 — Implement server-side TOTP verification (otplib); issue admin session via iron-session v8 (encrypted HttpOnly cookie, Secure, SameSite=Strict, 8-hour expiry); use v8 README directly — v7 patterns are incompatible
 - [ ] 7.4 — Build Master Admin dashboard UI
 - [ ] 7.5 — Implement list search and deletion (by name or ID, with confirmation)
-- [ ] 7.6 — Implement user search and deletion (by display name or ID, with confirmation)
+- [ ] 7.6 — Implement user search and deletion (by display name or ID, with confirmation); deletion must remove both `public.users` row AND `auth.users` record (via `supabase.auth.admin.deleteUser()` using service role key)
 - [ ] 7.7 — Protect all /admin routes; redirect to login if no valid admin session
 - [ ] 7.8 — End-to-end TOTP test with a real authenticator app (Google Authenticator, Authy)
 
 ### Phase 8: PWA and Accessibility (May 18-31, 2026) — Post-MVP
-- [ ] 8.1 — Add PWA manifest and service worker via `@serwist/next` for home screen installation
-- [ ] 8.2 — Implement offline graceful degradation: cached list data via TanStack Query `persistQueryClient` (IndexedDB), disable write actions with message; do not queue offline writes
-- [ ] 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)
+
+- [ ] 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.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
+
 - [ ] 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.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.7 — Master Admin test: TOTP login, iron-session cookie validation, list/user search and deletion, 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
-- [ ] 9.9 — Landing page test: slot-machine reel animation plays using automatically fetched TMDB posters and lands on a valid movie result; result displays as a static teaser card (poster, title, genres) with no tap/link action; roll buttons work without login; 3-step demo alignment renders correctly on all screen sizes; TMDB attribution footer visible; privacy policy accessible
+- [ ] 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 displays as a static teaser card (poster with alt text, title, genres) with no tap/link action; roll buttons work without login; 3-step demo alignment renders correctly on all screen sizes; TMDB attribution footer visible; privacy policy accessible and contains all required sections
 - [ ] 9.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.12 — Trailer URL refresh test: confirm pg_cron job processes only movies with null trailer_url and updates timestamps correctly; verify movies with existing trailer URLs are not re-processed; verify URL domain validation
-- [ ] 9.13 — Security headers test: verify CSP, HSTS, X-Frame-Options are correctly applied on all routes
-- [ ] 9.14 — RLS test: verify unauthorized access attempts are blocked at the database level (attempt direct Supabase queries outside group membership)
+- [ ] 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
 - [ ] 9.15 — Invite code rate limiting test: verify brute-force protection on join endpoint
-- [ ] 9.16 — Run Playwright E2E tests for critical paths: onboarding flow, add movie + real-time sync, roll the dice
-- [ ] 9.17 — Data retention test: verify inactive account cleanup job correctly identifies and deletes accounts inactive for 12+ months
+- [ ] 9.16 — Run Playwright E2E tests against Docker production stack for critical paths: onboarding flow, add movie + real-time sync, roll the dice
+- [ ] 9.17 — Data retention test: verify inactive account cleanup job correctly identifies and deletes accounts inactive for 12+ months; verify orphan group handling (admin transfer or cascade delete); verify auth.users record removal
+- [ ] 9.18 — Docker security test: verify Kong/Postgres ports not accessible from host; verify Studio not publicly accessible; verify all default Supabase secrets have been replaced; verify Caddy TLS certificates persist across container restart
+- [ ] 9.19 — Backup test: verify pg_dump backup runs daily; test restore procedure
 
 ### Phase 10: Launch (June 22 - July 5, 2026) — Full Feature Complete
+
 - [ ] 10.1 — Final Docker production deployment and full smoke test
 - [ ] 10.2 — Confirm PWA install flows work on iOS and Android
-- [ ] 10.3 — Verify Sentry error monitoring is capturing errors correctly; review and resolve any outstanding issues
+- [ ] 10.3 — Verify Sentry error monitoring is capturing errors correctly (with UUID sanitization); review and resolve any outstanding issues
 - [ ] 10.4 — Soft launch: share with initial test group, gather feedback
 - [ ] 10.5 — Address any launch-blocking bugs found during soft launch
-- [ ] 10.6 — Full feature complete sign-off by July 5, 2026
+- [ ] 10.6 — Promote HSTS max-age to production value (2 years)
+- [ ] 10.7 — Full feature complete sign-off by July 5, 2026
 
 ## 9. Success Criteria
 
@@ -522,9 +577,10 @@ Configure HTTP security headers in `next.config.js` or at the Caddy reverse prox
 - 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
-- Master Admin can log in with TOTP (credentials from environment variables) and delete any list or user; session expires after 8 hours
-- Trailer URL refresh job processes only movies where trailer_url is null (bi-weekly cadence via pg_cron)
-- Landing page slot-machine reel animation uses automatically fetched TMDB posters and lands on a valid movie result displayed as a static teaser card (poster, title, genres) with no tap/link action
+- 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 slot-machine reel animation uses automatically fetched TMDB posters (no adult content) and lands on a valid movie result displayed as a static teaser card (poster with alt text, title, genres) with no tap/link action
 - Landing page roll 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
@@ -534,36 +590,42 @@ Configure HTTP security headers in `next.config.js` or at the Caddy reverse prox
 - 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
-- RLS policies prevent unauthorized data access at the database level
+- RLS policies prevent unauthorized data access at the database level; WITH CHECK prevents column spoofing
 - All animations respect `prefers-reduced-motion` preference
-- Inactive accounts are automatically deleted after 12 months
-- Security headers (CSP, HSTS, X-Frame-Options) are correctly applied
+- Inactive accounts are automatically deleted after 12 months with correct orphan group handling
+- Security headers (CSP with self-hosted URLs, HSTS, X-Frame-Options) are correctly applied
 - Invite code join endpoint is rate-limited against brute force
-- Docker deployment runs with non-root user and health check
+- Docker deployment runs with non-root user, health check, and no exposed internal ports
+- All default Supabase secrets have been replaced before production deployment
+- Supabase Studio is not publicly accessible
+- Database backups run daily with successful restore verification
+- Sentry error events do not contain user UUIDs
+- Privacy policy contains all required GDPR/CCPA sections
 
 ## 10. Reference: Emotion-to-Genre Mapping
 
 Static mapping used by Genre Roll to translate emotion keywords into TMDB genre IDs. Implemented as a static TypeScript constant. Normalize input to lowercase and tokenize on spaces and commas before matching.
 
-| Emotion Keywords | Primary Genres | Secondary Genres |
-|-----------------|----------------|-----------------|
-| happy, cheerful, upbeat, fun | Comedy, Animation, Family | Adventure, Musical |
-| sad, emotional, cry, tearjerker | Drama, Romance | War, Biography |
-| excited, hyped, energetic, pumped | Action, Adventure | Science Fiction, Thriller |
-| scared, tense, nervous, creepy | Horror, Thriller | Mystery |
-| calm, relaxed, chill, cozy | Documentary, Drama | Animation |
-| romantic, lovey, date night | Romance, Comedy | Drama |
-| thoughtful, reflective, deep | Documentary, Drama | History, Biography |
-| funny, silly, goofy, laugh | Comedy, Animation | Family |
-| dark, gritty, intense, serious | Crime, Thriller | Drama, War |
-| nostalgic, classic, retro | (any genre, filtered to release year < 2000) | |
+| Emotion Keywords                  | Primary Genres                               | Secondary Genres          |
+| --------------------------------- | -------------------------------------------- | ------------------------- |
+| happy, cheerful, upbeat, fun      | Comedy, Animation, Family                    | Adventure, Musical        |
+| sad, emotional, cry, tearjerker   | Drama, Romance                               | War, Biography            |
+| excited, hyped, energetic, pumped | Action, Adventure                            | Science Fiction, Thriller |
+| scared, tense, nervous, creepy    | Horror, Thriller                             | Mystery                   |
+| calm, relaxed, chill, cozy        | Documentary, Drama                           | Animation                 |
+| romantic, lovey, date night       | Romance, Comedy                              | Drama                     |
+| thoughtful, reflective, deep      | Documentary, Drama                           | History, Biography        |
+| funny, silly, goofy, laugh        | Comedy, Animation                            | Family                    |
+| dark, gritty, intense, serious    | Crime, Thriller                              | Drama, War                |
+| nostalgic, classic, retro         | (any genre, filtered to release year < 2000) |                           |
 
 Union all matched genre IDs from primary and secondary columns and filter the movie pool. If no tokens match any keyword, notify user with "No matches — showing full list" and proceed with the unfiltered pool.
 
 ## 11. Extra Features
+
 _Features added after initial scope. Complete current Implementation Plan progress before starting these._
 
-| Feature | Description | Added On | Rationale |
-|---------|-------------|----------|-----------|
-| Compact list/grid toggle | Toggle between poster grid and a compact list layout (title, year, metadata per row) | 2026-04-05 | Deferred from MVP — user unfamiliar with it at scoping; low priority relative to core flows |
-| Self-service account deletion | User-facing account deletion flow with cascading deletes, ownership transfer for administered groups, and anonymization of added_by references in movies | 2026-04-06 | GDPR Article 17 (Right to Erasure); MVP relies on Master Admin deletion on request as interim |
+| Feature                       | Description                                                                                                                                                                                                                                                                                                 | Added On   | Rationale                                                                                     |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------- |
+| Compact list/grid toggle      | Toggle between poster grid and a compact list layout (title, year, metadata per row)                                                                                                                                                                                                                        | 2026-04-05 | Deferred from MVP — user unfamiliar with it at scoping; low priority relative to core flows   |
+| Self-service account deletion | User-facing account deletion flow with cascading deletes (wrapped in transaction), ownership transfer for administered groups (auto-transfer to longest-tenured member), anonymization of added_by references (ON DELETE SET NULL), and removal of auth.users record via `supabase.auth.admin.deleteUser()` | 2026-04-06 | GDPR Article 17 (Right to Erasure); MVP relies on Master Admin deletion on request as interim |

+ 18 - 0
eslint.config.mjs

@@ -0,0 +1,18 @@
+import { defineConfig, globalIgnores } from "eslint/config";
+import nextVitals from "eslint-config-next/core-web-vitals";
+import nextTs from "eslint-config-next/typescript";
+
+const eslintConfig = defineConfig([
+  ...nextVitals,
+  ...nextTs,
+  // Override default ignores of eslint-config-next.
+  globalIgnores([
+    // Default ignores of eslint-config-next:
+    ".next/**",
+    "out/**",
+    "build/**",
+    "next-env.d.ts",
+  ]),
+]);
+
+export default eslintConfig;

+ 16 - 0
next.config.ts

@@ -0,0 +1,16 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+  output: "standalone",
+  images: {
+    remotePatterns: [
+      {
+        protocol: "https",
+        hostname: "image.tmdb.org",
+        pathname: "/t/p/**",
+      },
+    ],
+  },
+};
+
+export default nextConfig;

+ 12076 - 0
package-lock.json

@@ -0,0 +1,12076 @@
+{
+  "name": "moviedice",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "moviedice",
+      "version": "0.1.0",
+      "dependencies": {
+        "@node-rs/argon2": "^2.0.2",
+        "@supabase/ssr": "^0.6.1",
+        "@supabase/supabase-js": "^2.49.4",
+        "@t3-oss/env-nextjs": "^0.12.0",
+        "@tanstack/react-query": "^5.75.5",
+        "iron-session": "^8.0.4",
+        "next": "16.2.2",
+        "otplib": "^12.0.1",
+        "react": "19.2.4",
+        "react-dom": "19.2.4",
+        "sharp": "^0.33.5",
+        "zod": "^3.24.4"
+      },
+      "devDependencies": {
+        "@sentry/nextjs": "^9.14.0",
+        "@tailwindcss/postcss": "^4",
+        "@testing-library/react": "^16.3.0",
+        "@types/node": "^20",
+        "@types/react": "^19",
+        "@types/react-dom": "^19",
+        "@vitejs/plugin-react": "^4.5.2",
+        "eslint": "^9",
+        "eslint-config-next": "16.2.2",
+        "husky": "^9.1.7",
+        "jsdom": "^26.1.0",
+        "lint-staged": "^16.1.0",
+        "prettier": "^3.5.3",
+        "tailwindcss": "^4",
+        "typescript": "^5",
+        "vitest": "^3.2.1"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@asamuzakjp/css-color": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+      "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/css-calc": "^2.1.3",
+        "@csstools/css-color-parser": "^3.0.9",
+        "@csstools/css-parser-algorithms": "^3.0.4",
+        "@csstools/css-tokenizer": "^3.0.3",
+        "lru-cache": "^10.4.3"
+      }
+    },
+    "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+      "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+      "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+      "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+      "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@csstools/color-helpers": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+      "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@csstools/css-calc": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+      "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^3.0.5",
+        "@csstools/css-tokenizer": "^3.0.4"
+      }
+    },
+    "node_modules/@csstools/css-color-parser": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+      "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/color-helpers": "^5.1.0",
+        "@csstools/css-calc": "^2.1.4"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^3.0.5",
+        "@csstools/css-tokenizer": "^3.0.4"
+      }
+    },
+    "node_modules/@csstools/css-parser-algorithms": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+      "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@csstools/css-tokenizer": "^3.0.4"
+      }
+    },
+    "node_modules/@csstools/css-tokenizer": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+      "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+      "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.2.1",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+      "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+      "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+      "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+      "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+      "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+      "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+      "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+      "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+      "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+      "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+      "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+      "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+      "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+      "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+      "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+      "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+      "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+      "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+      "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+      "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+      "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+      "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+      "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+      "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+      "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+      "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+      "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+      "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.9.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+      "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+      "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.21.2",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+      "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/object-schema": "^2.1.7",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.5"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+      "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@eslint/config-array/node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+      "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^0.17.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+      "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "3.3.5",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+      "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.14.0",
+        "debug": "^4.3.2",
+        "espree": "^10.0.1",
+        "globals": "^14.0.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.1",
+        "minimatch": "^3.1.5",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+      "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "9.39.4",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+      "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      }
+    },
+    "node_modules/@eslint/object-schema": {
+      "version": "2.1.7",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+      "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+      "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^0.17.0",
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.7",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+      "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.4.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/retry": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@img/colour": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+      "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+      "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.0.4"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+      "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.0.4"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+      "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+      "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+      "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+      "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+      "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-riscv64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+      "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+      "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+      "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+      "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+      "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+      "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.0.5"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+      "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.0.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+      "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-riscv64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+      "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-riscv64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+      "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.0.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+      "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.0.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+      "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+      "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+      "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.2.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+      "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+      "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+      "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "0.2.12",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
+      "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.4.3",
+        "@emnapi/runtime": "^1.4.3",
+        "@tybys/wasm-util": "^0.10.0"
+      }
+    },
+    "node_modules/@next/env": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.2.tgz",
+      "integrity": "sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==",
+      "license": "MIT"
+    },
+    "node_modules/@next/eslint-plugin-next": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.2.tgz",
+      "integrity": "sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-glob": "3.3.1"
+      }
+    },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.2.tgz",
+      "integrity": "sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-darwin-x64": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.2.tgz",
+      "integrity": "sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-gnu": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.2.tgz",
+      "integrity": "sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-musl": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.2.tgz",
+      "integrity": "sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.2.tgz",
+      "integrity": "sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.2.tgz",
+      "integrity": "sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.2.tgz",
+      "integrity": "sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-x64-msvc": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.2.tgz",
+      "integrity": "sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.0.2.tgz",
+      "integrity": "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      },
+      "optionalDependencies": {
+        "@node-rs/argon2-android-arm-eabi": "2.0.2",
+        "@node-rs/argon2-android-arm64": "2.0.2",
+        "@node-rs/argon2-darwin-arm64": "2.0.2",
+        "@node-rs/argon2-darwin-x64": "2.0.2",
+        "@node-rs/argon2-freebsd-x64": "2.0.2",
+        "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2",
+        "@node-rs/argon2-linux-arm64-gnu": "2.0.2",
+        "@node-rs/argon2-linux-arm64-musl": "2.0.2",
+        "@node-rs/argon2-linux-x64-gnu": "2.0.2",
+        "@node-rs/argon2-linux-x64-musl": "2.0.2",
+        "@node-rs/argon2-wasm32-wasi": "2.0.2",
+        "@node-rs/argon2-win32-arm64-msvc": "2.0.2",
+        "@node-rs/argon2-win32-ia32-msvc": "2.0.2",
+        "@node-rs/argon2-win32-x64-msvc": "2.0.2"
+      }
+    },
+    "node_modules/@node-rs/argon2-android-arm-eabi": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz",
+      "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-android-arm64": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz",
+      "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-darwin-arm64": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz",
+      "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-darwin-x64": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz",
+      "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-freebsd-x64": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz",
+      "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-linux-arm-gnueabihf": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz",
+      "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-linux-arm64-gnu": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz",
+      "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-linux-arm64-musl": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz",
+      "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-linux-x64-gnu": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz",
+      "integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-linux-x64-musl": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz",
+      "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-wasm32-wasi": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz",
+      "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^0.2.5"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@node-rs/argon2-win32-arm64-msvc": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz",
+      "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-win32-ia32-msvc": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz",
+      "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@node-rs/argon2-win32-x64-msvc": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz",
+      "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nolyfill/is-core-module": {
+      "version": "1.0.39",
+      "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
+      "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.4.0"
+      }
+    },
+    "node_modules/@opentelemetry/api": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+      "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/@opentelemetry/api-logs": {
+      "version": "0.57.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz",
+      "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/context-async-hooks": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
+      "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/core": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
+      "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation": {
+      "version": "0.57.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz",
+      "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/api-logs": "0.57.2",
+        "@types/shimmer": "^1.2.0",
+        "import-in-the-middle": "^1.8.1",
+        "require-in-the-middle": "^7.1.1",
+        "semver": "^7.5.2",
+        "shimmer": "^1.2.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-amqplib": {
+      "version": "0.46.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz",
+      "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-connect": {
+      "version": "0.43.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.1.tgz",
+      "integrity": "sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/connect": "3.4.38"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-dataloader": {
+      "version": "0.16.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.1.tgz",
+      "integrity": "sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-express": {
+      "version": "0.47.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.1.tgz",
+      "integrity": "sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-fs": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.1.tgz",
+      "integrity": "sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-generic-pool": {
+      "version": "0.43.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.1.tgz",
+      "integrity": "sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-graphql": {
+      "version": "0.47.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.1.tgz",
+      "integrity": "sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-hapi": {
+      "version": "0.45.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.2.tgz",
+      "integrity": "sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-http": {
+      "version": "0.57.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.2.tgz",
+      "integrity": "sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1",
+        "@opentelemetry/instrumentation": "0.57.2",
+        "@opentelemetry/semantic-conventions": "1.28.0",
+        "forwarded-parse": "2.1.2",
+        "semver": "^7.5.2"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-ioredis": {
+      "version": "0.47.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.1.tgz",
+      "integrity": "sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/redis-common": "^0.36.2",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-kafkajs": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.1.tgz",
+      "integrity": "sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-knex": {
+      "version": "0.44.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.1.tgz",
+      "integrity": "sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-koa": {
+      "version": "0.47.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.1.tgz",
+      "integrity": "sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-lru-memoizer": {
+      "version": "0.44.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.1.tgz",
+      "integrity": "sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mongodb": {
+      "version": "0.52.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.52.0.tgz",
+      "integrity": "sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mongoose": {
+      "version": "0.46.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.1.tgz",
+      "integrity": "sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mysql": {
+      "version": "0.45.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.1.tgz",
+      "integrity": "sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/mysql": "2.15.26"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-mysql2": {
+      "version": "0.45.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.2.tgz",
+      "integrity": "sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@opentelemetry/sql-common": "^0.40.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-pg": {
+      "version": "0.51.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.1.tgz",
+      "integrity": "sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.26.0",
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@opentelemetry/sql-common": "^0.40.1",
+        "@types/pg": "8.6.1",
+        "@types/pg-pool": "2.0.6"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-redis-4": {
+      "version": "0.46.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.1.tgz",
+      "integrity": "sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/redis-common": "^0.36.2",
+        "@opentelemetry/semantic-conventions": "^1.27.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-tedious": {
+      "version": "0.18.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.1.tgz",
+      "integrity": "sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.57.1",
+        "@opentelemetry/semantic-conventions": "^1.27.0",
+        "@types/tedious": "^4.0.14"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.3.0"
+      }
+    },
+    "node_modules/@opentelemetry/instrumentation-undici": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.1.tgz",
+      "integrity": "sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.8.0",
+        "@opentelemetry/instrumentation": "^0.57.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.7.0"
+      }
+    },
+    "node_modules/@opentelemetry/redis-common": {
+      "version": "0.36.2",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz",
+      "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/resources": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
+      "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1",
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-base": {
+      "version": "1.30.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz",
+      "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "1.30.1",
+        "@opentelemetry/resources": "1.30.1",
+        "@opentelemetry/semantic-conventions": "1.28.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": ">=1.0.0 <1.10.0"
+      }
+    },
+    "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.28.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
+      "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/semantic-conventions": {
+      "version": "1.40.0",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
+      "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@opentelemetry/sql-common": {
+      "version": "0.40.1",
+      "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz",
+      "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/core": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.1.0"
+      }
+    },
+    "node_modules/@otplib/core": {
+      "version": "12.0.1",
+      "resolved": "https://registry.npmjs.org/@otplib/core/-/core-12.0.1.tgz",
+      "integrity": "sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==",
+      "license": "MIT"
+    },
+    "node_modules/@otplib/plugin-crypto": {
+      "version": "12.0.1",
+      "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz",
+      "integrity": "sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==",
+      "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths",
+      "license": "MIT",
+      "dependencies": {
+        "@otplib/core": "^12.0.1"
+      }
+    },
+    "node_modules/@otplib/plugin-thirty-two": {
+      "version": "12.0.1",
+      "resolved": "https://registry.npmjs.org/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz",
+      "integrity": "sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==",
+      "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths",
+      "license": "MIT",
+      "dependencies": {
+        "@otplib/core": "^12.0.1",
+        "thirty-two": "^1.0.2"
+      }
+    },
+    "node_modules/@otplib/preset-default": {
+      "version": "12.0.1",
+      "resolved": "https://registry.npmjs.org/@otplib/preset-default/-/preset-default-12.0.1.tgz",
+      "integrity": "sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==",
+      "deprecated": "Please upgrade to v13 of otplib. Refer to otplib docs for migration paths",
+      "license": "MIT",
+      "dependencies": {
+        "@otplib/core": "^12.0.1",
+        "@otplib/plugin-crypto": "^12.0.1",
+        "@otplib/plugin-thirty-two": "^12.0.1"
+      }
+    },
+    "node_modules/@otplib/preset-v11": {
+      "version": "12.0.1",
+      "resolved": "https://registry.npmjs.org/@otplib/preset-v11/-/preset-v11-12.0.1.tgz",
+      "integrity": "sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==",
+      "license": "MIT",
+      "dependencies": {
+        "@otplib/core": "^12.0.1",
+        "@otplib/plugin-crypto": "^12.0.1",
+        "@otplib/plugin-thirty-two": "^12.0.1"
+      }
+    },
+    "node_modules/@prisma/instrumentation": {
+      "version": "6.11.1",
+      "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.11.1.tgz",
+      "integrity": "sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.8"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/plugin-commonjs": {
+      "version": "28.0.1",
+      "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz",
+      "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rollup/pluginutils": "^5.0.1",
+        "commondir": "^1.0.1",
+        "estree-walker": "^2.0.2",
+        "fdir": "^6.2.0",
+        "is-reference": "1.2.1",
+        "magic-string": "^0.30.3",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=16.0.0 || 14 >= 14.17"
+      },
+      "peerDependencies": {
+        "rollup": "^2.68.0||^3.0.0||^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "rollup": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@rollup/pluginutils": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+      "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0",
+        "estree-walker": "^2.0.2",
+        "picomatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "rollup": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+      "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+      "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+      "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+      "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+      "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+      "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+      "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+      "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+      "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+      "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+      "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+      "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+      "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+      "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+      "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+      "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+      "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+      "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+      "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+      "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+      "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+      "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+      "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+      "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+      "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rtsao/scc": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+      "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@sentry-internal/browser-utils": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.47.1.tgz",
+      "integrity": "sha512-twv6YhrUlPkvKz4/iQDH4KHgcv9t4cMjmZPf4/dCSCXn4/GOjzjx2d74c1w+1KOdS7lcsQzI+MtbK6SeYLiGfQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry/core": "9.47.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry-internal/feedback": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.47.1.tgz",
+      "integrity": "sha512-xJ4vKvIpAT8e+Sz80YrsNinPU0XV7jPxPjdZ4ex8R2mMvx7pM0gq8JiR/sIVmNiOE0WiUDr6VwLDE8j2APSRMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry/core": "9.47.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry-internal/replay": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.47.1.tgz",
+      "integrity": "sha512-O9ZEfySpstGtX1f73m3NbdbS2utwPikaFt6sgp74RG4ZX4LlXe99VAjKR464xKECpYsLmj2bYpiK4opURF0pBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry-internal/browser-utils": "9.47.1",
+        "@sentry/core": "9.47.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry-internal/replay-canvas": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.47.1.tgz",
+      "integrity": "sha512-r9nve+l5+elGB9NXSN1+PUgJy790tXN1e8lZNH2ziveoU91jW4yYYt34mHZ30fU9tOz58OpaRMj3H3GJ/jYZVA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry-internal/replay": "9.47.1",
+        "@sentry/core": "9.47.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry/babel-plugin-component-annotate": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.6.1.tgz",
+      "integrity": "sha512-zmvUa4RpzDG3LQJFpGCE8lniz8Rk1Wa6ZvvK+yEH+snZeaHHRbSnAQBMR607GOClP+euGHNO2YtaY4UAdNTYbg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@sentry/browser": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.47.1.tgz",
+      "integrity": "sha512-at5JOLziw5QpVYytxTDU6xijdV6lDQ/Rxp/qXJaHXud3gIK4suv2cXW+tupJfwoUoHFCnDNfccjCmPmP0yRqiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry-internal/browser-utils": "9.47.1",
+        "@sentry-internal/feedback": "9.47.1",
+        "@sentry-internal/replay": "9.47.1",
+        "@sentry-internal/replay-canvas": "9.47.1",
+        "@sentry/core": "9.47.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry/bundler-plugin-core": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.6.1.tgz",
+      "integrity": "sha512-/ubWjPwgLep84sUPzHfKL2Ns9mK9aQrEX4aBFztru7ygiJidKJTxYGtvjh4dL2M1aZ0WRQYp+7PF6+VKwdZXcQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.18.5",
+        "@sentry/babel-plugin-component-annotate": "3.6.1",
+        "@sentry/cli": "^2.49.0",
+        "dotenv": "^16.3.1",
+        "find-up": "^5.0.0",
+        "glob": "^9.3.2",
+        "magic-string": "0.30.8",
+        "unplugin": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": {
+      "version": "0.30.8",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
+      "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.4.15"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@sentry/cli": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.5.tgz",
+      "integrity": "sha512-tavJ7yGUZV+z3Ct2/ZB6mg339i08sAk6HDkgqmSRuQEu2iLS5sl9HIvuXfM6xjv8fwlgFOSy++WNABNAcGHUbg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "FSL-1.1-MIT",
+      "dependencies": {
+        "https-proxy-agent": "^5.0.0",
+        "node-fetch": "^2.6.7",
+        "progress": "^2.0.3",
+        "proxy-from-env": "^1.1.0",
+        "which": "^2.0.2"
+      },
+      "bin": {
+        "sentry-cli": "bin/sentry-cli"
+      },
+      "engines": {
+        "node": ">= 10"
+      },
+      "optionalDependencies": {
+        "@sentry/cli-darwin": "2.58.5",
+        "@sentry/cli-linux-arm": "2.58.5",
+        "@sentry/cli-linux-arm64": "2.58.5",
+        "@sentry/cli-linux-i686": "2.58.5",
+        "@sentry/cli-linux-x64": "2.58.5",
+        "@sentry/cli-win32-arm64": "2.58.5",
+        "@sentry/cli-win32-i686": "2.58.5",
+        "@sentry/cli-win32-x64": "2.58.5"
+      }
+    },
+    "node_modules/@sentry/cli-darwin": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.5.tgz",
+      "integrity": "sha512-lYrNzenZFJftfwSya7gwrHGxtE+Kob/e1sr9lmHMFOd4utDlmq0XFDllmdZAMf21fxcPRI1GL28ejZ3bId01fQ==",
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/cli-linux-arm": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.5.tgz",
+      "integrity": "sha512-KtHweSIomYL4WVDrBrYSYJricKAAzxUgX86kc6OnlikbyOhoK6Fy8Vs6vwd52P6dvWPjgrMpUYjW2M5pYXQDUw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "linux",
+        "freebsd",
+        "android"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/cli-linux-arm64": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.5.tgz",
+      "integrity": "sha512-/4gywFeBqRB6tR/iGMRAJ3HRqY6Z7Yp4l8ZCbl0TDLAfHNxu7schEw4tSnm2/Hh9eNMiOVy4z58uzAWlZXAYBQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "linux",
+        "freebsd",
+        "android"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/cli-linux-i686": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.5.tgz",
+      "integrity": "sha512-G7261dkmyxqlMdyvyP06b+RTIVzp1gZNgglj5UksxSouSUqRd/46W/2pQeOMPhloDYo9yLtCN2YFb3Mw4aUsWw==",
+      "cpu": [
+        "x86",
+        "ia32"
+      ],
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "linux",
+        "freebsd",
+        "android"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/cli-linux-x64": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.5.tgz",
+      "integrity": "sha512-rP04494RSmt86xChkQ+ecBNRYSPbyXc4u0IA7R7N1pSLCyO74e5w5Al+LnAq35cMfVbZgz5Sm0iGLjyiUu4I1g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "linux",
+        "freebsd",
+        "android"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/cli-win32-arm64": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.5.tgz",
+      "integrity": "sha512-AOJ2nCXlQL1KBaCzv38m3i2VmSHNurUpm7xVKd6yAHX+ZoVBI8VT0EgvwmtJR2TY2N2hNCC7UrgRmdUsQ152bA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/cli-win32-i686": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.5.tgz",
+      "integrity": "sha512-EsuboLSOnlrN7MMPJ1eFvfMDm+BnzOaSWl8eYhNo8W/BIrmNgpRUdBwnWn9Q2UOjJj5ZopukmsiMYtU/D7ml9g==",
+      "cpu": [
+        "x86",
+        "ia32"
+      ],
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/cli-win32-x64": {
+      "version": "2.58.5",
+      "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.5.tgz",
+      "integrity": "sha512-IZf+XIMiQwj+5NzqbOQfywlOitmCV424Vtf9c+ep61AaVScUFD1TSrQbOcJJv5xGxhlxNOMNgMeZhdexdzrKZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "FSL-1.1-MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@sentry/core": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.47.1.tgz",
+      "integrity": "sha512-KX62+qIt4xgy8eHKHiikfhz2p5fOciXd0Cl+dNzhgPFq8klq4MGMNaf148GB3M/vBqP4nw/eFvRMAayFCgdRQw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry/nextjs": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-9.47.1.tgz",
+      "integrity": "sha512-uUcYbUHIXfmPDfakoXWoZl4u/6IMTzrlinQxlbHLYqIHRuclkqvViq6AMNmIwEYrLjRsNKFvFe32QMAHsce2NQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/semantic-conventions": "^1.34.0",
+        "@rollup/plugin-commonjs": "28.0.1",
+        "@sentry-internal/browser-utils": "9.47.1",
+        "@sentry/core": "9.47.1",
+        "@sentry/node": "9.47.1",
+        "@sentry/opentelemetry": "9.47.1",
+        "@sentry/react": "9.47.1",
+        "@sentry/vercel-edge": "9.47.1",
+        "@sentry/webpack-plugin": "^3.5.0",
+        "chalk": "3.0.0",
+        "resolve": "1.22.8",
+        "rollup": "^4.35.0",
+        "stacktrace-parser": "^0.1.10"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0"
+      }
+    },
+    "node_modules/@sentry/node": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.47.1.tgz",
+      "integrity": "sha512-CDbkasBz3fnWRKSFs6mmaRepM2pa+tbZkrqhPWifFfIkJDidtVW40p6OnquTvPXyPAszCnDZRnZT14xyvNmKPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/context-async-hooks": "^1.30.1",
+        "@opentelemetry/core": "^1.30.1",
+        "@opentelemetry/instrumentation": "^0.57.2",
+        "@opentelemetry/instrumentation-amqplib": "^0.46.1",
+        "@opentelemetry/instrumentation-connect": "0.43.1",
+        "@opentelemetry/instrumentation-dataloader": "0.16.1",
+        "@opentelemetry/instrumentation-express": "0.47.1",
+        "@opentelemetry/instrumentation-fs": "0.19.1",
+        "@opentelemetry/instrumentation-generic-pool": "0.43.1",
+        "@opentelemetry/instrumentation-graphql": "0.47.1",
+        "@opentelemetry/instrumentation-hapi": "0.45.2",
+        "@opentelemetry/instrumentation-http": "0.57.2",
+        "@opentelemetry/instrumentation-ioredis": "0.47.1",
+        "@opentelemetry/instrumentation-kafkajs": "0.7.1",
+        "@opentelemetry/instrumentation-knex": "0.44.1",
+        "@opentelemetry/instrumentation-koa": "0.47.1",
+        "@opentelemetry/instrumentation-lru-memoizer": "0.44.1",
+        "@opentelemetry/instrumentation-mongodb": "0.52.0",
+        "@opentelemetry/instrumentation-mongoose": "0.46.1",
+        "@opentelemetry/instrumentation-mysql": "0.45.1",
+        "@opentelemetry/instrumentation-mysql2": "0.45.2",
+        "@opentelemetry/instrumentation-pg": "0.51.1",
+        "@opentelemetry/instrumentation-redis-4": "0.46.1",
+        "@opentelemetry/instrumentation-tedious": "0.18.1",
+        "@opentelemetry/instrumentation-undici": "0.10.1",
+        "@opentelemetry/resources": "^1.30.1",
+        "@opentelemetry/sdk-trace-base": "^1.30.1",
+        "@opentelemetry/semantic-conventions": "^1.34.0",
+        "@prisma/instrumentation": "6.11.1",
+        "@sentry/core": "9.47.1",
+        "@sentry/node-core": "9.47.1",
+        "@sentry/opentelemetry": "9.47.1",
+        "import-in-the-middle": "^1.14.2",
+        "minimatch": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry/node-core": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.47.1.tgz",
+      "integrity": "sha512-7TEOiCGkyShJ8CKtsri9lbgMCbB+qNts2Xq37itiMPN2m+lIukK3OX//L8DC5nfKYZlgikrefS63/vJtm669hQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry/core": "9.47.1",
+        "@sentry/opentelemetry": "9.47.1",
+        "import-in-the-middle": "^1.14.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0",
+        "@opentelemetry/core": "^1.30.1 || ^2.0.0",
+        "@opentelemetry/instrumentation": ">=0.57.1 <1",
+        "@opentelemetry/resources": "^1.30.1 || ^2.0.0",
+        "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0",
+        "@opentelemetry/semantic-conventions": "^1.34.0"
+      }
+    },
+    "node_modules/@sentry/opentelemetry": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.47.1.tgz",
+      "integrity": "sha512-STtFpjF7lwzeoedDJV+5XA6P89BfmFwFftmHSGSe3UTI8z8IoiR5yB6X2vCjSPvXlfeOs13qCNNCEZyznxM8Xw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry/core": "9.47.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0",
+        "@opentelemetry/core": "^1.30.1 || ^2.0.0",
+        "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0",
+        "@opentelemetry/semantic-conventions": "^1.34.0"
+      }
+    },
+    "node_modules/@sentry/react": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.47.1.tgz",
+      "integrity": "sha512-Anqt0hG1R+nktlwEiDc2FmD+6DUGMJOLuArgr7q1cSCdPbK2Gb1eZ2rF57Ui+CDo9XLvlX9QP2is/M08rrVe3w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry/browser": "9.47.1",
+        "@sentry/core": "9.47.1",
+        "hoist-non-react-statics": "^3.3.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "react": "^16.14.0 || 17.x || 18.x || 19.x"
+      }
+    },
+    "node_modules/@sentry/vercel-edge": {
+      "version": "9.47.1",
+      "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-9.47.1.tgz",
+      "integrity": "sha512-mLdI/wF+toYu2i3VRcGdUn3AeTpPmAemI2Pnu6oomLKBDFnkjhCLnwCd5xuHLESJR1aJkB4M3g2+7DZcGTspXg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@opentelemetry/api": "^1.9.0",
+        "@opentelemetry/resources": "^1.30.1",
+        "@opentelemetry/semantic-conventions": "^1.34.0",
+        "@sentry/core": "9.47.1",
+        "@sentry/opentelemetry": "9.47.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@sentry/webpack-plugin": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.6.1.tgz",
+      "integrity": "sha512-F2yqwbdxfCENMN5u4ih4WfOtGjW56/92DBC0bU6un7Ns/l2qd+wRONIvrF+58rl/VkCFfMlUtZTVoKGRyMRmHA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sentry/bundler-plugin-core": "3.6.1",
+        "unplugin": "1.0.1",
+        "uuid": "^9.0.0"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "webpack": ">=4.40.0"
+      }
+    },
+    "node_modules/@supabase/auth-js": {
+      "version": "2.101.1",
+      "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.101.1.tgz",
+      "integrity": "sha512-Kd0Wey+RkFHgyVep7adS6UOE2pN6MJ3mZ32PAXSvfw6IjUkFRC7IQpdZZjUOcUe5pXr1ejufCRgF6lsGINe4Tw==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "2.8.1"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@supabase/functions-js": {
+      "version": "2.101.1",
+      "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.101.1.tgz",
+      "integrity": "sha512-OZWU7YtaG+NNNFZK8p/FuJ6gpq7pFyrG2fLOopP73HAIDHDGpOttPJapvO8ADu3RkqfQfkwrB354vPkSBbZ20A==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "2.8.1"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@supabase/phoenix": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz",
+      "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==",
+      "license": "MIT"
+    },
+    "node_modules/@supabase/postgrest-js": {
+      "version": "2.101.1",
+      "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.101.1.tgz",
+      "integrity": "sha512-UW1RajH5jbZoK+ldAJ1I6VZ+HWwZ2oaKjEQ6Gn+AQ67CHQVxGl8wNQoLYyumbyaExm41I+wn7arulcY1eHeZJw==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "2.8.1"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@supabase/realtime-js": {
+      "version": "2.101.1",
+      "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.101.1.tgz",
+      "integrity": "sha512-Oa6dno0OB9I+hv5do5zsZHbFu41ViZnE9IWjmkeeF/8fPmB5fWoHGqeTYEC3/0DAgtpUoFJa4FpvzFH0SBHo1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@supabase/phoenix": "^0.4.0",
+        "@types/ws": "^8.18.1",
+        "tslib": "2.8.1",
+        "ws": "^8.18.2"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@supabase/ssr": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.6.1.tgz",
+      "integrity": "sha512-QtQgEMvaDzr77Mk3vZ3jWg2/y+D8tExYF7vcJT+wQ8ysuvOeGGjYbZlvj5bHYsj/SpC0bihcisnwPrM4Gp5G4g==",
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "^1.0.1"
+      },
+      "peerDependencies": {
+        "@supabase/supabase-js": "^2.43.4"
+      }
+    },
+    "node_modules/@supabase/storage-js": {
+      "version": "2.101.1",
+      "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.101.1.tgz",
+      "integrity": "sha512-WhTaUOBgeEvnKLy95Cdlp6+D5igSF/65yC727w1olxbet5nzUvMlajKUWyzNtQu2efrz2cQ7FcdVBdQqgT9YKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "iceberg-js": "^0.8.1",
+        "tslib": "2.8.1"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@supabase/supabase-js": {
+      "version": "2.101.1",
+      "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.101.1.tgz",
+      "integrity": "sha512-Jnhm3LfuACwjIzvk2pfUbGQn7pa7hi6MFzfSyPrRYWVCCu69RPLCFyHSBl7HSBwadbQ3UZOznnD3gPca3ePrRA==",
+      "license": "MIT",
+      "dependencies": {
+        "@supabase/auth-js": "2.101.1",
+        "@supabase/functions-js": "2.101.1",
+        "@supabase/postgrest-js": "2.101.1",
+        "@supabase/realtime-js": "2.101.1",
+        "@supabase/storage-js": "2.101.1"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.15",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+      "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.8.0"
+      }
+    },
+    "node_modules/@t3-oss/env-core": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/@t3-oss/env-core/-/env-core-0.12.0.tgz",
+      "integrity": "sha512-lOPj8d9nJJTt81mMuN9GMk8x5veOt7q9m11OSnCBJhwp1QrL/qR+M8Y467ULBSm9SunosryWNbmQQbgoiMgcdw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "typescript": ">=5.0.0",
+        "valibot": "^1.0.0-beta.7 || ^1.0.0",
+        "zod": "^3.24.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "valibot": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@t3-oss/env-nextjs": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/@t3-oss/env-nextjs/-/env-nextjs-0.12.0.tgz",
+      "integrity": "sha512-rFnvYk1049RnNVUPvY8iQ55AuQh1Rr+qZzQBh3t++RttCGK4COpXGNxS4+45afuQq02lu+QAOy/5955aU8hRKw==",
+      "license": "MIT",
+      "dependencies": {
+        "@t3-oss/env-core": "0.12.0"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0",
+        "valibot": "^1.0.0-beta.7 || ^1.0.0",
+        "zod": "^3.24.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "valibot": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@tailwindcss/node": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
+      "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "enhanced-resolve": "^5.19.0",
+        "jiti": "^2.6.1",
+        "lightningcss": "1.32.0",
+        "magic-string": "^0.30.21",
+        "source-map-js": "^1.2.1",
+        "tailwindcss": "4.2.2"
+      }
+    },
+    "node_modules/@tailwindcss/oxide": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
+      "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20"
+      },
+      "optionalDependencies": {
+        "@tailwindcss/oxide-android-arm64": "4.2.2",
+        "@tailwindcss/oxide-darwin-arm64": "4.2.2",
+        "@tailwindcss/oxide-darwin-x64": "4.2.2",
+        "@tailwindcss/oxide-freebsd-x64": "4.2.2",
+        "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
+        "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
+        "@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
+        "@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
+        "@tailwindcss/oxide-linux-x64-musl": "4.2.2",
+        "@tailwindcss/oxide-wasm32-wasi": "4.2.2",
+        "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
+        "@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-android-arm64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
+      "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-arm64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
+      "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-darwin-x64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
+      "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-freebsd-x64": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
+      "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
+      "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
+      "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
+      "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
+      "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
+      "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
+      "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
+      "bundleDependencies": [
+        "@napi-rs/wasm-runtime",
+        "@emnapi/core",
+        "@emnapi/runtime",
+        "@tybys/wasm-util",
+        "@emnapi/wasi-threads",
+        "tslib"
+      ],
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "^1.8.1",
+        "@emnapi/runtime": "^1.8.1",
+        "@emnapi/wasi-threads": "^1.1.0",
+        "@napi-rs/wasm-runtime": "^1.1.1",
+        "@tybys/wasm-util": "^0.10.1",
+        "tslib": "^2.8.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
+      "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
+      "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@tailwindcss/postcss": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz",
+      "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "@tailwindcss/node": "4.2.2",
+        "@tailwindcss/oxide": "4.2.2",
+        "postcss": "^8.5.6",
+        "tailwindcss": "4.2.2"
+      }
+    },
+    "node_modules/@tanstack/query-core": {
+      "version": "5.96.2",
+      "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.2.tgz",
+      "integrity": "sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
+    "node_modules/@tanstack/react-query": {
+      "version": "5.96.2",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.2.tgz",
+      "integrity": "sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tanstack/query-core": "5.96.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": "^18 || ^19"
+      }
+    },
+    "node_modules/@testing-library/react": {
+      "version": "16.3.2",
+      "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
+      "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@testing-library/dom": "^10.0.0",
+        "@types/react": "^18.0.0 || ^19.0.0",
+        "@types/react-dom": "^18.0.0 || ^19.0.0",
+        "react": "^18.0.0 || ^19.0.0",
+        "react-dom": "^18.0.0 || ^19.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.38",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/mysql": {
+      "version": "2.15.26",
+      "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz",
+      "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "20.19.39",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
+      "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/pg": {
+      "version": "8.6.1",
+      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
+      "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "pg-protocol": "*",
+        "pg-types": "^2.2.0"
+      }
+    },
+    "node_modules/@types/pg-pool": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz",
+      "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/pg": "*"
+      }
+    },
+    "node_modules/@types/react": {
+      "version": "19.2.14",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+      "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "19.2.3",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+      "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^19.2.0"
+      }
+    },
+    "node_modules/@types/shimmer": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
+      "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/tedious": {
+      "version": "4.0.14",
+      "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
+      "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/ws": {
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+      "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
+      "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.12.2",
+        "@typescript-eslint/scope-manager": "8.58.0",
+        "@typescript-eslint/type-utils": "8.58.0",
+        "@typescript-eslint/utils": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0",
+        "ignore": "^7.0.5",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^8.58.0",
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz",
+      "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.58.0",
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0",
+        "debug": "^4.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/project-service": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz",
+      "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/tsconfig-utils": "^8.58.0",
+        "@typescript-eslint/types": "^8.58.0",
+        "debug": "^4.4.3"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz",
+      "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/tsconfig-utils": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz",
+      "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz",
+      "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0",
+        "@typescript-eslint/utils": "8.58.0",
+        "debug": "^4.4.3",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
+      "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz",
+      "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/project-service": "8.58.0",
+        "@typescript-eslint/tsconfig-utils": "8.58.0",
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/visitor-keys": "8.58.0",
+        "debug": "^4.4.3",
+        "minimatch": "^10.2.2",
+        "semver": "^7.7.3",
+        "tinyglobby": "^0.2.15",
+        "ts-api-utils": "^2.5.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+      "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz",
+      "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.9.1",
+        "@typescript-eslint/scope-manager": "8.58.0",
+        "@typescript-eslint/types": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz",
+      "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.58.0",
+        "eslint-visitor-keys": "^5.0.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+      "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
+      "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-android-arm64": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
+      "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-darwin-arm64": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
+      "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-darwin-x64": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
+      "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-freebsd-x64": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
+      "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
+      "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
+      "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
+      "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
+      "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
+      "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
+      "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
+      "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
+      "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
+      "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
+      "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
+      "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^0.2.11"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
+      "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
+      "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
+      "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.27",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/@vitest/expect": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+      "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "3.2.4",
+        "@vitest/utils": "3.2.4",
+        "chai": "^5.2.0",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+      "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.2.4",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.17"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+      "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+      "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "3.2.4",
+        "pathe": "^2.0.3",
+        "strip-literal": "^3.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+      "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.2.4",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+      "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyspy": "^4.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+      "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.2.4",
+        "loupe": "^3.1.4",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.16.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-import-attributes": {
+      "version": "1.9.5",
+      "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+      "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^8"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.14.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+      "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
+      "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "environment": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/anymatch/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "node_modules/aria-query": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+      "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/array-buffer-byte-length": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+      "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "is-array-buffer": "^3.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array-includes": {
+      "version": "3.1.9",
+      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+      "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.24.0",
+        "es-object-atoms": "^1.1.1",
+        "get-intrinsic": "^1.3.0",
+        "is-string": "^1.1.1",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.findlast": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+      "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.findlastindex": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+      "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "es-shim-unscopables": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flat": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+      "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flatmap": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+      "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.tosorted": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+      "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.3",
+        "es-errors": "^1.3.0",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/arraybuffer.prototype.slice": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+      "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.1",
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "is-array-buffer": "^3.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/ast-types-flow": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+      "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/async-function": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+      "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/available-typed-arrays": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "possible-typed-array-names": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/axe-core": {
+      "version": "4.11.2",
+      "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.2.tgz",
+      "integrity": "sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==",
+      "dev": true,
+      "license": "MPL-2.0",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/axobject-query": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+      "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.15",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz",
+      "integrity": "sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+      "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.12",
+        "caniuse-lite": "^1.0.30001782",
+        "electron-to-chromium": "^1.5.328",
+        "node-releases": "^2.0.36",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/cac": {
+      "version": "6.7.14",
+      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+      "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/call-bind": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001785",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz",
+      "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chai": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+      "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "assertion-error": "^2.0.1",
+        "check-error": "^2.1.1",
+        "deep-eql": "^5.0.1",
+        "loupe": "^3.1.0",
+        "pathval": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+      "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/check-error": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+      "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/cjs-module-lexer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+      "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cli-cursor": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-truncate": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
+      "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "slice-ansi": "^8.0.0",
+        "string-width": "^8.2.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/client-only": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+      "license": "MIT"
+    },
+    "node_modules/color": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+      "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1",
+        "color-string": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=12.5.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/color-string": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+      "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "^1.0.0",
+        "simple-swizzle": "^0.2.2"
+      }
+    },
+    "node_modules/colorette": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/commander": {
+      "version": "14.0.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+      "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssstyle": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+      "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/css-color": "^3.2.0",
+        "rrweb-cssom": "^0.8.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/damerau-levenshtein": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+      "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/data-urls": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+      "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-mimetype": "^4.0.0",
+        "whatwg-url": "^14.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/data-view-buffer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+      "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/data-view-byte-length": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+      "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/inspect-js"
+      }
+    },
+    "node_modules/data-view-byte-offset": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+      "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decimal.js": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+      "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/deep-eql": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+      "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.331",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
+      "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.20.1",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
+      "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.3.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/environment": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+      "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/es-abstract": {
+      "version": "1.24.1",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz",
+      "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.2",
+        "arraybuffer.prototype.slice": "^1.0.4",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "data-view-buffer": "^1.0.2",
+        "data-view-byte-length": "^1.0.2",
+        "data-view-byte-offset": "^1.0.1",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "es-set-tostringtag": "^2.1.0",
+        "es-to-primitive": "^1.3.0",
+        "function.prototype.name": "^1.1.8",
+        "get-intrinsic": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "get-symbol-description": "^1.1.0",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "internal-slot": "^1.1.0",
+        "is-array-buffer": "^3.0.5",
+        "is-callable": "^1.2.7",
+        "is-data-view": "^1.0.2",
+        "is-negative-zero": "^2.0.3",
+        "is-regex": "^1.2.1",
+        "is-set": "^2.0.3",
+        "is-shared-array-buffer": "^1.0.4",
+        "is-string": "^1.1.1",
+        "is-typed-array": "^1.1.15",
+        "is-weakref": "^1.1.1",
+        "math-intrinsics": "^1.1.0",
+        "object-inspect": "^1.13.4",
+        "object-keys": "^1.1.1",
+        "object.assign": "^4.1.7",
+        "own-keys": "^1.0.1",
+        "regexp.prototype.flags": "^1.5.4",
+        "safe-array-concat": "^1.1.3",
+        "safe-push-apply": "^1.0.0",
+        "safe-regex-test": "^1.1.0",
+        "set-proto": "^1.0.0",
+        "stop-iteration-iterator": "^1.1.0",
+        "string.prototype.trim": "^1.2.10",
+        "string.prototype.trimend": "^1.0.9",
+        "string.prototype.trimstart": "^1.0.8",
+        "typed-array-buffer": "^1.0.3",
+        "typed-array-byte-length": "^1.0.3",
+        "typed-array-byte-offset": "^1.0.4",
+        "typed-array-length": "^1.0.7",
+        "unbox-primitive": "^1.1.0",
+        "which-typed-array": "^1.1.19"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-iterator-helpers": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz",
+      "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.24.1",
+        "es-errors": "^1.3.0",
+        "es-set-tostringtag": "^2.1.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.3.0",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "internal-slot": "^1.1.0",
+        "iterator.prototype": "^1.1.5",
+        "math-intrinsics": "^1.1.0",
+        "safe-array-concat": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+      "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-shim-unscopables": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+      "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-to-primitive": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+      "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-callable": "^1.2.7",
+        "is-date-object": "^1.0.5",
+        "is-symbol": "^1.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+      "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.27.7",
+        "@esbuild/android-arm": "0.27.7",
+        "@esbuild/android-arm64": "0.27.7",
+        "@esbuild/android-x64": "0.27.7",
+        "@esbuild/darwin-arm64": "0.27.7",
+        "@esbuild/darwin-x64": "0.27.7",
+        "@esbuild/freebsd-arm64": "0.27.7",
+        "@esbuild/freebsd-x64": "0.27.7",
+        "@esbuild/linux-arm": "0.27.7",
+        "@esbuild/linux-arm64": "0.27.7",
+        "@esbuild/linux-ia32": "0.27.7",
+        "@esbuild/linux-loong64": "0.27.7",
+        "@esbuild/linux-mips64el": "0.27.7",
+        "@esbuild/linux-ppc64": "0.27.7",
+        "@esbuild/linux-riscv64": "0.27.7",
+        "@esbuild/linux-s390x": "0.27.7",
+        "@esbuild/linux-x64": "0.27.7",
+        "@esbuild/netbsd-arm64": "0.27.7",
+        "@esbuild/netbsd-x64": "0.27.7",
+        "@esbuild/openbsd-arm64": "0.27.7",
+        "@esbuild/openbsd-x64": "0.27.7",
+        "@esbuild/openharmony-arm64": "0.27.7",
+        "@esbuild/sunos-x64": "0.27.7",
+        "@esbuild/win32-arm64": "0.27.7",
+        "@esbuild/win32-ia32": "0.27.7",
+        "@esbuild/win32-x64": "0.27.7"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.39.4",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+      "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.8.0",
+        "@eslint-community/regexpp": "^4.12.1",
+        "@eslint/config-array": "^0.21.2",
+        "@eslint/config-helpers": "^0.4.2",
+        "@eslint/core": "^0.17.0",
+        "@eslint/eslintrc": "^3.3.5",
+        "@eslint/js": "9.39.4",
+        "@eslint/plugin-kit": "^0.4.1",
+        "@humanfs/node": "^0.16.6",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.4.2",
+        "@types/estree": "^1.0.6",
+        "ajv": "^6.14.0",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.4.0",
+        "eslint-visitor-keys": "^4.2.1",
+        "espree": "^10.4.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.5",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-config-next": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.2.tgz",
+      "integrity": "sha512-6VlvEhwoug2JpVgjZDhyXrJXUEuPY++TddzIpTaIRvlvlXXFgvQUtm3+Zr84IjFm0lXtJt73w19JA08tOaZVwg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@next/eslint-plugin-next": "16.2.2",
+        "eslint-import-resolver-node": "^0.3.6",
+        "eslint-import-resolver-typescript": "^3.5.2",
+        "eslint-plugin-import": "^2.32.0",
+        "eslint-plugin-jsx-a11y": "^6.10.0",
+        "eslint-plugin-react": "^7.37.0",
+        "eslint-plugin-react-hooks": "^7.0.0",
+        "globals": "16.4.0",
+        "typescript-eslint": "^8.46.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=9.0.0",
+        "typescript": ">=3.3.1"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-config-next/node_modules/globals": {
+      "version": "16.4.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
+      "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint-import-resolver-node": {
+      "version": "0.3.10",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz",
+      "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^3.2.7",
+        "is-core-module": "^2.16.1",
+        "resolve": "^2.0.0-next.6"
+      }
+    },
+    "node_modules/eslint-import-resolver-node/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-import-resolver-node/node_modules/resolve": {
+      "version": "2.0.0-next.6",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
+      "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "is-core-module": "^2.16.1",
+        "node-exports-info": "^1.6.0",
+        "object-keys": "^1.1.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/eslint-import-resolver-typescript": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
+      "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@nolyfill/is-core-module": "1.0.39",
+        "debug": "^4.4.0",
+        "get-tsconfig": "^4.10.0",
+        "is-bun-module": "^2.0.0",
+        "stable-hash": "^0.0.5",
+        "tinyglobby": "^0.2.13",
+        "unrs-resolver": "^1.6.2"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint-import-resolver-typescript"
+      },
+      "peerDependencies": {
+        "eslint": "*",
+        "eslint-plugin-import": "*",
+        "eslint-plugin-import-x": "*"
+      },
+      "peerDependenciesMeta": {
+        "eslint-plugin-import": {
+          "optional": true
+        },
+        "eslint-plugin-import-x": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-module-utils": {
+      "version": "2.12.1",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
+      "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^3.2.7"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-module-utils/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import": {
+      "version": "2.32.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+      "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rtsao/scc": "^1.1.0",
+        "array-includes": "^3.1.9",
+        "array.prototype.findlastindex": "^1.2.6",
+        "array.prototype.flat": "^1.3.3",
+        "array.prototype.flatmap": "^1.3.3",
+        "debug": "^3.2.7",
+        "doctrine": "^2.1.0",
+        "eslint-import-resolver-node": "^0.3.9",
+        "eslint-module-utils": "^2.12.1",
+        "hasown": "^2.0.2",
+        "is-core-module": "^2.16.1",
+        "is-glob": "^4.0.3",
+        "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.8",
+        "object.groupby": "^1.0.3",
+        "object.values": "^1.2.1",
+        "semver": "^6.3.1",
+        "string.prototype.trimend": "^1.0.9",
+        "tsconfig-paths": "^3.15.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+      "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y": {
+      "version": "6.10.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz",
+      "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "aria-query": "^5.3.2",
+        "array-includes": "^3.1.8",
+        "array.prototype.flatmap": "^1.3.2",
+        "ast-types-flow": "^0.0.8",
+        "axe-core": "^4.10.0",
+        "axobject-query": "^4.1.0",
+        "damerau-levenshtein": "^1.0.8",
+        "emoji-regex": "^9.2.2",
+        "hasown": "^2.0.2",
+        "jsx-ast-utils": "^3.3.5",
+        "language-tags": "^1.0.9",
+        "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.8",
+        "safe-regex-test": "^1.0.3",
+        "string.prototype.includes": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependencies": {
+        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+      "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/eslint-plugin-react": {
+      "version": "7.37.5",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+      "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-includes": "^3.1.8",
+        "array.prototype.findlast": "^1.2.5",
+        "array.prototype.flatmap": "^1.3.3",
+        "array.prototype.tosorted": "^1.1.4",
+        "doctrine": "^2.1.0",
+        "es-iterator-helpers": "^1.2.1",
+        "estraverse": "^5.3.0",
+        "hasown": "^2.0.2",
+        "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+        "minimatch": "^3.1.2",
+        "object.entries": "^1.1.9",
+        "object.fromentries": "^2.0.8",
+        "object.values": "^1.2.1",
+        "prop-types": "^15.8.1",
+        "resolve": "^2.0.0-next.5",
+        "semver": "^6.3.1",
+        "string.prototype.matchall": "^4.0.12",
+        "string.prototype.repeat": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+      }
+    },
+    "node_modules/eslint-plugin-react-hooks": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+      "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.24.4",
+        "@babel/parser": "^7.24.4",
+        "hermes-parser": "^0.25.1",
+        "zod": "^3.25.0 || ^4.0.0",
+        "zod-validation-error": "^3.5.0 || ^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+      }
+    },
+    "node_modules/eslint-plugin-react/node_modules/brace-expansion": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+      "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint-plugin-react/node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/eslint-plugin-react/node_modules/resolve": {
+      "version": "2.0.0-next.6",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
+      "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "is-core-module": "^2.16.1",
+        "node-exports-info": "^1.6.0",
+        "object-keys": "^1.1.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/eslint-plugin-react/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/brace-expansion": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+      "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/eslint/node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/espree": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.15.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.2.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+      "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+      "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/expect-type": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+      "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+      "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fastq": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+      "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+      "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/for-each": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+      "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/forwarded-parse": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
+      "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/function.prototype.name": {
+      "version": "1.1.8",
+      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+      "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "functions-have-names": "^1.2.3",
+        "hasown": "^2.0.2",
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/generator-function": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+      "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
+      "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/get-symbol-description": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+      "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-tsconfig": {
+      "version": "4.13.7",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
+      "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
+    "node_modules/glob": {
+      "version": "9.3.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
+      "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==",
+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "minimatch": "^8.0.2",
+        "minipass": "^4.2.4",
+        "path-scurry": "^1.6.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "8.0.7",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz",
+      "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/globals": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/globalthis": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+      "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/has-bigints": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+      "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+      "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hermes-estree": {
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+      "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/hermes-parser": {
+      "version": "0.25.1",
+      "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+      "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hermes-estree": "0.25.1"
+      }
+    },
+    "node_modules/hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "react-is": "^16.7.0"
+      }
+    },
+    "node_modules/html-encoding-sniffer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+      "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-encoding": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/http-proxy-agent/node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/husky": {
+      "version": "9.1.7",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
+      "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "husky": "bin.js"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/typicode"
+      }
+    },
+    "node_modules/iceberg-js": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+      "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/import-in-the-middle": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz",
+      "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "acorn": "^8.14.0",
+        "acorn-import-attributes": "^1.9.5",
+        "cjs-module-lexer": "^1.2.2",
+        "module-details-from-path": "^1.0.3"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/internal-slot": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+      "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "hasown": "^2.0.2",
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/iron-session": {
+      "version": "8.0.4",
+      "resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.4.tgz",
+      "integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==",
+      "funding": [
+        "https://github.com/sponsors/vvo",
+        "https://github.com/sponsors/brc-dd"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "cookie": "^0.7.2",
+        "iron-webcrypto": "^1.2.1",
+        "uncrypto": "^0.1.3"
+      }
+    },
+    "node_modules/iron-session/node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/iron-webcrypto": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
+      "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/brc-dd"
+      }
+    },
+    "node_modules/is-array-buffer": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+      "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+      "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+      "license": "MIT"
+    },
+    "node_modules/is-async-function": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+      "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "async-function": "^1.0.0",
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-bigint": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+      "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-bigints": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-boolean-object": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+      "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-bun-module": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
+      "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^7.7.1"
+      }
+    },
+    "node_modules/is-callable": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-data-view": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+      "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "is-typed-array": "^1.1.13"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-date-object": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+      "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-finalizationregistry": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+      "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+      "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.3.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-generator-function": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+      "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.4",
+        "generator-function": "^2.0.0",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-map": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+      "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-negative-zero": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+      "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-number-object": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+      "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-potential-custom-element-name": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-reference": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+      "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "*"
+      }
+    },
+    "node_modules/is-regex": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+      "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-set": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+      "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-shared-array-buffer": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+      "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-string": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+      "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-symbol": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+      "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-typed-array": {
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+      "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakmap": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+      "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakref": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+      "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakset": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+      "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+      "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/iterator.prototype": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+      "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.6",
+        "get-proto": "^1.0.0",
+        "has-symbols": "^1.1.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "2.6.1",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+      "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "lib/jiti-cli.mjs"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+      "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/jsdom": {
+      "version": "26.1.0",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+      "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssstyle": "^4.2.1",
+        "data-urls": "^5.0.0",
+        "decimal.js": "^10.5.0",
+        "html-encoding-sniffer": "^4.0.0",
+        "http-proxy-agent": "^7.0.2",
+        "https-proxy-agent": "^7.0.6",
+        "is-potential-custom-element-name": "^1.0.1",
+        "nwsapi": "^2.2.16",
+        "parse5": "^7.2.1",
+        "rrweb-cssom": "^0.8.0",
+        "saxes": "^6.0.0",
+        "symbol-tree": "^3.2.4",
+        "tough-cookie": "^5.1.1",
+        "w3c-xmlserializer": "^5.0.0",
+        "webidl-conversions": "^7.0.0",
+        "whatwg-encoding": "^3.1.1",
+        "whatwg-mimetype": "^4.0.0",
+        "whatwg-url": "^14.1.1",
+        "ws": "^8.18.0",
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "canvas": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "canvas": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jsdom/node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/jsdom/node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsx-ast-utils": {
+      "version": "3.3.5",
+      "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+      "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-includes": "^3.1.6",
+        "array.prototype.flat": "^1.3.1",
+        "object.assign": "^4.1.4",
+        "object.values": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/language-subtag-registry": {
+      "version": "0.3.23",
+      "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+      "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/language-tags": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+      "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "language-subtag-registry": "^0.3.20"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+      "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "dev": true,
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+      "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+      "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+      "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+      "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+      "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+      "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+      "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+      "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+      "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+      "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+      "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lint-staged": {
+      "version": "16.4.0",
+      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz",
+      "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "commander": "^14.0.3",
+        "listr2": "^9.0.5",
+        "picomatch": "^4.0.3",
+        "string-argv": "^0.3.2",
+        "tinyexec": "^1.0.4",
+        "yaml": "^2.8.2"
+      },
+      "bin": {
+        "lint-staged": "bin/lint-staged.js"
+      },
+      "engines": {
+        "node": ">=20.17"
+      },
+      "funding": {
+        "url": "https://opencollective.com/lint-staged"
+      }
+    },
+    "node_modules/listr2": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
+      "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cli-truncate": "^5.0.0",
+        "colorette": "^2.0.20",
+        "eventemitter3": "^5.0.1",
+        "log-update": "^6.1.0",
+        "rfdc": "^1.4.1",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/log-update": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+      "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^7.0.0",
+        "cli-cursor": "^5.0.0",
+        "slice-ansi": "^7.1.0",
+        "strip-ansi": "^7.1.0",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-update/node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/log-update/node_modules/slice-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+      "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "is-fullwidth-code-point": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/loupe": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+      "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.9",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+      "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "4.2.8",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz",
+      "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/module-details-from-path": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
+      "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/napi-postinstall": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
+      "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "napi-postinstall": "lib/cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/napi-postinstall"
+      }
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/next": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/next/-/next-16.2.2.tgz",
+      "integrity": "sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==",
+      "license": "MIT",
+      "dependencies": {
+        "@next/env": "16.2.2",
+        "@swc/helpers": "0.5.15",
+        "baseline-browser-mapping": "^2.9.19",
+        "caniuse-lite": "^1.0.30001579",
+        "postcss": "8.4.31",
+        "styled-jsx": "5.1.6"
+      },
+      "bin": {
+        "next": "dist/bin/next"
+      },
+      "engines": {
+        "node": ">=20.9.0"
+      },
+      "optionalDependencies": {
+        "@next/swc-darwin-arm64": "16.2.2",
+        "@next/swc-darwin-x64": "16.2.2",
+        "@next/swc-linux-arm64-gnu": "16.2.2",
+        "@next/swc-linux-arm64-musl": "16.2.2",
+        "@next/swc-linux-x64-gnu": "16.2.2",
+        "@next/swc-linux-x64-musl": "16.2.2",
+        "@next/swc-win32-arm64-msvc": "16.2.2",
+        "@next/swc-win32-x64-msvc": "16.2.2",
+        "sharp": "^0.34.5"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.1.0",
+        "@playwright/test": "^1.51.1",
+        "babel-plugin-react-compiler": "*",
+        "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "sass": "^1.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@playwright/test": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+      "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+      "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+      "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+      "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+      "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+      "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+      "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+      "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+      "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+      "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+      "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+      "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+      "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+      "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+      "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+      "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-wasm32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+      "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.7.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+      "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+      "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/next/node_modules/postcss": {
+      "version": "8.4.31",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/next/node_modules/sharp": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+      "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "@img/colour": "^1.0.0",
+        "detect-libc": "^2.1.2",
+        "semver": "^7.7.3"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.5",
+        "@img/sharp-darwin-x64": "0.34.5",
+        "@img/sharp-libvips-darwin-arm64": "1.2.4",
+        "@img/sharp-libvips-darwin-x64": "1.2.4",
+        "@img/sharp-libvips-linux-arm": "1.2.4",
+        "@img/sharp-libvips-linux-arm64": "1.2.4",
+        "@img/sharp-libvips-linux-ppc64": "1.2.4",
+        "@img/sharp-libvips-linux-riscv64": "1.2.4",
+        "@img/sharp-libvips-linux-s390x": "1.2.4",
+        "@img/sharp-libvips-linux-x64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+        "@img/sharp-linux-arm": "0.34.5",
+        "@img/sharp-linux-arm64": "0.34.5",
+        "@img/sharp-linux-ppc64": "0.34.5",
+        "@img/sharp-linux-riscv64": "0.34.5",
+        "@img/sharp-linux-s390x": "0.34.5",
+        "@img/sharp-linux-x64": "0.34.5",
+        "@img/sharp-linuxmusl-arm64": "0.34.5",
+        "@img/sharp-linuxmusl-x64": "0.34.5",
+        "@img/sharp-wasm32": "0.34.5",
+        "@img/sharp-win32-arm64": "0.34.5",
+        "@img/sharp-win32-ia32": "0.34.5",
+        "@img/sharp-win32-x64": "0.34.5"
+      }
+    },
+    "node_modules/node-exports-info": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
+      "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array.prototype.flatmap": "^1.3.3",
+        "es-errors": "^1.3.0",
+        "object.entries": "^1.1.9",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/node-exports-info/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-fetch/node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-fetch/node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/node-fetch/node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.37",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+      "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/nwsapi": {
+      "version": "2.2.23",
+      "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+      "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.assign": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+      "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0",
+        "has-symbols": "^1.1.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.entries": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+      "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.fromentries": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+      "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.groupby": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+      "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.values": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+      "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-function": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/otplib": {
+      "version": "12.0.1",
+      "resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
+      "integrity": "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==",
+      "license": "MIT",
+      "dependencies": {
+        "@otplib/core": "^12.0.1",
+        "@otplib/preset-default": "^12.0.1",
+        "@otplib/preset-v11": "^12.0.1"
+      }
+    },
+    "node_modules/own-keys": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+      "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-intrinsic": "^1.2.6",
+        "object-keys": "^1.1.1",
+        "safe-push-apply": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+      "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^6.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/path-scurry/node_modules/minipass": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+      "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pathval": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+      "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16"
+      }
+    },
+    "node_modules/pg-int8": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+      "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pg-types": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pg-int8": "1.0.1",
+        "postgres-array": "~2.0.0",
+        "postgres-bytea": "~1.0.0",
+        "postgres-date": "~1.0.4",
+        "postgres-interval": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/possible-typed-array-names": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+      "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.8",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+      "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postgres-array": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postgres-bytea": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+      "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-date": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-interval": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.8.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+      "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/react": {
+      "version": "19.2.4",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+      "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.2.4",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+      "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "scheduler": "^0.27.0"
+      },
+      "peerDependencies": {
+        "react": "^19.2.4"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/readdirp/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/reflect.getprototypeof": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+      "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.1",
+        "which-builtin-type": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/regexp.prototype.flags": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+      "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/require-in-the-middle": {
+      "version": "7.5.2",
+      "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
+      "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.5",
+        "module-details-from-path": "^1.0.3",
+        "resolve": "^1.22.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.8",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+      "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.1",
+        "@rollup/rollup-android-arm64": "4.60.1",
+        "@rollup/rollup-darwin-arm64": "4.60.1",
+        "@rollup/rollup-darwin-x64": "4.60.1",
+        "@rollup/rollup-freebsd-arm64": "4.60.1",
+        "@rollup/rollup-freebsd-x64": "4.60.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+        "@rollup/rollup-linux-arm64-musl": "4.60.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+        "@rollup/rollup-linux-loong64-musl": "4.60.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+        "@rollup/rollup-linux-x64-gnu": "4.60.1",
+        "@rollup/rollup-linux-x64-musl": "4.60.1",
+        "@rollup/rollup-openbsd-x64": "4.60.1",
+        "@rollup/rollup-openharmony-arm64": "4.60.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+        "@rollup/rollup-win32-x64-gnu": "4.60.1",
+        "@rollup/rollup-win32-x64-msvc": "4.60.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/rrweb-cssom": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+      "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-array-concat": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+      "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "has-symbols": "^1.1.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">=0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-push-apply": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+      "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-regex-test": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+      "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-regex": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/saxes": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "xmlchars": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=v12.22.7"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+      "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-function-name": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-proto": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+      "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.33.5",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+      "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "color": "^4.2.3",
+        "detect-libc": "^2.0.3",
+        "semver": "^7.6.3"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.33.5",
+        "@img/sharp-darwin-x64": "0.33.5",
+        "@img/sharp-libvips-darwin-arm64": "1.0.4",
+        "@img/sharp-libvips-darwin-x64": "1.0.4",
+        "@img/sharp-libvips-linux-arm": "1.0.5",
+        "@img/sharp-libvips-linux-arm64": "1.0.4",
+        "@img/sharp-libvips-linux-s390x": "1.0.4",
+        "@img/sharp-libvips-linux-x64": "1.0.4",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+        "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+        "@img/sharp-linux-arm": "0.33.5",
+        "@img/sharp-linux-arm64": "0.33.5",
+        "@img/sharp-linux-s390x": "0.33.5",
+        "@img/sharp-linux-x64": "0.33.5",
+        "@img/sharp-linuxmusl-arm64": "0.33.5",
+        "@img/sharp-linuxmusl-x64": "0.33.5",
+        "@img/sharp-wasm32": "0.33.5",
+        "@img/sharp-win32-ia32": "0.33.5",
+        "@img/sharp-win32-x64": "0.33.5"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shimmer": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
+      "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/simple-swizzle": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+      "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.3.1"
+      }
+    },
+    "node_modules/slice-ansi": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
+      "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.3",
+        "is-fullwidth-code-point": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/slice-ansi/node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/stable-hash": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
+      "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/stacktrace-parser": {
+      "version": "0.1.11",
+      "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
+      "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.7.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/std-env": {
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+      "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/stop-iteration-iterator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+      "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "internal-slot": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/string-argv": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+      "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6.19"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
+      "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.5.0",
+        "strip-ansi": "^7.1.2"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string.prototype.includes": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
+      "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/string.prototype.matchall": {
+      "version": "4.0.12",
+      "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+      "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.6",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.6",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "internal-slot": "^1.1.0",
+        "regexp.prototype.flags": "^1.5.3",
+        "set-function-name": "^2.0.2",
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.repeat": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+      "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.5"
+      }
+    },
+    "node_modules/string.prototype.trim": {
+      "version": "1.2.10",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+      "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-data-property": "^1.1.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-object-atoms": "^1.0.0",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.trimend": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+      "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.trimstart": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+      "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-literal": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+      "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^9.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/strip-literal/node_modules/js-tokens": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+      "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/styled-jsx": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+      "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+      "license": "MIT",
+      "dependencies": {
+        "client-only": "0.0.1"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        },
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/symbol-tree": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tailwindcss": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
+      "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tapable": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
+      "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/thirty-two": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
+      "integrity": "sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==",
+      "engines": {
+        "node": ">=0.2.6"
+      }
+    },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
+      "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinypool": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+      "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      }
+    },
+    "node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tinyspy": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+      "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tldts": {
+      "version": "6.1.86",
+      "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+      "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tldts-core": "^6.1.86"
+      },
+      "bin": {
+        "tldts": "bin/cli.js"
+      }
+    },
+    "node_modules/tldts-core": {
+      "version": "6.1.86",
+      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+      "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/tough-cookie": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+      "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tldts": "^6.1.32"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+      "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+      "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
+    "node_modules/tsconfig-paths": {
+      "version": "3.15.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+      "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "node_modules/tsconfig-paths/node_modules/json5": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
+      "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/typed-array-buffer": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/typed-array-byte-length": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+      "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typed-array-byte-offset": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+      "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.15",
+        "reflect.getprototypeof": "^1.0.9"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typed-array-length": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+      "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "is-typed-array": "^1.1.13",
+        "possible-typed-array-names": "^1.0.0",
+        "reflect.getprototypeof": "^1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/typescript-eslint": {
+      "version": "8.58.0",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz",
+      "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "8.58.0",
+        "@typescript-eslint/parser": "8.58.0",
+        "@typescript-eslint/typescript-estree": "8.58.0",
+        "@typescript-eslint/utils": "8.58.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+        "typescript": ">=4.8.4 <6.1.0"
+      }
+    },
+    "node_modules/unbox-primitive": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+      "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-bigints": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "which-boxed-primitive": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/uncrypto": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
+      "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "license": "MIT"
+    },
+    "node_modules/unplugin": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz",
+      "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.8.1",
+        "chokidar": "^3.5.3",
+        "webpack-sources": "^3.2.3",
+        "webpack-virtual-modules": "^0.5.0"
+      }
+    },
+    "node_modules/unrs-resolver": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
+      "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "napi-postinstall": "^0.3.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/unrs-resolver"
+      },
+      "optionalDependencies": {
+        "@unrs/resolver-binding-android-arm-eabi": "1.11.1",
+        "@unrs/resolver-binding-android-arm64": "1.11.1",
+        "@unrs/resolver-binding-darwin-arm64": "1.11.1",
+        "@unrs/resolver-binding-darwin-x64": "1.11.1",
+        "@unrs/resolver-binding-freebsd-x64": "1.11.1",
+        "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
+        "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
+        "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
+        "@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
+        "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
+        "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
+        "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
+        "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
+        "@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
+        "@unrs/resolver-binding-linux-x64-musl": "1.11.1",
+        "@unrs/resolver-binding-wasm32-wasi": "1.11.1",
+        "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
+        "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
+        "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+      "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.27.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-node": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+      "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cac": "^6.7.14",
+        "debug": "^4.4.1",
+        "es-module-lexer": "^1.7.0",
+        "pathe": "^2.0.3",
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+      },
+      "bin": {
+        "vite-node": "vite-node.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vitest": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+      "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/chai": "^5.2.2",
+        "@vitest/expect": "3.2.4",
+        "@vitest/mocker": "3.2.4",
+        "@vitest/pretty-format": "^3.2.4",
+        "@vitest/runner": "3.2.4",
+        "@vitest/snapshot": "3.2.4",
+        "@vitest/spy": "3.2.4",
+        "@vitest/utils": "3.2.4",
+        "chai": "^5.2.0",
+        "debug": "^4.4.1",
+        "expect-type": "^1.2.1",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.2",
+        "std-env": "^3.9.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.2",
+        "tinyglobby": "^0.2.14",
+        "tinypool": "^1.1.1",
+        "tinyrainbow": "^2.0.0",
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+        "vite-node": "3.2.4",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@types/debug": "^4.1.12",
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "@vitest/browser": "3.2.4",
+        "@vitest/ui": "3.2.4",
+        "happy-dom": "*",
+        "jsdom": "*"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@types/debug": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitest/node_modules/tinyexec": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+      "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/w3c-xmlserializer": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+      "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+      "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/webpack-sources": {
+      "version": "3.3.4",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz",
+      "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
+      "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/whatwg-encoding": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+      "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+      "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "iconv-lite": "0.6.3"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/whatwg-mimetype": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+      "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "14.2.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+      "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "^5.1.0",
+        "webidl-conversions": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which-boxed-primitive": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+      "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-bigint": "^1.1.0",
+        "is-boolean-object": "^1.2.1",
+        "is-number-object": "^1.1.1",
+        "is-string": "^1.1.1",
+        "is-symbol": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-builtin-type": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+      "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "function.prototype.name": "^1.1.6",
+        "has-tostringtag": "^1.0.2",
+        "is-async-function": "^2.0.0",
+        "is-date-object": "^1.1.0",
+        "is-finalizationregistry": "^1.1.0",
+        "is-generator-function": "^1.0.10",
+        "is-regex": "^1.2.1",
+        "is-weakref": "^1.0.2",
+        "isarray": "^2.0.5",
+        "which-boxed-primitive": "^1.1.0",
+        "which-collection": "^1.0.2",
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-collection": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+      "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-map": "^2.0.3",
+        "is-set": "^2.0.3",
+        "is-weakmap": "^2.0.2",
+        "is-weakset": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/which-typed-array": {
+      "version": "1.1.20",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
+      "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "for-each": "^0.3.5",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+      "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "string-width": "^7.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/emoji-regex": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+      "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+      "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xml-name-validator": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/xmlchars": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yaml": {
+      "version": "2.8.3",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+      "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/eemeli"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zod": {
+      "version": "3.25.76",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+      "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zod-validation-error": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+      "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "peerDependencies": {
+        "zod": "^3.25.0 || ^4.0.0"
+      }
+    }
+  }
+}

+ 58 - 0
package.json

@@ -0,0 +1,58 @@
+{
+  "name": "moviedice",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev",
+    "build": "next build",
+    "start": "next start",
+    "lint": "eslint",
+    "typecheck": "tsc --noEmit",
+    "format": "prettier --write .",
+    "format:check": "prettier --check .",
+    "test": "vitest run",
+    "test:watch": "vitest",
+    "prepare": "husky"
+  },
+  "dependencies": {
+    "@node-rs/argon2": "^2.0.2",
+    "@supabase/ssr": "^0.6.1",
+    "@supabase/supabase-js": "^2.49.4",
+    "@t3-oss/env-nextjs": "^0.12.0",
+    "@tanstack/react-query": "^5.75.5",
+    "iron-session": "^8.0.4",
+    "next": "16.2.2",
+    "otplib": "^12.0.1",
+    "react": "19.2.4",
+    "react-dom": "19.2.4",
+    "sharp": "^0.33.5",
+    "zod": "^3.24.4"
+  },
+  "devDependencies": {
+    "@sentry/nextjs": "^9.14.0",
+    "@tailwindcss/postcss": "^4",
+    "@testing-library/react": "^16.3.0",
+    "@types/node": "^20",
+    "@types/react": "^19",
+    "@types/react-dom": "^19",
+    "@vitejs/plugin-react": "^4.5.2",
+    "eslint": "^9",
+    "eslint-config-next": "16.2.2",
+    "husky": "^9.1.7",
+    "jsdom": "^26.1.0",
+    "lint-staged": "^16.1.0",
+    "prettier": "^3.5.3",
+    "tailwindcss": "^4",
+    "typescript": "^5",
+    "vitest": "^3.2.1"
+  },
+  "lint-staged": {
+    "*.{ts,tsx}": [
+      "eslint --fix",
+      "prettier --write"
+    ],
+    "*.{json,md,css}": [
+      "prettier --write"
+    ]
+  }
+}

+ 7 - 0
postcss.config.mjs

@@ -0,0 +1,7 @@
+const config = {
+  plugins: {
+    "@tailwindcss/postcss": {},
+  },
+};
+
+export default config;

+ 1 - 0
public/file.svg

@@ -0,0 +1 @@
+<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

+ 1 - 0
public/globe.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

+ 1 - 0
public/next.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

+ 1 - 0
public/vercel.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

+ 1 - 0
public/window.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

+ 177 - 0
research/COMPLIANCE-RESEARCH.md

@@ -0,0 +1,177 @@
+# Compliance Research Cache
+
+## TMDB API Terms of Use
+
+_Researched: 2026-04-05 (from existing knowledge, web search unavailable)_
+
+Key requirements from TMDB API Terms of Use:
+
+1. **Attribution Required**: Must display "This product uses the TMDB API but is not endorsed or certified by TMDB" or equivalent. The TMDB logo must be displayed with a link to tmdb.org.
+2. **No Data Caching Beyond 6 Months**: TMDB data cached locally should be refreshed. Long-term storage of TMDB metadata is permitted but images must be served from TMDB's CDN (image.tmdb.org).
+3. **Image Serving**: Poster images MUST be served from TMDB's image CDN. You cannot download and host TMDB images on your own servers.
+4. **Rate Limits**: Approximately 40 requests per 10 seconds per API key (free tier). No hard monthly cap but abuse triggers throttling.
+5. **No Commercial Use Without Permission**: Free tier is for personal/non-commercial projects. Commercial use requires separate agreement.
+6. **Data Freshness**: Apps should periodically refresh stored data to keep it current.
+7. **API Key Security**: API keys must not be exposed in client-side code. Use server-side proxying.
+
+## GDPR and Anonymous Users
+
+_Researched: 2026-04-05 (from existing knowledge, web search unavailable)_
+
+Key considerations:
+
+1. **UUIDs as Personal Data**: Under GDPR, any unique identifier that can single out an individual is personal data, including UUIDs stored in local storage.
+2. **Local Storage/Cookies**: Even without traditional cookies, local storage containing user identifiers triggers GDPR obligations.
+3. **IP Logging**: Server logs that capture IP addresses are personal data under GDPR.
+4. **Lawful Basis**: Even for anonymous-style auth, you need a lawful basis (likely legitimate interest or consent).
+5. **Right to Deletion**: Users must be able to delete their account and all associated data.
+6. **Privacy Policy Required**: Even minimal data collection requires a privacy policy.
+7. **Data Minimization**: Only collect what is necessary.
+8. **Cookie/Storage Consent**: Depending on jurisdiction (ePrivacy Directive in EU), storing identifiers in local storage may require consent banners.
+
+## WCAG 2.1 AA Requirements
+
+_Researched: 2026-04-05 (from existing knowledge, web search unavailable)_
+
+Key requirements for a PWA like MovieDice:
+
+1. **Color Contrast**: Minimum 4.5:1 for normal text, 3:1 for large text (18pt+ or 14pt+ bold).
+2. **Touch Targets**: Minimum 44x44 CSS pixels (mentioned in scope).
+3. **Focus Management**: All interactive elements must be keyboard-focusable with visible focus indicators.
+4. **Screen Reader Support**: aria-labels on icon buttons, semantic HTML, alt text on images.
+5. **Motion/Animation**: Respect `prefers-reduced-motion` media query; provide option to disable animations.
+6. **Text Resize**: Content must be readable at 200% zoom.
+7. **Form Labels**: All inputs must have associated labels.
+8. **Error Identification**: Form errors must be clearly identified and described.
+9. **Heading Hierarchy**: Proper heading structure (h1-h6).
+10. **Link Purpose**: Link text must describe the destination.
+
+## PWA Standards
+
+_Researched: 2026-04-05 (from existing knowledge, web search unavailable)_
+
+1. **Web App Manifest**: Required fields: name, short_name, icons (multiple sizes), start_url, display, theme_color, background_color.
+2. **Service Worker**: Required for installability. Must handle fetch events.
+3. **HTTPS**: Required for service workers and PWA installation.
+4. **Offline Support**: Service worker should cache critical assets for offline use.
+5. **Icons**: Multiple sizes required (192x192, 512x512 minimum). Maskable icons recommended.
+6. **Splash Screen**: Configured via manifest properties.
+
+## Docker + Next.js Best Practices
+
+_Researched: 2026-04-05 (from existing knowledge, web search unavailable)_
+
+1. **Multi-stage builds**: Use separate build and runtime stages to minimize image size.
+2. **Non-root user**: Run the application as a non-root user in the container.
+3. **Next.js standalone output**: Use `output: 'standalone'` in next.config.js for optimized Docker images.
+4. **Health checks**: Include HEALTHCHECK instruction in Dockerfile.
+5. **Environment variables**: Use ARG for build-time, ENV for runtime. Never bake secrets into images.
+6. **Signal handling**: Use `dumb-init` or `tini` for proper signal forwarding (PID 1 problem).
+7. **Layer caching**: Copy package.json and install dependencies before copying source code.
+8. **Security scanning**: Scan images for vulnerabilities regularly.
+9. **.dockerignore**: Exclude node_modules, .git, .env files from build context.
+
+## TMDB Terms of Service — Deep Dive
+
+_Researched: 2026-04-05 (second pass, from existing knowledge)_
+
+Additional requirements beyond basic attribution:
+
+1. **Image Hotlinking vs Caching**: TMDB ToS Section 3(A) prohibits "storing" TMDB content except for reasonable caching. The distinction: serving images directly from `image.tmdb.org` via `<img>` tags is compliant. Downloading images to your own server/CDN and re-serving them is a violation. Using `next/image` as a proxy (which downloads, optimizes, and re-serves) is a gray area — the scope correctly avoids this by using native TMDB URLs.
+2. **Data Freshness Obligation**: TMDB ToS require that cached/stored data be refreshed periodically. The scope's bi-weekly trailer refresh is a start, but the core movie metadata (title, genres, poster_path) stored in the `movies` table has NO refresh mechanism. If TMDB updates a poster or corrects metadata, the app would serve stale data indefinitely.
+3. **Rate Limits (Free Tier)**: ~40 requests per 10 seconds (~4/s average). No hard monthly cap. HTTP 429 responses include `Retry-After` header. The scope does not specify handling of 429 responses in the server proxy.
+4. **TMDB Logo Requirements**: The logo must be used as provided (no modification). Minimum size requirements exist. The logo files are available at https://www.themoviedb.org/about/logos-attribution. The scope says "logo + link + disclaimer" but does not specify using the official logo assets.
+5. **Content Filtering**: TMDB includes adult content. The scope does not specify filtering `adult: true` results from API responses.
+
+## GDPR and Supabase Anonymous Auth — Deep Dive
+
+_Researched: 2026-04-05 (second pass, from existing knowledge)_
+
+1. **Supabase Internal Logging**: Self-hosted Supabase components (GoTrue, PostgREST, Kong) all produce their own logs containing IP addresses, user agent strings, and auth tokens. These are personal data under GDPR. The scope mentions no log management for the Supabase stack — only the Next.js app.
+2. **JWT as Personal Data**: The JWT issued by `signInAnonymously()` contains the user's UUID in the `sub` claim, token expiry, and role. While the JWT itself is ephemeral, it is transmitted over the network and stored in the browser. Under GDPR, the UUID within is personal data (it singles out an individual). The JWT is not the concern — the UUID it carries is.
+3. **Supabase Auth Tables**: GoTrue stores its own `auth.users` table with: id, created_at, updated_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, is_anonymous flag, and more. This is separate from the app's `public.users` table. Account deletion must also clean up `auth.users`.
+4. **Data Processor Agreement**: Under GDPR, if using a third-party to process data, a Data Processing Agreement (DPA) is required. For self-hosted Supabase, the deployer IS the data controller and processor — no DPA needed with Supabase Inc. However, TMDB receives search queries (which could contain personal preferences) — TMDB's privacy policy should be referenced.
+5. **Legitimate Interest Basis**: For anonymous auth with no email, the lawful basis is likely "legitimate interest" (Art. 6(1)(f)) or "performance of a contract" (Art. 6(1)(b)). The privacy policy must state which basis is used.
+
+## WCAG 2.1 AA — Animation and Component Accessibility Deep Dive
+
+_Researched: 2026-04-05 (second pass, from existing knowledge)_
+
+1. **WCAG 2.2.2 Pause, Stop, Hide**: Any auto-playing animation that (a) starts automatically, (b) lasts more than 5 seconds, and (c) is presented in parallel with other content MUST have a mechanism to pause, stop, or hide it. The landing page slot-machine reel (if it auto-plays or loops) would fall under this. The scope's `prefers-reduced-motion` handling addresses motion but NOT the pause/stop requirement for users who want motion but need control over it.
+2. **WCAG 2.4.7 Focus Visible**: All interactive elements must have a visible focus indicator. The scope mentions focus management for the inline panel but does not mention visible focus styles for poster cards, buttons, or other interactive elements. Tailwind's default `outline-none` on focus is a common violation.
+3. **WCAG 1.1.1 Non-text Content**: All poster images need meaningful `alt` text (movie title + year minimum). The scope does not specify alt text strategy. Reel animation posters spinning at high speed should have `aria-hidden="true"` during spin and meaningful alt on the final result.
+4. **WCAG 1.3.1 Info and Relationships**: The poster grid should use semantic list markup (`<ul>/<li>`) or `role="list"`/`role="listitem"`. The watched/unwatched sections should have headings or `aria-label`.
+5. **WCAG 4.1.3 Status Messages**: The roll result, search results count, filter results, and "No matches" messages are status messages that should use `role="status"` or `aria-live="polite"` to announce to screen readers without stealing focus.
+6. **WCAG 2.4.3 Focus Order**: The grid + inline panel expansion must maintain logical focus order. When a panel is open, Tab should move through panel controls, not skip to the next grid row behind it.
+7. **Poster Grid Alt Text**: Each poster `<img>` must have `alt="[Movie Title] ([Year]) poster"`. Decorative overlays (avatar, binoculars) should be `aria-hidden="true"` with their meaning conveyed via `aria-label` on the card or via screen-reader-only text.
+
+## PWA Compliance — Requirements Deep Dive
+
+_Researched: 2026-04-05 (second pass, from existing knowledge)_
+
+1. **Minimum Installability Criteria (Chrome)**: (a) Valid web app manifest with `name` or `short_name`, `icons` (192x192 + 512x512 PNG), `start_url`, `display` (standalone/fullscreen/minimal-ui). (b) Served over HTTPS. (c) Registered service worker with a fetch handler. (d) Chrome removed the offline-page requirement for installability in 2022, but best practice still expects it.
+2. **iOS/Safari PWA Limitations**: Safari does not support the `beforeinstallprompt` event. PWA on iOS uses `<meta name="apple-mobile-web-app-capable">` and apple-touch-icon. Push notifications were added in iOS 16.4 but are limited. `@serwist/next` handles most of this.
+3. **Maskable Icons**: Required for adaptive icon display on Android. The scope does not mention maskable icons. The manifest should include `"purpose": "any maskable"` icons.
+4. **Splash Screen**: Generated from manifest `name`, `background_color`, `theme_color`, and the 512x512 icon. iOS requires separate `apple-touch-startup-image` meta tags for different screen sizes, which `@serwist/next` may not auto-generate.
+5. **`display: standalone`**: The scope does not specify the display mode. This should be explicitly set.
+6. **Service Worker Scope**: `@serwist/next` registers at `/` by default, which is correct. But the service worker must NOT cache API routes (`/api/*`) or Supabase WebSocket connections.
+
+## CIS Docker Benchmark — Key Requirements
+
+_Researched: 2026-04-05 (from existing knowledge)_
+
+CIS Docker Benchmark v1.6.0 key controls for container configuration:
+
+1. **4.1 — Create a user for the container**: Do not run as root. The scope specifies non-root user — good.
+2. **4.2 — Use trusted base images**: `node:22-slim` is an official image — acceptable. Should pin to a specific digest or version tag, not `latest`.
+3. **4.5 — Enable Content Trust**: `DOCKER_CONTENT_TRUST=1` ensures only signed images are pulled.
+4. **4.6 — Add HEALTHCHECK**: The scope specifies `/api/health` — good.
+5. **5.2 — Do not use host networking**: docker-compose should use bridge networking (default).
+6. **5.4 — Restrict Linux capabilities**: Drop all capabilities and add only what's needed: `cap_drop: [ALL]`.
+7. **5.10 — Limit memory**: The scope should specify `mem_limit` in docker-compose.
+8. **5.11 — Set CPU shares**: Consider `cpus` limit to prevent runaway containers.
+9. **5.12 — Mount root filesystem as read-only**: Use `read_only: true` with explicit `tmpfs` mounts for writable paths.
+10. **5.15 — Do not share host PID namespace**: Default in docker-compose.
+11. **5.25 — Restrict container from gaining additional privileges**: `security_opt: [no-new-privileges:true]`.
+12. **Secrets Management**: Env vars in docker-compose should use Docker secrets or `.env` file with restricted permissions, not inline values.
+
+## Self-Hosted Supabase — Data Handling
+
+_Researched: 2026-04-05 (from existing knowledge)_
+
+1. **Backup Strategy**: Self-hosted Supabase runs PostgreSQL in Docker. No automatic backups unless configured. Options: (a) `pg_dump` cron job to a mounted volume, (b) WAL archiving for point-in-time recovery, (c) volume snapshots if using a cloud VM with snapshot support. The scope mentions this gap was flagged in review 1, but no specific solution is specified in the updated scope.
+2. **Encryption at Rest**: PostgreSQL in Docker does NOT encrypt data at rest by default. Options: (a) Use LUKS/dm-crypt on the host volume, (b) Use PostgreSQL's pgcrypto for column-level encryption (selective), (c) Use an encrypted filesystem for Docker volumes. For GDPR, encryption at rest is not strictly required but is a recommended safeguard (Art. 32).
+3. **Log Retention**: Self-hosted Supabase components (Kong, GoTrue, PostgREST, Realtime) all output logs to Docker's logging driver. Default: json-file with no rotation. This means logs grow unbounded. Must configure Docker log rotation (`max-size`, `max-file`) or use a log driver with rotation (e.g., `local`).
+4. **Supabase Studio**: The self-hosted stack includes Supabase Studio (admin UI). This should NOT be publicly accessible — restrict to localhost or VPN only. If exposed, it provides full database access with the service role key.
+
+## Privacy Policy — Required Sections (EU/US)
+
+_Researched: 2026-04-05 (from existing knowledge)_
+
+For an app operating in the EU (GDPR) and US (CCPA/state laws):
+
+1. **Identity of the controller**: Who is responsible for the data (name, contact details).
+2. **What data is collected**: Exhaustive list (UUID, display name, avatar color, group membership, movie preferences, IP addresses in logs, device info from user agent).
+3. **Purpose of processing**: Why each data point is collected.
+4. **Lawful basis (GDPR)**: Which Art. 6(1) basis applies to each processing activity.
+5. **Data retention periods**: How long each data type is kept (the 12-month inactive deletion is a start but all categories need periods).
+6. **Third-party data sharing**: Any third parties that receive data (TMDB receives search queries; Sentry receives error data including potentially user context).
+7. **International data transfers**: If data crosses borders (self-hosted means data stays on the server's jurisdiction, but TMDB API calls go to TMDB servers, Sentry data goes to Sentry's servers).
+8. **User rights**: Right to access, rectify, erase, restrict processing, data portability, object. For anonymous auth, some rights are limited (no email to verify identity).
+9. **How to exercise rights**: Contact method or self-service mechanism.
+10. **Cookie/storage disclosure**: What is stored in localStorage/cookies and why.
+11. **Children's data (COPPA/GDPR)**: State whether the app is intended for users under 13/16. If not, disclaim it.
+12. **Changes to the policy**: How users will be notified of updates.
+13. **CCPA-specific (California)**: Right to know, right to delete, right to opt-out of sale (even if no data is sold, must state this), categories of personal information.
+
+## Caddy HTTPS and TLS Compliance
+
+_Researched: 2026-04-05 (from existing knowledge)_
+
+1. **Caddy Auto-HTTPS**: Caddy automatically obtains and renews Let's Encrypt (or ZeroSSL) certificates. TLS 1.2 is the minimum by default; TLS 1.3 is preferred. This meets PCI-DSS and general compliance requirements for TLS.
+2. **HSTS**: Caddy does NOT add HSTS headers by default. Must be configured explicitly in the Caddyfile. The scope mentions HSTS in security headers but at the Next.js/Caddy level — must ensure it's actually configured in Caddy, not just Next.js (since Caddy terminates TLS).
+3. **OCSP Stapling**: Caddy enables OCSP stapling by default — good for performance and security.
+4. **Certificate Transparency**: Let's Encrypt certificates are logged to CT logs by default.
+5. **Internal Supabase Traffic**: Traffic between Caddy and the Next.js container, and between Next.js and the Supabase stack, is internal Docker network traffic. This is unencrypted by default. For compliance-sensitive deployments, internal traffic should also be encrypted (mTLS), but for a personal/small-group app this is typically acceptable.
+6. **Cipher Suite**: Caddy's default cipher suite is modern and secure. No configuration needed unless targeting older clients.
+7. **Certificate Renewal**: Caddy renews certificates ~30 days before expiry. For self-hosted with a dynamic DNS or non-standard domain setup, ensure the ACME challenge can succeed (HTTP-01 requires port 80 access, TLS-ALPN-01 requires port 443).

+ 675 - 0
research/COMPLIANCE.md

@@ -0,0 +1,675 @@
+# Compliance Review -- MovieDice
+
+Reviewed: 2026-04-05 | Reviewer: Claude (automated)
+Scope: Pre-implementation architecture review of PROJECT_SCOPE.md | Standard: General (TMDB ToS, GDPR, WCAG 2.1 AA, PWA, Docker)
+
+**Note:** This is a pre-implementation review. No source code exists yet. All findings are based on the architectural design and project scope document. Line references point to PROJECT_SCOPE.md.
+
+---
+
+## CRITICAL
+
+### 1. TMDB API Key Exposed to Client-Side Code
+
+`PROJECT_SCOPE.md:259` -- The tech stack specifies TMDB API integration with client-side search (debounced queries from the browser). The scope does not specify server-side proxying of TMDB API calls. If the TMDB API key is used directly in client-side fetch calls, it will be exposed in browser network requests and potentially in bundled JavaScript. TMDB's terms require that API keys not be publicly accessible. This also enables abuse by third parties who extract the key.
+
+**Fix:** All TMDB API calls must be routed through Next.js API routes (or Server Actions). The `TMDB_API_KEY` environment variable must only be accessible server-side. Create a `/api/tmdb/search`, `/api/tmdb/popular`, and `/api/tmdb/trailer` proxy layer. Mark the env var in `next.config.js` without the `NEXT_PUBLIC_` prefix to ensure it is never bundled into client code.
+
+**Implementation Risk:** Adds a network hop (client -> Next.js server -> TMDB), which slightly increases latency for search queries. Mitigate with TanStack Query caching and appropriate `Cache-Control` headers on the proxy responses.
+
+---
+
+### 2. TMDB Attribution Requirement Not Addressed
+
+`PROJECT_SCOPE.md` (entire document) -- TMDB's Terms of Use require visible attribution on any application using their API. The scope document makes no mention of displaying the TMDB logo, a link to themoviedb.org, or the required disclaimer text ("This product uses the TMDB API but is not endorsed or certified by TMDB"). Omitting attribution violates the API terms and risks having the API key revoked.
+
+**Fix:** Add a mandatory implementation item: display the TMDB attribution logo and text in the app footer on every page. The logo must link to https://www.themoviedb.org/. Include this in Phase 1 (Foundation) as a hard requirement, not a polish item.
+
+**Implementation Risk:** Minimal. This is a UI addition only. Ensure the logo meets TMDB's brand guidelines for size and placement.
+
+---
+
+### 3. No Privacy Policy or Terms of Service Specified
+
+`PROJECT_SCOPE.md:313-319` -- The Privacy section notes "no personal data beyond display name is stored," but this is incorrect from a legal standpoint. The app collects and stores: UUIDs (personal data under GDPR), display names, avatar color preferences, recovery codes (hashed), group membership data, movie preferences (which movies a user added), and IP addresses in server logs. Even with anonymous auth, GDPR and similar privacy laws (CCPA, LGPD) require a privacy policy explaining what data is collected, how it is used, how long it is retained, and how users can request deletion.
+
+**Fix:** Create a privacy policy page accessible from the landing page footer. Document: (a) what data is collected (UUID, display name, avatar color, group membership, movie additions, IP addresses in logs), (b) purpose of each data point, (c) retention period, (d) how to request account deletion, (e) cookie/local storage usage, (f) third-party data sharing (Supabase as processor, TMDB API calls). Add this as a Phase 1 implementation item.
+
+**Implementation Risk:** Requires legal review for completeness. A template-based approach is acceptable for MVP but should be reviewed by a legal professional before public launch.
+
+---
+
+### 4. Recovery Code Security -- Insufficient Specification
+
+`PROJECT_SCOPE.md:34,269` -- Recovery codes are described as "a longer alphanumeric code shown once after account creation" with recovery codes "hashed before storage." However, the scope does not specify: (a) the entropy/length of recovery codes, (b) the hashing algorithm (must be bcrypt/argon2, not SHA-256), (c) rate limiting on recovery code attempts, (d) brute-force protection. A weak recovery code or fast hash function could allow an attacker to take over accounts by guessing recovery codes.
+
+**Fix:** Mandate the following in the implementation: (a) Recovery codes must be at least 128 bits of entropy (e.g., 24 alphanumeric characters or a mnemonic phrase). (b) Hash with bcrypt (cost factor 12+) or argon2id. (c) Rate limit recovery code attempts to 5 per minute per IP. (d) Lock out after 10 failed attempts with a cooldown period. (e) Log all recovery code attempts for security monitoring.
+
+**Implementation Risk:** Rate limiting requires server-side state tracking (Redis, Supabase table, or in-memory with caveats). Bcrypt hashing adds ~250ms per attempt, which is acceptable for a security-critical operation.
+
+---
+
+### 5. Invite Code as Sole Access Control -- Brute Force Risk
+
+`PROJECT_SCOPE.md:35-36,315` -- Invite codes are described as "short human-readable" (e.g., WOLF-42) and serve as the sole access control for group membership. A code like WOLF-42 has extremely low entropy (a word from a list + a 2-digit number). An attacker could enumerate valid invite codes and join arbitrary groups, accessing their movie lists. There is no mention of rate limiting on join attempts.
+
+**Fix:** (a) Increase invite code entropy: use at least 3 words + 3 digits (e.g., WOLF-RAIN-42X) or a longer random string. (b) Rate limit join attempts to 5 per minute per IP/user. (c) Lock out after 15 failed attempts. (d) Consider adding optional group passwords for sensitive groups. (e) The invite code regeneration feature (already in scope) is good -- ensure it is prominently surfaced to admins.
+
+**Implementation Risk:** Longer codes reduce shareability (harder to type/remember). Balance security with UX by testing code formats with users. Rate limiting adds server-side complexity.
+
+---
+
+### 6. Master Admin TOTP Secret in Environment Variable -- Rotation and Backup Gaps
+
+`PROJECT_SCOPE.md:329-330` -- The TOTP secret is stored as an environment variable. This means: (a) rotating the TOTP secret requires redeployment, (b) there is no backup/recovery mechanism if the authenticator app is lost, (c) the TOTP secret is visible to anyone with access to the deployment environment, (d) there is no session expiry specified for admin sessions.
+
+**Fix:** (a) Document the TOTP secret rotation procedure (generate new secret, update env var, redeploy, re-enroll authenticator). (b) Generate and store backup codes for the master admin (encrypted, stored separately from the TOTP secret). (c) Specify admin session expiry (recommend 1 hour max, 15 minutes idle timeout). (d) Implement CSRF protection on admin actions. (e) Log all admin actions with timestamps for audit trail.
+
+**Implementation Risk:** Adding backup codes increases the attack surface slightly but prevents lockout. Session expiry may be annoying for extended admin sessions but is necessary for security.
+
+---
+
+## CODE QUALITY
+
+### 7. No Linting, Formatting, or Type Checking Standards Specified
+
+`PROJECT_SCOPE.md` (entire document) -- The scope specifies no code quality tooling. For a Next.js/TypeScript project, the following should be established before any code is written: ESLint configuration, Prettier configuration, TypeScript strict mode, import ordering rules, and a pre-commit hook to enforce them. Without these, code quality will drift from the first commit, especially if multiple developers contribute.
+
+**Fix:** Add a Phase 0 or Phase 1.0 task: (a) Initialize with TypeScript strict mode (`"strict": true` in tsconfig.json). (b) Configure ESLint with `next/core-web-vitals` and `next/typescript` presets. (c) Configure Prettier with consistent rules (single quotes, trailing commas, 100-char line width). (d) Add `lint-staged` + `husky` for pre-commit enforcement. (e) Add `.editorconfig` for cross-editor consistency.
+
+**Implementation Risk:** None. This is a one-time setup cost that saves significant time later.
+
+---
+
+### 8. No Testing Strategy Defined
+
+`PROJECT_SCOPE.md:406-421` -- Phase 9 describes manual QA and cross-device testing, but there is no mention of automated testing at any level: no unit tests, no integration tests, no end-to-end tests, no API tests. For a real-time collaborative app with complex state management (group membership, watched state, cross-list rolls), manual testing alone is insufficient and will lead to regressions.
+
+**Fix:** Add a testing mandate to Phase 1: (a) Set up Vitest for unit tests (utility functions, emotion-to-genre mapping, invite code generation). (b) Set up React Testing Library for component tests. (c) Set up Playwright for E2E tests (at minimum: onboarding flow, add movie, roll dice, real-time sync). (d) Require tests for all business logic before merge. (e) Add CI pipeline (GitHub Actions) that runs tests on every PR.
+
+**Implementation Risk:** Adds development time but significantly reduces bug density. Start with critical path E2E tests and expand coverage over time.
+
+---
+
+### 9. No CI/CD Pipeline Specified
+
+`PROJECT_SCOPE.md:322-330` -- Deployment is described as "Vercel auto-deploys from main branch" (and Docker for the user's primary case), but there is no mention of: CI checks before deployment, automated testing gates, build verification, security scanning, or environment promotion (staging -> production).
+
+**Fix:** Establish a CI pipeline before development begins: (a) GitHub Actions workflow running on every PR: lint, type-check, test, build. (b) Block merges to main if any step fails. (c) For Docker: add a workflow that builds the Docker image and runs smoke tests against it. (d) Add dependency vulnerability scanning (npm audit, Dependabot/Renovate). (e) Consider a staging environment for pre-production validation.
+
+**Implementation Risk:** Minimal. GitHub Actions free tier is sufficient for this project size. The main risk is CI flakiness from E2E tests, which can be mitigated with retry logic.
+
+---
+
+### 10. Emotion-to-Genre Mapping Lacks Extensibility Pattern
+
+`PROJECT_SCOPE.md:452-469` -- The emotion-to-genre mapping is defined as a static table in the scope document. The implementation should use a configuration-driven approach (JSON file or database table) rather than hardcoded if-else/switch statements, to allow easy updates without code changes.
+
+**Fix:** Implement the mapping as a typed constant object (e.g., `EMOTION_GENRE_MAP`) in a dedicated configuration file. Include the mapping table in the scope as a data contract. Consider moving this to the database post-MVP to allow runtime updates.
+
+**Implementation Risk:** Minimal. A configuration file is slightly more complex than inline constants but much more maintainable.
+
+---
+
+## DOCUMENTATION GAPS
+
+### 11. No API Route Documentation Standard
+
+`PROJECT_SCOPE.md` (entire document) -- The scope defines several API-like interactions (TMDB search, group join, movie add/remove, admin actions) but does not specify an API documentation standard. For a project with both client-side and server-side routes, undocumented APIs lead to integration errors and make onboarding new developers difficult.
+
+**Fix:** Establish a documentation requirement: every Next.js API route and Server Action must include a JSDoc header documenting: HTTP method, path, request body/query parameters with types, response shape with status codes, authentication requirements, and error responses. Consider generating an OpenAPI spec from these annotations.
+
+---
+
+### 12. No Error Code or Error Response Standard
+
+`PROJECT_SCOPE.md:383` -- Phase 5.6 mentions "Error handling: invalid invite code, TMDB API failure, network errors" but does not define an error response format, error codes, or error categorization. Without a standard, each error will be handled ad-hoc with inconsistent user-facing messages.
+
+**Fix:** Define a standard error response shape before development: `{ error: { code: string, message: string, details?: object } }`. Create an error code enum (e.g., `INVALID_INVITE_CODE`, `TMDB_UNAVAILABLE`, `RATE_LIMITED`, `UNAUTHORIZED`). Map each code to a user-friendly message. Centralize error handling in a shared utility.
+
+---
+
+### 13. Database Migration Strategy Not Specified
+
+`PROJECT_SCOPE.md:265-308` -- The data model is defined but there is no mention of how database schema changes will be managed. Without a migration strategy, schema changes during development will be ad-hoc and difficult to reproduce across environments.
+
+**Fix:** Use Supabase's migration system or a tool like `supabase db diff` / `supabase migration new`. Store all migrations in version control. Never modify the database schema directly in production -- always through versioned migrations. Document this in the project README.
+
+---
+
+### 14. Supabase Row-Level Security (RLS) Not Mentioned
+
+`PROJECT_SCOPE.md:265-308` -- The data model defines tables and relationships but makes no mention of Row-Level Security policies. Supabase strongly recommends RLS for all tables. Without RLS, any authenticated user (or anyone with the anon key) could read/write any row in any table via the Supabase client, completely bypassing group membership checks.
+
+**Fix:** This is a borderline-critical security issue. Define RLS policies for every table: (a) `users`: users can only read/update their own row. (b) `groups`: only members can read; only admin can update/delete. (c) `group_members`: only members of the group can read; only admin can delete others. (d) `movies`: only group members can CRUD. (e) `landing_reel_posters`: public read, no public write. (f) `admin_sessions`: no public access. Implement these in Phase 1 alongside schema creation.
+
+**Implementation Risk:** RLS policies that are too restrictive will break functionality. Test each policy thoroughly. Supabase's service role key (used server-side) bypasses RLS, which is correct for admin operations.
+
+---
+
+## PERFORMANCE
+
+### 15. Landing Page Slot-Machine Animation -- Heavy Asset Loading
+
+`PROJECT_SCOPE.md:74-79` -- The landing page slot-machine animation spins through ~20 movie poster images across 3 reels. This means loading 20 poster images before the animation can play smoothly. On mobile connections, this could be 2-4 MB of images that must load before the primary CTA is interactive, causing slow page loads -- the user's primary concern.
+
+**Fix:** (a) Use TMDB's smallest poster size (`w92` or `w154`) for the reel animation -- full-size posters are unnecessary at reel-spin speed. (b) Preload the reel poster set using `<link rel="preload">` or a service worker cache. (c) Store the poster URLs in the `landing_reel_posters` table (already planned) and serve them from a Next.js API route with aggressive `Cache-Control` headers (e.g., `max-age=86400`). (d) Use CSS `will-change: transform` on reel elements for GPU-accelerated animation. (e) Consider using a single sprite sheet or CSS-based animation with `background-position` for the reel spin to avoid per-image loading. (f) Show a skeleton/placeholder while posters load, and allow the Roll button to be interactive even before all images are loaded.
+
+**Implementation Risk:** Smaller poster sizes may look blurry on high-DPI screens. Use `w185` as a compromise. Sprite sheets add build complexity but dramatically improve animation performance.
+
+---
+
+### 16. No Image Optimization Strategy
+
+`PROJECT_SCOPE.md:293` -- The scope stores `poster_path` (TMDB relative path) and constructs full URLs at render time. However, there is no mention of: (a) using Next.js `<Image>` component for automatic optimization, (b) responsive image sizing (different sizes for grid view vs. expanded panel vs. reel animation), (c) lazy loading for below-the-fold images, (d) blur placeholder generation, (e) WebP/AVIF format negotiation.
+
+**Fix:** Mandate the use of Next.js `<Image>` component (or `next/image` with a custom TMDB loader) for all poster images. Configure TMDB image sizes per context: `w185` for grid thumbnails, `w342` for expanded panel, `w92`/`w154` for reel animation. Enable lazy loading for all images below the fold. Add TMDB's image domain to `next.config.js` `images.remotePatterns`. Use `placeholder="blur"` with a low-resolution blur data URL generated at add-time or cached.
+
+**Implementation Risk:** Next.js `<Image>` with external domains requires proper configuration. TMDB images are already served from a CDN, so double-optimization is unnecessary -- focus on correct sizing and lazy loading.
+
+---
+
+### 17. Unbounded Real-Time Subscriptions
+
+`PROJECT_SCOPE.md:48,365` -- Supabase real-time subscriptions are specified for live add/remove/watched-status updates. If a user belongs to multiple groups, the app would need to maintain one subscription per group (or a single subscription filtered to all their groups). The scope does not address: (a) subscription lifecycle management (subscribe on mount, unsubscribe on unmount), (b) reconnection logic on network interruption, (c) subscription count limits (Supabase free tier has limits on concurrent connections).
+
+**Fix:** (a) Only subscribe to the currently-viewed list, not all lists simultaneously. Unsubscribe when navigating away. (b) On the home page, use polling or a single subscription for movie counts rather than full list subscriptions. (c) Implement exponential backoff reconnection. (d) Set a maximum subscription count and degrade gracefully if exceeded. (e) Use Supabase's channel-based subscriptions with proper cleanup in React `useEffect` return functions.
+
+**Implementation Risk:** Limiting subscriptions to the active view means the home page movie counts may be slightly stale. This is an acceptable trade-off for resource efficiency. Document the staleness window (e.g., "counts refresh every 30 seconds on the home page").
+
+---
+
+### 18. Infinite Scroll Without Virtualization
+
+`PROJECT_SCOPE.md:41,145-147` -- The grid loads 12 movies initially and appends more on scroll. For groups with large watchlists (50-200+ movies), all previously loaded movie cards remain in the DOM. This causes memory bloat and rendering slowdowns, especially on low-end mobile devices with the full-bleed poster images.
+
+**Fix:** Implement windowed/virtualized scrolling using a library like `@tanstack/react-virtual` or `react-window`. Only render DOM nodes for movies currently in or near the viewport. This keeps memory and DOM node count constant regardless of list size.
+
+**Implementation Risk:** Virtualization adds complexity to the inline panel expansion (since the expanded panel occupies space between rows). The virtualization library must account for variable row heights. Test thoroughly with the inline expansion UX.
+
+---
+
+### 19. Cross-List Roll May Require Expensive Query
+
+`PROJECT_SCOPE.md:119,369` -- Rolling across all user lists combined requires fetching all unwatched movies from all groups the user belongs to. Without optimization, this is an N+1 query pattern (one query per group) or a complex join. For users in many groups with large lists, this could be slow.
+
+**Fix:** Implement a single optimized SQL query: `SELECT m.* FROM movies m JOIN group_members gm ON m.group_id = gm.group_id WHERE gm.user_id = $1 AND m.watched = false`. Create a composite index on `movies(group_id, watched)` and `group_members(user_id)`. Consider caching the roll pool briefly (TanStack Query with a 30-second `staleTime`).
+
+**Implementation Risk:** The query is straightforward but must be tested with realistic data volumes. Index creation is a one-time cost.
+
+---
+
+### 20. Trailer URL Refresh Job -- Potential TMDB Rate Limit Violation
+
+`PROJECT_SCOPE.md:52,388-392` -- The bi-weekly background job refreshes trailer URLs for movies where `trailer_url IS NULL`. If many movies have null trailers, this job could fire dozens or hundreds of TMDB API requests in rapid succession, exceeding the ~40 requests/10 seconds rate limit.
+
+**Fix:** (a) Implement request throttling in the refresh job: maximum 3 requests per second with exponential backoff on 429 responses. (b) Process movies in batches of 10 with a 5-second delay between batches. (c) Log the total count of movies needing refresh at job start. (d) Set a maximum of 100 movies processed per job run to prevent runaway execution. (e) Track and alert on persistent null trailers (some movies genuinely have no trailer).
+
+**Implementation Risk:** Throttling means the job takes longer to complete. For large backlogs, the job may not process all null trailers in a single run, which is acceptable -- it will catch up over multiple runs.
+
+---
+
+### 21. No Code Splitting or Bundle Analysis Strategy
+
+`PROJECT_SCOPE.md` (entire document) -- The app has several distinct page types (landing, home, list view, admin panel) with distinct dependencies (e.g., the admin panel needs `otplib`, the landing page needs the reel animation, the list view needs real-time subscriptions). Without explicit code splitting, the entire app's JavaScript will be loaded on every page, leading to slow initial page loads -- the user's primary concern.
+
+**Fix:** (a) Next.js App Router provides route-based code splitting by default -- ensure each major view is a separate route segment. (b) Use `next/dynamic` with `{ ssr: false }` for heavy client-only components (reel animation, roll animation, inline panel). (c) Lazy-load `otplib` only on the admin route. (d) Add `@next/bundle-analyzer` to the project and review bundle sizes during Phase 5 polish. (e) Set a performance budget: initial JS bundle under 200KB gzipped, First Contentful Paint under 1.5s on 4G.
+
+**Implementation Risk:** Lazy loading animations may cause a flash of empty content. Use suspense boundaries with skeleton loaders to handle the loading state.
+
+---
+
+## INFRASTRUCTURE
+
+### 22. Docker Deployment Not Fully Specified
+
+`PROJECT_SCOPE.md:262,322` -- The scope mentions Vercel deployment but the user's primary deployment target is Docker. The scope does not include: Dockerfile creation, Docker Compose configuration, health checks, container resource limits, log aggregation, or reverse proxy setup.
+
+**Fix:** Add Docker infrastructure tasks to Phase 1: (a) Create a multi-stage Dockerfile using `node:20-alpine` base, `output: 'standalone'` in next.config.js, non-root user, and `tini` for PID 1 signal handling. (b) Create `docker-compose.yml` with the Next.js service, environment variable mapping, health check (`/api/health`), restart policy, and resource limits (`mem_limit: 512m`). (c) Create a `.dockerignore` excluding `node_modules`, `.git`, `.env`, `research/`. (d) Document the Docker deployment procedure in the README. (e) Add a `/api/health` endpoint that checks Supabase connectivity.
+
+**Implementation Risk:** Standalone Next.js output has different behavior from the standard build (e.g., static assets must be copied separately). Test the Docker build thoroughly against all routes.
+
+---
+
+### 23. No Logging or Monitoring Strategy
+
+`PROJECT_SCOPE.md:427` -- Phase 10.3 briefly mentions "basic error monitoring (Vercel logs + Sentry free tier)" but this is the final phase. For a Docker-deployed app, logging and monitoring must be planned from Phase 1: (a) structured logging format, (b) log levels, (c) log aggregation for containerized deployments, (d) error tracking, (e) uptime monitoring.
+
+**Fix:** (a) Use a structured logger (e.g., `pino`) from Phase 1 -- output JSON logs so they can be parsed by any log aggregation tool. (b) Define log levels: ERROR for failures, WARN for degraded states (TMDB rate limited, Supabase reconnecting), INFO for significant events (user created, group created, admin action), DEBUG for development. (c) For Docker: output logs to stdout/stderr (Docker captures these by default). (d) Add Sentry error tracking in Phase 1, not Phase 10. (e) Create a `/api/health` endpoint for uptime monitoring. (f) Log all Master Admin actions for audit trail.
+
+**Implementation Risk:** Structured logging adds a dependency but is essential for debugging production issues. `pino` is lightweight and performant.
+
+---
+
+### 24. Environment Variable Validation Not Specified
+
+`PROJECT_SCOPE.md:324-330` -- Five environment variables are listed as required, but there is no mention of validating them at startup. If any are missing or malformed, the app will fail at runtime with cryptic errors rather than at startup with a clear message.
+
+**Fix:** Use a validation library (e.g., `zod` or `t3-env`) to validate all environment variables at application startup. Fail fast with a descriptive error message if any required variable is missing or invalid. Include format validation (e.g., TMDB_API_KEY must be a 32-character hex string, SUPABASE_URL must be a valid URL, MASTER_ADMIN_TOTP_SECRET must be valid base32).
+
+**Implementation Risk:** None. This is a reliability improvement with no downside.
+
+---
+
+### 25. No Database Backup Strategy
+
+`PROJECT_SCOPE.md:323` -- Supabase free tier is mentioned with an "upgrade path available if scale demands," but there is no mention of database backups. Supabase free tier includes daily backups with 7-day retention, but (a) this is not documented in the scope, (b) for Docker/self-hosted Supabase scenarios, backups must be manually configured, (c) there is no documented restore procedure.
+
+**Fix:** (a) Document Supabase's backup capabilities and retention for the hosted scenario. (b) For Docker deployment, add a backup cron job (e.g., `pg_dump` daily to a mounted volume or object storage). (c) Document the restore procedure. (d) Test the restore procedure at least once before launch.
+
+**Implementation Risk:** Backup storage costs are minimal but non-zero. For self-hosted Supabase, backup automation requires additional Docker service configuration.
+
+---
+
+## COMPLIANCE
+
+### 26. TMDB Terms of Use -- Image Hosting Violation Risk
+
+`PROJECT_SCOPE.md:293,302-308` -- The data model correctly stores `poster_path` as a relative path and constructs the full URL at render time from TMDB's CDN. However, the `landing_reel_posters` table also stores `poster_path`. If the periodic refresh job or any caching layer downloads and re-serves these images from the app's own domain (e.g., via Next.js image optimization proxy), this may violate TMDB's requirement that images be served from their CDN.
+
+**Fix:** (a) Ensure all TMDB images are served directly from `image.tmdb.org` in production. (b) If using Next.js `<Image>` with a custom loader, configure it to rewrite URLs to TMDB's CDN rather than proxying through the Next.js server. (c) Alternatively, add `image.tmdb.org` to `images.remotePatterns` in `next.config.js` and let Next.js optimize on-the-fly (this is a gray area -- TMDB's ToS should be checked for whether CDN-proxied optimization is permitted). (d) Document the decision and rationale.
+
+**Implementation Risk:** Direct CDN serving means no control over image availability if TMDB's CDN has an outage. This is an acceptable risk given the ToS requirement.
+
+---
+
+### 27. GDPR -- Local Storage User ID Without Consent Mechanism
+
+`PROJECT_SCOPE.md:107-108` -- The app stores a user ID in local storage (or cookie) to identify returning users. Under the EU ePrivacy Directive (which complements GDPR), storing non-essential identifiers on a user's device requires informed consent. The scope does not include any consent mechanism (cookie banner, storage consent prompt).
+
+**Fix:** (a) Determine if the stored user ID qualifies as "strictly necessary" for the service (it likely does, since the app cannot function without identifying the user). If so, consent is not required but a privacy policy must explain the storage. (b) If the app stores any analytics identifiers, tracking pixels, or third-party cookies (e.g., Sentry, Vercel Analytics), those require explicit consent. (c) Add a privacy policy link in the footer. (d) If deploying for EU users, implement a minimal consent banner for any non-essential storage.
+
+**Implementation Risk:** Consent banners add friction to the onboarding flow, which conflicts with the "low tolerance for signup friction" user trait. Minimize non-essential storage to avoid needing a banner.
+
+---
+
+### 28. GDPR -- No Account Deletion Flow for Regular Users
+
+`PROJECT_SCOPE.md:53,399` -- The Master Admin can delete any user, but there is no self-service account deletion flow for regular users. GDPR Article 17 (Right to Erasure) requires that users can request deletion of their personal data. The scope only mentions "Leave this list" for regular users, which removes group membership but does not delete the user account or their data across other groups.
+
+**Fix:** Add a "Delete My Account" option accessible from user settings. This must: (a) Remove the user from all groups (triggering ownership transfer or list deletion as applicable). (b) Delete the user record from the `users` table. (c) Anonymize or delete the user's `added_by` attributions in the `movies` table (set to null or a "[deleted user]" sentinel). (d) Clear local storage. (e) Confirm deletion is complete. (f) This should cascade properly -- define the cascade behavior in the database schema.
+
+**Implementation Risk:** Cascade deletion is complex when a user is the admin of multiple groups. Each group must be handled independently (transfer or delete). Test edge cases: user is admin of 3 groups, member of 2 others, has added movies to all of them.
+
+---
+
+### 29. GDPR -- Data Retention Period Not Defined
+
+`PROJECT_SCOPE.md` (entire document) -- No data retention period is specified for any data type. GDPR requires that personal data not be kept longer than necessary. Questions that need answers: How long is a user account retained if unused? How long are deleted lists' data retained? Are server logs with IP addresses rotated?
+
+**Fix:** Define retention policies: (a) Inactive user accounts: auto-delete after 12 months of no activity (or prompt re-confirmation). (b) Deleted lists: hard delete immediately (no soft delete unless needed for recovery). (c) Server logs: rotate every 30 days. (d) Admin action logs: retain for 90 days. (e) Document these in the privacy policy.
+
+**Implementation Risk:** Auto-deletion of inactive accounts could surprise returning users. Implement a warning mechanism (though without email, this is challenging with anonymous auth). Consider extending the retention period and documenting it clearly.
+
+---
+
+### 30. WCAG 2.1 AA -- Animation Accessibility (prefers-reduced-motion)
+
+`PROJECT_SCOPE.md:242-244` -- The scope defines two distinct animations (slot-machine reel, scatter/eliminate) as core features. WCAG 2.3.3 (AAA) and general best practice at AA level require respecting the `prefers-reduced-motion` media query. Users with vestibular disorders can be physically affected by spinning/scattering animations. The scope mentions no motion preference handling.
+
+**Fix:** (a) Implement `prefers-reduced-motion` detection. When active: replace the slot-machine reel with an instant reveal, replace the scatter/eliminate animation with a simple fade-in or card flip. (b) The result should still feel satisfying -- use opacity transitions and scale transforms rather than spatial movement. (c) Add a manual toggle in user settings for users who want reduced motion regardless of system setting. (d) Test both animation paths.
+
+**Implementation Risk:** Designing two animation variants doubles animation development work. Start with the full animation, then create the reduced version. The reduced version can be much simpler.
+
+---
+
+### 31. WCAG 2.1 AA -- Inline Panel Expansion Accessibility
+
+`PROJECT_SCOPE.md:149-172` -- The inline panel expansion (tapping a poster to expand details below the row) has several accessibility concerns: (a) No mention of keyboard navigation (how does a keyboard user open/close the panel?). (b) No mention of focus management (focus should move to the panel on open, return to the trigger on close). (c) No mention of screen reader announcements (the panel expansion should be announced). (d) The "tap outside to collapse" interaction has no keyboard equivalent.
+
+**Fix:** (a) Make movie cards focusable and openable with Enter/Space. (b) On panel open, move focus to the first interactive element in the panel. (c) On panel close, return focus to the triggering card. (d) Add Escape key to close the panel. (e) Use `aria-expanded` on the trigger card and `role="region"` with `aria-label` on the panel. (f) Announce panel open/close with `aria-live="polite"`.
+
+**Implementation Risk:** Focus management with a virtualized grid (if implementing finding #18) adds complexity. Ensure the focus trap works correctly within the expanded panel.
+
+---
+
+### 32. WCAG 2.1 AA -- Delete Confirmation Accessibility
+
+`PROJECT_SCOPE.md:163-166` -- The two-tap delete flow (shake animation + text change) relies entirely on visual feedback. Screen reader users will not perceive the shake animation or the visual text change unless it is announced. Additionally, the shake animation may be disorienting for users with vestibular disorders.
+
+**Fix:** (a) On first tap, change the button's `aria-label` to "Click to confirm delete" and announce it via `aria-live="assertive"`. (b) Respect `prefers-reduced-motion` -- replace shake with a color change or border highlight. (c) Ensure the confirmation state is visually distinct even without animation (e.g., red background, different text). (d) Consider using a standard confirmation dialog (`window.confirm()` or a custom accessible modal) as an alternative to the shake pattern for accessibility.
+
+**Implementation Risk:** A confirmation dialog is more universally accessible but less visually distinctive than the shake pattern. Consider offering both: shake for sighted users, dialog for screen reader users (detect via accessibility API or offer a setting).
+
+---
+
+### 33. PWA -- Offline Strategy Needs Definition
+
+`PROJECT_SCOPE.md:248,403` -- The scope mentions "offline tolerance: show cached list, disable write actions" and Phase 8.2 mentions "offline graceful degradation." However, the caching strategy is not defined: (a) Which assets are precached by the service worker? (b) How are movie lists cached for offline viewing? (c) How is the transition between online and offline states communicated to the user? (d) What happens to in-flight writes when the connection drops?
+
+**Fix:** Define the offline strategy: (a) Precache: app shell, critical CSS/JS, fonts. (b) Runtime cache: movie list data (via TanStack Query's persistence plugin or service worker). (c) On connection loss: show a non-intrusive banner ("You're offline -- viewing cached data"), disable all write buttons (add, delete, mark watched, roll), gray out disabled controls. (d) On reconnection: auto-reconnect Supabase subscription, refresh stale data, remove the banner. (e) Do NOT queue offline writes (too complex for MVP; risk of conflicts).
+
+**Implementation Risk:** TanStack Query's `persistQueryClient` plugin can handle offline caching but adds complexity. For MVP, a simpler approach is acceptable: cache the most recent list view and show it read-only when offline.
+
+---
+
+## Positives
+
+- Recovery codes hashed before storage -- good security practice for account recovery
+- Invite code regeneration feature allows revoking compromised codes
+- TOTP 2FA for Master Admin rather than password-only auth
+- TMDB poster_path stored as relative path, not full URL -- allows flexible CDN URL construction
+- Debounced search (300ms) prevents excessive API calls
+- Real-time subscriptions for collaborative UX rather than polling
+- Display names only, no email/password -- good data minimization for MVP
+- Trailer URL stored at add-time reduces real-time API dependency
+- Admin self-removal triggers ownership transfer rather than orphaning the list
+- Two-tap delete confirmation prevents accidental deletions
+- Phase-based implementation plan with clear MVP cutoff
+
+---
+
+## Summary
+
+| Severity           | Total |
+| ------------------ | ----- |
+| Critical           | 6     |
+| Code Quality       | 4     |
+| Documentation Gaps | 4     |
+| Performance        | 7     |
+| Infrastructure     | 4     |
+| Compliance         | 8     |
+
+**Total findings: 33**
+
+This is a pre-implementation review. All findings are recommendations for the architecture and implementation plan. No source code was reviewed because none exists yet.
+
+**Top 5 actions to take before writing any code:**
+
+1. Add TMDB API server-side proxy requirement to Phase 1 (Finding #1)
+2. Establish linting, TypeScript strict mode, and testing infrastructure (Findings #7, #8, #9)
+3. Define Supabase RLS policies alongside the schema (Finding #14)
+4. Add TMDB attribution to every page (Finding #2)
+5. Create Docker deployment infrastructure in Phase 1 (Finding #22)
+
+---
+
+## Files Reviewed
+
+_Every file reviewed, listed for coverage verification._
+
+| #   | File               | Type                                  |
+| --- | ------------------ | ------------------------------------- |
+| 1   | `PROJECT_SCOPE.md` | Project scope / architecture document |
+
+**Note:** This project contains only the scope document. No source code, configuration files, or infrastructure files exist yet. This review covers the architectural design and implementation plan specified in PROJECT_SCOPE.md.
+
+---
+
+## Second Review -- Updated Scope Analysis
+
+Reviewed: 2026-04-05 | Reviewer: Claude (automated)
+Scope: Second-pass review of updated PROJECT_SCOPE.md | Standard: TMDB ToS, GDPR, WCAG 2.1 AA, PWA, Docker CIS, TLS
+
+This section contains only NEW findings. The following first-review items are now addressed in the updated scope and are confirmed resolved: #1 (TMDB server proxy), #2 (TMDB attribution), #3 (privacy policy), #4 (recovery code security), #7 (linting/formatting), #8 (testing), #9 (CI via husky), #11 (API docs in markdown), #13 (migration strategy), #14 (RLS), #22 (Docker deployment), #23 (Sentry from Phase 1), #24 (env validation), #27 (privacy policy link), #28 (self-service deletion in Extra Features), #29 (12-month retention), #30 (prefers-reduced-motion), #31 (inline panel keyboard/aria).
+
+---
+
+### CRITICAL
+
+### 34. TMDB Metadata Staleness -- No Refresh Mechanism for Stored Movie Data
+
+`PROJECT_SCOPE.md:326-338` -- The `movies` table stores TMDB metadata (title, year, poster_path, genres) at add-time and never refreshes it. The trailer URL refresh job only targets `trailer_url IS NULL` entries. TMDB's Terms of Service require that cached data be kept reasonably current. If TMDB updates a movie's poster, corrects a title, or reclassifies genres, the app serves stale data indefinitely. Over the 12-month retention window, this drift could be significant. Additionally, poster_path values can become invalid if TMDB re-processes images, resulting in broken poster images.
+
+**Fix:** Add a metadata freshness job (monthly cadence via pg_cron) that re-fetches title, poster_path, genres, and year from TMDB for movies added more than 30 days ago. Throttle at 3 requests/second with 429 backoff. Process in batches of 50 per run. Add a `metadata_refreshed_at` column to the `movies` table. This is the same pattern as the trailer refresh job but for core metadata.
+
+**Implementation Risk:** Additional TMDB API usage. At 3 req/s and 50 movies/run, a batch takes ~17 seconds. For large datasets, spread across multiple runs. Risk of overwriting user-visible data if TMDB makes incorrect changes -- consider logging changes for review.
+
+---
+
+### 35. TMDB Adult Content Not Filtered
+
+`PROJECT_SCOPE.md:299-300` -- The TMDB API proxy routes search queries to TMDB but the scope does not specify filtering adult content from results. TMDB's API includes an `include_adult` parameter (default false for search, but true for discover/popular). The landing page "popular/top-rated" fetch for reel posters could surface adult content if the parameter is not explicitly set. Similarly, the search endpoint should explicitly exclude adult results to prevent inappropriate content appearing in a social app used by families and friend groups.
+
+**Fix:** Explicitly set `include_adult=false` on all TMDB API calls in the server proxy. Add this as a documented requirement in the TMDB proxy implementation (Phase 1.3 / Phase 3.1). Also filter results server-side by checking the `adult` field on each result object, since some adult-flagged movies can still appear in non-adult searches depending on TMDB's classification.
+
+**Implementation Risk:** Minimal. This is a query parameter addition. Double-filtering (parameter + server-side check) provides defense in depth.
+
+---
+
+### 36. Supabase Studio Publicly Accessible in Docker Stack
+
+`PROJECT_SCOPE.md:391` -- The self-hosted Supabase Docker stack includes Supabase Studio (the admin dashboard UI), which by default binds to a port accessible from outside the container. Studio provides full database access using the service role key. If Studio is exposed through Caddy or directly on a public port, anyone who discovers it has unrestricted read/write access to every table, bypassing all RLS policies.
+
+**Fix:** (a) Do NOT expose the Supabase Studio port in docker-compose (remove or comment out the port mapping). (b) If Studio access is needed, bind it to `127.0.0.1` only and access via SSH tunnel. (c) Alternatively, add Caddy basic auth or IP restriction in front of the Studio route. (d) Document this as a security-critical operational requirement.
+
+**Implementation Risk:** Developers lose convenient Studio access during development. SSH tunnel is the standard solution for self-hosted deployments. During local development, Studio can be accessed directly.
+
+---
+
+### COMPLIANCE
+
+### 37. GDPR -- Supabase `auth.users` Table Not Addressed in Deletion Flow
+
+`PROJECT_SCOPE.md:569` -- The self-service account deletion (Extra Features) and Master Admin deletion mention cascading deletes on the app's `public.users` table. However, Supabase's GoTrue auth system maintains its own `auth.users` table containing: user UUID, created_at, last_sign_in_at, raw_app_meta_data, raw_user_meta_data, and the `is_anonymous` flag. Deleting a row from `public.users` does NOT delete the corresponding `auth.users` record. Under GDPR Article 17, erasure must be complete. An orphaned `auth.users` record still constitutes retained personal data.
+
+**Fix:** Account deletion must call `supabase.auth.admin.deleteUser(userId)` using the service role key to remove the `auth.users` record. This should be performed server-side (API route or Server Action) after the application-level cascade completes. Document this as a required step in both the self-service deletion flow and the Master Admin deletion flow.
+
+**Implementation Risk:** The `admin.deleteUser` call requires the service role key and must be server-side only. If the GoTrue delete fails after the public table cascade succeeds, the user is partially deleted -- implement as a transaction or handle the error with a retry/alert mechanism.
+
+---
+
+### 38. GDPR -- Supabase Component Logs Contain Personal Data with No Rotation
+
+`PROJECT_SCOPE.md:389-394` -- The self-hosted Docker stack includes Kong (API gateway), GoTrue (auth), PostgREST, and Realtime -- each producing logs containing IP addresses, user agent strings, JWTs, and request paths (which may include user IDs). Docker's default logging driver (`json-file`) has no size limit or rotation. These logs (a) contain personal data under GDPR, (b) grow unbounded consuming disk space, and (c) have no defined retention period. The scope's 12-month data retention policy and privacy policy mention server logs with IPs but do not address Supabase's own container logs.
+
+**Fix:** (a) Configure Docker log rotation for ALL containers in docker-compose: `logging: { driver: "json-file", options: { max-size: "10m", max-file: "5" } }`. (b) Document that Supabase container logs contain personal data. (c) Include Supabase logs in the privacy policy's log retention disclosure (recommend 30-day effective retention via rotation). (d) If log aggregation is added later, ensure the aggregation target also has a retention policy.
+
+**Implementation Risk:** Log rotation means older logs are lost. For debugging production issues, 50MB per container (5 x 10MB) is sufficient for recent history. If forensic logging is needed, consider shipping logs to a dedicated store with its own retention policy.
+
+---
+
+### 39. GDPR -- Sentry Error Reports May Contain User Context
+
+`PROJECT_SCOPE.md:422` -- Sentry is added in Phase 1.8 for error monitoring. By default, Sentry's JavaScript SDK captures: error stack traces, breadcrumbs (user interactions, console logs, network requests), request URLs (which may contain user/group IDs), and browser metadata. If Sentry's `setUser()` is called or if user context appears in error messages, Sentry receives personal data. This constitutes an international data transfer (to Sentry's US servers) requiring disclosure in the privacy policy and potentially a Standard Contractual Clauses (SCC) basis under GDPR.
+
+**Fix:** (a) Configure Sentry's `beforeSend` callback to strip any user-identifying information from error events. (b) Do NOT call `Sentry.setUser()` with the anonymous UUID. (c) Sanitize URLs in breadcrumbs to remove UUID path segments. (d) Disclose Sentry as a third-party data processor in the privacy policy. (e) If using Sentry's cloud service, reference Sentry's DPA and SCC compliance in the privacy policy. (f) Alternatively, self-host Sentry to keep all error data on-premises.
+
+**Implementation Risk:** Aggressive sanitization may make debugging harder (errors without user context are harder to reproduce). A middle ground: use a hashed/truncated user identifier that cannot be reversed to the original UUID.
+
+---
+
+### 40. GDPR -- Privacy Policy Missing Required Sections
+
+`PROJECT_SCOPE.md:373` -- The scope describes the privacy policy as "factual description of what data is stored, how it is used, and user rights." This is a good start but GDPR (Articles 13-14) and CCPA require specific sections that are not mentioned: (a) identity and contact details of the data controller, (b) lawful basis for each processing activity (Art. 6(1) -- likely legitimate interest or contract performance), (c) international data transfers (TMDB API calls to TMDB servers, Sentry to Sentry servers), (d) automated decision-making disclosure (the emotion-to-genre mapping is algorithmic but likely does not qualify as "solely automated" under Art. 22), (e) right to lodge a complaint with a supervisory authority, (f) CCPA categories of personal information and "do not sell" disclosure, (g) children's data disclaimer (COPPA: under-13; GDPR: under-16 in some member states).
+
+**Fix:** Create a privacy policy template with all required sections before implementation. At minimum: controller identity, data inventory with lawful basis per item, retention periods per data type, third-party recipients (TMDB, Sentry), international transfers, full list of user rights with exercise instructions, children's disclaimer, cookie/localStorage disclosure, and change notification procedure. Phase 5.1 should reference this template.
+
+**Implementation Risk:** A comprehensive privacy policy for an anonymous-auth app is unusual and may feel heavyweight. Keep language plain and concise. Consider using a privacy policy generator as a starting point, then customizing.
+
+---
+
+### 41. WCAG 2.1 AA -- Slot Machine Reel Violates 2.2.2 Pause, Stop, Hide
+
+`PROJECT_SCOPE.md:74-79` -- WCAG 2.2.2 requires that any moving, blinking, or scrolling content that (a) starts automatically, (b) lasts more than 5 seconds, and (c) is presented alongside other content must have a mechanism to pause, stop, or hide it. The slot-machine reel animation is user-triggered (button press), so it does NOT auto-start -- however, if the reel spin duration exceeds 5 seconds or if the reels loop/auto-play on page load as a decorative element, this criterion applies. The scope specifies the in-app roll at "2-3 seconds" but does not specify the landing page reel duration. Additionally, `prefers-reduced-motion` replaces the animation entirely but does not provide a pause/stop control for users who want motion but need control.
+
+**Fix:** (a) Ensure the landing page reel animation is strictly user-triggered (no auto-play or looping on page load). (b) Cap the reel animation duration at under 5 seconds. (c) If any decorative animation loops (e.g., a subtle poster scroll preview before the user taps Roll), add a pause button. (d) Document the animation durations as testable success criteria.
+
+**Implementation Risk:** Minimal if animations are user-triggered and time-bounded. The main risk is a future design decision to add auto-playing reel teasers on the landing page -- if added, a pause control would be required.
+
+---
+
+### 42. WCAG 2.1 AA -- Poster Images Missing Alt Text Specification
+
+`PROJECT_SCOPE.md:146-152, 160-161` -- The scope specifies poster `<img>` tags with `loading="lazy"` but does not specify `alt` text for any poster image. WCAG 1.1.1 (Non-text Content) requires meaningful alt text for all informative images. Posters in the grid, the expanded panel, the reel animation, and the roll result all need alt text. During the reel spin animation, rapidly cycling posters should be `aria-hidden="true"` (they are decorative in that context), but the final result poster must have alt text.
+
+**Fix:** (a) All poster `<img>` tags: `alt="[Movie Title] ([Year]) poster"`. (b) Reel animation posters during spin: `aria-hidden="true"` on the container or individual images. (c) Roll result poster: meaningful alt text. (d) Added-by avatar overlay and binoculars emoji overlay: `aria-hidden="true"` with meaning conveyed via `aria-label` on the parent card element or screen-reader-only text (e.g., `<span class="sr-only">Watched</span>`). (e) Add this to the component requirements in Phase 3.4 and 3.6.
+
+**Implementation Risk:** None. This is a straightforward implementation requirement.
+
+---
+
+### 43. WCAG 2.1 AA -- Status Messages Not Announced (WCAG 4.1.3)
+
+`PROJECT_SCOPE.md:194, 459` -- Several user actions produce status messages that sighted users perceive visually but screen reader users would miss: (a) roll result ("You got: [Movie Title]"), (b) search results count, (c) genre filter applied ("Showing [N] movies in [Genre]"), (d) "No matches -- showing full list" fallback, (e) movie added/removed confirmation, (f) watched state toggle confirmation. WCAG 4.1.3 (Status Messages, AA) requires that status messages be programmatically determinable via `role="status"` or `aria-live` without receiving focus.
+
+**Fix:** Wrap dynamic status messages in an `aria-live="polite"` region (or use `role="status"`) for: roll results, search result counts, filter state changes, empty state messages, and action confirmations. Use `aria-live="assertive"` only for error messages and the delete confirmation state change. Do NOT move focus to these messages -- let the live region announce them.
+
+**Implementation Risk:** Multiple `aria-live` regions competing can cause announcement queuing issues. Use a single shared announcer component (common pattern: a visually hidden div that receives message text via state) rather than multiple live regions scattered across components.
+
+---
+
+### INFRASTRUCTURE
+
+### 44. Docker Compose Missing Security Hardening (CIS Benchmark)
+
+`PROJECT_SCOPE.md:389-394` -- The scope specifies a docker-compose configuration with non-root user, tini, and health check, which is good. However, several CIS Docker Benchmark controls are not addressed: (a) no capability dropping (`cap_drop: [ALL]`), (b) no `no-new-privileges` security option, (c) no memory limits specified, (d) no CPU limits, (e) no read-only root filesystem. These are standard hardening measures for production Docker deployments.
+
+**Fix:** Add to the docker-compose service definition for the Next.js container:
+
+```yaml
+security_opt:
+  - no-new-privileges:true
+cap_drop:
+  - ALL
+mem_limit: 512m
+memswap_limit: 512m
+cpus: 1.0
+read_only: true
+tmpfs:
+  - /tmp
+  - /app/.next/cache
+```
+
+Apply similar hardening to Supabase containers where applicable (some Supabase containers may need specific capabilities).
+
+**Implementation Risk:** `read_only: true` requires identifying all writable paths and mounting them as `tmpfs` or named volumes. The Next.js standalone output writes to `.next/cache` at runtime. Test thoroughly to ensure no write operations fail. Supabase containers (especially Postgres) need writable data directories.
+
+---
+
+### 45. Caddy HSTS Header Must Be Configured at Proxy Level
+
+`PROJECT_SCOPE.md:379-384, 392` -- The scope mentions HSTS in the security headers section and suggests configuring it in `next.config.js` or Caddy. For HSTS to be effective, it MUST be set at the TLS termination point, which is Caddy. If HSTS is only set in Next.js, it is applied after TLS has already been established, which is correct timing but means Caddy could theoretically serve a non-HSTS response if the request bypasses Next.js. More importantly, Caddy does NOT add HSTS by default. The scope should explicitly mandate HSTS configuration in the Caddyfile rather than leaving it ambiguous ("or").
+
+**Fix:** Configure HSTS in the Caddyfile explicitly:
+
+```
+header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
+```
+
+Other security headers (CSP, X-Frame-Options, etc.) can be set in either Caddy or Next.js, but HSTS should be at the Caddy level. Remove the ambiguity -- specify which headers go where.
+
+**Implementation Risk:** HSTS with `preload` and a long max-age is difficult to undo once deployed. Start with a shorter max-age (e.g., `max-age=86400`) during testing, then increase to production values. Do NOT submit to the HSTS preload list until confident the domain will always serve HTTPS.
+
+---
+
+### 46. Self-Hosted Supabase PostgreSQL -- No Encryption at Rest
+
+`PROJECT_SCOPE.md:391` -- The self-hosted Supabase stack runs PostgreSQL in a Docker container with data stored on a Docker volume. Docker volumes are NOT encrypted by default. The database contains user data (UUIDs, display names, group membership, movie preferences) that constitutes personal data under GDPR. While GDPR does not strictly mandate encryption at rest, Article 32 requires "appropriate technical measures" to protect personal data, and encryption at rest is listed as an example measure. An unencrypted volume on a compromised host exposes all user data.
+
+**Fix:** (a) Use full-disk encryption on the Docker host (LUKS on Linux, FileVault on macOS, BitLocker on Windows). This is the simplest approach and protects all Docker volumes. (b) Document this as a deployment requirement. (c) If the host is a cloud VM, enable the cloud provider's disk encryption feature. (d) For defense in depth, consider column-level encryption via pgcrypto for the most sensitive fields (recovery_code is already hashed, but display_name and group membership are plaintext).
+
+**Implementation Risk:** Full-disk encryption has negligible performance impact on modern hardware with AES-NI. Column-level encryption adds application complexity and prevents SQL queries on encrypted columns. Recommend host-level encryption as sufficient for this app's threat model.
+
+---
+
+### 47. No Database Backup Strategy in Updated Scope
+
+`PROJECT_SCOPE.md:389-404` -- Finding #25 from the first review flagged the missing backup strategy. The updated scope now specifies self-hosted Supabase but still does not include a backup mechanism. For self-hosted PostgreSQL, there are NO automatic backups. A Docker volume failure, accidental `DROP TABLE`, or host disk failure results in complete data loss. This is especially critical because anonymous auth means users cannot be contacted to rebuild their accounts.
+
+**Fix:** Add a backup task to Phase 1.7 (Docker infrastructure): (a) Create a `pg_dump` cron job (daily, retain 7 days) writing to a separate volume or off-host storage. (b) Add a `backup` service in docker-compose that runs the dump on a schedule. (c) Document the restore procedure. (d) Test restore at least once before launch. (e) Consider WAL archiving for point-in-time recovery if data volume warrants it. Example:
+
+```yaml
+backup:
+  image: postgres:16
+  command: >
+    sh -c 'while true; do
+      pg_dump -h db -U postgres > /backups/moviedice_$$(date +%Y%m%d).sql
+      find /backups -mtime +7 -delete
+      sleep 86400
+    done'
+  volumes:
+    - ./backups:/backups
+```
+
+**Implementation Risk:** Backup storage consumes disk space. For a small app, daily dumps are small (< 1 MB). The backup container needs access to the Postgres container's network and credentials. Ensure backup files are not publicly accessible.
+
+---
+
+### PERFORMANCE
+
+### 48. TMDB Server Proxy Missing 429 Rate Limit Handling
+
+`PROJECT_SCOPE.md:299-300` -- The TMDB server proxy (`/api/tmdb/*`) routes all client requests through the Next.js server to TMDB. The scope specifies debounce (300ms) and TanStack Query caching, which reduces request volume. However, there is no specification for handling TMDB's HTTP 429 (Too Many Requests) responses. If multiple users search simultaneously or the trailer/reel refresh jobs run during peak usage, the proxy could hit the ~40 req/10s limit. Without 429 handling, the proxy would pass through TMDB error responses to the client, causing confusing failures.
+
+**Fix:** (a) Implement server-side rate limiting on the TMDB proxy using a token bucket or sliding window (e.g., 30 req/10s to stay under TMDB's 40 req/10s with headroom for background jobs). (b) On receiving a 429 from TMDB, read the `Retry-After` header and return a friendly error to the client with a retry suggestion. (c) Queue or delay background job requests to avoid competing with interactive user requests. (d) Add `Cache-Control` response headers on the proxy to enable HTTP-level caching of TMDB responses (e.g., `public, max-age=300` for search results).
+
+**Implementation Risk:** Server-side rate limiting requires in-memory state (or Redis). For a single-instance Docker deployment, in-memory is fine. The rate limiter should prioritize interactive requests over background jobs.
+
+---
+
+### DOCUMENTATION GAPS
+
+### 49. Invite Code Entropy Not Quantified
+
+`PROJECT_SCOPE.md:35, 315` -- The first review (Finding #5) flagged the low entropy of invite codes like WOLF-42. The updated scope changes the format to WORD-WORD (e.g., WOLF-MOON), but does not specify the word list size. If the word list has 1,000 words, WORD-WORD yields 1,000,000 combinations -- brute-forceable in under a day even with rate limiting at 5 attempts/window. If the word list has 4,000 words, it yields 16,000,000 combinations, which is more robust but still feasible for a determined attacker. The rate limiting on the join endpoint ("5-10 failed attempts per IP per window") does not specify the window duration.
+
+**Fix:** (a) Specify the word list size (recommend 4,000+ words for ~22 bits of entropy, or add a third word for ~36 bits). (b) Specify the rate limit window duration (recommend 15 minutes). (c) Document the expected brute-force time given the word list size and rate limit parameters. (d) Consider adding a numeric suffix (WOLF-MOON-73) for additional entropy without significant UX cost.
+
+**Implementation Risk:** Larger word lists or three-word codes reduce memorability. WOLF-MOON-73 is still human-readable and provides ~29 bits with a 4,000-word list + 2-digit suffix. Rate limiting is the primary defense; code entropy is defense in depth.
+
+---
+
+### 50. PWA Manifest and Icon Requirements Not Specified
+
+`PROJECT_SCOPE.md:275, 481` -- The scope specifies `@serwist/next` for PWA support and mentions "home screen installation" but does not specify: (a) the `display` mode for the manifest (should be `standalone`), (b) required icon sizes (192x192 and 512x512 minimum, plus maskable variants for Android adaptive icons), (c) iOS-specific meta tags (`apple-mobile-web-app-capable`, `apple-touch-icon`, `apple-touch-startup-image` for splash screens), (d) manifest fields (`name`, `short_name`, `start_url`, `theme_color`, `background_color`). Without these specifications, Phase 8.1 implementation may miss requirements for cross-platform installability.
+
+**Fix:** Add a PWA configuration specification to the scope or Phase 8.1: (a) `display: "standalone"`, (b) Icons: 192x192, 512x512 (PNG), plus maskable variants with safe zone, (c) `name: "MovieDice"`, `short_name: "MovieDice"`, (d) `start_url: "/"`, (e) `theme_color` and `background_color` matching the app's color scheme, (f) iOS meta tags for Safari PWA support, (g) Service worker must NOT cache `/api/*` routes or WebSocket connections.
+
+**Implementation Risk:** Maskable icon design requires a safe zone (inner 80% circle). The app icon must look good both as a full-bleed and as a masked circle. iOS splash screen images require multiple resolutions -- `@serwist/next` may or may not auto-generate these.
+
+---
+
+### 51. Error Response Format Not Standardized
+
+`PROJECT_SCOPE.md:459` -- Finding #12 from the first review flagged the missing error response standard. The updated scope adds error handling in Phase 5.6 ("invalid invite code, TMDB API failure, network errors") but still does not define a standard error response shape, error codes, or error categorization. Without a standard, each API route will return errors in ad-hoc formats, making client-side error handling inconsistent and brittle.
+
+**Fix:** Define before Phase 2 implementation begins: (a) Standard error response: `{ error: { code: string, message: string, details?: unknown } }`. (b) Error code enum: `INVALID_INVITE_CODE`, `TMDB_UNAVAILABLE`, `TMDB_RATE_LIMITED`, `RATE_LIMITED`, `UNAUTHORIZED`, `NOT_FOUND`, `VALIDATION_ERROR`, `INTERNAL_ERROR`. (c) Map each code to a user-friendly message string. (d) Create a shared `createErrorResponse(code, details?)` utility used by all API routes. (e) Document error codes in the API markdown documentation.
+
+**Implementation Risk:** None. Defining this early prevents inconsistency. Retrofitting error formats after multiple API routes exist is much harder.
+
+---
+
+## Second Review Summary
+
+| Severity           | New Findings           |
+| ------------------ | ---------------------- |
+| Critical           | 3 (#34, #35, #36)      |
+| Compliance         | 4 (#37, #38, #39, #40) |
+| Infrastructure     | 4 (#44, #45, #46, #47) |
+| Performance        | 1 (#48)                |
+| Documentation Gaps | 3 (#49, #50, #51)      |
+
+**New findings total: 15** (numbered 34-51, continuing from first review)
+
+### Positives -- Improvements Since First Review
+
+- Server-side TMDB proxy now explicitly specified with `NEXT_PUBLIC_` prohibition -- eliminates API key exposure
+- TMDB attribution footer on all pages with logo + link + disclaimer -- ToS compliant
+- Privacy policy page added as Phase 5.1 deliverable
+- 12-month data retention policy with auto-delete -- addresses GDPR storage limitation
+- Recovery code hardened: 128-bit entropy, Argon2id, rate limiting, single-use -- solid specification
+- Invite code format improved to WORD-WORD with rate limiting on join
+- RLS policies defined per-table with specific access rules
+- prefers-reduced-motion respected for both animation types
+- Inline panel keyboard navigation with aria-expanded and role="region"
+- ESLint + Prettier + TypeScript strict + husky enforced from Phase 1
+- Vitest + Playwright testing established
+- Sentry monitoring moved to Phase 1 (was Phase 10)
+- Supabase migrations workflow via CLI
+- Security headers (CSP, HSTS, X-Frame-Options) specified
+- Self-service account deletion added to Extra Features backlog
+- Env validation via zod at startup
+- Docker infrastructure fully specified: multi-stage build, non-root user, tini, health check
+- iron-session with HttpOnly/Secure/SameSite=Strict and 8-hour expiry for admin
+- Caddy reverse proxy for HTTPS termination
+- TMDB native image sizes avoiding in-container sharp processing -- good performance decision
+- Real-time subscriptions scoped to active list only with useEffect cleanup
+
+### Top 5 Actions for Updated Scope
+
+1. Secure Supabase Studio -- do not expose publicly in docker-compose (Finding #36)
+2. Configure Docker log rotation and container security hardening (Findings #38, #44)
+3. Add `auth.users` cleanup to account deletion flows (Finding #37)
+4. Specify TMDB adult content filtering and 429 handling in proxy (Findings #35, #48)
+5. Add database backup mechanism to Phase 1.7 (Finding #47)

+ 714 - 0
research/PM_ASSESSMENT.md

@@ -0,0 +1,714 @@
+# PM Assessment — MovieDice Audit Consolidation
+
+Produced: 2026-04-05 | Assessor: Claude (PM Agent)
+Sources reviewed: TECHFILE.md (tech stack), SECFILE.md (security), COMPLIANCE.md (compliance)
+Deployment context: Docker container (primary), bare server (potential later). NOT Vercel.
+Supabase context: Self-hosted via Docker (NOT Supabase managed hosting). Full Supabase Docker stack (Postgres, GoTrue, Realtime, PostgREST, Kong, Studio).
+Constraint: User reviews each suggestion before any changes are made to PROJECT_SCOPE.md.
+
+---
+
+## Summary
+
+| Recommendation Level       | Count  |
+| -------------------------- | ------ |
+| MUST DO (before code)      | 7      |
+| MUST DO (during Phase 1)   | 5      |
+| MUST DO (during Phase 3+)  | 3      |
+| SHOULD DO                  | 11     |
+| NICE TO HAVE               | 5      |
+| SKIP/DEFER                 | 4      |
+| NEEDS DISCUSSION           | 4      |
+| **Total actionable items** | **39** |
+
+**Conflicts between reports:** 3 flagged (see Section at bottom)
+
+**Top 3 issues that would be expensive to fix post-launch:**
+
+1. Auth model (items A1/A2) — touches every DB query if changed after build
+2. RLS policies (item A3) — foundational to all data security; retrofitting is painful
+3. Vercel Cron references (item A4) — baked into Phase 5 MVP work; breaks on day one
+
+---
+
+## MUST DO (before code)
+
+_These are foundational decisions. Starting any Phase 1 work without resolving them means expensive rework._
+
+---
+
+### A1. Replace custom UUID auth with Supabase Anonymous Sign-In
+
+**Source:** TECHFILE (Finding 3, 11), SECFILE (Finding 4, 19)
+
+**Original Finding:**
+The scope proposes generating a UUID client-side, storing it in localStorage, and using it as the user's identity. This bypasses Supabase Auth entirely. Without a real JWT, Supabase's Row Level Security cannot use `auth.uid()` in policies — meaning the publicly-exposed anon key becomes a master key to the entire database. Additionally, a bare UUID in localStorage is vulnerable to XSS theft and trivial impersonation (anyone who knows a UUID can write it to their own localStorage and act as that user).
+
+**PM Assessment:**
+Valid and critical. This is the single highest-impact pre-code decision. The custom auth approach would require building session management, JWT issuance, and token refresh from scratch — all security-critical primitives that Supabase already implements correctly. Adopting `supabase.auth.signInAnonymously()` resolves the RLS gap, the localStorage spoofing risk, and the JWT management problem simultaneously. The user experience is identical: no email, no password, instant account. The recovery code flow still works as a layer on top. TECHFILE estimates this saves 2-3 days of Phase 1 work. The only reason not to do this is unfamiliarity with the Supabase Auth SDK — budget a half-day to read the anonymous sign-in docs before starting.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Revise Phase 1 tasks 1.3–1.6 to reference Supabase Anonymous Sign-In instead of custom UUID generation. Update the Data Model section: `users.id` becomes the Supabase Auth UID. Update the Technical Considerations section to reflect Supabase Auth as the auth mechanism.
+
+---
+
+### A2. Specify recovery code entropy and hashing algorithm
+
+**Source:** SECFILE (Finding 5), COMPLIANCE (Finding 4)
+
+**Original Finding:**
+The scope says recovery codes are "hashed before storage" but does not specify: code length/entropy, the hashing algorithm, whether a salt is used, rate limiting on the claim endpoint, or single-use enforcement. A weak recovery code (e.g., 8 characters) or a fast hash (SHA-256 without salt) can be brute-forced.
+
+**PM Assessment:**
+Valid and must be decided before implementation. This is a security design gap — the scope makes a promise ("hashed") without specifying how. The implementation choices here (bcrypt vs. Argon2id, 16 chars vs. 24 chars) need to be locked in before Phase 1.5 is built, not discovered mid-implementation. The fix is low-complexity: pick Argon2id (it is the modern recommendation over bcrypt), generate at least 128 bits of entropy (24 alphanumeric characters), add rate limiting on the claim endpoint, and invalidate the code after a successful claim. Both reports agree on these specifics.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Add entropy and algorithm specification to the Data Model section under `recovery_code`. Add rate limiting requirement to Phase 1.6.
+
+---
+
+### A3. Define and enable Supabase RLS policies on every table
+
+**Source:** TECHFILE (Finding 3), SECFILE (Findings 1, 2, 6), COMPLIANCE (Finding 14)
+
+**Original Finding:**
+All three reports flag this independently. No RLS policies are specified anywhere in the scope. Without them, the publicly-exposed Supabase anon key grants full read/write access to every row in every table. This affects: user data isolation, group data isolation, movie list privacy, admin session protection, and real-time subscription authorization. Supabase Realtime also respects RLS — without it, any client can subscribe to any group's movie changes.
+
+**PM Assessment:**
+Valid and critical. Flagged by all three reports — this is the most unanimously agreed-upon finding. RLS is not optional on Supabase; it is the fundamental authorization mechanism. The anon key is deliberately public. Every data access rule the scope describes (only group members can see a group's movies, only admins can remove members, etc.) must be enforced at the RLS layer. This cannot be retrofitted after data access code is written without touching every query. Define policies at the same time as the schema in Phase 1.2.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Add RLS policy definitions to Phase 1.2 as a required subtask alongside schema creation. Add a note to the Technical Considerations section specifying that RLS is enabled on all tables with explicit policies (no permissive catch-all).
+
+---
+
+### A4. Replace all Vercel Cron references with Supabase pg_cron
+
+**Source:** TECHFILE (Finding 1)
+
+**Original Finding:**
+The scope explicitly plans two background jobs using Vercel Cron: the bi-weekly landing reel poster refresh (Phase 5.2) and the trailer URL refresh (Phase 6.2). In a Docker deployment, `vercel.json` cron entries are ignored entirely — these jobs will never run. The landing page reel refresh is an MVP deliverable (Phase 5); it will be broken on the first Docker deployment.
+
+**PM Assessment:**
+Valid and critical for this project's deployment target. The scope was written with a Vercel-first assumption that conflicts with the stated deployment target. Since Supabase is self-hosted, pg_cron is directly available on the Postgres instance — no free tier limitations apply. Both jobs are fundamentally database operations (fetch from TMDB, write to Postgres), so running them from a pg_cron job that calls a Supabase Edge Function (running via the local Deno runtime in Docker) is architecturally cleaner and deployment-agnostic. This decision affects Phase 5.2 (MVP) and Phase 6.2 (post-MVP). Must be decided before Phase 5 is built; if a Vercel Cron implementation is coded as a placeholder it will need full rewriting.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Update Phase 5.2, Phase 6.2, and Phase 6.2 task descriptions to reference Supabase pg_cron + Edge Functions (self-hosted Deno runtime) instead of Vercel Cron. Update the Deployment section to remove Vercel Cron as a mechanism.
+
+---
+
+### A5. Proxy all TMDB API calls through Next.js server routes
+
+**Source:** TECHFILE (Finding 7), SECFILE (Finding 8), COMPLIANCE (Finding 1)
+
+**Original Finding:**
+All three reports flag this. The TMDB API key must not appear in client-side code or browser network requests. If calls are made from React components, the key is visible in the JS bundle and in DevTools network panel. TMDB's Terms of Use prohibit publicly exposing the API key. A leaked key can be used to exhaust your rate limit and get your key revoked. The landing page's unauthenticated TMDB calls further suggest the current design may intend client-side usage.
+
+**PM Assessment:**
+Valid, mandatory, and doubles as a compliance requirement (TMDB ToS). Routing all TMDB calls through a `/api/tmdb/*` Next.js Route Handler is the correct pattern. It keeps the key server-side, enables server-side caching with `Cache-Control` headers, and allows rate limiting the proxy endpoint. The slight added latency (one extra server hop) is fully offset by TanStack Query's client-side caching (which the scope already plans for). This is a Phase 1 architectural decision — all TMDB-calling code in Phases 3-5 should be written assuming a server-side proxy from the start.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Add API proxy route creation to Phase 1 (or early Phase 3). Add a note to Technical Considerations that `TMDB_API_KEY` must never use the `NEXT_PUBLIC_` prefix.
+
+---
+
+### A6. Add TMDB attribution to every page
+
+**Source:** COMPLIANCE (Finding 2)
+
+**Original Finding:**
+TMDB's Terms of Use require visible attribution on any app using their API: the TMDB logo, a link to themoviedb.org, and the disclaimer text "This product uses the TMDB API but is not endorsed or certified by TMDB." The scope makes no mention of this anywhere.
+
+**PM Assessment:**
+Valid and non-negotiable. This is a Terms of Service requirement, not a best practice. Violating it risks API key revocation, which would break the entire app. It is a low-effort fix: add the logo and text to a site footer that appears on all pages. Should be in Phase 1 or Phase 5 at the latest. Notably, this is the only finding from the compliance report that is both critical and low-effort — no architectural changes required, just a UI element.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Add a footer component requirement to Phase 1 (or Phase 5.1). Add a note to Technical Considerations referencing TMDB attribution requirements.
+
+---
+
+### A7. Configure `output: 'standalone'` and Docker base image before writing code
+
+**Source:** TECHFILE (Findings 5, 6)
+
+**Original Finding:**
+Two related infrastructure decisions that must be made before the first Docker build. (1) Without `output: 'standalone'` in `next.config.ts`, the Docker image will include the full `node_modules` tree and balloon to 500MB–1GB+. (2) Node.js 18 is EOL; Node.js 20 enters maintenance mode now (April 2026). A new project should start on Node.js 22 LTS.
+
+**PM Assessment:**
+Valid. Both are trivial configuration decisions that have zero cost to implement now and significant cost to change later (a Node version upgrade mid-project can break native dependencies; removing `output: 'standalone'` from a Dockerfile structure requires restructuring the entire multi-stage build). Pin Node 22 LTS (`node:22-slim`) and set `output: 'standalone'` in `next.config.ts` before Phase 1.1 is committed. TECHFILE notes one gotcha: `public/` and `.next/static/` must be copied separately in the Dockerfile since they are not included in standalone output. With self-hosted Supabase, the docker-compose.yml will reference both the Next.js app image and the Supabase service images — the Node version and standalone output only apply to the Next.js app container.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Update Deployment section to specify Node.js 22 LTS, `output: 'standalone'`, and self-hosted Supabase Docker stack. Update Phase 1.7 to include Docker configuration validation for both the app and Supabase services.
+
+---
+
+## MUST DO (during Phase 1)
+
+_These need to be built early to avoid blocking later phases, but do not require a decision before the first commit._
+
+---
+
+### B1. Add Docker infrastructure tasks to Phase 1
+
+**Source:** TECHFILE (Finding 8), SECFILE (Finding 12), COMPLIANCE (Finding 22)
+
+**Original Finding:**
+All three reports flag the Docker deployment gap. The scope has no Dockerfile, no docker-compose.yml, no health check endpoint, no reverse proxy specification, and no non-root user configuration. Running the container as root, baking secrets into the image, and having no health check are the specific risks called out.
+
+**PM Assessment:**
+Valid. Docker infrastructure is not optional infrastructure for a Docker-deployed app — it is the deployment plan. Since Supabase is also self-hosted, the Docker setup must orchestrate the full Supabase stack (Postgres, GoTrue, Realtime, PostgREST, Kong, Studio, etc.) alongside the Next.js app via docker-compose. The specific items needed before Phase 2 work begins: multi-stage Dockerfile for the Next.js app with `node:22-slim`, non-root user, `.dockerignore`, health check endpoint (`/api/health`), Supabase's official docker-compose.yml adapted for this project, and a Caddy or Traefik reverse proxy for HTTPS (required for service workers, Supabase Realtime's `wss://`, and secure cookies). The compliance report also notes that `tini` should be used for PID 1 signal handling. This work is currently only in Phase 5.7 ("Deploy skeleton to Vercel") and Phase 10.1 ("Final Vercel production deployment") — both wrong deployment target and too late.
+
+**Recommendation:** MUST DO (during Phase 1)
+
+**Scope Impact:** Add Docker infrastructure subtasks to Phase 1.7 (or a new 1.8). Update Deployment section to describe Docker + self-hosted Supabase + Caddy/Traefik architecture. Remove Vercel-specific deployment language from Phases 1 and 10. Add Supabase self-hosted docker-compose setup to Phase 1.2.
+
+---
+
+### B2. Invite code entropy increase and rate limiting
+
+**Source:** TECHFILE (Finding 9), SECFILE (Finding 3), COMPLIANCE (Finding 5)
+
+**Original Finding:**
+All three reports flag this independently. The `WOLF-42` format (word + 2-digit number) has ~100,000–200,000 combinations, which is trivially enumerable. Since the invite code is the sole access control for group membership, a brute-force attack lets an attacker join any group. All three reports also call for rate limiting on the join endpoint (5–10 failed attempts per IP per time window).
+
+**PM Assessment:**
+Valid. The entropy issue and rate limiting issue are separable: rate limiting is the higher-priority fix (it makes brute force impractical even with low entropy), and the format change is a secondary improvement. For rate limiting: implement server-side middleware on the join-code endpoint before Phase 2 ships. For code format: the scope can keep human-readable codes but should expand to at least `WORD-WORD` or `WORD-4DIGITS` to reach millions of combinations. This is a Phase 2 decision (group creation), so it needs to be locked in before Phase 2.1 is coded.
+
+**Recommendation:** MUST DO (during Phase 1)
+
+**Scope Impact:** Update Phase 2.1 to specify the expanded invite code format. Add a rate limiting requirement to Phase 2.2 (join flow). Optionally update the Data Model section's invite code description.
+
+---
+
+### B3. Admin session: HttpOnly cookie with iron-session (or equivalent)
+
+**Source:** TECHFILE (Finding 10), SECFILE (Finding 7)
+
+**Original Finding:**
+The scope notes `admin_sessions` as a "secure server-side token store" but does not specify the mechanism. A naive implementation (plain cookie, localStorage state) would be insecure for a panel that controls global deletion. The TOTP secret also has no rotation mechanism and no session expiry specified.
+
+**PM Assessment:**
+Valid. The admin session mechanism needs to be decided before Phase 7 is built — but Phase 7 is post-MVP (May 4-17). The recommendation to use `iron-session` (encrypted cookie, no DB required) is solid for a single-admin use case. The admin session should expire after 8 hours (or shorter with activity extension) and be bound to an HttpOnly, Secure, SameSite=Strict cookie. The TOTP rotation gap (requires redeployment to change the secret) is a documented operational constraint that is acceptable for MVP — document it explicitly rather than building a rotation UI. Also: remove `is_master_admin` from the `users` table (see item D1 below) since admin status is determined by the session, not a DB flag.
+
+**Recommendation:** MUST DO (during Phase 1) — decide the session mechanism now; implement in Phase 7
+
+**Scope Impact:** Update Phase 7 tasks to specify `iron-session` (or equivalent). Add session expiry requirement. Add TOTP rotation procedure documentation to Phase 7.1. Consider removing `is_master_admin` from the Data Model.
+
+---
+
+### B4. Input validation for display names and group names
+
+**Source:** SECFILE (Findings 9, 15), COMPLIANCE (implied)
+
+**Original Finding:**
+User-entered display names and group names have no validation rules. Both are rendered throughout the app in other users' browsers. Without server-side length limits and character restrictions, these fields are potential stored XSS vectors and layout-breaking inputs.
+
+**PM Assessment:**
+Valid. React's default JSX escaping provides strong baseline XSS protection (it escapes `<`, `>`, `&`, etc. in rendered text), so stored XSS via display names is low-risk as long as developers consistently avoid `dangerouslySetInnerHTML`. However, the SECFILE recommendation to add `CHECK` constraints in Supabase and server-side validation is still correct: it's a one-time setup that prevents garbage data and gives a clear contract. Recommended limits: display name 1-30 characters, group name 1-50 characters, basic character set that allows Unicode letters (don't restrict to Latin-only — international users exist) but rejects HTML angle brackets and control characters. Add these to the schema in Phase 1.2.
+
+**Recommendation:** MUST DO (during Phase 1)
+
+**Scope Impact:** Add CHECK constraints to Phase 1.2 schema creation. Minimal scope document change needed — could be a note under Data Model.
+
+---
+
+### B5. Environment variable validation at startup
+
+**Source:** COMPLIANCE (Finding 24)
+
+**Original Finding:**
+Five required environment variables are listed in the scope but nothing validates them at startup. Missing or malformed variables will produce cryptic runtime errors rather than a clear startup failure.
+
+**PM Assessment:**
+Valid and trivially easy. Using `t3-env` or `zod` to validate env vars at build/startup time is a 30-minute task with zero downside. Catches misconfigured deployments immediately. Should be in Phase 1.3 (when Supabase env vars are first configured). This is one of the lowest-effort, highest-reliability items in the entire audit.
+
+**Recommendation:** MUST DO (during Phase 1)
+
+**Scope Impact:** Add env validation library to Phase 1.3 task. No scope document structural changes needed.
+
+---
+
+## MUST DO (during Phase 3 or later)
+
+_These are valid and important but can be deferred until the relevant feature phase._
+
+---
+
+### C1. HTTP security headers (CSP, HSTS, X-Frame-Options, etc.)
+
+**Source:** SECFILE (Finding 13)
+
+**Original Finding:**
+No security headers are specified. Without them: clickjacking via iframes is possible, XSS is easier to exploit, MIME sniffing can be weaponized, and HSTS is absent. The recommended header set includes CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and HSTS.
+
+**PM Assessment:**
+Valid. Security headers are configured once in `next.config.js` (or at the reverse proxy level for Docker) and apply globally. The main complexity is getting the CSP right: Tailwind may need `style-src 'unsafe-inline'`, TMDB images need `img-src https://image.tmdb.org`, and Supabase Realtime needs `connect-src wss://*.supabase.co`. The SECFILE provides a reasonable starting CSP — use `Content-Security-Policy-Report-Only` mode during development to catch violations without blocking. This should be done during Phase 5 (Landing Page and MVP Polish) since the CSP must account for all third-party resources once the app is complete.
+
+**Recommendation:** MUST DO (during Phase 5)
+
+**Scope Impact:** Add security headers task to Phase 5 (MVP Polish) or Phase 1 initial config. No structural scope changes needed.
+
+---
+
+### C2. Define the TMDB image strategy before Phase 3
+
+**Source:** TECHFILE (Findings 2, 13), COMPLIANCE (Findings 16, 26)
+
+**Original Finding:**
+Multiple related findings across two reports converge on the same decision. In Docker, Next.js `<Image>` optimization runs `sharp` in-container (CPU/memory pressure under load). TMDB already serves posters at discrete sizes (`w92`, `w154`, `w185`, `w342`, `w500`). Using TMDB's native sized URLs with a plain `<img>` tag bypasses the optimization pipeline entirely and uses TMDB's own CDN — better for performance, simpler for Docker, and avoids any ToS gray area around re-serving images through the Next.js proxy. COMPLIANCE flags a ToS concern: if Next.js image optimization proxies TMDB images through the app's own domain, this may violate TMDB's requirement that images be served from their CDN.
+
+**PM Assessment:**
+Valid, and the two reports converge on the same solution. Decision: use TMDB native sized URLs directly (`w342` for grid on mobile, `w185` for reel animation, `w500` for expanded panel) with plain `<img>` tags (or `<Image unoptimized>`). Install `sharp` as an explicit production dependency anyway (for any locally-served assets where optimization is valuable, like the logo). Add `image.tmdb.org` to `remotePatterns` in `next.config.js` regardless. This decision must be made before Phase 3 (Movie List Core) since the poster grid is the first TMDB image-heavy component.
+
+**Recommendation:** MUST DO (during Phase 3)
+
+**Scope Impact:** Add an image strategy note to Technical Considerations. Add `sharp` installation to Phase 1.1 setup. No major structural changes.
+
+---
+
+### C3. Real-time subscription lifecycle management
+
+**Source:** SECFILE (Finding 6), COMPLIANCE (Finding 17)
+
+**Original Finding:**
+Two reports flag the real-time subscriptions issue from different angles. SECFILE: without RLS, subscriptions are unauthorized. COMPLIANCE: subscription count limits, reconnection logic, and cleanup in `useEffect` return functions are unaddressed. Users in multiple groups should only subscribe to the currently-viewed list, not all lists simultaneously.
+
+**PM Assessment:**
+Valid. The RLS piece is already covered by A3. The lifecycle management piece (subscribe on mount, unsubscribe on unmount, one subscription per viewed list, exponential backoff reconnection) is a Phase 3.9 implementation detail that should be specified before that task is built. The compliance report's suggestion to use polling for home page movie counts (rather than full subscriptions) instead of maintaining subscriptions for all groups is pragmatic and correct. Not a scope document change — more of an implementation constraint for Phase 3.9.
+
+**Recommendation:** MUST DO (during Phase 3)
+
+**Scope Impact:** Add subscription lifecycle requirements as a note to Phase 3.9. Minimal structural scope change.
+
+---
+
+## SHOULD DO
+
+_Valid findings that meaningfully improve quality, security, or compliance but are not blockers._
+
+---
+
+### D1. Remove `is_master_admin` from the users table
+
+**Source:** SECFILE (Finding 20)
+
+**Original Finding:**
+The `is_master_admin (boolean, default false)` column on the users table is redundant — admin status is determined by a valid TOTP-authenticated session, not a DB flag. Worse, if RLS policies are misconfigured, this flag could be set by a client to self-escalate privileges.
+
+**PM Assessment:**
+Valid and simple fix. Remove the column from the Data Model. Admin status checked via the session only. Zero implementation cost since no code exists yet. This is a pre-code decision with no downside.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Remove `is_master_admin` from the users table definition in Section 7.
+
+---
+
+### D2. Add serwist instead of next-pwa for Phase 8 PWA support
+
+**Source:** TECHFILE (Finding 4)
+
+**Original Finding:**
+`next-pwa` (the most commonly referenced PWA package) is effectively unmaintained since 2022 — no App Router support, open security issues, outdated Workbox. The community-maintained successor `@serwist/next` supports App Router, Workbox 7, TypeScript, and is actively maintained.
+
+**PM Assessment:**
+Valid and a simple substitution. Phase 8 (PWA) is post-MVP and well ahead, so this is not urgent. When Phase 8 begins, use `@serwist/next` from the start rather than `next-pwa`. The API is similar; the main difference is writing the service worker in `app/sw.ts`.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Update Phase 8.1 to reference `@serwist/next` instead of `next-pwa`.
+
+---
+
+### D3. Implement structured logging from Phase 1 (not Phase 10)
+
+**Source:** SECFILE (Finding 18), COMPLIANCE (Finding 23)
+
+**Original Finding:**
+The scope defers error monitoring to Phase 10.3 ("basic error monitoring — Vercel logs + Sentry free tier"). For a Docker-deployed app, structured logging (JSON to stdout, captured by Docker logging driver) must be in place from Phase 1 to debug issues in production. Sentry error tracking is also more valuable when added early (it catches issues across the entire development period, not just post-launch).
+
+**PM Assessment:**
+Mostly valid. The COMPLIANCE report makes the stronger case: logging is harder to retrofit and production issues during the soft launch (Phase 10.4) will be impossible to investigate without it. However, full structured logging with `pino` in Phase 1 is overkill for a solo developer or small team. A pragmatic middle ground: add Sentry in Phase 3 (when real features are being built), use `console.error` with structured JSON early on, and do a proper logging pass in Phase 9 or 10 before the soft launch. The Phase 10.3 placement for monitoring is too late for a Docker deployment.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Move Sentry/monitoring setup from Phase 10.3 to Phase 3 or Phase 5. Update Phase 10.3 to be a verification step rather than initial setup.
+
+---
+
+### D4. Establish linting, TypeScript strict mode, and Prettier before first commit
+
+**Source:** COMPLIANCE (Finding 7)
+
+**Original Finding:**
+No code quality tooling is specified. ESLint, Prettier, TypeScript strict mode, and a pre-commit hook should be established before any code is written.
+
+**PM Assessment:**
+Valid. This is a half-day setup that prevents significant debt accumulation. TypeScript strict mode in particular catches a class of bugs that are expensive to fix later if the codebase grows without it. `next/core-web-vitals` + `next/typescript` ESLint presets cover most of the important rules. `husky` + `lint-staged` for pre-commit enforcement is standard. Should be in Phase 1.1 alongside project initialization.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add linting/TypeScript setup subtask to Phase 1.1.
+
+---
+
+### D5. Define a minimum automated testing strategy
+
+**Source:** COMPLIANCE (Finding 8)
+
+**Original Finding:**
+Phase 9 is entirely manual QA. No automated tests are specified at any level. For a real-time collaborative app with complex state (group membership, watched state, cross-list rolls), manual testing alone will produce regressions.
+
+**PM Assessment:**
+Partially valid. Full test coverage from Phase 1 is overkill for a solo developer on an MVP deadline of April 26. However, the scope has complex business logic (emotion-to-genre mapping, ownership transfer flows, RLS policy enforcement) that is excellent candidate for unit tests. Recommended pragmatic approach: add Vitest for unit tests of pure logic (invite code generation, emotion mapping, recovery code hashing) in Phase 1. Add Playwright E2E for the three critical paths (onboarding, add movie + real-time sync, roll the dice) in Phase 4 or 5. Skip component-level tests for MVP. This is less than the COMPLIANCE report recommends but more than the current scope has.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add Vitest setup to Phase 1.1. Add Playwright E2E for critical paths to Phase 5 (MVP Polish) or Phase 9. No full CI gate needed for MVP deadline.
+
+---
+
+### D6. `prefers-reduced-motion` support for both animations
+
+**Source:** COMPLIANCE (Finding 30)
+
+**Original Finding:**
+Both animations (slot-machine reel, scatter/eliminate) have no motion preference handling. WCAG 2.3.3 and general accessibility best practice require respecting the `prefers-reduced-motion` media query. Users with vestibular disorders can be physically affected by spinning/scattering animations.
+
+**PM Assessment:**
+Valid and achievable. This is a CSS media query check plus a fallback state for both animations. For the landing page reel: instant reveal or simple fade-in when reduced motion is preferred. For the in-app roll: fade-in on winner rather than scatter animation. The full animation is still the default. This is genuinely WCAG-relevant (not theoretical) given that the animations are the visual centerpiece. Should be implemented in Phase 4 (Randomizer) and Phase 5 (Landing Page) alongside the animation builds. Does not require a separate implementation phase.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add `prefers-reduced-motion` note to both animation implementation tasks (Phase 4.3, Phase 5.3) and to the Usability Concerns section.
+
+---
+
+### D7. Inline panel keyboard navigation and focus management
+
+**Source:** COMPLIANCE (Finding 31)
+
+**Original Finding:**
+The inline panel expansion has no specified keyboard navigation: how a keyboard user opens/closes the panel, focus management on open/close, Escape key to close, `aria-expanded` on trigger, `role="region"` on panel.
+
+**PM Assessment:**
+Valid accessibility requirement. The inline panel is the primary interaction for viewing movie details. Without focus management, screen reader users and keyboard users cannot use the core app feature. Phase 8.3 already has an "accessibility pass" task — this work belongs there. The scope's Usability Concerns section mentions 44x44px tap targets and screen reader labels on icon buttons, so accessibility is already on the radar; the inline panel focus management is an extension of that.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add inline panel keyboard/focus management requirements to Phase 8.3. Add a note to Section 6 (Usability Concerns) about keyboard navigation for the inline panel.
+
+---
+
+### D8. Implement a CI pipeline before the MVP cutoff
+
+**Source:** COMPLIANCE (Finding 9)
+
+**Original Finding:**
+No CI/CD pipeline is specified. For Docker deployment: a GitHub Actions workflow running lint, type-check, and build on every PR; Docker image build verification; dependency vulnerability scanning.
+
+**PM Assessment:**
+Valid but scoped to what's realistic for the April 26 MVP deadline. A full CI pipeline with E2E tests and staging environments is post-MVP. The minimum viable CI for this project is: lint + type-check on every push (5-minute setup via GitHub Actions), and a Docker build test before the Phase 5.7 deployment. Dependabot/Renovate for dependency scanning can be enabled in one click and runs automatically. This is the practical floor, not the ceiling.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add GitHub Actions setup to Phase 1 or Phase 5. Minimal — a note in the Deployment section.
+
+---
+
+### D9. Add privacy policy page
+
+**Source:** COMPLIANCE (Finding 3, 27, 29)
+
+**Original Finding:**
+Three related compliance findings: (1) No privacy policy exists. (2) The ePrivacy Directive requires disclosure of any persistent identifiers stored on the user's device. (3) No data retention periods are defined. The compliance report also notes the privacy section's claim that "no personal data beyond display name is stored" is incorrect under GDPR — UUIDs, group membership data, movie preferences, and IP addresses in server logs are all personal data.
+
+**PM Assessment:**
+Partially valid. MovieDice's data footprint is genuinely minimal (anonymous UUID, display name, movie preferences — no email, no location, no payment info), which simplifies the privacy policy. The ePrivacy Directive concern about storing user IDs is satisfied if the storage is "strictly necessary" for the service to function (it is). The GDPR right-to-erasure issue is covered by item D10 below. A short, honest privacy policy page that accurately describes what is stored is sufficient and should be part of the Phase 5 landing page build. No legal review is required for a simple hobbyist/small-scale app, but the document should be factually accurate.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add a privacy policy page to Phase 5.1 (landing page build). Add footer with privacy policy link to the landing page layout.
+
+---
+
+### D10. Self-service account deletion for users
+
+**Source:** SECFILE (Finding 16), COMPLIANCE (Finding 28)
+
+**Original Finding:**
+No self-service account deletion flow exists. GDPR Article 17 (Right to Erasure) requires users can request deletion of their data. The scope only allows Master Admin deletion of users — there is no user-facing delete option.
+
+**PM Assessment:**
+Valid. Both reports flag this. The implementation is complex (cascading deletes, ownership transfer for groups the user admins, anonymizing `added_by` references in movies) but the feature itself belongs in the post-MVP plan, not the MVP. The compliance gap is partially mitigated by the Master Admin's ability to delete users on request — acceptable for MVP where real user volume is minimal. This should be added to the Extra Features table now so it is not forgotten, with a target of Phase 8 or 9.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add to Extra Features table or post-MVP roadmap. Add cascade delete behavior notes to the Data Model section for `users`.
+
+---
+
+### D11. Database migration strategy using Supabase migrations
+
+**Source:** COMPLIANCE (Finding 13)
+
+**Original Finding:**
+No schema migration strategy is specified. Ad-hoc schema changes during development are untrackable and irreproducible across environments.
+
+**PM Assessment:**
+Valid. Using `supabase migration new` and storing migrations in version control is the correct approach. This is a Phase 1 setup decision with no ongoing overhead once established. Supabase's CLI makes this straightforward. Must be in place before the Phase 1.2 schema is first created.
+
+**Recommendation:** SHOULD DO
+
+**Scope Impact:** Add migration workflow note to Phase 1.2.
+
+---
+
+## NICE TO HAVE
+
+_Real improvements, but low priority relative to everything above. Post-MVP or opportunistic._
+
+---
+
+### E1. Virtualized scrolling for the movie grid
+
+**Source:** COMPLIANCE (Finding 18)
+
+**Original Finding:**
+Infinite scroll appends cards to the DOM indefinitely. For large lists (50-200+ movies with full-bleed poster images), all loaded cards remain in the DOM simultaneously, causing memory bloat on low-end mobile devices. `@tanstack/react-virtual` or `react-window` would keep DOM node count constant.
+
+**PM Assessment:**
+Real concern but a post-MVP optimization. The complicating factor is the inline panel expansion: it inserts a full-width element between grid rows, which breaks simple list virtualization. A virtualized grid with inline expansion is a non-trivial implementation. For MVP, 12 items initially + infinite scroll is reasonable; most groups won't hit 50+ movies in early usage. Revisit if performance testing in Phase 8.4 shows actual issues.
+
+**Recommendation:** NICE TO HAVE
+
+**Scope Impact:** None for MVP. Could be added to Phase 8.4 (Performance Tuning) as a conditional task.
+
+---
+
+### E2. Add a `/api/health` endpoint
+
+**Source:** SECFILE (Finding 12), COMPLIANCE (Findings 22, 23)
+
+**Original Finding:**
+Multiple reports call for a health check endpoint for Docker `HEALTHCHECK` and uptime monitoring. Should check Supabase connectivity and return a structured response.
+
+**PM Assessment:**
+Valid and genuinely low effort. A health endpoint is one file, ~20 lines. Valuable for Docker `HEALTHCHECK` (prevents traffic routing to an unhealthy container) and for uptime monitoring tools. Should be in Phase 1.7 alongside the Docker skeleton deployment.
+
+**Recommendation:** NICE TO HAVE (but very easy — could be SHOULD DO)
+
+**Scope Impact:** Add to Phase 1.7.
+
+---
+
+### E3. Trailer URL validation before storage
+
+**Source:** SECFILE (Finding 17)
+
+**Original Finding:**
+Trailer URLs are fetched from TMDB and stored as-is. If a stored URL is corrupted or manipulated, users could be redirected to a malicious site. Validate URLs match expected YouTube/TMDB patterns before storage.
+
+**PM Assessment:**
+Low risk in practice. TMDB is the data source and is a trusted third party. The realistic attack vector here is extremely narrow. However, adding `rel="noopener noreferrer"` to the trailer link (already standard practice in React for `target="_blank"`) and a domain allowlist check (must be youtube.com or themoviedb.org) before storage is a 10-line addition that is worth including in Phase 3.3 (add-movie flow).
+
+**Recommendation:** NICE TO HAVE
+
+**Scope Impact:** Add URL validation note to Phase 3.3.
+
+---
+
+### E4. Add a `--nnnn` or `WORD-WORD` format for CORS on API routes
+
+**Source:** SECFILE (Finding 14)
+
+**Original Finding:**
+No CORS configuration is specified on API routes. For a same-origin app served from a Docker container, CORS misconfiguration on API routes (e.g., `Access-Control-Allow-Origin: *`) would allow any website to make authenticated requests.
+
+**PM Assessment:**
+Lower priority than the other security findings. For a Docker deployment where frontend and API are on the same origin, CORS is not an active concern — the browser's same-origin policy handles it. The risk is only if someone writes a new API route and adds a permissive CORS header. The fix is: add a linting rule or code review note. Not worth a scope document change.
+
+**Recommendation:** NICE TO HAVE
+
+**Scope Impact:** None — a developer convention note.
+
+---
+
+### E5. Invite code expiry
+
+**Source:** SECFILE (Finding 3)
+
+**Original Finding:**
+The SECFILE suggests invite codes should have an optional expiry (e.g., 7 days) so old codes cannot be used indefinitely.
+
+**PM Assessment:**
+Nice-to-have. The scope already includes invite code regeneration (the admin's tool for revoking access), which addresses the main use case. Automatic expiry adds complexity to the join flow ("this code has expired" error state) for limited benefit given that regeneration is available. Post-MVP consideration.
+
+**Recommendation:** NICE TO HAVE
+
+**Scope Impact:** None for MVP.
+
+---
+
+## SKIP / DEFER
+
+_Either overkill for this project's scale and stage, or the concern is not actionable._
+
+---
+
+### F1. Full CI/CD with staging environment
+
+**Source:** COMPLIANCE (Finding 9)
+
+**PM Assessment:**
+Defer. A full staging environment with pre-production validation and Docker image smoke tests is reasonable for a team project but overkill for an MVP solo build. The minimum CI (lint + type-check + build) is captured in D8 above. A staging environment can be added post-launch.
+
+**Recommendation:** SKIP/DEFER
+
+---
+
+### F2. API route documentation with OpenAPI spec
+
+**Source:** COMPLIANCE (Finding 11)
+
+**PM Assessment:**
+Skip for MVP. This is a documentation standard appropriate for a team with external API consumers. For a single-developer project with internal-only routes, JSDoc headers on Route Handlers are sufficient. OpenAPI generation can be added if the project grows a team.
+
+**Recommendation:** SKIP/DEFER
+
+---
+
+### F3. CAPTCHA or proof-of-work on public endpoints
+
+**Source:** SECFILE (Finding 11)
+
+**PM Assessment:**
+Skip for MVP. IP-based rate limiting on public endpoints (covered in item A5 via server-side middleware) is sufficient to deter automated abuse at this scale. CAPTCHA adds meaningful UX friction for users who have "low tolerance for signup friction" (per the scope's own user trait). Revisit if abuse is observed post-launch.
+
+**Recommendation:** SKIP/DEFER
+
+---
+
+### F4. Password alongside TOTP for Master Admin
+
+**Source:** SECFILE (Finding 7)
+
+**PM Assessment:**
+Skip. The scope explicitly says "no password-only fallback" and uses username + TOTP. Adding a password would require storing and managing a password credential, adding a password reset flow, and contradicting the established design decision. TOTP alone (when implemented correctly with IP binding and session expiry per item B3) is sufficient for a single-admin panel used by the site owner.
+
+**Recommendation:** SKIP/DEFER
+
+---
+
+## NEEDS DISCUSSION
+
+_These findings involve real tradeoffs or design tensions that the user should weigh in on._
+
+---
+
+### G1. Emotion-to-genre mapping: static TypeScript constant vs. database table
+
+**Source:** TECHFILE (Finding 14), COMPLIANCE (Finding 10)
+
+**PM Assessment:**
+Both reports agree the mapping should not be hardcoded as if-else logic, but diverge on the right home for it. TECHFILE recommends a static TypeScript `const` object in a config file (never changes without a deploy). COMPLIANCE recommends a database table (allows runtime updates without a deploy). For MVP, the TypeScript constant is clearly correct — the mapping is fixed, known at build time, and never changes without a deliberate decision. The "move to DB post-MVP for runtime updates" suggestion from COMPLIANCE is a valid future option but adds a query per Genre Roll with no immediate benefit. The scope's Section 10 table is the right source of truth; it just needs to be translated to TypeScript numeric TMDB genre IDs during Phase 4.6 implementation.
+
+**Recommendation:** NEEDS DISCUSSION (recommend static TypeScript constant for MVP)
+
+**Scope Impact:** None — implementation detail for Phase 4.6.
+
+---
+
+### G2. Offline caching strategy for the PWA
+
+**Source:** COMPLIANCE (Finding 33)
+
+**PM Assessment:**
+The scope says "show cached list, disable write actions." The compliance report asks for more detail: what is precached (app shell, fonts, CSS/JS), how is list data cached (TanStack Query persistence plugin or service worker), and what happens to in-flight writes when connection drops. The "do not queue offline writes" recommendation in the compliance report is the right call for MVP (conflict resolution is a v2 problem). But the specific caching library approach (TanStack Query `persistQueryClient` vs. service worker cache) needs a decision before Phase 8.2 is built.
+
+**Recommendation:** NEEDS DISCUSSION (decide caching mechanism before Phase 8)
+
+**Scope Impact:** Phase 8.2 description could be more specific about the caching strategy once decided.
+
+---
+
+### G3. GDPR data retention periods
+
+**Source:** COMPLIANCE (Finding 29)
+
+**PM Assessment:**
+The compliance report recommends auto-deleting inactive accounts after 12 months. For an app without email, notifying users before deletion is impossible — they could lose their account silently. This is a genuine design tension: GDPR compliance favors bounded retention, but the user experience favors persistence. The pragmatic MVP answer: store data indefinitely (the data footprint is minimal and the compliance risk is low at small scale), add account deletion on request via Master Admin, and revisit retention policy before any public marketing or scale-up. Add retention policy to the privacy policy page (item D9) as a deferred decision note.
+
+**Recommendation:** NEEDS DISCUSSION
+
+**Scope Impact:** Add a retention policy note to the Privacy section.
+
+---
+
+### G4. Pre-render the landing page as a static page with ISR
+
+**Source:** TECHFILE (Finding 12)
+
+**PM Assessment:**
+The TECHFILE recommends making the landing page root layout a Server Component with ISR-fetched reel posters (revalidate every ~12 hours instead of fetching from DB on every request) and a thin Client Component wrapper for the localStorage redirect check. This is architecturally sound and directly addresses the user's stated priority of fast page loads. However, it requires careful Server/Client Component boundary design: the redirect check must be in a Client Component (localStorage is browser-only), while the static shell can be a Server Component. The risk is a hydration mismatch if the boundaries are drawn incorrectly. This is worth doing but requires developer familiarity with Next.js App Router's Server/Client Component model.
+
+**Recommendation:** NEEDS DISCUSSION (recommend doing it; assess team familiarity with App Router patterns first)
+
+**Scope Impact:** Phase 5.1 could be updated to specify Server Component + Client Component boundary design for the landing page.
+
+---
+
+## Conflicts Between Reports
+
+### Conflict 1: next/image vs. plain `<img>` for posters
+
+**TECHFILE** recommends bypassing `next/image` for TMDB poster images entirely and using plain `<img>` with TMDB's sized URLs. This avoids Docker's in-container `sharp` overhead and uses TMDB's CDN directly.
+
+**COMPLIANCE** recommends _using_ Next.js `<Image>` component with a custom TMDB loader for all poster images, citing lazy loading, blur placeholders, and WebP conversion.
+
+**PM Resolution:** TECHFILE's recommendation is correct for this deployment target. The COMPLIANCE report was not written with Docker deployment context in mind. In Docker, image optimization runs in-container and creates CPU/memory pressure under load — the opposite of the user's fast page load goal. TMDB already serves optimized JPEGs from their own CDN at discrete sizes. Using `<img>` with TMDB's native sizes (`w342` for grid, `w500` for expanded panel) is both simpler and faster. Reserve `next/image` for locally-served assets only. Add lazy loading via the native `loading="lazy"` attribute on `<img>` tags (no `next/image` required).
+
+---
+
+### Conflict 2: Severity rating of missing RLS
+
+**SECFILE** rates missing RLS as Critical (2 findings at Critical level).
+**COMPLIANCE** rates it as borderline-critical under Documentation Gaps.
+**TECHFILE** treats it as Critical (Finding 3).
+
+**PM Resolution:** No practical conflict — all three reports agree it must be done. The severity label disagreement is a classification difference, not a substantive disagreement. Treat as Critical (item A3 above).
+
+---
+
+### Conflict 3: When to add monitoring/Sentry
+
+**COMPLIANCE** argues Sentry should be added in Phase 1 to catch issues throughout development.
+**PROJECT_SCOPE.md** places basic error monitoring in Phase 10.3 (final phase).
+
+**PM Resolution:** Partially resolved as item D3 above. The recommendation is Phase 3 as a compromise — early enough to be useful during active feature development, without the overhead of setting it up during Phase 1 when the project structure is still being established. The current Phase 10.3 placement is too late for a Docker deployment where server logs are the primary debugging tool.
+
+---
+
+## Notes on Items Not Requiring Scope Changes
+
+The following findings from the reports are valid implementation notes but do not require changes to PROJECT_SCOPE.md — they are implementation details:
+
+- TECHFILE Finding 14 (emotion map as TypeScript constant): Phase 4.6 implementation note
+- TECHFILE Finding 7 (TanStack Query `staleTime`): Phase 3.1/4 implementation note
+- SECFILE Finding 22 (invite code rotation clarification): Documentation note, not a functional gap
+- COMPLIANCE Finding 12 (error response standard): A development convention to establish in Phase 1; minimal scope impact
+- COMPLIANCE Finding 15 (reel animation poster size): Phase 5.3 implementation note; use `w185` for reel
+- COMPLIANCE Finding 19 (cross-list roll query with index): Phase 4.2 implementation note; add composite index
+- COMPLIANCE Finding 20 (trailer refresh rate limiting): Phase 6.1 implementation note
+- COMPLIANCE Finding 21 (code splitting / `next/dynamic`): Phase 5 implementation practice; App Router does route-based splitting automatically
+
+---
+
+_This assessment is for review only. PROJECT_SCOPE.md has not been modified. Each item above should be reviewed individually before any changes are made to the scope document._

+ 478 - 0
research/PM_ASSESSMENT_R2.md

@@ -0,0 +1,478 @@
+# PM Assessment — Round 2 Audit Findings
+
+**Prepared:** 2026-04-05
+**Scope version reviewed:** Post-Round-1 PROJECT_SCOPE.md
+**Sources:** TECHFILE.md (Second Review), SECFILE.md (Second Review), COMPLIANCE.md (Second Review)
+**Status:** Research/analysis only — PROJECT_SCOPE.md NOT modified
+
+---
+
+## Summary
+
+| Priority Level                | Count  |
+| ----------------------------- | ------ |
+| MUST DO (before code)         | 7      |
+| MUST DO (during Phase 1)      | 6      |
+| SHOULD DO                     | 8      |
+| NICE TO HAVE                  | 3      |
+| SKIP / DEFER                  | 2      |
+| NEEDS DISCUSSION              | 4      |
+| **Total actionable findings** | **30** |
+
+**Deduplicated findings:** 30 unique items derived from 30 raw findings across the three reports. Three near-duplicates were merged (noted below). No conflicts between reports — the three audits are complementary and non-contradictory on all overlapping topics.
+
+**Conflicts between reports:** None. SECFILE finding 24 and COMPLIANCE finding 36 cover the same issue (Supabase Studio exposure) and have been merged. SECFILE finding 27 and TECHFILE finding R7 both address Kong/Supabase network architecture and have been cross-referenced but are distinct enough to keep separate.
+
+---
+
+## PRIORITY 1 — MUST DO (before writing any code)
+
+These are items that will break the implementation if not resolved first, or that are architectural decisions that cannot be retrofitted cheaply.
+
+---
+
+### Finding A1 — GoTrue Anonymous Sign-In Is Disabled by Default in Self-Hosted Supabase
+
+**Source:** TECHFILE C1
+**Original Finding:** Self-hosted GoTrue requires `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` in the docker-compose `auth` service environment. Without it, `signInAnonymously()` returns a `400` error. The Studio UI setting does not survive container recreation — the env var is the only durable configuration.
+
+**PM Assessment:** This is a silent, total blocker for the entire onboarding flow. It takes five seconds to add the env var and zero seconds to forget it. The failure mode ("failed auth call") gives no indication the problem is configuration, not code — it will look like a bug and waste significant debugging time. This must be treated as a pre-flight checklist item for Phase 1.2 when the Supabase stack is first stood up.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** Add `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true` to the env var list in §7 Deployment. Add a note to task 1.2 to verify this is set before Phase 1.4 work begins. Optionally extend the `/api/health` endpoint (task 1.7) to verify GoTrue reachability.
+
+---
+
+### Finding A2 — Edge Functions Do Not Exist in the Default Self-Hosted Docker Stack
+
+**Source:** TECHFILE C2
+**Original Finding:** The standard Supabase self-hosted docker-compose does NOT include the `supabase/edge-runtime` (Deno) container. The scope's plan of `pg_cron → net.http_post() → Edge Function` will silently fail with 404s — the background jobs for landing reel refresh (task 5.2) and trailer URL refresh (task 6.2) will never run. Additionally, `pg_cron` calling `net.http_post()` also requires the `pg_net` extension, which is not mentioned in the scope.
+
+**PM Assessment:** This is a critical architecture decision that affects two MVP deliverables (5.2 is in the MVP cutoff phase). The finding offers a clear simpler alternative: replace the Edge Function approach with a small Node.js cron container (`node:22-alpine` + `node-cron`) that calls TMDB and writes directly to Postgres via the service role key. That alternative eliminates the Deno runtime entirely, reduces operational complexity, and is easier to debug.
+
+Given the April 26 MVP deadline, the Node.js cron container alternative is strongly preferable. Adding and correctly wiring the `supabase/edge-runtime` container is non-trivial and introduces a new runtime to operate. The Node.js cron container uses the same language and toolchain already in the project.
+
+**Recommendation:** MUST DO (before code) — but this is a scope decision, not just an additive fix. The architecture of the background jobs must be decided before Phase 1.7.
+
+**Scope Impact:** Either (a) replace "pg_cron + Edge Functions" throughout the scope with "Node.js cron container (node:22-alpine + node-cron)" and add `pg_net` to the extensions list — OR — (b) add the `supabase/edge-runtime` container to the docker-compose spec and add `pg_net` extension. Task 5.2, 6.2, and the §7 Deployment description all reference this. Recommend option (a).
+
+---
+
+### Finding A3 — All Supabase Default Secrets Must Be Replaced Before First Deployment
+
+**Source:** SECFILE 23
+**Original Finding:** The official Supabase self-hosted `.env` ships with well-known placeholder values for `JWT_SECRET`, `POSTGRES_PASSWORD`, `ANON_KEY`, `SERVICE_ROLE_KEY`, `DASHBOARD_USERNAME`, and `DASHBOARD_PASSWORD`. A public `JWT_SECRET` means anyone can forge valid JWTs and bypass RLS entirely. The anon key and service role key are both derived from the JWT secret and must all be regenerated together as a lockstep set.
+
+**PM Assessment:** Unambiguously critical. This is not theoretical — the defaults are published on GitHub. An instance deployed with default secrets is compromised before launch. The fix is a one-time 15-minute checklist during setup. The lockstep regeneration requirement (JWT_SECRET → regenerate both ANON_KEY and SERVICE_ROLE_KEY together) is the only complexity here, and forgetting it results in silent auth failures that look like application bugs.
+
+**Recommendation:** MUST DO (before code) — specifically, before the first `docker compose up` in Phase 1.2.
+
+**Scope Impact:** Add an explicit "Replace all default Supabase secrets" step to task 1.2 and/or task 1.7 with a checklist of the six values that must be replaced. Document the ANON_KEY + SERVICE_ROLE_KEY regeneration dependency on JWT_SECRET. Consider adding a startup check in the app that refuses to start if detected-default values are present.
+
+---
+
+### Finding A4 — NEXT_PUBLIC_SUPABASE_URL Naming and Server vs. Client URL Split
+
+**Source:** TECHFILE R5 (partially), TECHFILE R7
+**Original Finding (combined):** Two related issues. First (R5): the scope lists `SUPABASE_URL` and `SUPABASE_ANON_KEY` as server-side env vars, but the browser-side Supabase client needs them with the `NEXT_PUBLIC_` prefix. Only `SUPABASE_SERVICE_ROLE_KEY` must be server-side only. Second (R7): server-side Supabase calls should use an internal Docker network URL (`http://supabase_kong:8000`) rather than routing through Caddy, requiring a separate `SUPABASE_INTERNAL_URL` env var. These issues compound: if the env var naming is wrong, the browser client cannot initialize; if the URL strategy is wrong, server-side calls either fail or incur unnecessary latency.
+
+**PM Assessment:** The naming fix (R5) is a pre-code correctness issue — getting it wrong means the entire auth flow breaks on first run with a confusing error. The internal URL split (R7) is an important architectural pattern for self-hosted deployments that is genuinely non-obvious. Both are zero-effort to fix in the scope now and expensive to discover during Phase 3 or 4 when server-rendered pages fail auth in non-obvious ways.
+
+**Recommendation:** MUST DO (before code)
+
+**Scope Impact:** In §7 Deployment env var list: rename `SUPABASE_URL` → `NEXT_PUBLIC_SUPABASE_URL` and `SUPABASE_ANON_KEY` → `NEXT_PUBLIC_SUPABASE_ANON_KEY`. Add `SUPABASE_INTERNAL_URL` as a new server-only env var (the Docker internal Kong URL). Note in §7 Tech Stack or Deployment that the Supabase server client uses `SUPABASE_INTERNAL_URL` + `SUPABASE_SERVICE_ROLE_KEY`, while the browser client uses `NEXT_PUBLIC_SUPABASE_URL` + `NEXT_PUBLIC_SUPABASE_ANON_KEY`. Also note using `@supabase/ssr` (`createBrowserClient` / `createServerClient`) rather than base `@supabase/supabase-js` for correct Next.js App Router session handling.
+
+---
+
+### Finding A5 — t3-env Over Raw Zod for Env Validation (NEXT*PUBLIC* Structural Enforcement)
+
+**Source:** TECHFILE R5
+**Original Finding:** `t3-env` (`@t3-oss/env-nextjs`) structurally enforces the server/client split for env vars — server secrets in a `server` block, public vars in a `client` block — producing a build-time error if, for example, `TMDB_API_KEY` is placed in the wrong block. Raw zod requires enforcing this split by convention. The boilerplate is nearly identical.
+
+**PM Assessment:** This is a build-time safety net for the project's most important security constraint: `TMDB_API_KEY` must never be `NEXT_PUBLIC_`. The structural enforcement costs nothing. It is directly relevant here because the scope already acknowledges the risk ("TMDB*API_KEY environment variable must NEVER use the `NEXT_PUBLIC*` prefix"). The scope already says "zod (or t3-env)" — this is just a recommendation to resolve that ambiguity in favor of t3-env.
+
+**Recommendation:** MUST DO (before code) — resolve the "zod (or t3-env)" ambiguity in favor of t3-env in task 1.3.
+
+**Scope Impact:** In §7 Tech Stack table and task 1.3, change "zod (or t3-env)" to "t3-env (`@t3-oss/env-nextjs`) with zod". Minor wording change, no structural scope impact.
+
+---
+
+### Finding A6 — Supabase Studio Must Not Be Publicly Accessible
+
+**Source:** SECFILE 24, COMPLIANCE 36 (merged — both flag the same issue)
+**Original Finding:** The self-hosted Supabase stack exposes Studio (full database admin UI) on port 3000 by default. Studio provides unrestricted read/write access to all data, bypassing RLS, using the service role key. If port 3000 is mapped to the host network or proxied through Caddy, it is internet-accessible. Default Studio credentials are well-known.
+
+**PM Assessment:** This is a pre-deployment configuration requirement, not a code issue. The fix is removing or restricting a single port mapping in docker-compose. The operational cost (SSH tunnel to access Studio during development) is minimal and standard practice for self-hosted deployments. This should be a line item in the Phase 1.7 deployment checklist, not something left to developer judgment.
+
+**Recommendation:** MUST DO (before code) — must be part of the docker-compose specification.
+
+**Scope Impact:** Add to §7 Deployment: Studio port must not be mapped to the host network in the production docker-compose (remove or restrict to `127.0.0.1:3000`). Access via SSH tunnel only. Optionally note this is acceptable during local development. Task 1.7 should include this as an explicit checklist item.
+
+---
+
+### Finding A7 — Kong and Postgres Ports Must Not Be Internet-Accessible
+
+**Source:** SECFILE 27, SECFILE 33
+**Original Finding (combined):** Kong (Supabase API gateway, ports 8000/8443) and Postgres (port 5432) must not be exposed to the host network in production docker-compose. Kong exposed directly allows bypassing the Next.js app and probing Supabase APIs directly. Postgres exposed allows direct database connections that bypass all application security and RLS. Both are development conveniences that must be removed for production.
+
+**PM Assessment:** Same category as A6 — docker-compose configuration, not code. Both are standard Docker hardening steps. The Kong exposure is particularly relevant because the Supabase JS browser client needs to reach Supabase services — the correct path is through Caddy, not direct Kong port exposure. This is already implied by the Caddy architecture but must be made explicit in the docker-compose specification.
+
+**Recommendation:** MUST DO (before code) — part of the docker-compose specification.
+
+**Scope Impact:** Add to §7 Deployment: Kong ports (8000, 8443) must be internal to the Docker network only — no host port mapping. Postgres port 5432 must be internal to Docker network only — no host port mapping. Access via SSH tunnel for administration. Task 1.7 should include these as checklist items.
+
+---
+
+## PRIORITY 2 — MUST DO (during Phase 1 or specified phase)
+
+These findings are not blocking to start coding, but must be addressed in the phase indicated or they become expensive to fix later.
+
+---
+
+### Finding B1 — argon2 npm Package Requires Native Build Tools in Docker Multi-Stage Build
+
+**Source:** TECHFILE R1
+**Original Finding:** The `argon2` npm package compiles a C extension at install time via `node-gyp`. The `node:22-slim` base image does not include `python3`, `make`, or `gcc`. These must be in the builder stage of the multi-stage Dockerfile or `npm install` will fail. The compiled `.node` binary must be verified to survive the standalone output trace; if not, it must be explicitly copied.
+
+**PM Assessment:** Valid and practical. This will cause a hard Docker build failure if not addressed — not a subtle bug, a complete build failure. The fix is a two-line apt-get addition to the Dockerfile builder stage. It is a Phase 1.7 item by nature. The alternative (`@node-rs/argon2`, which ships pre-compiled NAPI binaries eliminating node-gyp entirely) is worth noting as a simpler option if build complexity becomes a concern.
+
+**Recommendation:** MUST DO (during Phase 1.7)
+
+**Scope Impact:** Add to task 1.7: "Dockerfile builder stage must install `python3 make g++` for argon2 native build; verify argon2 compiled binary is present in standalone output." A note in §7 Tech Stack about `@node-rs/argon2` as a drop-in alternative if node-gyp causes issues is optional.
+
+---
+
+### Finding B2 — GoTrue Auth Methods Must Be Restricted to Anonymous-Only
+
+**Source:** SECFILE 28
+**Original Finding:** GoTrue enables multiple auth providers by default (email/password, magic link, OAuth). Since the app uses only anonymous sign-in, all other methods should be explicitly disabled via environment variables: `GOTRUE_EXTERNAL_EMAIL_ENABLED=false`, `GOTRUE_EXTERNAL_PHONE_ENABLED=false`, all OAuth providers disabled. This prevents attackers from creating non-anonymous accounts via unexpected endpoints, which could have different RLS treatment.
+
+**PM Assessment:** Valid and low-effort. Disabling unused auth methods reduces attack surface with zero cost. This is a GoTrue environment variable configuration task that belongs in Phase 1.2 alongside the other Supabase setup work. The finding correctly notes this also addresses the anonymous account linking issue (finding 37 in SECFILE, an informational finding not separately listed here).
+
+**Recommendation:** MUST DO (during Phase 1.2)
+
+**Scope Impact:** Add to task 1.2: "Configure GoTrue to restrict auth methods to anonymous sign-in only: disable email, phone, and all OAuth providers via environment variables." Add the relevant env vars to the §7 Deployment env var list.
+
+---
+
+### Finding B3 — RLS INSERT Policies Must Include WITH CHECK to Prevent Column Spoofing
+
+**Source:** SECFILE 26
+**Original Finding:** INSERT policies without explicit `WITH CHECK` clauses allow group members to insert movies attributed to other users (`added_by` spoofing) or to insert group_members records for other users (`user_id` spoofing). UPDATE policies must also prevent users from changing `added_by` or escalating their own `role` from `member` to `admin`.
+
+**PM Assessment:** Valid. This is an RLS design gap that must be addressed when writing the migration in Phase 1.2. The scope defines the policies at a high level ("Full CRUD for members of the owning group only") but does not specify `WITH CHECK` constraints. Missing these is easy to overlook. Worth noting: the `group_members` INSERT (joining a group) is probably best handled as a server-side operation via the service role key rather than a direct client INSERT — this eliminates the `WITH CHECK` complexity for that specific case and is more consistent with the rate-limited join endpoint pattern already in the scope.
+
+**Recommendation:** MUST DO (during Phase 1.2)
+
+**Scope Impact:** Extend the RLS section in §7 to add `WITH CHECK` requirements: `movies` INSERT must check `added_by = auth.uid()`. `movies` UPDATE must prevent changing `added_by`. `group_members`: consider whether client-side INSERT is appropriate or if joining should be server-side via service role key. `group_members` UPDATE must prevent role escalation.
+
+---
+
+### Finding B4 — Database Backup Strategy Missing
+
+**Source:** COMPLIANCE 47
+**Original Finding:** First-round finding flagged missing backups; the updated scope has self-hosted Supabase but still no backup mechanism. Self-hosted Postgres has no automatic backups. Volume failure = total data loss. Anonymous auth means users cannot be contacted to rebuild their accounts.
+
+**PM Assessment:** Critical operational gap. The scope is otherwise well-specified for a self-hosted deployment, but a `docker volume rm` or disk failure without backups is a project-ending event. The fix is a simple `pg_dump` cron container added to docker-compose. This is a Phase 1.7 item — add it when the Docker infrastructure is being built, not as an afterthought post-launch.
+
+**Recommendation:** MUST DO (during Phase 1.7)
+
+**Scope Impact:** Add to §7 Deployment: a `backup` service in docker-compose running daily `pg_dump` with 7-day retention. Add to task 1.7: "Add pg_dump backup container to docker-compose; document restore procedure; test restore before launch."
+
+---
+
+### Finding B5 — Caddy TLS Certificate Volume Must Be Persistent
+
+**Source:** SECFILE 32
+**Original Finding:** Caddy obtains TLS certificates via Let's Encrypt. If `/data` is not mounted as a persistent Docker volume, certificates are lost on every container restart. Let's Encrypt rate-limits duplicate certificate requests to 5 per domain per week — hitting this limit makes the site inaccessible for up to a week.
+
+**PM Assessment:** Straightforward and important. This is a one-line volume mount in docker-compose. Missing it produces a production outage that takes a week to recover from. No reason not to specify it explicitly.
+
+**Recommendation:** MUST DO (during Phase 1.7)
+
+**Scope Impact:** Add to §7 Deployment or task 1.7: Caddy container must mount persistent named volumes for `/data` and `/config`. Use Let's Encrypt staging endpoint for initial testing.
+
+---
+
+### Finding B6 — Argon2id Parameters Must Be Explicitly Specified
+
+**Source:** SECFILE 31
+**Original Finding:** The scope specifies Argon2id for recovery code hashing but does not define the memory, iterations, or parallelism parameters. The `argon2` npm library default is 64 MiB memory per hash, which could cause OOM kills in constrained Docker containers if concurrent operations occur. OWASP 2024 recommended parameters: memory=19,456 KiB (~19 MiB), iterations=2, parallelism=1, output=32 bytes.
+
+**PM Assessment:** Valid. The library default (64 MiB) is risky in a Docker container. Specifying OWASP-recommended parameters is a one-line change to the hash call and prevents unexpected OOM behavior. This belongs in Phase 1.5 when the recovery code hashing is implemented.
+
+**Recommendation:** MUST DO (during Phase 1.5)
+
+**Scope Impact:** Add to §7 Data Model or the Privacy section: Argon2id parameters — memory=19,456 KiB, iterations=2, parallelism=1, output=32 bytes. Add to task 1.5: "Use explicit Argon2id parameters (OWASP 2024 recommended) rather than library defaults."
+
+---
+
+## PRIORITY 3 — SHOULD DO
+
+Valid findings that improve correctness, security, or maintainability but are not project-ending if deferred briefly. These should all land before the MVP deadline.
+
+---
+
+### Finding C1 — GDPR: auth.users Table Not Deleted in Account Deletion Flow
+
+**Source:** COMPLIANCE 37
+**Original Finding:** Deleting a row from the app's `public.users` table does NOT delete the corresponding `auth.users` record in GoTrue. Under GDPR Art. 17, erasure must be complete. Account deletion (both self-service in Extra Features and Master Admin deletion) must also call `supabase.auth.admin.deleteUser(userId)` server-side.
+
+**PM Assessment:** Valid compliance requirement. This is a small but meaningful gap — a soft delete of the app table alone is incomplete. The fix is adding a server-side `admin.deleteUser` call to both deletion flows. Relevant to Phase 7.6 (Master Admin user deletion) and the Extra Features self-service deletion. Worth specifying now before those features are built.
+
+**Recommendation:** SHOULD DO (during Phase 7.6 and Extra Features self-service deletion)
+
+**Scope Impact:** Add to §7 or the privacy section: account deletion must call `supabase.auth.admin.deleteUser(userId)` via the service role key to remove the `auth.users` record. Add to task 7.6: "Deletion must remove both `public.users` row and `auth.users` record." Add the same note to the Extra Features self-service deletion entry.
+
+---
+
+### Finding C2 — GDPR: Docker Container Log Rotation Not Configured
+
+**Source:** COMPLIANCE 38
+**Original Finding:** Docker's default `json-file` logging driver has no size limit or rotation. All Supabase containers (Kong, GoTrue, PostgREST, Realtime) produce logs containing IP addresses, user agents, and JWTs — personal data under GDPR. Logs grow unbounded. Privacy policy mentions server logs but does not address container logs.
+
+**PM Assessment:** Valid and low-effort. `logging: { driver: "json-file", options: { max-size: "10m", max-file: "5" } }` added to each container in docker-compose is a five-minute fix that prevents both disk exhaustion and unaddressed GDPR retention. This belongs in Phase 1.7.
+
+**Recommendation:** SHOULD DO (during Phase 1.7)
+
+**Scope Impact:** Add to §7 Deployment: all containers must configure Docker log rotation (max-size: 10m, max-file: 5). Update the Privacy section to note Supabase container logs contain IP data and are retained for ~30 days via rotation.
+
+---
+
+### Finding C3 — CSP Must Reflect Self-Hosted URLs, Not Managed Supabase Wildcard
+
+**Source:** SECFILE 35
+**Original Finding:** The scope's CSP example uses `*.supabase.co` for `connect-src`, which is the managed hosting pattern. Self-hosted Supabase routes through Caddy on the project's own domain. The CSP must use the actual deployment domain. If the Supabase API is proxied through Caddy, `connect-src 'self'` may be sufficient for API calls; only the Realtime WebSocket needs an explicit `wss://yourdomain.com` entry.
+
+**PM Assessment:** Valid. The current CSP template is wrong for the deployment model. An incorrect CSP blocks Realtime WebSocket connections and breaks the real-time sync feature, which is an MVP requirement. This needs to be correct before task 5.7 (security headers configuration). Low effort to fix in the scope — remove `*.supabase.co`, replace with `wss://[deployment domain]`.
+
+**Recommendation:** SHOULD DO (during Phase 5.7)
+
+**Scope Impact:** Update the §7 Security Headers section: replace `*.supabase.co` in `connect-src` with `wss://[deployment domain]`. Note that if Supabase API calls go through Caddy at the same origin, `connect-src 'self'` covers API; only the WebSocket URL needs an explicit entry.
+
+---
+
+### Finding C4 — HSTS Should Be Configured in Caddy, Not Left Ambiguous
+
+**Source:** COMPLIANCE 45
+**Original Finding:** The scope says "configure HSTS in `next.config.js` or Caddy" — the "or" is ambiguous. HSTS must be configured at the TLS termination point (Caddy). The scope should be explicit. Caddy does not add HSTS by default. Recommendation: start with a short max-age during testing, increase to production values before launch.
+
+**PM Assessment:** Valid. The ambiguity creates a risk that HSTS ends up only in Next.js headers, which works correctly for most requests but is less robust than having it at the Caddy level. More importantly, the "or" leaves room for neither being done. This is a small wording fix in the scope.
+
+**Recommendation:** SHOULD DO (during Phase 5.7)
+
+**Scope Impact:** Update §7 Security Headers: specify that HSTS is configured in the Caddyfile (not in Next.js). Add a note to use a short `max-age` during testing (e.g., `max-age=86400`) before promoting to the two-year production value. Do not submit to the HSTS preload list until confident.
+
+---
+
+### Finding C5 — TMDB Adult Content Must Be Explicitly Filtered
+
+**Source:** COMPLIANCE 35
+**Original Finding:** TMDB search results and popular/top-rated endpoints can include adult content if `include_adult` is not explicitly set to `false`. The scope does not specify this parameter. The landing page reel poster fetch and search proxy both need explicit filtering. Defense in depth: also check the `adult` field on each result object server-side.
+
+**PM Assessment:** Valid. This is a one-line parameter addition to all TMDB API calls in the server proxy. The consequences of not doing it (adult posters appearing in a social app used by families) are worse than the effort to fix it. Straightforward to add to task 3.1 / 5.2 proxy implementation.
+
+**Recommendation:** SHOULD DO (during Phase 3.1 and 5.2)
+
+**Scope Impact:** Add to §7 TMDB API Proxy: "All TMDB API calls must explicitly set `include_adult=false` and server-side filter results by the `adult` field." Add to task 3.1 and 5.2.
+
+---
+
+### Finding C6 — Auto-Deletion Must Handle Orphan Groups and Admin Transfer
+
+**Source:** SECFILE 29
+**Original Finding:** The 12-month inactivity auto-deletion does not address what happens to groups when the sole admin is deleted (orphan admin-less group) or when the last member is deleted (orphan group with data). The `added_by` foreign key needs `ON DELETE SET NULL` to prevent FK violations. The deletion job should run in a transaction per user.
+
+**PM Assessment:** Valid. The auto-deletion job as currently described could leave the database in inconsistent states. The logic is not complex — check group admin status before delete, auto-transfer or cascade-delete as appropriate — but it must be specified before implementation. This is relevant to the data retention job, which is a background task rather than a specific implementation phase.
+
+**Recommendation:** SHOULD DO (specify before implementing the auto-deletion job, likely Phase 6 or later)
+
+**Scope Impact:** Add to §7 Data Model or Privacy section: auto-deletion job must handle orphaned groups (auto-transfer admin to longest-tenured member; cascade-delete group if last member). `added_by` FK should use `ON DELETE SET NULL`. Deletion must be wrapped in a transaction per user.
+
+---
+
+### Finding C7 — TMDB Metadata Staleness (TMDB ToS Compliance)
+
+**Source:** COMPLIANCE 34
+**Original Finding:** The `movies` table stores TMDB metadata at add-time and never refreshes it. The trailer URL refresh job only targets null entries. TMDB ToS requires keeping cached data reasonably current. Poster paths can become invalid. A monthly metadata refresh job for movies added more than 30 days ago is needed. Requires a `metadata_refreshed_at` column.
+
+**PM Assessment:** This is a legitimate TMDB ToS compliance issue. However, it is a post-MVP background job, not an MVP feature. The scope already has a pattern for this (the trailer URL refresh job). Adding a metadata refresh job is a natural extension of that same pattern. It requires a schema column addition (`metadata_refreshed_at`) — that column is easier to add now (Phase 1.2 migration) than to retrofit later. The job itself can be implemented post-MVP.
+
+**Recommendation:** SHOULD DO — add `metadata_refreshed_at` column to `movies` table in Phase 1.2 migration; defer the refresh job implementation to post-MVP (alongside or after Phase 6 trailer refresh).
+
+**Scope Impact:** Add `metadata_refreshed_at` (timestamp, nullable) to the `movies` table in §7 Data Model. Add a post-MVP task (likely Phase 6) for a monthly TMDB metadata refresh job covering title, poster_path, genres, year.
+
+---
+
+### Finding C8 — Invite Code Word List Requirements Not Specified
+
+**Source:** SECFILE 30, COMPLIANCE 49 (both flag the same gap, slightly different focus — merged)
+**Original Finding (combined):** The scope specifies WORD-WORD format but does not specify word list size, case sensitivity, filtering criteria, or collision handling. SECFILE notes a minimum of 2,000 words for adequate brute-force resistance with the current rate limiting. COMPLIANCE notes that with a small word list (1,000 words = 1M combinations), brute force is feasible even with rate limiting. Both recommend 4,000+ words. Additional specifications needed: uppercase display, case-insensitive comparison, word length 3-8 characters, filtering for offensive/confusing words, collision check on generation.
+
+**PM Assessment:** Valid. The word list decision should be explicit because it directly affects the user experience (longer words are harder to share verbally) and the security margin. With the rate limit at 5 attempts per 15-minute window, even 2,000 words gives adequate protection (brute force would take years). The filtering and collision check requirements are good hygiene to specify up front. This is a task 2.1 implementation concern, not a pre-code blocker, but it should be in the scope before Phase 2.
+
+**Recommendation:** SHOULD DO (specify before Phase 2.1)
+
+**Scope Impact:** Add to §7 Data Model or the groups section: word list must contain at least 2,000 words (4,000+ preferred); uppercase display, case-insensitive comparison; filter words shorter than 3 characters, longer than 8 characters, and offensive/confusing terms; collision check on generation. Rate limit window is 15 minutes (add to the existing rate-limit spec in §4).
+
+---
+
+## PRIORITY 4 — NICE TO HAVE
+
+Valid but not urgent. Acceptable to defer past MVP.
+
+---
+
+### Finding D1 — iron-session v8 API Breakage Warning (Awareness Item)
+
+**Source:** TECHFILE R2
+**Original Finding:** iron-session v8 has a breaking API change from v7 — `withIronSessionApiRoute` and `withIronSessionSsr` exports no longer exist. Many tutorials still reference the v7 API. The v8 pattern uses `getIronSession()` directly in Route Handlers. Also: `sameSite: 'strict'` means the admin cookie is not sent on the first request following cross-site navigation (not a bug, expected behavior to document).
+
+**PM Assessment:** This is an implementation awareness note, not a scope gap. The scope already correctly specifies iron-session with the right cookie settings. The PM value here is adding a note to Phase 7.3 to explicitly use the v8 README as the reference rather than tutorials. Not worth a scope change, but worth flagging before someone codes it.
+
+**Recommendation:** NICE TO HAVE — add a note to task 7.3 to use the v8 README directly.
+
+**Scope Impact:** Optional: add a parenthetical to task 7.3: "(use v8 README directly — v7 patterns are incompatible)."
+
+---
+
+### Finding D2 — TanStack Query persistQueryClient Requires Three Additional Packages
+
+**Source:** TECHFILE R3
+**Original Finding:** `persistQueryClient` with IndexedDB requires `@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister`, and `idb-keyval` — not a one-line config. Phase 8.2 should budget 2-4 hours. API changed between TanStack Query v4 and v5 (uses `PersistQueryClientProvider` wrapper in v5).
+
+**PM Assessment:** Useful implementation detail for Phase 8.2, which is a post-MVP phase. The main value is preventing schedule underestimation. Not a scope change — the feature is already specified correctly. A time budget note on task 8.2 would be the only relevant addition.
+
+**Recommendation:** NICE TO HAVE — add a time budget note to task 8.2.
+
+**Scope Impact:** Optional: add to task 8.2: "Requires three packages (`@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister`, `idb-keyval`); budget 2-4 hours including serialization testing."
+
+---
+
+### Finding D3 — serwist Requires Custom sw.ts File and Separate tsconfig.worker.json
+
+**Source:** TECHFILE R4
+**Original Finding:** `@serwist/next` does not auto-generate a service worker — `app/sw.ts` must be authored manually. The TypeScript config requires a separate `tsconfig.worker.json` because the service worker needs `lib: ['WebWorker']`, which conflicts with the main app's `lib: ['DOM']`. Phase 8.1 should budget a half-day, not an hour.
+
+**PM Assessment:** Useful implementation detail for Phase 8.1, which is post-MVP. The main value is preventing the schedule underestimation that `@serwist/next` is a "quick config step." The scope already correctly specifies the package — this is just a time-budget and implementation complexity note.
+
+**Recommendation:** NICE TO HAVE — add a time budget note to task 8.1.
+
+**Scope Impact:** Optional: add to task 8.1: "Requires authoring `app/sw.ts`, a `tsconfig.worker.json` for service worker TypeScript compilation, and `public/manifest.json`. Budget a half-day."
+
+---
+
+## PRIORITY 5 — SKIP / DEFER
+
+These findings are valid in theory but are overkill for this project's scale, threat model, or MVP timeline.
+
+---
+
+### Finding E1 — Docker Container Security Hardening (CIS Benchmark: cap_drop, no-new-privileges, memory limits, read-only root filesystem)
+
+**Source:** COMPLIANCE 44
+**Original Finding:** CIS Docker Benchmark controls not addressed: no `cap_drop: [ALL]`, no `no-new-privileges`, no memory/CPU limits, no `read_only: true` root filesystem.
+
+**PM Assessment:** These are legitimate hardening measures for high-value production deployments. For a small self-hosted friend group app, they add meaningful operational complexity (particularly `read_only: true`, which requires identifying all writable paths and mounting tmpfs). The scope already has non-root user, tini, and health checks — these represent a reasonable baseline. The additional CIS controls would be appropriate for a security-sensitive business application but are overkill for this project's threat model and scale. The risk of getting `read_only` wrong and causing mysterious production failures outweighs the security benefit for this use case.
+
+**Recommendation:** SKIP / DEFER — the existing Docker hardening (non-root user, tini, health check) is adequate. Revisit if the project scales significantly.
+
+**Scope Impact:** None.
+
+---
+
+### Finding E2 — Encryption at Rest for Postgres Volume
+
+**Source:** COMPLIANCE 46
+**Original Finding:** Docker volumes are not encrypted by default. GDPR Art. 32 lists encryption at rest as an example of "appropriate technical measures." Recommendation is host-level full-disk encryption (LUKS on Linux).
+
+**PM Assessment:** Host-level full-disk encryption is a valid recommendation. However, this is an infrastructure/hosting decision that is entirely outside the scope of the application and docker-compose configuration. If the host is a cloud VM (DigitalOcean, Hetzner, etc.), the cloud provider likely offers disk encryption as a one-click option. A brief note in §7 Deployment as a deployment prerequisite is worth adding, but it is not a scope item and cannot be enforced via docker-compose. The data model already hashes recovery codes — the most sensitive field. Display names and movie preferences are low-sensitivity data. Full column-level encryption would be overkill.
+
+**Recommendation:** SKIP / DEFER for scope purposes — optionally add a single line to §7 Deployment recommending host-level disk encryption as a deployment prerequisite.
+
+**Scope Impact:** Optional single-line note in §7 Deployment: "Recommendation: enable full-disk encryption on the Docker host (LUKS or cloud provider equivalent) to protect Postgres volume data at rest."
+
+---
+
+## PRIORITY 6 — NEEDS DISCUSSION
+
+These findings require a product or architecture decision before a recommendation can be finalized.
+
+---
+
+### Finding F1 — Supabase Auth Tokens Stored in localStorage (XSS Risk)
+
+**Source:** SECFILE 25
+**Original Finding:** The Supabase JS client stores JWTs (access token and refresh token) in localStorage by default. This is vulnerable to XSS exfiltration. Mitigation requires switching to `@supabase/ssr` with cookie-based token storage, reducing GoTrue refresh token lifetime, and prioritizing CSP as primary defense. Note: `@supabase/ssr` tokens in cookies are still not httpOnly (the JS client must read them) but provide SameSite/Secure benefits.
+
+**PM Assessment:** This is a known Supabase design tradeoff. The `@supabase/ssr` recommendation aligns with Supabase's own guidance for Next.js App Router projects — this was actually already mentioned in Finding A4 (R7), where `@supabase/ssr`'s `createBrowserClient`/`createServerClient` was recommended over base `@supabase/supabase-js`. If Finding A4 is accepted, `@supabase/ssr` is already in scope and this finding is partially addressed.
+
+What remains as a decision: reducing GoTrue's refresh token lifetime (default ~1 week in self-hosted GoTrue). This is a `GOTRUE_JWT_EXP` and refresh token configuration change. A 24-hour refresh token lifetime is more conservative but means users need to re-authenticate more often — for an anonymous auth app where re-auth means starting from scratch or using a recovery code, this could frustrate users. The decision involves a UX/security tradeoff.
+
+**Recommendation:** NEEDS DISCUSSION — if `@supabase/ssr` is adopted via Finding A4, the main residual question is GoTrue refresh token lifetime configuration. Recommend discussing the acceptable session duration before Phase 1.4.
+
+**Scope Impact (if accepted):** Add GoTrue JWT and refresh token lifetime configuration to §7 Deployment env vars. Note that `@supabase/ssr` is the required Supabase client library (not base `@supabase/supabase-js`) — this may already be addressed by Finding A4.
+
+---
+
+### Finding F2 — GDPR: Sentry May Capture Personal Data (International Transfer)
+
+**Source:** COMPLIANCE 39
+**Original Finding:** Sentry's default SDK captures request URLs (which may contain user UUIDs), breadcrumbs, and potentially user context. This constitutes a potential international data transfer (US servers) requiring disclosure and a `beforeSend` sanitization hook. Alternative: self-host Sentry.
+
+**PM Assessment:** This is a real compliance concern but the practical fix is a `beforeSend` callback that strips UUID path segments from URLs — a 15-minute implementation task. The privacy policy update is also straightforward (Sentry is a common third-party processor with existing DPA/SCC documentation). Self-hosting Sentry adds significant operational overhead for a small project and is not worth considering. The open question is whether the user wants to configure Sentry with sanitization from the start (recommended) or defer the detailed privacy policy wording until Phase 5.1.
+
+**Recommendation:** NEEDS DISCUSSION — the `beforeSend` sanitization should be added to task 1.8 (Sentry setup). The privacy policy wording is a Phase 5.1 item.
+
+**Scope Impact (if accepted):** Add to task 1.8: "Configure Sentry `beforeSend` to strip UUID path segments from error events; do not call `Sentry.setUser()` with user identifiers." Add to Phase 5.1 privacy policy task: "Disclose Sentry as a third-party processor."
+
+---
+
+### Finding F3 — GDPR: Privacy Policy Missing Required Sections
+
+**Source:** COMPLIANCE 40
+**Original Finding:** GDPR Arts. 13-14 and CCPA require specific sections not currently mentioned in the scope's privacy policy description: controller identity, lawful basis per processing activity, international transfers, right to complain to supervisory authority, CCPA categories/do-not-sell disclosure, children's data disclaimer (COPPA/GDPR under-16).
+
+**PM Assessment:** The scope's current description ("factual description of what data is stored, how it is used, and user rights") is underspecified for compliance. The full list of required sections is well-defined by the finding. However, the practical question is: what jurisdiction is the operator in, and how strictly must GDPR/CCPA be followed for a small personal project? For a commercial product with EU users, full GDPR compliance is mandatory. For a personal project, a good-faith effort covers most practical risk.
+
+This is also a Phase 5.1 item that will be written by the developer — specifying the required sections in the scope gives the developer a checklist rather than having them guess. Low risk to add this to the scope.
+
+**Recommendation:** NEEDS DISCUSSION — likely SHOULD DO for any app with public access. Recommend expanding the §7 Privacy section to enumerate required policy sections as a checklist for the Phase 5.1 author.
+
+**Scope Impact (if accepted):** Expand §7 Privacy to list required privacy policy sections: controller identity, lawful basis, data inventory with retention, third-party recipients (TMDB, Sentry), international transfer basis, full user rights with exercise instructions, children's disclaimer (under-13/under-16), cookie/localStorage disclosure, change notification procedure.
+
+---
+
+### Finding F4 — WCAG Accessibility Gaps: Alt Text, Status Messages, Reel Pause Control
+
+**Source:** COMPLIANCE 41, 42, 43 (three separate findings, combined here as they form a coherent accessibility cluster)
+**Original Findings:** (41) Slot machine reel must be strictly user-triggered and capped under 5 seconds per WCAG 2.2.2. (42) All poster `<img>` tags need meaningful `alt` text; spinning reel posters should be `aria-hidden`; result posters need alt text. (43) Dynamic status messages (roll result, filter state, watched toggle, action confirmations) need `aria-live="polite"` regions per WCAG 4.1.3.
+
+**PM Assessment:** These are legitimate WCAG 2.1 AA requirements, and the scope already commits to accessibility (§6 Usability: "Accessibility: sufficient color contrast, tap targets, screen reader labels"). The inline panel keyboard navigation is already specified in detail — these three findings extend that pattern to the rest of the app. However, the scope currently defers the accessibility pass to Phase 8.3. The question is whether these specific items belong in Phase 8.3 or whether they should be embedded into the Phase 3 (poster grid) and Phase 4 (animation) implementation tasks as inline requirements, which would reduce the Phase 8.3 rework burden.
+
+The reel pause control finding (41) is largely a design confirmation — since the reel is user-triggered and time-bounded, WCAG 2.2.2 compliance is easy. Alt text (42) and aria-live (43) are best addressed at component build time, not in a post-hoc accessibility pass.
+
+**Recommendation:** NEEDS DISCUSSION — decide whether to embed these requirements into Phase 3/4 tasks or consolidate into Phase 8.3. Embedding is more efficient.
+
+**Scope Impact (if accepted):** Option A: add alt text requirement to task 3.4 (poster grid), 4.3 (roll animation), 5.3 (landing reel); add `aria-live` requirement to tasks 4.2, 3.7, 3.8; confirm user-triggered and time-bounded constraint in task 5.3. Option B: expand task 8.3 to include these as explicit line items. Either option is a clarifying addition, not a new feature.
+
+---
+
+## Cross-Report Notes
+
+**Confirmed Confirmations (No Action Required):**
+
+- **TECHFILE R6 (tini):** Confirms tini is correct for Node.js 22 Docker containers and should not be removed. Scope is already right. No change needed.
+- **TECHFILE R8 (Vitest/Playwright scoping):** Vitest cannot test React Server Components — Vitest tests should be scoped to pure utilities and Client Components. Playwright should run against the full Docker stack, not `next dev`. This is an implementation guidance note. If accepted, add a clarifying note to tasks 1.1 and 9.16, but it does not change any feature or deliverable.
+- **SECFILE 36 (pg_cron superuser):** Keep pg_cron job SQL static and simple. Use it only to trigger HTTP calls, not complex data operations. This is a best practice note consistent with the current scope design. No scope change needed; guidance for implementation.
+- **SECFILE 34 (iron-session secret rotation):** Documents a rotation procedure for if the secret is compromised. Worth adding to operational documentation. Low priority for scope — rotation is documented behavior of iron-session's array API. Optionally add a one-line note to §7 that secret rotation procedure uses the array API without redeployment-invalidating sessions.
+- **SECFILE 37 (anonymous user linking):** Addressed if Finding B2 (disable all GoTrue auth methods except anonymous) is accepted. No additional action needed.
+- **SECFILE 38 (last_active_at update strategy):** Specifies which events should update `last_active_at` (write operations, not reads; throttled to once per 24 hours per user). This is a useful implementation note but is a development detail rather than a scope gap. Could be added as a parenthetical to the `last_active_at` field in the data model.
+- **COMPLIANCE 50 (PWA manifest and icon requirements):** Detailed specification of manifest fields, icon sizes (192x192, 512x512, maskable variants), iOS meta tags. This is useful Phase 8.1 implementation detail. Could be added to task 8.1 as a checklist.
+- **COMPLIANCE 51 (error response format standardization):** A standard error response shape (`{ error: { code: string, message: string } }`) and error code enum should be defined before Phase 2. This was flagged in Round 1 and partially addressed in Phase 5.6. Worth consolidating into a Phase 1 or 2 task to ensure it precedes API route implementation.
+- **COMPLIANCE 48 (TMDB 429 rate limit handling):** Server-side rate limiting on the TMDB proxy (token bucket, 30 req/10s) and `Retry-After` header handling on 429 responses. Useful addition to task 3.1, though the debounce and TanStack Query caching already reduce risk significantly.
+
+---
+
+_End of PM Assessment Round 2_

+ 50 - 0
research/PROJECT_INFO.md

@@ -0,0 +1,50 @@
+# Project Info
+
+## Purpose
+
+MovieDice is a mobile-first web app that helps friend groups collaboratively build a shared movie watchlist and randomly select movies using an animated "Roll the Dice" mechanic.
+
+## Tech Stack
+
+| Layer            | Technology                                                                    | Version     |
+| ---------------- | ----------------------------------------------------------------------------- | ----------- |
+| Frontend         | Next.js (App Router, React), output: standalone                               | 15.x        |
+| Styling          | Tailwind CSS                                                                  | v3/v4       |
+| Backend/Database | Supabase self-hosted Docker (Postgres + GoTrue + Realtime + PostgREST + Kong) | self-hosted |
+| Auth             | Supabase Anonymous Sign-In (signInAnonymously())                              | —           |
+| Movie Data       | TMDB API (free tier)                                                          | v3          |
+| State Management | TanStack Query v5                                                             | v5          |
+| Admin Sessions   | iron-session v8                                                               | v8          |
+| 2FA (Admin)      | TOTP via otplib                                                               | v12.x       |
+| PWA              | @serwist/next                                                                 | current     |
+| Password Hashing | argon2 (Argon2id)                                                             | current     |
+| Background Jobs  | Supabase pg_cron + Edge Functions (Deno)                                      | —           |
+| Reverse Proxy    | Caddy                                                                         | current     |
+| Runtime          | Node.js 22 LTS                                                                | 22.x        |
+| Env Validation   | t3-env (@t3-oss/env-nextjs) + zod                                             | current     |
+| Testing          | Vitest (unit) + Playwright (E2E)                                              | current     |
+| Deployment       | Self-hosted Docker via docker-compose                                         | —           |
+
+## Key Directories
+
+- Project is pre-implementation; no source directories exist yet
+
+## Architecture Notes
+
+- Anonymous auth with UUID-based accounts, no email/password
+- Recovery codes for cross-device account restoration
+- Groups with invite codes as sole access control for regular users
+- Master Admin with TOTP 2FA, credentials via environment variables
+- Real-time sync via Supabase subscriptions
+- PWA with offline graceful degradation
+- Two distinct roll animations: landing page slot-machine reels, in-app scatter/eliminate
+
+## Review History
+
+| Date       | Type                                                           | Report                                   |
+| ---------- | -------------------------------------------------------------- | ---------------------------------------- |
+| 2026-04-05 | Pre-implementation architecture review                         | ./research/COMPLIANCE.md                 |
+| 2026-04-05 | Full tech stack audit (Report Mode) — Docker/self-hosted focus | ./research/TECHFILE.md                   |
+| 2026-04-05 | Second review -- technology verification on updated scope      | ./research/TECHFILE.md (Second Review)   |
+| 2026-04-05 | Second security review -- updated architecture analysis        | ./research/SECFILE.md (Second Review)    |
+| 2026-04-05 | Second compliance review -- updated scope (15 new findings)    | ./research/COMPLIANCE.md (Second Review) |

+ 567 - 0
research/SECFILE.md

@@ -0,0 +1,567 @@
+# Security Audit — MovieDice
+
+Audited: 2026-04-05 | Auditor: Claude (automated) | Type: Pre-implementation architecture and design review
+
+**Note:** This is a design-phase review based on PROJECT_SCOPE.md. No source code exists yet. All findings are architectural risks and recommendations to be addressed during implementation.
+
+---
+
+## CRITICAL
+
+### 1. No Row Level Security (RLS) Policies Specified for Supabase
+
+`PROJECT_SCOPE.md:Data Model` — The scope defines five database tables (users, groups, group_members, movies, landing_reel_posters, admin_sessions) but does not mention Supabase Row Level Security (RLS) policies anywhere. Supabase exposes a PostgREST API directly to the client using the `SUPABASE_ANON_KEY`. Without RLS policies, any client with the anon key can read, insert, update, and delete ALL rows in ALL tables. This means any user could: read every group's movie list, delete other groups' data, modify other users' accounts, escalate their own role to admin, or forge the `added_by` field. This is the single most critical security gap in the design. Supabase's anon key is meant to be public — RLS is what enforces authorization.
+
+**Fix:** Before writing any application code, define and enable RLS policies on every table:
+
+- `users`: Users can only read/update their own row. No user can delete another user.
+- `groups`: Only members of a group can read it. Only the creator can update/delete.
+- `group_members`: Users can only read memberships for groups they belong to. Only admins can insert (via invite validation server-side) or delete members. Users can delete their own membership (leave).
+- `movies`: Only members of the movie's group can read/insert/update/delete movies in that group.
+- `landing_reel_posters`: Public read, no client write (server-only inserts via service role key).
+- `admin_sessions`: No client access whatsoever (server-only via service role key).
+  Enable RLS on every table with `ALTER TABLE [table] ENABLE ROW LEVEL SECURITY;` and write explicit policies. Do NOT use permissive catch-all policies.
+
+**Implementation Risk:** RLS policies that are too restrictive will break real-time subscriptions and legitimate queries. Test every policy against the actual query patterns the app uses. Supabase real-time respects RLS, so subscriptions will only receive rows the user is authorized to see — but only if policies are correctly written. Overly complex policies can also degrade query performance.
+
+---
+
+### 2. Supabase Anon Key Exposed Client-Side Grants Direct Database Access
+
+`PROJECT_SCOPE.md:329` — The scope lists `SUPABASE_ANON_KEY` as an environment variable and the architecture implies direct client-to-Supabase communication. The Supabase anon key is designed to be public, but it provides full PostgREST API access constrained only by RLS policies. If RLS is not implemented (Finding 1), the anon key becomes a master key to the entire database. Even with RLS, the anon key allows any authenticated or anonymous client to call any Supabase API endpoint, including auth endpoints, storage endpoints, and edge functions. An attacker who understands the Supabase URL and anon key (both extractable from the client-side JavaScript bundle) can craft arbitrary SQL-like queries via PostgREST.
+
+**Fix:** (a) Implement comprehensive RLS policies (see Finding 1). (b) For sensitive operations (user deletion, group deletion, admin actions, invite code validation, recovery code claims), use Next.js Server Actions or API routes that call Supabase with the `service_role` key (never exposed to the client). The service role key bypasses RLS and must never appear in client-side code. (c) Restrict the anon key's permissions to the minimum needed: read-only on public tables, insert-only on specific tables, and no direct update/delete from the client for sensitive columns like `role` in `group_members`.
+
+**Implementation Risk:** Moving operations to server-side API routes increases latency slightly. Ensure the service role key is only used in server-side code (Next.js API routes, Server Actions, or server components) and never imported in client components.
+
+---
+
+### 3. Invite Code as Sole Access Control — Insufficient Entropy and No Brute Force Protection
+
+`PROJECT_SCOPE.md:99,349` — The invite code format is described as "short human-readable" (e.g., WOLF-42). This format has extremely low entropy. Assuming a pattern of WORD-NN (one word from a dictionary of ~2000 common words, a dash, and a two-digit number), the total keyspace is approximately 200,000 combinations. An attacker can enumerate all valid invite codes in minutes with automated requests. Since the invite code is the ONLY mechanism preventing unauthorized users from joining a group, a brute-force attack would allow an attacker to join any group, see all movies, modify the list, and potentially disrupt the group.
+
+**Fix:** (a) Increase invite code entropy: use a format like WORD-WORD-NNNN (e.g., WOLF-MESA-4829) or a random alphanumeric string of at least 8 characters, yielding a keyspace of at least 2 billion. (b) Implement strict server-side rate limiting on the invite code validation endpoint: maximum 5 attempts per IP per 10 minutes, with exponential backoff. (c) Add a lockout mechanism: after 10 failed attempts from the same IP, block for 1 hour. (d) Consider adding invite code expiry (e.g., 7 days) so old codes cannot be used indefinitely. (e) Log failed invite code attempts for monitoring.
+
+**Implementation Risk:** Longer codes reduce usability (harder to share verbally). Balance security with UX by using memorable word pairs. Rate limiting by IP alone can be bypassed with rotating proxies, so consider also rate-limiting by client fingerprint or requiring a valid user session before attempting to join.
+
+---
+
+## HIGH
+
+### 4. Client-Side Local Storage for User Identity Enables Account Theft and Spoofing
+
+`PROJECT_SCOPE.md:107-108` — User identity is persisted in browser local storage. The scope states "App reads stored user ID from browser (local storage or cookie)" to determine the logged-in user. Local storage is accessible to any JavaScript running on the same origin, making it vulnerable to XSS-based theft. If an attacker can inject any script (via a stored XSS in display names, a compromised dependency, or a browser extension), they can exfiltrate the user ID and impersonate that user from any device. Furthermore, since the user ID is a UUID with no accompanying authentication secret, anyone who knows or guesses a user's UUID can impersonate them by simply writing it to their own local storage.
+
+**Fix:** (a) Do not rely solely on a user ID in local storage for authentication. Instead, issue a signed session token (JWT or opaque token) upon account creation that includes the user ID and is validated server-side on every request. Store this token in an httpOnly, Secure, SameSite=Strict cookie — not local storage. (b) Use Supabase Auth's anonymous sign-in feature, which provides proper JWT-based sessions with refresh tokens. (c) If local storage must be used for offline display purposes, ensure all server-side operations validate the session token, not just the user ID.
+
+**Implementation Risk:** Moving from local storage to httpOnly cookies changes the client-side auth flow. Supabase's JS client stores tokens in local storage by default; you may need to configure a custom storage adapter or use server-side session management. Offline mode will need a fallback strategy.
+
+---
+
+### 5. Recovery Code Scheme Lacks Specification of Entropy, Hashing Algorithm, and Claim Security
+
+`PROJECT_SCOPE.md:34,271,345-346` — The scope mentions "a longer alphanumeric code shown once" and "recovery_code (text, hashed)" but does not specify: (a) the length or entropy of the recovery code, (b) the hashing algorithm used, (c) whether a salt is used, (d) rate limiting on the claim endpoint, or (e) single-use enforcement. A weak recovery code (e.g., 8 alphanumeric characters = ~41 bits of entropy) could be brute-forced. Using a fast hash like SHA-256 without a salt makes rainbow table attacks feasible. Without rate limiting on the claim endpoint, an attacker can attempt thousands of recovery codes per second to hijack accounts.
+
+**Fix:** (a) Generate recovery codes with at least 128 bits of entropy (e.g., 22 alphanumeric characters or 6 random words from a wordlist). (b) Hash recovery codes with bcrypt (cost factor 12+) or Argon2id — these are computationally expensive and include built-in salting, making brute-force impractical. (c) Rate-limit the recovery code claim endpoint: maximum 3 attempts per IP per 15 minutes. (d) After a successful claim, invalidate the old recovery code and optionally issue a new one. (e) Log all claim attempts (successful and failed) for security monitoring. (f) Consider adding a secondary verification step (e.g., requiring the user to enter their display name along with the recovery code).
+
+**Implementation Risk:** bcrypt/Argon2id are slow by design, which is the point. Ensure the claim endpoint handles this latency gracefully (show a loading state). Longer recovery codes are harder for users to write down — provide a copy-to-clipboard button and consider a QR code or downloadable text file.
+
+---
+
+### 6. Real-Time Supabase Subscriptions May Lack Authorization
+
+`PROJECT_SCOPE.md:48,365` — The scope calls for Supabase real-time subscriptions on the movies table for live updates. Supabase Realtime respects RLS policies IF they are configured, but the scope does not mention RLS (Finding 1). Without RLS, any client can subscribe to changes on ANY group's movies table and receive real-time updates for groups they do not belong to. Even with RLS, Supabase Realtime requires careful channel-level authorization. If the subscription is set up as a broadcast or presence channel without proper filtering, users could eavesdrop on other groups' activity.
+
+**Fix:** (a) Implement RLS policies (Finding 1) — Supabase Realtime filters events based on RLS automatically for Postgres Changes subscriptions. (b) Subscribe to changes with a filter matching the user's group_id, e.g., `.on('postgres_changes', { event: '*', schema: 'public', table: 'movies', filter: 'group_id=eq.[group_id]' })`. (c) Verify server-side that the subscribing user is a member of the group they are subscribing to. (d) Do not use Broadcast or Presence channels for sensitive data unless you implement custom authorization via Supabase Edge Functions.
+
+**Implementation Risk:** RLS-filtered real-time subscriptions may have slightly higher latency. Test that real-time updates still arrive promptly (within the 3-second target) after RLS is enabled.
+
+---
+
+### 7. Master Admin TOTP Secret in Environment Variables — No Rotation, Single Factor Risk
+
+`PROJECT_SCOPE.md:329,396` — The TOTP secret is stored as a plain-text environment variable (`MASTER_ADMIN_TOTP_SECRET`). While this is standard for TOTP secrets, the design has several gaps: (a) There is no mechanism to rotate the TOTP secret without redeploying the application. (b) If the environment variable is leaked (e.g., through a Docker image layer, a CI/CD log, or a `.env` file committed to version control), the attacker has permanent access until the secret is changed. (c) The admin session management is described only as "secure server-side token store" with no detail on session expiry, invalidation, or binding.
+
+**Fix:** (a) Set a short session TTL for admin sessions (e.g., 30 minutes) with no refresh — require re-authentication with TOTP. (b) Bind admin sessions to the originating IP address and User-Agent. (c) Implement a "logout all admin sessions" capability. (d) Store the TOTP secret using Docker secrets or a secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager) rather than plain environment variables where possible. (e) Log all admin authentication attempts and actions with timestamps and IP addresses. (f) Consider adding a password alongside the TOTP code for defense-in-depth (the scope explicitly says "no password-only fallback" but a password + TOTP combination is stronger than username + TOTP alone).
+
+**Implementation Risk:** Short session TTLs may frustrate the admin if they need to perform multiple actions. A 30-minute window with activity-based extension (reset TTL on each action) is a reasonable compromise. IP binding can cause issues if the admin's IP changes mid-session (e.g., mobile network switching).
+
+---
+
+### 8. TMDB API Key Exposure — Client vs. Server Usage Not Specified
+
+`PROJECT_SCOPE.md:259,326` — The scope lists `TMDB_API_KEY` as an environment variable and describes client-side TMDB search with debounce. If the TMDB API key is used in client-side fetch calls (e.g., from a React component), it will be visible in browser network requests and the JavaScript bundle. While TMDB API keys are free-tier and the data is public, a leaked key can be abused for: (a) excessive API calls that exhaust your rate limit and get your key revoked, (b) associating malicious usage with your account. The landing page makes unauthenticated TMDB calls, which further suggests client-side usage.
+
+**Fix:** (a) Proxy all TMDB API calls through Next.js API routes or Server Actions. The client sends search queries to your server, which forwards them to TMDB with the API key. This keeps the key server-side only. (b) Implement server-side caching (e.g., in-memory or Redis) for TMDB search results to reduce API calls and improve response times. (c) Rate-limit the proxy endpoint to prevent abuse (e.g., 30 requests per user per minute). (d) Use Next.js `NEXT_PUBLIC_` prefix convention correctly — do NOT prefix the TMDB key with `NEXT_PUBLIC_`.
+
+**Implementation Risk:** Proxying adds a network hop and slight latency. Server-side caching mitigates this. Ensure the proxy endpoint does not become an open relay — validate and sanitize the search query parameter.
+
+---
+
+## MEDIUM
+
+### 9. No Input Validation Specified for Display Names
+
+`PROJECT_SCOPE.md:95,268` — Users enter a free-text display name during onboarding. The scope does not mention any validation, sanitization, or length limits. Display names are rendered throughout the app (movie cards, member lists, "Added by" attribution, admin panel search). Without server-side validation: (a) Stored XSS: a display name containing `<script>alert(1)</script>` or event handlers could execute in other users' browsers. (b) Excessively long names could break layouts or be used for denial of service. (c) Empty or whitespace-only names could cause display issues.
+
+**Fix:** (a) Enforce server-side validation: minimum 1 character, maximum 30 characters, alphanumeric plus spaces and basic punctuation only (regex: `^[a-zA-Z0-9 _\-'.]{1,30}$`). (b) HTML-encode all display names before rendering — use React's default JSX escaping (which escapes by default) and never use `dangerouslySetInnerHTML` with user data. (c) Strip or reject control characters and zero-width characters. (d) Apply the same validation in Supabase via a CHECK constraint on the `display_name` column.
+
+**Implementation Risk:** Restrictive character sets may exclude users with non-Latin names. Consider supporting Unicode letter categories while still blocking HTML/script characters. React's JSX auto-escaping provides strong XSS protection by default, but this only works if developers consistently avoid `dangerouslySetInnerHTML`.
+
+---
+
+### 10. No CSRF Protection Mentioned for State-Changing Operations
+
+`PROJECT_SCOPE.md` — The scope describes numerous state-changing operations (create group, join group, add/remove movies, mark watched, delete list, admin actions) but does not mention CSRF protection. If sessions are implemented via cookies (as recommended in Finding 4), every state-changing request is vulnerable to cross-site request forgery unless CSRF tokens are used. An attacker could craft a page that, when visited by a logged-in user, automatically adds movies to their list, deletes groups, or performs admin actions.
+
+**Fix:** (a) If using Next.js Server Actions (App Router), CSRF protection is built in — Server Actions check the `Origin` header automatically. Use Server Actions for all mutations. (b) If using API routes instead, implement CSRF tokens (double-submit cookie pattern or synchronizer token). (c) Set `SameSite=Strict` on session cookies. (d) Validate the `Origin` and `Referer` headers on all state-changing API routes.
+
+**Implementation Risk:** `SameSite=Strict` cookies are not sent on cross-origin navigations, which can break login flows from external links. `SameSite=Lax` is a reasonable compromise that still protects against POST-based CSRF.
+
+---
+
+### 11. No Rate Limiting on Public Endpoints (Landing Page Rolls, Search)
+
+`PROJECT_SCOPE.md:69-90` — The landing page allows unauthenticated users to roll dice and search genres against TMDB without any login. These public endpoints can be abused for: (a) TMDB API quota exhaustion via automated requests, (b) server resource consumption, (c) scraping movie data. The scope mentions client-side debounce (~300ms) but this is trivially bypassed.
+
+**Fix:** (a) Implement server-side rate limiting on all public-facing endpoints (landing page rolls, genre rolls, search): 20 requests per IP per minute. (b) Use a rate-limiting middleware (e.g., `express-rate-limit` equivalent for Next.js, or Supabase Edge Function rate limits, or Docker-level reverse proxy rate limiting). (c) For Docker deployments, configure rate limiting at the reverse proxy level (nginx/Traefik) as the first line of defense. (d) Consider CAPTCHA or proof-of-work challenges if abuse is detected.
+
+**Implementation Risk:** Aggressive rate limiting can block legitimate users on shared networks (NAT, corporate proxies). Use per-IP limits with reasonable thresholds and provide clear error messages when limits are hit.
+
+---
+
+### 12. Docker Security Not Addressed in Scope
+
+`Deployment context` — The scope mentions Vercel deployment but the actual deployment target is Docker. Docker deployments introduce specific security concerns not addressed in the scope: (a) Running as root inside the container, (b) Exposing unnecessary ports, (c) Secrets in Docker image layers (environment variables baked into the image), (d) No health check endpoint defined, (e) No network isolation between services, (f) Base image vulnerabilities.
+
+**Fix:** In the Dockerfile: (a) Use a minimal base image (e.g., `node:20-alpine`). (b) Create and switch to a non-root user: `RUN addgroup -S app && adduser -S app -G app` then `USER app`. (c) Use multi-stage builds to exclude build tools and source maps from the final image. (d) Never bake secrets into the image — use Docker secrets, a `.env` file mounted at runtime, or environment variables passed via `docker run -e`. (e) Expose only the necessary port (e.g., 3000). (f) Add a `HEALTHCHECK` instruction. (g) Use `docker-compose` with a dedicated network for the app, and do not expose the database port to the host if running Supabase locally. (h) Scan the image for vulnerabilities with `docker scout` or `trivy` before deployment.
+
+**Implementation Risk:** Non-root users may lack permissions for certain operations (e.g., binding to port 80). Use a port above 1024 (like 3000) and reverse-proxy with nginx/Traefik. Alpine-based images may have compatibility issues with some npm packages that require native compilation.
+
+---
+
+### 13. Missing Security Headers (CSP, X-Frame-Options, etc.)
+
+`PROJECT_SCOPE.md` — The scope does not mention any HTTP security headers. Without these, the application is exposed to: (a) Clickjacking (no `X-Frame-Options` or `frame-ancestors` CSP directive), (b) XSS exploitation (no Content-Security-Policy), (c) MIME type sniffing attacks (no `X-Content-Type-Options`), (d) Information leakage (no `Referrer-Policy`).
+
+**Fix:** Configure the following headers in Next.js `next.config.js` (or via reverse proxy for Docker):
+
+```
+Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://image.tmdb.org; connect-src 'self' https://*.supabase.co wss://*.supabase.co; frame-ancestors 'none';
+X-Frame-Options: DENY
+X-Content-Type-Options: nosniff
+Referrer-Policy: strict-origin-when-cross-origin
+Permissions-Policy: camera=(), microphone=(), geolocation=()
+Strict-Transport-Security: max-age=31536000; includeSubDomains
+```
+
+Adjust CSP directives as needed for inline styles (Tailwind may require `unsafe-inline` for styles) and any third-party resources.
+
+**Implementation Risk:** An overly strict CSP can break the application. Tailwind CSS may require `style-src 'unsafe-inline'`. TMDB poster images need `img-src https://image.tmdb.org`. Supabase real-time needs `connect-src wss://*.supabase.co`. Test thoroughly after applying headers. Use `Content-Security-Policy-Report-Only` during development to catch violations without blocking.
+
+---
+
+### 14. No CORS Configuration Specified
+
+`PROJECT_SCOPE.md` — The scope does not mention CORS configuration. For a Docker deployment serving both the frontend and API from the same origin, CORS may not be strictly necessary. However, the Supabase client makes cross-origin requests to the Supabase hosted service, and if any API routes are added, CORS misconfiguration could allow unauthorized cross-origin access. Additionally, a permissive CORS policy (e.g., `Access-Control-Allow-Origin: *`) on API routes would allow any website to make authenticated requests to the application.
+
+**Fix:** (a) Configure CORS on any API routes to allow only the application's own origin. (b) Do not set `Access-Control-Allow-Origin: *` on any endpoint that processes cookies or authentication. (c) Supabase handles its own CORS; ensure the Supabase project's allowed origins are configured to include only your application's domain. (d) For Docker, if using a reverse proxy, configure CORS at the proxy level.
+
+**Implementation Risk:** CORS misconfigurations can be hard to debug. Test cross-origin requests from the browser console during development.
+
+---
+
+## LOW
+
+### 15. Group Name and List Metadata Input Validation Not Specified
+
+`PROJECT_SCOPE.md:99,349` — Group names are user-provided text with no validation rules mentioned. Similar to display names (Finding 9), group names are rendered across the application and could be vectors for stored XSS or layout disruption if not validated.
+
+**Fix:** Apply the same validation approach as display names: length limits (1-50 characters), character restrictions, server-side enforcement via Supabase CHECK constraints. React's JSX escaping provides baseline XSS protection.
+
+**Implementation Risk:** Minimal. Standard input validation.
+
+---
+
+### 16. No Account Deletion or Data Retention Policy
+
+`PROJECT_SCOPE.md` — The scope describes user deletion by the Master Admin but does not mention self-service account deletion. Under GDPR and similar regulations, users have the right to delete their accounts and associated data. Even though the app collects minimal data (display name and avatar color only), the absence of a self-service deletion flow could be a compliance gap.
+
+**Fix:** (a) Add a "Delete my account" option in user settings. (b) Define what happens to a user's data on deletion: movies they added should either be reassigned or have the `added_by` reference set to null. (c) Group memberships should be cleaned up; if the user is a List Admin, trigger the ownership transfer flow. (d) Recovery codes should be deleted. (e) Document the data retention policy.
+
+**Implementation Risk:** Cascading deletions can have unintended side effects. Use soft deletes initially (mark as deleted, purge after 30 days) to allow recovery from accidental deletions.
+
+---
+
+### 17. Trailer URL External Redirect Without Validation
+
+`PROJECT_SCOPE.md:296,366` — Trailer URLs are fetched from TMDB and stored in the database. When a user clicks "Trailer," the URL opens in a new tab. If the stored URL is somehow corrupted or manipulated (e.g., via a TMDB API response manipulation, a database injection, or an admin error), the user could be redirected to a malicious site.
+
+**Fix:** (a) Validate that stored trailer URLs match expected YouTube/TMDB URL patterns before storing them (e.g., must start with `https://www.youtube.com/watch?v=` or `https://www.themoviedb.org/`). (b) When rendering the trailer link, add `rel="noopener noreferrer"` to prevent the opened page from accessing the opener window. (c) Consider proxying trailer URLs through an interstitial "You are leaving MovieDice" page.
+
+**Implementation Risk:** TMDB may change their trailer URL format. Use a flexible allowlist (domain-based rather than full URL pattern) and update as needed.
+
+---
+
+### 18. No Logging or Audit Trail Specified
+
+`PROJECT_SCOPE.md` — The scope does not mention application-level logging for security events. Without logging, it is impossible to detect or investigate: brute-force attempts on invite codes or recovery codes, unauthorized admin access, mass deletion of data, or API abuse.
+
+**Fix:** Implement structured logging for: (a) All authentication events (user creation, recovery code claims, admin login attempts). (b) All authorization failures (accessing groups the user does not belong to, admin route access without session). (c) Rate limit violations. (d) Admin actions (list/user deletions). (e) Use structured JSON logging and forward to a log aggregation service (e.g., Sentry, Datadog, or even Docker logging drivers).
+
+**Implementation Risk:** Excessive logging can create performance overhead and storage costs. Log at appropriate levels (INFO for auth events, WARN for failures, ERROR for system issues). Ensure logs do not contain sensitive data (recovery codes, TOTP codes).
+
+---
+
+## INFORMATIONAL
+
+### 19. Supabase Anonymous Auth vs. Custom UUID Scheme
+
+`PROJECT_SCOPE.md:95-104` — The scope describes a custom anonymous auth implementation (generate UUID, store in local storage). Supabase provides a built-in Anonymous Sign-In feature that handles session management, JWT issuance, refresh tokens, and RLS integration automatically. The custom approach requires implementing all of these security primitives from scratch, increasing the risk of implementation errors.
+
+**Fix:** Evaluate using Supabase's built-in anonymous auth (`supabase.auth.signInAnonymously()`). This provides: automatic JWT session management, secure token storage, RLS integration via `auth.uid()`, and a migration path to linking the anonymous account with email/password later if needed. The recovery code mechanism could still be layered on top for device transfer.
+
+**Implementation Risk:** Supabase anonymous auth may not perfectly match the desired UX flow. Test whether the anonymous sign-in flow integrates cleanly with the display name and recovery code features. The anonymous user could be linked to a full account later, but this adds complexity beyond MVP scope.
+
+---
+
+### 20. `is_master_admin` Boolean on Users Table Is Unnecessary and Risky
+
+`PROJECT_SCOPE.md:272` — The data model includes `is_master_admin (boolean, default false)` on the users table. Since the master admin is configured via environment variables and has a separate session, storing an admin flag on a regular user row is redundant. Worse, if RLS policies are not correctly configured, an attacker could potentially set `is_master_admin = true` on their own user row and gain admin privileges (depending on how the application checks admin status).
+
+**Fix:** Remove `is_master_admin` from the users table entirely. Admin status should be determined solely by a valid admin session token issued after TOTP verification. The admin session is already described as separate from regular user sessions — there is no need for a database flag. If admin status must be checked in database queries, use a separate `admins` table accessible only via the service role key.
+
+**Implementation Risk:** If any application logic already references `is_master_admin`, it must be refactored to check the admin session instead. This is a design-phase change, so the risk is minimal.
+
+---
+
+### 21. Service Worker and PWA Caching Security Considerations
+
+`PROJECT_SCOPE.md:401-402` — The scope plans for PWA support with a service worker for offline capability. Service workers can cache sensitive data (user sessions, group data, movie lists) in the browser's Cache API, which persists even after the tab is closed. On shared devices, this cached data could be accessed by subsequent users.
+
+**Fix:** (a) Do not cache authentication tokens or session data in the service worker cache. (b) Cache only static assets (images, CSS, JS) and public data (TMDB posters). (c) Implement cache expiry and versioning. (d) On logout or account deletion, clear all service worker caches (`caches.delete()`). (e) Use the `Clear-Site-Data` header on logout to clear caches, cookies, and storage in one operation.
+
+**Implementation Risk:** Aggressive cache clearing can degrade the offline experience. Balance security with usability — cache static assets aggressively but treat user-specific data as ephemeral.
+
+---
+
+### 22. Invite Code Rotation Does Not Revoke Existing Members
+
+`PROJECT_SCOPE.md:51,353` — The scope states invite code regeneration "revokes access for anyone with the old code." This is misleading — regenerating the code only prevents new joins with the old code. Existing members who joined with the old code remain in the group. There is no mechanism to forcibly remove members who joined with a compromised code, other than the admin manually removing them one by one.
+
+**Fix:** This is a design clarification, not a vulnerability. Document clearly that code regeneration prevents new joins but does not remove existing members. Consider adding a "Remove all non-admin members" bulk action for scenarios where a code was widely compromised.
+
+**Implementation Risk:** Minimal. This is primarily a documentation and UX expectation issue.
+
+---
+
+## Summary
+
+| Severity | Total |
+| -------- | ----- |
+| Critical | 2     |
+| High     | 5     |
+| Medium   | 6     |
+| Low      | 4     |
+| Info     | 4     |
+
+## Top 3 Priorities
+
+1. **Implement Supabase RLS policies on every table before writing any client-side data access code.** Without RLS, the entire database is exposed to any client. This is the single most important security task. (Findings 1, 2, 6)
+2. **Redesign the authentication model: use Supabase anonymous auth or issue signed session tokens instead of relying on a bare UUID in local storage.** Combine with httpOnly cookies. Specify recovery code entropy (128+ bits) and hashing algorithm (bcrypt/Argon2id). (Findings 4, 5, 19)
+3. **Increase invite code entropy and implement server-side rate limiting on all authentication and access-control endpoints** (invite code validation, recovery code claims, admin login, TMDB proxy). (Findings 3, 7, 11)
+
+## Positive Observations
+
+- Recovery codes are hashed before storage (the right approach, just needs algorithm specification)
+- Master Admin uses TOTP rather than a static password, and credentials are kept server-side in environment variables
+- No email or real identity data is collected, minimizing privacy risk and data breach impact
+- The scope explicitly separates admin sessions from regular user sessions
+- TOTP secret is specified as never exposed client-side
+- Display name and recovery code are the only user-provided data stored, keeping the data model minimal
+- The scope plans for offline graceful degradation with disabled write actions, which is the correct pattern
+
+---
+
+## Files Reviewed
+
+_This is a pre-implementation architecture review. No source code exists._
+
+| #   | File                       | Type                              |
+| --- | -------------------------- | --------------------------------- |
+| 1   | `PROJECT_SCOPE.md`         | Architecture/design specification |
+| 2   | `research/PROJECT_INFO.md` | Project context file              |
+
+---
+
+---
+
+## Second Review -- Updated Architecture Analysis
+
+Reviewed: 2026-04-05 | Auditor: Claude (automated) | Type: Second-pass review of updated PROJECT_SCOPE.md
+
+**Context:** The scope has been significantly updated since the first review. RLS policies, Supabase Anonymous Sign-In, Argon2id hashing, WORD-WORD invite codes with rate limiting, iron-session for admin, security headers, Docker hardening, and input validation have all been added. This section covers only NEW security gaps in the updated architecture. Findings from the first review that are now addressed in the scope are not repeated.
+
+**Note on research:** Web search was unavailable during this review. All analysis is based on the reviewer's existing knowledge of these technologies. Findings are cached in `research/SECURITY-RESEARCH.md` for future reference.
+
+---
+
+### CRITICAL
+
+### 23. Self-Hosted Supabase Default Secrets Must All Be Replaced
+
+`PROJECT_SCOPE.md:391` -- The scope calls for a "self-hosted Supabase Docker stack" using the official docker-compose configuration, but does not mention replacing the default secrets. Supabase's official self-hosted `.env` file ships with well-known placeholder values for `JWT_SECRET`, `POSTGRES_PASSWORD`, `ANON_KEY`, `SERVICE_ROLE_KEY`, `DASHBOARD_USERNAME`, and `DASHBOARD_PASSWORD`. These defaults are published on GitHub and known to every attacker. If any of these are left at their default values: (a) the `JWT_SECRET` is public, meaning anyone can forge valid JWTs and impersonate any user or the service role, completely bypassing RLS; (b) the `POSTGRES_PASSWORD` allows direct database access; (c) the `SERVICE_ROLE_KEY` (derived from `JWT_SECRET`) bypasses all RLS policies. This is the single most critical risk for the self-hosted deployment model.
+
+**Fix:** Add an explicit checklist to the deployment documentation and Phase 1.7 requiring replacement of ALL Supabase default secrets before first deployment:
+
+1. Generate a new `JWT_SECRET` (at least 32 random characters, recommend 64)
+2. Regenerate `ANON_KEY` and `SERVICE_ROLE_KEY` using the new `JWT_SECRET` via Supabase's JWT generation tool or `jsonwebtoken` library
+3. Set a strong `POSTGRES_PASSWORD` (32+ random characters)
+4. Change `DASHBOARD_USERNAME` and `DASHBOARD_PASSWORD` for Studio
+5. Ensure `GOTRUE_JWT_SECRET` and `PGRST_JWT_SECRET` match the new `JWT_SECRET`
+6. Add a startup validation check (similar to the zod env validation for the Next.js app) that refuses to start if any known default values are detected
+
+**Implementation Risk:** Regenerating the JWT secret requires regenerating both the anon key and service role key, since these are JWTs signed with that secret. All three values must be updated in lockstep. If they are mismatched, authentication will silently fail.
+
+---
+
+### 24. Supabase Studio Dashboard Exposed to the Internet
+
+`PROJECT_SCOPE.md:391` -- The self-hosted Supabase stack includes Supabase Studio, which is a full administrative dashboard providing direct access to the database, table editor, SQL editor, auth user management, and configuration. By default, Studio is exposed on port 3000 in the docker-compose configuration. If the Caddy reverse proxy or Docker port mapping makes Studio accessible from the internet, any attacker who discovers it (or any scanner hitting common ports) can attempt to log in. Studio's default credentials are `supabase`/`this_password_is_insecure_and_should_be_updated` (or similar well-known defaults). Even with changed credentials, exposing a full database admin panel to the internet is unnecessary attack surface.
+
+**Fix:** (a) Do NOT expose Studio's port to the host network in docker-compose. Remove or comment out the `ports:` mapping for the Studio service, or bind it to `127.0.0.1:3000` only. (b) Access Studio only via SSH tunnel when needed: `ssh -L 3000:localhost:3000 your-server`. (c) If Studio must be exposed, place it behind Caddy with HTTP basic auth and restrict to a non-guessable path. (d) Consider removing the Studio service entirely from the production docker-compose and only running it in development. (e) Add this to the Phase 1.7 deployment checklist.
+
+**Implementation Risk:** Removing Studio from production means database administration must be done via SSH + psql or a tunneled connection. This is acceptable for a single-admin project and significantly reduces attack surface.
+
+---
+
+### HIGH
+
+### 25. Supabase Auth Tokens in localStorage Remain Vulnerable to XSS
+
+`PROJECT_SCOPE.md:97-98,270` -- The updated scope correctly adopts Supabase Anonymous Sign-In, which issues JWTs managed by GoTrue. However, the Supabase JavaScript client (`@supabase/supabase-js`) stores both access tokens and refresh tokens in **localStorage** by default. This is inherent to how the Supabase client works and cannot be changed to httpOnly cookies without server-side session management, because the JavaScript client needs to read the token to attach it to requests. If an XSS vulnerability exists anywhere in the application (a compromised npm dependency, a CSP bypass, a stored XSS in a field not covered by React's auto-escaping), an attacker can exfiltrate both the access token and the refresh token from localStorage, gaining full session hijack capability. The refresh token is especially dangerous because it has a long lifetime (typically 1 week by default in GoTrue).
+
+This is a known Supabase design tradeoff, but it should be explicitly acknowledged and mitigated in the scope.
+
+**Fix:** (a) Use `@supabase/ssr` with the Next.js App Router to store tokens in cookies instead of localStorage. While these cookies are not httpOnly (the JS client must read them), this still provides some benefit: cookies can be set with `SameSite=Lax` and `Secure` flags, and they are not accessible to injected scripts that use `localStorage` APIs without also reading `document.cookie`. (b) Reduce refresh token lifetime in GoTrue configuration to the minimum acceptable (e.g., 24 hours instead of 7 days). (c) Prioritize a strict CSP to prevent XSS as the primary mitigation. (d) Consider implementing Content-Security-Policy nonces for inline scripts if any are needed. (e) Ensure no use of `dangerouslySetInnerHTML` anywhere in the codebase.
+
+**Implementation Risk:** `@supabase/ssr` changes the auth flow and requires middleware to handle token refresh on the server side. This adds complexity but is the recommended approach for Next.js App Router projects. Test that real-time subscriptions still authenticate correctly when tokens are in cookies.
+
+---
+
+### 26. RLS INSERT Policies Must Restrict user_id/added_by Column Spoofing
+
+`PROJECT_SCOPE.md:351-358` -- The RLS policy descriptions say "Full CRUD for members of the owning group only" for movies and "insertable via valid invite code flow" for group_members. However, INSERT policies must also verify that the user cannot spoof the `added_by` column in movies or the `user_id` column in group_members. Without a `WITH CHECK (added_by = auth.uid())` clause on the movies INSERT policy, a group member could insert movies attributed to another user. Without `WITH CHECK (user_id = auth.uid())` on group_members, a user could potentially insert membership records for other users.
+
+**Fix:** For every INSERT policy, add explicit `WITH CHECK` constraints:
+
+- `movies` INSERT: `WITH CHECK (added_by = auth.uid() AND group_id IN (SELECT group_id FROM group_members WHERE user_id = auth.uid()))`
+- `group_members` INSERT: `WITH CHECK (user_id = auth.uid())` -- and consider whether client-side INSERT is even appropriate here, or if joining should be a server-side operation via the service role key
+- `movies` UPDATE: Ensure users cannot change `added_by` to another user's ID
+- `group_members`: Ensure users cannot change their own `role` from `member` to `admin`
+
+**Implementation Risk:** Overly restrictive policies can break legitimate operations. Test that the List Admin can still perform admin actions (member removal, etc.) -- these may need to go through server-side API routes using the service role key rather than direct client-side queries.
+
+---
+
+### 27. Kong API Gateway Ports Must Not Be Internet-Accessible
+
+`PROJECT_SCOPE.md:391` -- The self-hosted Supabase stack uses Kong as the API gateway, which exposes PostgREST, GoTrue, Realtime, and Storage endpoints. By default, Kong listens on port 8000 (HTTP) and 8443 (HTTPS). In the architecture, the Next.js application communicates with Supabase through Kong. If Kong's ports are also exposed to the internet directly (via Docker port mapping), attackers can bypass the Next.js application entirely and interact with the Supabase API directly -- crafting arbitrary PostgREST queries, calling GoTrue auth endpoints, and probing for misconfigurations. While RLS provides protection, direct API access exposes more attack surface than necessary (schema introspection, auth endpoint abuse, etc.).
+
+**Fix:** (a) Do NOT expose Kong ports (8000, 8443) to the host network. Bind them to `127.0.0.1` only, or better, keep them internal to the Docker network with no port mapping. (b) The Next.js container communicates with Kong over the internal Docker network (e.g., `http://kong:8000`). (c) Caddy should only proxy requests to the Next.js application, not to Kong. (d) If the Supabase JS client in the browser needs direct access to Supabase (for Realtime WebSocket connections), route those through Caddy to Kong on a specific path (e.g., `/supabase/`) rather than exposing Kong directly. (e) Disable PostgREST schema introspection in production by setting `PGRST_OPENAPI_MODE=disabled`.
+
+**Implementation Risk:** The Supabase JS client is configured with `SUPABASE_URL`, which tells the browser where to connect for auth, database queries, and real-time subscriptions. If Kong is not directly accessible, `SUPABASE_URL` must point to a Caddy-proxied path, which requires Caddy configuration to forward Supabase API and WebSocket traffic correctly. This is a non-trivial configuration step.
+
+---
+
+### 28. GoTrue Auth Methods Must Be Restricted to Anonymous Only
+
+`PROJECT_SCOPE.md:270` -- The scope uses only Supabase Anonymous Sign-In. However, GoTrue (Supabase's auth service) enables multiple auth providers by default, including email/password signup, magic link, and potentially OAuth providers. If these are not explicitly disabled, an attacker could create a non-anonymous authenticated user via the email/password signup endpoint, which would have the `authenticated` role and potentially different RLS treatment than expected. Depending on RLS policy design, this could grant unintended access.
+
+**Fix:** In the GoTrue configuration (docker-compose environment variables for the `auth` service):
+
+- Set `GOTRUE_EXTERNAL_EMAIL_ENABLED=false`
+- Set `GOTRUE_EXTERNAL_PHONE_ENABLED=false`
+- Disable all OAuth providers (`GOTRUE_EXTERNAL_*_ENABLED=false`)
+- Ensure `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true`
+- Set `GOTRUE_DISABLE_SIGNUP=false` (anonymous sign-in counts as signup)
+- Consider setting `GOTRUE_MAILER_AUTOCONFIRM=true` as a safety net so any accidentally enabled email flows don't send actual emails
+
+**Implementation Risk:** If auth methods are disabled after users have been created via those methods, existing sessions remain valid until they expire. Since this is a new deployment, there is no migration risk.
+
+---
+
+### MEDIUM
+
+### 29. 12-Month Auto-Deletion Lacks Orphan Group Handling
+
+`PROJECT_SCOPE.md:309,374` -- The scope defines a 12-month inactivity auto-deletion policy for users, with `last_active_at` tracked on the users table. However, the scope does not address what happens to groups when the last member (or the only admin) is auto-deleted. Scenarios: (a) User is the sole admin of a group with other members -- deleting the admin leaves an admin-less group where no one can perform admin actions (rename, delete, regenerate invite code, remove members). (b) User is the last member of a group -- the group and its movies become orphaned data with no one who can access or delete them. (c) If `added_by` has a foreign key to `users.id` without ON DELETE handling, deletion will either fail (FK violation) or cascade-delete all movies the user added.
+
+**Fix:** The auto-deletion job must handle these cases:
+
+1. Before deleting a user, check if they are the sole admin of any group with other members. If so, auto-transfer admin role to the longest-tenured member.
+2. If the user is the last member of a group, delete the group and all its movies (cascade).
+3. Set `added_by` foreign key to `ON DELETE SET NULL` so movies are preserved but the attribution is anonymized.
+4. Log all auto-deletions and the cascading actions taken.
+5. The `last_active_at` timestamp must be updated on meaningful activity (not just login). Relevant activity: adding/removing movies, marking watched, rolling dice, joining/creating groups. Use Supabase's `auth.users.last_sign_in_at` as one input but supplement it with application-level activity tracking.
+
+**Implementation Risk:** The admin transfer logic adds complexity to the deletion job. If the job fails partway through (deletes some users but crashes before handling their groups), the database could be left in an inconsistent state. Wrap the entire per-user deletion in a transaction.
+
+---
+
+### 30. WORD-WORD Invite Code Word List Requirements Not Specified
+
+`PROJECT_SCOPE.md:35,315` -- The scope specifies WORD-WORD format (e.g., WOLF-MOON) with rate limiting on the join endpoint (5-10 failed attempts per IP per window). The rate limiting makes brute force impractical (see research cache), but the scope does not specify: (a) the word list source or size, (b) whether words are case-sensitive, (c) maximum word length for usability, or (d) filtering for offensive or confusing words. These implementation details affect both security and usability.
+
+**Fix:** (a) Use a curated word list of at least 2,000 words (yielding 4 million combinations with WORD-WORD; with rate limiting at 5/15min, brute force would take ~24 years). A list of 4,000+ words is even better. (b) Use uppercase only for display; comparison should be case-insensitive. (c) Filter the word list to remove: offensive/vulgar words, words shorter than 3 characters, words longer than 8 characters (for verbal sharing), easily confused word pairs (e.g., "THEIR"/"THERE"), and culturally sensitive terms. (d) When generating a code, verify the combination is not already in use (collision check). (e) Consider using a well-known word list as a base, such as the EFF short word list (1,296 words) or a filtered subset of the BIP39 word list (2,048 words). (f) Document the word list in the codebase with a rationale for its size.
+
+**Implementation Risk:** Smaller word lists are easier to share verbally but have higher collision probability and lower brute-force resistance. With rate limiting in place, a 2,000-word list provides adequate security. If multiple groups exist simultaneously, the collision check becomes important -- with 4M possible codes and hundreds of active groups, collisions are rare but should be handled.
+
+---
+
+### 31. Argon2id Parameters Must Be Specified and Tested Against Container Limits
+
+`PROJECT_SCOPE.md:34,308,368` -- The scope specifies Argon2id for recovery code hashing but does not define the memory, time (iterations), or parallelism parameters. In a Docker container with limited memory (typical: 512MB-1GB), using Argon2id's high-memory settings (e.g., 64 MiB per hash) could cause OOM kills if multiple recovery code operations run concurrently. Conversely, using parameters that are too low undermines the purpose of Argon2id.
+
+**Fix:** Specify parameters in the scope or implementation docs:
+
+- Memory: 19,456 KiB (~19 MiB) -- aligns with OWASP 2024 recommendation
+- Iterations: 2
+- Parallelism: 1
+- Output hash length: 32 bytes
+- Salt: 16 bytes (generated automatically by the argon2 npm library)
+  These settings use ~19 MiB per hash operation. Since recovery code hashing only occurs on account creation and code claims (both infrequent), concurrent memory pressure is unlikely. However, add a concurrency limit (e.g., max 2 simultaneous hash operations via a semaphore) as a safety measure. Test that a hash operation completes within 500ms on the target hardware. If the Docker container has less than 512 MiB RAM, reduce memory to 12,288 KiB.
+
+**Implementation Risk:** If parameters are not specified, developers may use library defaults which vary: the `argon2` npm package defaults to memory=65536 KiB (64 MiB), which could be problematic in constrained containers. Always pass explicit parameters rather than relying on defaults.
+
+---
+
+### 32. Caddy TLS Certificate Persistence Must Be Configured in Docker
+
+`PROJECT_SCOPE.md:278,392` -- Caddy is used as the reverse proxy for HTTPS termination. Caddy automatically obtains TLS certificates via Let's Encrypt and renews them. However, if Caddy's data directory (`/data`) is not mounted as a persistent Docker volume, certificates are lost on every container restart. This causes Caddy to request new certificates each time, and Let's Encrypt enforces strict rate limits: 5 duplicate certificates per registered domain per week. After hitting this limit, the site will fail to obtain a certificate and HTTPS will stop working for up to a week.
+
+**Fix:** In docker-compose.yml, mount persistent volumes for Caddy:
+
+```yaml
+volumes:
+  - caddy_data:/data
+  - caddy_config:/config
+```
+
+And declare the named volumes. This ensures certificates survive container restarts, upgrades, and redeployments. Additionally: (a) Test certificate renewal in a staging environment using Let's Encrypt's staging ACME endpoint first (`acme_ca https://acme-staging-v02.api.letsencrypt.org/directory` in Caddyfile). (b) Ensure ports 80 and 443 are accessible from the internet for ACME HTTP-01 challenges. (c) Do NOT use `on_demand` TLS for a single-domain deployment.
+
+**Implementation Risk:** If volumes are accidentally deleted during a cleanup, certificates are lost. Back up the `caddy_data` volume periodically, though Caddy will re-obtain certificates automatically (subject to rate limits).
+
+---
+
+### 33. Postgres Port Must Not Be Exposed to Host Network
+
+`PROJECT_SCOPE.md:391` -- The self-hosted Supabase docker-compose typically maps Postgres port 5432 to the host for local development convenience. In production, exposing the Postgres port to the host network (and potentially the internet, depending on firewall configuration) allows direct database connections that bypass all application-level security, RLS policies (since a direct connection uses the postgres role, not the anon role), and authentication.
+
+**Fix:** (a) Remove the `ports:` mapping for the Postgres service in the production docker-compose, or bind it to `127.0.0.1:5432` only. (b) All services that need database access (PostgREST, GoTrue, Realtime, pg_cron) communicate over the internal Docker network. (c) For database administration, use SSH tunneling: `ssh -L 5432:localhost:5432 your-server` then connect via `psql`. (d) Ensure the `POSTGRES_PASSWORD` is strong (32+ random characters) as a defense-in-depth measure even if the port is not exposed.
+
+**Implementation Risk:** Removing the port mapping means tools like pgAdmin or Supabase Studio cannot connect from outside the Docker network without an SSH tunnel. This is an acceptable operational tradeoff for production security.
+
+---
+
+### LOW
+
+### 34. iron-session Secret Rotation Requires Application Restart
+
+`PROJECT_SCOPE.md:53,402` -- The scope specifies a 32+ character `IRON_SESSION_SECRET` for encrypting admin session cookies. iron-session supports secret rotation by passing an array of secrets (newest first), which allows changing the secret without immediately invalidating all active sessions. However, the scope does not mention a rotation procedure. If the secret is compromised (e.g., via a leaked `.env` file or Docker image layer), all admin sessions encrypted with that secret can be forged. Without a rotation procedure, the response time to a compromise is slower.
+
+**Fix:** (a) Document the secret rotation procedure: generate a new secret, add it as the first element of an array with the old secret as the second, deploy, then remove the old secret after 8 hours (the session expiry). (b) Use a secret of at least 64 characters for additional entropy margin. (c) Ensure the secret is never committed to version control or baked into Docker image layers.
+
+**Implementation Risk:** Minimal. iron-session's array-based rotation is straightforward. The main risk is forgetting to remove the old secret after the rotation window.
+
+---
+
+### 35. CSP Must Account for Supabase Self-Hosted URLs
+
+`PROJECT_SCOPE.md:379-386` -- The security headers section mentions restricting `connect-src` to "Supabase `wss://`" but uses the wildcard `*.supabase.co`, which is the pattern for Supabase's managed hosting. For a self-hosted deployment, the Supabase services are reached via the Caddy reverse proxy on the project's own domain (or a subdomain). The CSP directives must reflect the actual self-hosted URLs, not the managed hosting pattern.
+
+**Fix:** Update the CSP template to use the actual deployment domain:
+
+- `connect-src 'self' wss://yourdomain.com` (for Supabase Realtime via Caddy proxy)
+- `img-src 'self' https://image.tmdb.org` (unchanged)
+- Remove `*.supabase.co` references since traffic goes through the same origin or a known subdomain
+- If Supabase API calls go through the Next.js server proxy, `connect-src 'self'` may be sufficient for API calls, with only the WebSocket needing an explicit entry
+- Add `script-src 'self'` -- verify no inline scripts are needed; if Next.js injects inline scripts, use nonces
+
+**Implementation Risk:** An incorrect CSP will break the application in production. Use `Content-Security-Policy-Report-Only` during development (already in the scope) and test all functionality including Realtime WebSocket connections before switching to enforcement mode.
+
+---
+
+### 36. pg_cron Jobs Run as Superuser by Default
+
+`PROJECT_SCOPE.md:277,394` -- The scope uses `pg_cron` for background jobs (landing reel refresh, trailer URL refresh, and potentially inactive account deletion). In self-hosted Supabase, pg_cron jobs run as the `postgres` superuser by default, which bypasses all RLS policies. If a pg_cron job is configured to call an Edge Function via `net.http_post`, the Edge Function receives no auth context. If the Edge Function uses the service role key to perform database operations, this is expected and acceptable. However, if pg_cron SQL jobs directly modify tables, they do so with superuser privileges, which means a SQL injection in the job definition (unlikely but possible if job SQL is dynamically constructed) would have unrestricted access.
+
+**Fix:** (a) Keep pg_cron job SQL static and simple -- use it only to trigger Edge Functions via HTTP, not to perform complex data operations directly. (b) Edge Functions should use the service role key with explicit authorization checks. (c) If pg_cron must run SQL directly (e.g., for the inactive account deletion), review the SQL carefully for injection risks and ensure it is parameterized. (d) Monitor pg_cron job execution logs for failures or unexpected behavior.
+
+**Implementation Risk:** Minimal for this project since the pg_cron jobs are straightforward (refresh posters, refresh trailer URLs, delete inactive accounts). The risk increases if job definitions become more complex over time.
+
+---
+
+### INFORMATIONAL
+
+### 37. Supabase Anonymous Users Can Be Linked -- Design Decision Needed
+
+`PROJECT_SCOPE.md:270` -- Supabase's anonymous sign-in creates users with `is_anonymous: true` in the JWT. Supabase supports "linking" anonymous users to a permanent identity (email/password) via `updateUser()`. While the scope explicitly excludes email-based auth, the linking capability remains available in GoTrue unless disabled. An anonymous user who discovers this could call `supabase.auth.updateUser({ email: '...' })` from the browser console, potentially converting their anonymous account to a permanent one with email confirmation, which could conflict with the privacy-first design intent.
+
+**Fix:** This is a design decision, not a vulnerability. If account linking should not be possible: (a) add RLS or server-side middleware that rejects `updateUser` calls that add email/password to anonymous users, or (b) disable email auth entirely in GoTrue (see Finding 28, which covers this). If account linking is desirable for a future upgrade path, document it as intentional.
+
+**Implementation Risk:** Disabling email auth entirely (Finding 28) addresses this comprehensively. No additional action needed if that fix is implemented.
+
+---
+
+### 38. `last_active_at` Update Strategy Needs Definition
+
+`PROJECT_SCOPE.md:309` -- The `last_active_at` column is described as "updated on login/activity" but the update triggers are not specified. If `last_active_at` is only updated on explicit login (session creation), a user who remains active for months via token refresh without ever re-authenticating would appear inactive and could be auto-deleted while actively using the application. This is especially relevant with Supabase's automatic token refresh, which extends sessions without triggering a new "login."
+
+**Fix:** Define specific events that update `last_active_at`: (a) session creation (initial `signInAnonymously()`), (b) token refresh (via a Supabase `onAuthStateChange` listener on the server side), (c) any write operation (add/remove movie, mark watched, create/join group). Implement via: a Postgres trigger on INSERT/UPDATE to `movies` and `group_members` tables that updates `users.last_active_at`, plus server-side middleware that updates it on authenticated API route calls. Avoid updating on every read (too noisy) but ensure at least weekly updates for active users.
+
+**Implementation Risk:** Frequent updates to `last_active_at` create write amplification on the users table. Use a throttled approach: only update if the current value is more than 24 hours old (e.g., `UPDATE users SET last_active_at = now() WHERE id = auth.uid() AND last_active_at < now() - interval '1 day'`).
+
+---
+
+## Second Review Summary
+
+| Severity | New Findings |
+| -------- | ------------ |
+| Critical | 2 (23, 24)   |
+| High     | 4 (25-28)    |
+| Medium   | 5 (29-33)    |
+| Low      | 3 (34-36)    |
+| Info     | 2 (37, 38)   |
+
+## Top 3 New Priorities
+
+1. **Replace ALL Supabase default secrets and restrict Studio/Kong/Postgres network exposure.** The self-hosted deployment model introduces infrastructure-level risks that do not exist with managed Supabase. Default secrets are the most critical issue. (Findings 23, 24, 27, 33)
+2. **Disable all GoTrue auth methods except anonymous sign-in, and add INSERT column restrictions to RLS policies.** Unrestricted auth methods and missing `WITH CHECK` clauses on INSERT policies create authorization bypass vectors. (Findings 26, 28)
+3. **Specify Argon2id parameters, Caddy volume persistence, and the auto-deletion orphan handling logic** before implementation begins. These are medium-severity but easy to get wrong if left to developer discretion. (Findings 29, 31, 32)
+
+## New Positive Observations (Updated Architecture)
+
+- Supabase Anonymous Sign-In with JWT is a major improvement over the original custom UUID scheme -- it provides proper session management, refresh tokens, and RLS integration via `auth.uid()`
+- RLS policies are now specified at the design level with per-table rules, which is the correct approach
+- Argon2id for recovery codes is the strongest available choice for password/code hashing
+- iron-session with HttpOnly, Secure, SameSite=Strict cookies and 8-hour expiry for admin sessions is well-designed
+- Rate limiting on invite code join and recovery code claim endpoints addresses the brute-force vectors from the first review
+- TMDB API proxy is explicitly server-side only with the correct `NEXT_PUBLIC_` exclusion
+- Security headers are now in the scope with a plan for Report-Only mode during development
+- Docker non-root user, tini, and health check are all specified
+- Trailer URL domain allowlist validation (youtube.com, themoviedb.org, imdb.com) addresses the external redirect risk
+- `is_master_admin` has been removed from the users table as recommended
+- Input validation with CHECK constraints on display_name and group name is now specified
+- zod env validation at startup prevents deployment with missing configuration
+
+---
+
+## Files Reviewed (Second Review)
+
+| #   | File                                      | Type                              |
+| --- | ----------------------------------------- | --------------------------------- |
+| 1   | `PROJECT_SCOPE.md` (updated)              | Architecture/design specification |
+| 2   | `research/SECFILE.md` (first review)      | Previous security audit           |
+| 3   | `research/PROJECT_INFO.md`                | Project context file              |
+| 4   | `research/SECURITY-RESEARCH.md` (created) | Security research cache           |

+ 123 - 0
research/SECURITY-RESEARCH.md

@@ -0,0 +1,123 @@
+# Security Research Cache
+
+## Supabase Anonymous Sign-In Security Model
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- `signInAnonymously()` creates a real auth user in GoTrue with `is_anonymous: true` in the JWT claims
+- JWT is stored in **localStorage** by default via the Supabase JS client (`@supabase/supabase-js`)
+- The JWT contains: `sub` (user UUID), `aud`, `role` (defaults to `authenticated`), `is_anonymous: true`, `exp`, `iat`, `session_id`
+- Anonymous users CAN be linked/upgraded to a permanent identity later via `updateUser()` with email/password
+- The `is_anonymous` claim in the JWT can be used in RLS policies to distinguish anonymous from linked users
+- Refresh tokens are also stored in localStorage alongside the access token
+- The Supabase JS client does NOT support httpOnly cookie storage natively; server-side auth helpers (`@supabase/ssr`) use cookies but they are NOT httpOnly by default -- the JS client needs to read them
+- Anonymous sessions have the same `authenticated` role as regular users in PostgREST/RLS context
+- Risk: localStorage tokens are vulnerable to XSS exfiltration; this is a known Supabase design tradeoff
+
+## Self-Hosted Supabase Security Hardening
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- Self-hosted Supabase uses Kong as the API gateway in front of all services
+- **Critical defaults that MUST be changed:**
+  - `POSTGRES_PASSWORD` - default is `your-super-secret-and-long-postgres-password`
+  - `JWT_SECRET` - default is a known value; MUST be changed to a unique 32+ char secret
+  - `ANON_KEY` and `SERVICE_ROLE_KEY` must be regenerated using the new JWT_SECRET
+  - `DASHBOARD_USERNAME` and `DASHBOARD_PASSWORD` for Supabase Studio
+  - `GOTRUE_JWT_SECRET` must match `JWT_SECRET`
+  - `PGRST_JWT_SECRET` must match `JWT_SECRET`
+- **Supabase Studio** is exposed on port 3000 by default -- this is a full admin dashboard with direct DB access
+  - Must be either: firewalled to localhost only, password-protected, or removed from docker-compose in production
+- **Kong** exposes the API on port 8000 (HTTP) and 8443 (HTTPS) by default
+  - In production, only the reverse proxy should reach Kong; do not expose Kong ports to the internet
+- **PostgREST** allows introspection of the database schema by default via OPTIONS requests
+  - Consider restricting schema introspection in production
+- **Postgres port 5432** should NOT be exposed to the host network
+- The self-hosted `.env` file from Supabase's GitHub contains placeholder secrets -- ALL must be replaced
+- GoTrue email confirmation is enabled by default; for anonymous-only auth, this is irrelevant but other auth methods should be disabled
+- Edge Functions run in a local Deno runtime; ensure the Deno container has no unnecessary network access
+
+## Caddy Automatic HTTPS in Docker
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- Caddy uses ACME (Let's Encrypt) for automatic certificate provisioning and renewal
+- In Docker, Caddy needs persistent storage for certificates: mount `/data` and `/config` as volumes
+- If certificate storage is not persisted, Caddy will request new certificates on every container restart, risking Let's Encrypt rate limits (5 duplicate certs per week)
+- Caddy handles renewal automatically ~30 days before expiry; no cron needed
+- **Gotcha**: Caddy must be reachable on ports 80 and 443 from the internet for ACME HTTP-01 challenges
+- **Gotcha**: Internal service communication (Caddy -> Next.js, Caddy -> Kong) should use HTTP internally, not HTTPS; Caddy terminates TLS at the edge
+- **Gotcha**: If using Docker networks, services communicate via container names (e.g., `http://nextjs:3000`); no TLS needed on internal network
+- Caddy supports `on_demand` TLS but this should be disabled for a single-domain deployment
+- Caddy automatically redirects HTTP to HTTPS
+- For local development, Caddy can issue self-signed certificates via its internal CA
+
+## iron-session Encryption
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- iron-session uses the `iron` cryptographic protocol (originally from hapi.js)
+- Encryption: AES-256-CBC with HMAC-SHA256 for integrity
+- The "password" (secret) is used to derive encryption and integrity keys via PBKDF2
+- **32-character secret**: iron requires a minimum of 32 characters. This provides the input to PBKDF2 key derivation. 32 random characters (alphanumeric) provide ~190 bits of entropy, which is sufficient
+- The secret is NOT used directly as the AES key; PBKDF2 derives the actual key
+- iron-session v8+ supports secret rotation: pass an array of secrets, newest first
+- Cookie payload is: encrypted data + HMAC + IV + salt (all base64url encoded)
+- Expiry is enforced both in the cookie `maxAge` AND within the encrypted payload (double-checked)
+- No known CVEs against iron-session or the iron protocol as of early 2026
+
+## Supabase RLS Policy Pitfalls
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- **Common mistake 1**: Using `USING` clause but forgetting `WITH CHECK` -- allows reads but silently drops unauthorized writes instead of erroring, or vice versa
+- **Common mistake 2**: Permissive policies stack with OR logic; restrictive policies stack with AND. Most people want PERMISSIVE but don't realize multiple permissive policies on the same action create an OR (any match allows access)
+- **Common mistake 3**: Not testing DELETE policies -- users may be able to delete rows they shouldn't
+- **Common mistake 4**: RLS policies that call functions which query other tables can be slow or have security issues if the called function is `SECURITY DEFINER` (runs as owner, bypasses RLS on the queried table)
+- **Common mistake 5**: Forgetting to enable RLS on a table -- without `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`, policies are defined but not enforced
+- **Common mistake 6**: Using `auth.uid()` in policies but the user's JWT has expired -- Supabase returns null for `auth.uid()` on expired tokens, which could match rows where user_id is null
+- **Common mistake 7**: INSERT policies that don't restrict which `user_id` value can be inserted -- users can insert rows with another user's ID in the `added_by` or `user_id` column
+- **Common mistake 8**: Not accounting for the `service_role` key bypassing ALL RLS -- any endpoint using the service role key must have its own authorization logic
+
+## 12-Month Auto-Deletion of Inactive Accounts
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- Key risk: `last_active_at` must be updated reliably on every meaningful activity, not just login
+- If `last_active_at` is only set on login, a user who stays logged in for months (never closing the tab) would appear inactive
+- Supabase anonymous auth sessions have refresh tokens; each token refresh could update `last_active_at`
+- The deletion job must handle cascading correctly: user -> group_members (remove memberships) -> movies (anonymize or nullify `added_by`) -> if user was last admin of a group, what happens to the group?
+- Race condition: user becomes active between the job's SELECT and DELETE -- use a transaction with `FOR UPDATE` or re-check `last_active_at` in the DELETE WHERE clause
+- Cannot notify anonymous users before deletion (no email) -- documented in scope, which is correct
+- The job should run infrequently (daily or weekly) and log all deletions for audit
+
+## WORD-WORD Invite Code Entropy
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- Entropy depends entirely on the word list size
+- Common word lists: EFF short wordlist (1296 words = 6^4), EFF long wordlist (7776 words = 6^5), BIP39 (2048 words)
+- WORD-WORD with 2048 words = 2048^2 = 4,194,304 combinations (~22 bits of entropy)
+- WORD-WORD with 7776 words = 7776^2 = 60,466,176 combinations (~26 bits of entropy)
+- At 5 attempts per window with rate limiting, brute force becomes impractical even at 22 bits:
+  - 4.2M combinations / 5 per 15 min = 12.6M minutes = ~24 years
+- Without rate limiting, 4.2M codes could be tried in minutes
+- The word list should avoid offensive words, easily confused words, and be culturally neutral
+- Only ONE invite code is valid per group at any time, so the attacker must find the specific active code
+- Comparison: Zoom meeting IDs are 9-11 digits (~30-37 bits) with waiting room as secondary control
+
+## Argon2id Parameters for Docker
+
+_Researched: 2026-04-05 (from knowledge base; web search unavailable)_
+
+- OWASP 2024 recommendation for Argon2id: memory=19456 KiB (19 MiB), iterations=2, parallelism=1
+- RFC 9106 recommends: first choice memory=2 GiB, iterations=1, parallelism=4; second choice memory=64 MiB, iterations=3, parallelism=4
+- For a resource-constrained Docker container (512MB-1GB RAM typical):
+  - memory=64 MiB per hash is too high if multiple concurrent requests hash simultaneously
+  - Recommended: memory=19456 KiB (19 MiB), iterations=2, parallelism=1 (OWASP recommendation)
+  - This uses ~19 MiB per hash operation; with a concurrency limit of 4, that's ~76 MiB
+- Salt length: minimum 16 bytes (128 bits); Argon2id generates this automatically in most libraries
+- Output hash length: 32 bytes (256 bits) is standard
+- Node.js library: `argon2` (npm) wraps the reference C implementation
+- Key concern: if the Docker container has a hard memory limit and Argon2id memory parameter is too high, the hash operation will OOM-kill the container
+- Recovery code hashing is infrequent (only on creation and claim), so higher memory cost is acceptable compared to password hashing on every login

+ 386 - 0
research/SUPPORT-RESEARCH.md

@@ -0,0 +1,386 @@
+# Support Research Cache
+
+_MovieDice Tech Audit — Initialized 2026-04-05_
+
+---
+
+## Next.js — Current Version & Docker/Self-Hosting
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025; project pre-implementation)_
+
+- **Latest stable as of knowledge cutoff**: Next.js 15.x (15.1/15.2 range). Next.js 14 is the previous LTS-style stable.
+- **Minimum Node.js**: 18.17.0+ required for Next.js 14+; Node.js 20 LTS recommended for new projects.
+- **App Router**: Stable since Next.js 13.4; recommended over Pages Router for all new projects.
+
+### Docker / Standalone Output
+
+- `output: 'standalone'` in `next.config.js` produces a self-contained `.next/standalone` folder with only necessary `node_modules` copied in — ideal for Docker.
+- The standalone server is a Node.js HTTP server (`server.js`); does not require `next start` or the full `node_modules` tree.
+- Static assets (`public/` and `.next/static/`) must be copied manually into the standalone output — they are NOT auto-included.
+- Docker multi-stage build is the officially recommended approach.
+
+### Vercel-Specific Features That Do NOT Work in Self-Hosted Docker
+
+1. **Vercel Image Optimization CDN** — The `next/image` component's `<Image>` tag still works in self-hosted mode, but uses Next.js's built-in image optimizer (sharp). This requires `sharp` to be installed as a dependency. The Vercel CDN's global edge caching, automatic format negotiation, and resize-on-demand are replaced by the local sharp processor running on the container. Performance difference is significant: Vercel's CDN serves from edge PoPs; Docker serves from one origin.
+2. **Vercel Cron Jobs** — `vercel.json` `crons` entries do not function outside Vercel. Must be replaced with an alternative scheduler (node-cron in-process, a separate cron container, Supabase pg_cron, or a system cron).
+3. **Vercel Edge Functions / Edge Runtime** — Route Handlers or Middleware with `export const runtime = 'edge'` run on Vercel's V8 isolate infrastructure. In self-hosted Docker they fall back to Node.js runtime but the V8 isolate API surface restrictions (no Node.js built-ins) can cause subtle incompatibilities. Avoid `runtime = 'edge'` for self-hosted.
+4. **ISR with Vercel's shared revalidation cache** — Incremental Static Regeneration works in self-hosted mode, but without Vercel's distributed cache layer. In a single Docker container this is acceptable; in a multi-replica setup, each container has its own ISR cache, meaning stale content can differ across replicas. Requires a custom cache handler (Redis etc.) for multi-instance correctness.
+5. **Vercel Analytics / Speed Insights** — `@vercel/analytics` and `@vercel/speed-insights` packages are no-ops or require Vercel infrastructure. Drop or replace with self-hosted alternatives (Plausible, Umami, PostHog).
+6. **Vercel Blob / KV / Postgres** — Vercel storage products are Vercel-only. Not applicable here (project uses Supabase).
+7. **Automatic HTTPS / SSL termination** — Vercel handles TLS automatically. In Docker, must be handled by a reverse proxy (nginx, Caddy, Traefik) in front of the container.
+8. **Deployment previews and branch URLs** — CI/CD feature; irrelevant to runtime.
+
+### sharp for Image Optimization
+
+- In Docker/self-hosted, Next.js image optimization requires `sharp` as an explicit production dependency.
+- Without `sharp`, Next.js falls back to a slower WASM-based optimizer.
+- `sharp` is a native module (libvips binding) — ensure the Docker base image has compatible glibc (use `node:20-alpine` with `apk add vips` or `node:20-slim`).
+
+**Sources:** Next.js official docs (nextjs.org/docs/app/guides/self-hosting), Next.js GitHub, community knowledge.
+
+---
+
+## Node.js LTS Schedule
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- **Node.js 20** (Iron LTS): LTS until April 2026, Maintenance until April 2026. Approaching end of active LTS.
+- **Node.js 22** (Jod LTS): Active LTS since October 2024. Maintenance until April 2027. **Recommended for new projects in 2025/2026.**
+- **Node.js 18** (Hydrogen): End of life October 2025. Do NOT use for new Docker images.
+- Node.js 24 may be in current/release phase as of early 2026; 22 remains the safe LTS choice.
+
+**Sources:** nodejs.org/en/about/releases
+
+---
+
+## Tailwind CSS — Version Status
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- **Tailwind CSS v3.x**: Stable, widely used. Latest 3.x is ~3.4.x.
+- **Tailwind CSS v4.x**: Released in early 2025. Major rewrite — CSS-first config (no more `tailwind.config.js` by default), uses native CSS cascade layers, PostCSS plugin rewritten, new `@import "tailwindcss"` syntax. Breaking changes vs v3.
+- **Next.js compatibility**: Tailwind v4 has official Next.js support. The `@tailwindcss/nextjs` or Vite plugin approach replaces the old PostCSS setup.
+- For a project starting April 2026, Tailwind v4 is the forward-looking choice, but v3 remains fully supported and is lower-risk for a first build.
+
+**Sources:** tailwindcss.com/blog/tailwindcss-v4, tailwindcss.com/docs/installation/framework-guides
+
+---
+
+## TanStack Query (React Query) — Version Status
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- **TanStack Query v5**: Current stable as of late 2023 and onwards. Major breaking changes vs v4: `useQuery` destructuring changed, `status: 'loading'` renamed to `status: 'pending'`, `cacheTime` renamed to `gcTime`, `isLoading` behavior changed, `onSuccess`/`onError`/`onSettled` callbacks removed from `useQuery` options.
+- **v4**: Still functional but v5 is recommended for new projects.
+- **Next.js App Router integration**: Use `@tanstack/react-query` with `HydrationBoundary` and `dehydrate`/`hydrate` for SSR prefetching pattern. Works well with Server Components prefetching data into query cache.
+- TanStack Query is the right choice for this project: it handles TMDB API caching (staleTime config prevents redundant re-fetches), loading states, and debounce integration cleanly.
+
+**Sources:** tanstack.com/query/v5/docs, tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5
+
+---
+
+## Supabase — Realtime in Docker/Containerized Environments
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- Supabase is used here as a **hosted service** (supabase.com), not self-hosted. The app's Docker container is only the Next.js frontend/backend — Supabase itself remains on Supabase's infrastructure.
+- **Supabase Realtime** uses WebSocket connections from the client browser to Supabase's Realtime servers. This is entirely client-initiated and infrastructure-agnostic — it works identically whether the Next.js app is on Vercel, Docker, or bare metal.
+- **No special container networking** is required for Supabase Realtime from the client side.
+- **Server-side Supabase calls** (Route Handlers, Server Components) make standard HTTPS requests to `SUPABASE_URL` — no issues in Docker.
+- **Row Level Security (RLS)**: For anonymous auth (UUID-based, no Supabase Auth), RLS policies must be written carefully. Without `auth.uid()`, policies will use custom claims or the anon key. Consider using Supabase's built-in Auth (anonymous sign-in) to get a proper JWT — this makes RLS much cleaner.
+- **Supabase anonymous sign-in**: Supabase supports anonymous authentication (GA as of late 2024). This provides a real `auth.uid()` in JWT without requiring email, making RLS straightforward. The project's current plan (UUID in localStorage) bypasses Supabase Auth entirely, which complicates RLS.
+- **Supabase free tier**: 2 active projects, 500MB database, 5GB bandwidth, 2GB file storage. Realtime: 200 concurrent connections, 2M messages/month. Suitable for MVP.
+- **Supabase pg_cron**: Available on Supabase's Postgres (free tier). Can replace Vercel Cron for the trailer URL refresh and landing reel refresh jobs — runs SQL/PL/pgSQL on a schedule entirely within Supabase, no external infrastructure needed.
+
+**Sources:** supabase.com/docs/guides/realtime, supabase.com/docs/guides/auth/anonymous-sign-ins, supabase.com/docs/guides/database/extensions/pg_cron
+
+---
+
+## TMDB API — Caching Strategy & Rate Limits
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- **TMDB free tier**: No strict published rate limit, but ~40-50 requests/10 seconds is the practical safe limit. Exceeding returns 429.
+- **API v3 vs v4**: v3 is the standard REST API (API key auth). v4 uses OAuth. For read-only movie data, v3 is sufficient and simpler.
+- **Image base URL**: TMDB posters are served from `image.tmdb.org/t/p/{size}/{poster_path}`. This is an external CDN — Next.js `<Image>` optimization will proxy/re-optimize these unless `remotePatterns` is configured to allow direct passthrough or a loader is used.
+- **Caching recommendations for Docker**:
+  - Server-side: Cache TMDB responses in Next.js `fetch()` with `revalidate` (Route Handler level) or use TanStack Query's `staleTime` on the client.
+  - Search queries: Client-side TanStack Query cache with `staleTime: 60_000` (1 min) prevents re-fetching the same search on rapid re-type after debounce.
+  - Popular/top-rated posters (landing reel): Fetch on a schedule (pg_cron) and store in `landing_reel_posters` table — correct approach already in scope.
+  - Trailer URLs: Stored in DB at add-time — correct approach already in scope.
+- **`next/image` with TMDB**: Add `image.tmdb.org` to `remotePatterns` in `next.config.js`. In Docker without Vercel CDN, each image optimization request is processed by the container's `sharp` instance — add adequate memory/CPU to the container.
+
+**Sources:** developer.themoviedb.org/docs, developer.themoviedb.org/reference/intro/rate-limiting
+
+---
+
+## otplib — TOTP for Node.js
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- **otplib**: Actively maintained TOTP/HOTP library. Latest major version is v12.x.
+- Works in Node.js server-side context — appropriate for Next.js Route Handlers / Server Actions.
+- TOTP verification should ONLY happen server-side. Never expose the TOTP secret to the client.
+- **Security note**: TOTP without a password is a single-factor auth (TOTP is "something you have"). The scope explicitly has no password fallback, which means the TOTP secret IS the credential. This is fine for a low-risk admin panel but worth documenting.
+- **Session management**: The scope mentions server-side session tokens for admin sessions. Use `iron-session` or `jose` (JWT) for stateless session cookies, or a DB-backed session table. Both work well in Docker.
+- **Alternative**: `speakeasy` is another TOTP library but less actively maintained than `otplib`. Stick with otplib.
+
+**Sources:** npmjs.com/package/otplib, github.com/yeojz/otplib
+
+---
+
+## PWA / Service Worker in Docker
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- PWA service workers are entirely client-side — the web server (Next.js in Docker) simply needs to serve the `manifest.json` and `sw.js` files as static assets with correct headers.
+- **No Docker-specific PWA issues** exist. The container is just the origin server.
+- **next-pwa**: The most popular Next.js PWA plugin. Note: the original `next-pwa` (by shadowwalker) has been **unmaintained** since ~2022. The actively maintained fork is `@ducanh2912/next-pwa` or `serwist/next`. For a project starting in 2026, use `serwist` (the community successor to `next-pwa` with Workbox 7 integration).
+- **Serwist**: `@serwist/next` is the modern replacement. Actively maintained, Workbox 7 based, App Router compatible.
+- **Service worker scope**: Must be served from the root path. Next.js handles this correctly with the public folder.
+- **HTTPS requirement**: Service workers require HTTPS (or localhost). In Docker behind a reverse proxy, ensure the proxy terminates TLS and forwards correctly.
+- **Offline caching strategy for this app**: Cache-first for static assets and TMDB poster images; network-first for API/Supabase calls with fallback to cached list data.
+
+**Sources:** serwist.pages.dev, github.com/serwist/serwist, web.dev/learn/pwa
+
+---
+
+## Cron Jobs in Docker (Replacing Vercel Cron)
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+### Options for Docker/self-hosted cron:
+
+1. **Supabase pg_cron** (RECOMMENDED for this project): Built into Supabase Postgres. Schedule SQL jobs directly. For trailer URL refresh, a `pg_cron` job can call a Supabase Edge Function or directly UPDATE rows via a stored procedure. No external process needed. Free tier supports it.
+
+2. **node-cron / node-schedule in-process**: Import `node-cron` into the Next.js process and schedule jobs at startup. Simple but couples the job lifecycle to the web server process — if the container restarts, jobs restart too. Fine for low-stakes bi-weekly jobs.
+
+3. **Separate cron container**: In Docker Compose, add a lightweight Alpine container running cron that calls an internal Next.js Route Handler endpoint. Clean separation but adds deployment complexity.
+
+4. **Supabase Edge Functions with pg_cron trigger**: pg_cron triggers an Edge Function that calls TMDB and updates the DB. Fully managed within Supabase infrastructure.
+
+5. **System cron on the host**: If deploying directly to a server, a system crontab entry that calls a `/api/cron/refresh` endpoint with a secret key. Simple, no additional dependencies.
+
+**Recommendation**: Supabase pg_cron + a Supabase Edge Function or stored procedure is the cleanest solution that works in both Docker and direct server deployment without touching the Next.js codebase.
+
+**Sources:** supabase.com/docs/guides/database/extensions/pg_cron, npmjs.com/package/node-cron
+
+---
+
+## Next.js Image Optimization — Self-Hosted Without Vercel CDN
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- **sharp** must be installed as a production dependency for Next.js image optimization in Docker.
+- Without sharp: Next.js uses a WASM fallback (`@squoosh/lib`) which is significantly slower and may time out under load.
+- **Memory**: sharp/libvips can be memory-intensive when processing many concurrent image requests. For a movie poster-heavy app, configure `next.config.js` `images.minimumCacheTTL` to a long value (86400 or higher) to reduce repeat processing.
+- **External image optimization alternative**: Instead of letting Next.js optimize TMDB poster images (which adds CPU/memory load to the container), consider one of:
+  - Using TMDB's own CDN directly with correct `sizes` attribute (TMDB serves multiple sizes: w92, w154, w185, w342, w500, w780, original).
+  - Using `unoptimized: true` on the `<Image>` component for TMDB poster images and referencing TMDB's appropriately-sized URL directly — eliminates the optimization overhead entirely.
+  - This is particularly valuable for a mobile-first app: TMDB already provides `w185` and `w342` which are appropriate for mobile poster grids.
+- **remotePatterns** must include `image.tmdb.org` in `next.config.js` regardless of approach.
+
+**Sources:** nextjs.org/docs/app/api-reference/components/image, sharp.pixelplumbing.com
+
+---
+
+## Supabase Realtime — WebSocket Considerations
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+- Supabase Realtime channels use WebSocket (wss://) connections from the browser.
+- Reverse proxy in front of Docker container must support WebSocket upgrades (nginx: `proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";`). Caddy and Traefik handle this automatically.
+- Supabase free tier: 200 concurrent Realtime connections. For a friend-group MVP this is ample.
+- **Broadcast vs Postgres Changes**: For the movies table (add/remove/watched), use Postgres Changes subscription (`supabase.channel().on('postgres_changes', ...)`). This requires enabling replication on the table.
+- **RLS and Realtime**: If RLS is enabled on the movies table, Realtime Postgres Changes respects RLS as of Supabase Realtime v2. The client must be authenticated (have a valid JWT) for RLS-filtered real-time events to arrive.
+
+**Sources:** supabase.com/docs/guides/realtime/postgres-changes
+
+---
+
+## Anonymous Auth Architecture — Security Considerations
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+The scope proposes: UUID generated client-side → stored in localStorage → sent to server as identity.
+
+**Risks with pure localStorage UUID approach (bypassing Supabase Auth):**
+
+- No JWT issued — Supabase's RLS cannot use `auth.uid()`. All RLS policies must use the anon key with custom logic, which is harder to get right.
+- If anon key is used for all operations with no RLS, any user who discovers the anon key can read/write any data.
+- localStorage UUIDs are not cryptographically bound to anything — trivial to spoof another user's UUID in API calls.
+
+**Recommended approach — Supabase Anonymous Sign-In:**
+
+- Call `supabase.auth.signInAnonymously()` on first visit → Supabase issues a real JWT with `auth.uid()`.
+- Store the Supabase session (access + refresh token) instead of a raw UUID.
+- On new device: recovery code → re-link anonymous account via a custom "claim" flow (look up hashed recovery code, then re-issue session).
+- This gives proper RLS support, proper JWT expiry/refresh, and is officially supported.
+- The `users` table `id` becomes the Supabase Auth UID.
+
+**Sources:** supabase.com/docs/guides/auth/anonymous-sign-ins
+
+---
+
+## Data Model — Efficiency Notes
+
+_Researched: 2026-04-05 (knowledge cutoff Aug 2025)_
+
+Key observations on the proposed schema:
+
+1. **genres stored as `text[]`**: Postgres array of genre label strings. Efficient for read and filter within a single group's movies. For genre filtering, a GIN index on `genres` enables fast `@>` containment queries. Consider adding this index.
+
+2. **`watched` boolean + `watched_at` on movies table**: This is a per-group watched state, which is correct per the scope. No normalization issue.
+
+3. **`landing_reel_posters` table**: ~20 rows, replaced entirely on each refresh. Simple and correct. No performance concern.
+
+4. **`invite_code` on groups**: Should have a unique index (already noted as `unique` in schema). Use a short but collision-resistant format — the proposed WOLF-42 style (word + 2-digit number) gives limited entropy (~several thousand combinations). Consider WORD-WORD or WORD-4DIGITS for more combinations as user base grows.
+
+5. **`admin_sessions` table**: Not fully specified. Needs: session_token (hashed), created_at, expires_at, ip_address (optional). Use server-side HttpOnly cookies to transmit the token — never expose session tokens in the JS bundle.
+
+6. **Missing index recommendations**:
+   - `movies(group_id)` — used on every list page load
+   - `movies(group_id, watched)` — used by Roll the Dice (unwatched filter)
+   - `group_members(user_id)` — used on home page to fetch all user's groups
+   - `groups(invite_code)` — used on join flow (already unique, so indexed)
+   - `movies(tmdb_id, group_id)` — used to check "already in list" on search
+
+**Sources:** postgresql.org/docs/current/indexes-types.html, supabase.com/docs/guides/database/query-optimization
+
+---
+
+## Supabase Anonymous Sign-In — Self-Hosted GoTrue Configuration
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- Anonymous sign-in is **disabled by default** in self-hosted GoTrue. Must be explicitly enabled.
+- Required env var in the GoTrue (`auth`) service in docker-compose: `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true`
+- On managed Supabase (supabase.com), this is toggled in the dashboard. On self-hosted, only the env var persists — dashboard settings are ephemeral if the container is recreated without a persistent volume for GoTrue config.
+- Without this env var, `supabase.auth.signInAnonymously()` returns HTTP 400: "Anonymous sign-ins are disabled."
+- Supabase anonymous sign-in was GA as of late 2024. All recent self-hosted stacks support it; the env var is the only missing piece.
+
+**Sources:** supabase.com/docs/guides/auth/anonymous-sign-ins, github.com/supabase/gotrue (environment variable reference)
+
+---
+
+## Supabase Edge Functions — Self-Hosted Docker Architecture
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- Self-hosted Supabase's default `docker-compose.yml` does **not** include the Edge Function runtime container.
+- Edge Functions require `supabase/edge-runtime` image (separate from the main stack) — a Deno-based service.
+- Must be added manually to docker-compose and wired to Kong for HTTP routing.
+- `pg_cron` calling an Edge Function via `net.http_post()` also requires the `pg_net` extension (separate from `pg_cron`).
+- Alternative: Use a standalone Node.js cron container that calls TMDB directly and writes to Postgres via the service role key — simpler to operate, no Deno dependency.
+- Edge Function directory: `./supabase/functions/` mounted into the edge-runtime container.
+- JWT verification must be configured on the edge-runtime container (`VERIFY_JWT=true`).
+
+**Sources:** github.com/supabase/edge-runtime, supabase.com/docs/guides/functions/self-hosting
+
+---
+
+## argon2 npm Package — Docker Native Build Requirements
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- Package: `argon2` by ranisalt (~2M weekly downloads). Provides Argon2id, Argon2i, Argon2d.
+- Uses native C bindings compiled via `node-gyp` at install time.
+- Node.js 22 is fully supported.
+- `node:22-slim` (Debian) does NOT include `python3`, `make`, or `g++` by default — required for node-gyp.
+- Multi-stage Dockerfile: install build tools in builder stage only (`apt-get install -y python3 make g++`); the compiled `.node` file is traced into `output: standalone` but must be verified.
+- Alternative: `@node-rs/argon2` (ships pre-compiled NAPI binaries, no node-gyp) — eliminates build tool requirement.
+- `node:22-alpine` additionally requires `apk add python3 make g++ libc6-compat` for node-gyp; the libc compatibility layer is error-prone. Stick with `node:22-slim` (Debian glibc).
+
+**Sources:** npmjs.com/package/argon2, github.com/ranisalt/node-argon2
+
+---
+
+## iron-session v8 — App Router API
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- Current major version: v8.x (released 2023). Breaking changes from v7.
+- v7 exports (`withIronSessionApiRoute`, `withIronSessionSsr`) do not exist in v8.
+- v8 correct pattern: `getIronSession(await cookies(), sessionOptions)` in Route Handlers or Server Actions.
+- Works with Next.js App Router natively. No additional wrapper needed.
+- `cookieOptions.secure` must be `true` in production (required for HTTPS-only cookies).
+- `sameSite: 'strict'` means cookie is not sent on first cross-site navigation (acceptable for admin panels).
+- Session password (`IRON_SESSION_SECRET`) must be 32+ characters; iron-session validates this at runtime.
+- No database required — session data is encrypted into the cookie payload (AES-256-GCM).
+
+**Sources:** github.com/vvo/iron-session (README, v8 branch)
+
+---
+
+## TanStack Query persistQueryClient — IndexedDB Adapter
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- `persistQueryClient` is a plugin available in `@tanstack/react-query-persist-client` (separate package from `@tanstack/react-query`).
+- Does not ship with an IndexedDB adapter. Ships with `createSyncStoragePersister` (localStorage) and `createAsyncStoragePersister` (any async KV store).
+- For IndexedDB, use `idb-keyval` as the storage backend with `createAsyncStoragePersister`.
+- In TanStack Query v5, the component wrapper is `PersistQueryClientProvider` (from `@tanstack/react-query-persist-client`), not a plugin passed to `QueryClient`.
+- Three packages required: `@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister`, `idb-keyval`.
+- Persisted data is serialized with `JSON.stringify` — non-serializable values (class instances, functions, Date objects) are silently dropped on restore.
+- Cache max age and buster key config available to prevent stale cache from being restored across deploys.
+
+**Sources:** tanstack.com/query/v5/docs/framework/react/plugins/persistQueryClient
+
+---
+
+## @serwist/next — Service Worker Authoring Requirement
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- `@serwist/next` does NOT auto-generate a service worker (unlike old `next-pwa`). You must author `app/sw.ts`.
+- The service worker TypeScript file needs `lib: ['WebWorker']` in tsconfig, which conflicts with the main app's `lib: ['DOM']`. Solution: separate `tsconfig.worker.json` referenced via `next.config.ts` Serwist config.
+- `next.config.ts` wraps with `withSerwist({ swSrc: 'app/sw.ts', swDest: 'public/sw.js' })`.
+- `self.__SW_MANIFEST` is the precache manifest injected by Serwist at build time.
+- `defaultCache` from `@serwist/next/worker` provides sensible runtime caching strategies.
+- Phase 8.1 budget: ~half-day (sw.ts authoring + config + tsconfig.worker.json + manifest.json).
+
+**Sources:** serwist.pages.dev/docs/next/getting-started, github.com/serwist/serwist
+
+---
+
+## t3-env vs Raw Zod for Next.js Env Validation
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- `@t3-oss/env-nextjs`: Thin wrapper around zod. Enforces client/server variable split structurally via `server` and `client` blocks.
+- Server block vars cannot be accessed in client code (build-time error). Client block vars must have `NEXT_PUBLIC_` prefix (build-time error if missing).
+- Raw zod: No structural enforcement of server/client split — relies on convention and code review.
+- For projects where leaking a server-only secret (e.g., `TMDB_API_KEY`, `SUPABASE_SERVICE_ROLE_KEY`) would be a security incident, t3-env's structural enforcement is directly valuable.
+- `SUPABASE_URL` and `SUPABASE_ANON_KEY` should be `NEXT_PUBLIC_` — the anon key is public by design in Supabase's security model. Only `SUPABASE_SERVICE_ROLE_KEY` must be server-only.
+
+**Sources:** env.t3.gg, github.com/t3-oss/t3-env
+
+---
+
+## Caddy + Next.js + Self-Hosted Supabase — Docker Networking
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- Self-hosted Supabase uses Kong as the API gateway (default internal port 8000). All Supabase services (GoTrue, PostgREST, Realtime, Storage) route through Kong.
+- Server-side Next.js code should reach Supabase via internal Docker network hostname (e.g., `http://supabase_kong:8000`), not through the public URL. Avoids unnecessary round-trips through Caddy.
+- Browser-side Supabase client uses the public URL (`NEXT_PUBLIC_SUPABASE_URL`) — must be routable from the user's browser.
+- Caddy proxies the public Supabase URL to Kong internally. Supabase Realtime (`wss://`) proxying: Caddy handles WebSocket upgrades automatically.
+- Use `@supabase/ssr` package (`createBrowserClient` / `createServerClient`) for Next.js App Router — handles cookie-based session persistence correctly. Base `@supabase/supabase-js` does not handle SSR cookies.
+- Add `SUPABASE_INTERNAL_URL` as a server-only env var for the server-side Supabase client.
+
+**Sources:** supabase.com/docs/guides/auth/server-side/nextjs, caddyserver.com/docs/quick-starts/reverse-proxy
+
+---
+
+## Vitest + Playwright with Next.js App Router
+
+_Researched: 2026-04-05 (training knowledge through Aug 2025)_
+
+- **Vitest**: Cannot render React Server Components (RSC) — jsdom environment does not support RSC context. Unit test scope: pure logic, utility functions, Client Components only.
+- Vitest config requires `@vitejs/plugin-react`, explicit tsconfig path aliases, `environment: 'jsdom'`, and a setup file for any global mocks.
+- Do not attempt to render Server Components or call Route Handlers in Vitest tests — use Playwright for those.
+- **Playwright**: Works against any running Next.js instance. For accuracy, run E2E tests against the Docker production stack (standalone build + real Supabase), not `next dev`.
+- `playwright.config.ts` `webServer` option can start the app, but for Docker compose testing, point `baseURL` at the running container.
+- Playwright supports Chromium, Firefox, WebKit (Safari) — relevant for cross-browser testing (iOS Safari, Android Chrome are critical targets for this app).
+
+**Sources:** vitest.dev/guide, playwright.dev/docs/intro, nextjs.org/docs/app/building-your-application/testing/vitest

+ 774 - 0
research/TECHFILE.md

@@ -0,0 +1,774 @@
+# Tech Stack Analysis — MovieDice
+
+Analyzed: 2026-04-05 | Analyst: Claude (automated)
+
+---
+
+## CURRENT STACK
+
+| Layer              | Package / Service       | Version Specified    | Status                          |
+| ------------------ | ----------------------- | -------------------- | ------------------------------- |
+| Runtime            | Node.js                 | unspecified          | Target 22 LTS                   |
+| Frontend Framework | Next.js (App Router)    | unspecified (latest) | Current — use 15.x              |
+| Styling            | Tailwind CSS            | unspecified (latest) | Current — v4 available          |
+| Database / Backend | Supabase (hosted)       | hosted service       | Current                         |
+| Movie Data         | TMDB API                | v3 (free tier)       | Current                         |
+| State Management   | TanStack Query          | unspecified          | Target v5                       |
+| 2FA                | otplib                  | unspecified          | Current — v12.x                 |
+| Deployment         | Vercel (specified)      | —                    | Incompatible with Docker target |
+| PWA                | next-pwa (implied)      | unspecified          | Unmaintained — use serwist      |
+| Cron               | Vercel Cron (specified) | —                    | Unavailable in Docker           |
+
+---
+
+## CRITICAL
+
+_Vercel-specific features in the scope that will silently fail or be completely absent in Docker deployment. These must be resolved before writing a single line of implementation code._
+
+---
+
+### 1. Vercel Cron Jobs Are Unavailable in Docker
+
+`PROJECT_SCOPE.md §8, Phase 6` — The scope explicitly plans two background jobs on Vercel Cron: the bi-weekly trailer URL refresh (Phase 6.2) and the landing reel poster refresh (Phase 5.2). In a Docker or self-hosted deployment, `vercel.json` cron entries are simply ignored — the jobs will never run. The landing page reels will never refresh, and trailer URLs for movies with a null value will stay null permanently. Because Phase 6 is scoped as post-MVP and Phase 5 is an MVP deliverable, this gap would silently break the landing reel on the very first deployment if a Vercel-first assumption is baked in.
+
+**Fix:** Replace Vercel Cron with Supabase pg_cron. Both the landing reel refresh and the trailer URL refresh are fundamentally database operations (read from TMDB, write to Postgres). A `pg_cron` job in Supabase can call a Supabase Edge Function that performs the TMDB fetch and upserts results into the relevant table. This requires zero changes to the Next.js app and works identically whether the frontend is on Docker, a bare server, or Vercel. Implement this at the same time as the landing reel and trailer refresh features — do not build them with Vercel Cron as a placeholder.
+
+**Implementation sketch:**
+
+```sql
+-- Enable pg_cron in Supabase dashboard (Dashboard > Database > Extensions)
+SELECT cron.schedule('refresh-landing-reels', '0 0 */14 * *',
+  $$ SELECT net.http_post(url := 'https://<project>.supabase.co/functions/v1/refresh-reels',
+     headers := '{"Authorization": "Bearer <service_role_key>"}') $$
+);
+```
+
+**⚠️ Implementation Risk:** Supabase Edge Functions have a 150ms CPU time limit per invocation on the free tier, but the trailer refresh job may need to loop over many movies. If the movie count grows large, the Edge Function may time out. Mitigate by processing in small batches (e.g., 10 movies per invocation) and letting pg_cron call it repeatedly, or use Supabase's background task queuing. For MVP scale (hundreds of movies) this is not an issue.
+
+---
+
+### 2. Vercel Image Optimization CDN Replaced by In-Container sharp — Performance Risk
+
+`PROJECT_SCOPE.md §7 Tech Stack` — The scope is a poster-forward, mobile-first app. Every list page renders a 2-column poster grid, with infinite scroll loading batches of 12 posters. Every poster is an external TMDB image that Next.js `<Image>` will attempt to optimize. On Vercel, these optimizations are served from a global CDN with edge caching. In Docker, every optimization is processed synchronously by `sharp` (a libvips binding) running inside the container. Under load — multiple users, large lists, fast scrolling — this creates CPU and memory pressure on the same process running the Next.js server. Without sharp installed, Next.js silently falls back to a slow WASM optimizer that will make every poster load feel sluggish.
+
+**Fix (two parts):**
+
+1. Install `sharp` as an explicit production dependency. Do not rely on automatic detection.
+
+   ```
+   npm install sharp
+   ```
+
+   In the Dockerfile, ensure the base image has compatible glibc. Use `node:22-slim` (Debian-based) rather than `node:22-alpine` unless you add the musl-compatible sharp build (`--platform linux/amd64`).
+
+2. Bypass Next.js image optimization for TMDB poster images entirely by referencing TMDB's own sized URLs. TMDB serves posters at discrete sizes: `w92`, `w154`, `w185`, `w342`, `w500`, `w780`, `original`. For a 2-column mobile grid, `w342` is the correct size. Use a standard `<img>` tag (or `<Image unoptimized>`) with the appropriately-sized TMDB URL:
+   ```
+   https://image.tmdb.org/t/p/w342/{poster_path}
+   ```
+   This eliminates per-request optimization overhead entirely, leverages TMDB's own CDN, and produces better poster quality than re-optimizing an already-compressed JPEG. Reserve `next/image` optimization for locally-served assets (logo, UI icons).
+
+**⚠️ Implementation Risk:** Bypassing `next/image` optimization means no automatic WebP conversion and no automatic responsive `srcset`. For poster images this is acceptable — TMDB already serves appropriately-sized JPEGs, and the quality difference is negligible on mobile. If sharp is kept in place for other images, ensure `remotePatterns` in `next.config.js` includes `image.tmdb.org`.
+
+---
+
+### 3. Anonymous Auth via Raw UUID in localStorage — No RLS Support
+
+`PROJECT_SCOPE.md §7 Tech Stack + Data Model` — The scope proposes generating a UUID client-side, storing it in localStorage, and using it as the user's identity. This bypasses Supabase Auth entirely. The consequence is that Supabase's Row Level Security system cannot use `auth.uid()` in policy definitions, because there is no authenticated JWT. Without RLS, the anon key (which is public and exposed in the client bundle) can be used by any visitor to read or write any row in any table — including other groups' movie lists, other users' data, and the admin_sessions table.
+
+This is not a theoretical concern. The Supabase anon key is not a secret — it is designed to be public. Security in Supabase comes from RLS policies evaluated against the authenticated JWT. Without Auth, there is no RLS, and there is no meaningful data isolation between groups.
+
+**Fix:** Use Supabase Anonymous Sign-In instead of a hand-rolled UUID:
+
+```typescript
+const { data, error } = await supabase.auth.signInAnonymously();
+// data.user.id is now the UUID — a real Supabase Auth UID
+```
+
+This issues a proper JWT, which Supabase stores in the browser and auto-refreshes. `auth.uid()` is available in all RLS policies. The user experience is identical — no email, no password, instant account. The `users` table `id` column becomes the Supabase Auth UID (`data.user.id`). Recovery code flow becomes: hash recovery code → store against user → on new device, call a Supabase RPC that looks up the hashed code and calls `auth.admin.linkIdentity()` or issues a custom token to restore the session.
+
+Write RLS policies such that:
+
+- Users can only read/write movies in groups they are members of
+- Users can only see group_members rows for their own groups
+- Admin sessions table is inaccessible to non-admin roles entirely
+
+**⚠️ Implementation Risk:** Migrating from localStorage UUID to Supabase Anonymous Auth is a foundational change that touches the auth initialization, the users table schema, and all DB queries that join on user ID. This is easiest to adopt before any code is written — which is the current state. Do not start Phase 1 with the localStorage UUID approach and plan to migrate later; the migration cost is high.
+
+---
+
+## RECOMMENDED UPGRADES
+
+---
+
+### 4. next-pwa Is Unmaintained — Use serwist Instead
+
+`PROJECT_SCOPE.md §8, Phase 8` — Phase 8 adds PWA support via "PWA manifest and service worker." The most widely referenced Next.js PWA package, `next-pwa` (by shadowwalker on npm), has been effectively unmaintained since 2022 — it has no App Router support, open security issues, and depends on an outdated version of Workbox (v6). Using it with Next.js 15 App Router will produce warnings, may break, and puts a dead dependency in the project from day one.
+
+**Fix:** Use `@serwist/next` (the community-maintained successor, based on Workbox 7) from the start.
+
+```
+npm install @serwist/next serwist
+```
+
+Configuration is straightforward:
+
+```typescript
+// next.config.ts
+import withSerwist from "@serwist/next";
+export default withSerwist({ swSrc: "app/sw.ts", swDest: "public/sw.js" })(nextConfig);
+```
+
+Serwist supports App Router, Workbox 7, TypeScript, and is actively maintained as of 2025.
+
+**⚠️ Implementation Risk:** Low — this is a pre-build decision. serwist's API is similar to next-pwa. The main adjustment is writing the service worker in `app/sw.ts` rather than relying on auto-generation.
+
+---
+
+### 5. Specify Node.js 22 LTS for the Docker Base Image
+
+`PROJECT_SCOPE.md §7 Deployment` — No Node.js version is specified in the scope. Node.js 18 reached end-of-life in October 2025 and must not be used. Node.js 20 LTS (Iron) enters maintenance mode in April 2026 — exactly now. Starting a new Docker-based project on Node.js 20 means hitting the maintenance window within months.
+
+**Fix:** Pin the Docker base image to Node.js 22 LTS (codename "Jod"), which has active LTS support until April 2026 and maintenance until April 2027. Use the slim variant for smaller image size:
+
+```dockerfile
+FROM node:22-slim AS base
+```
+
+For the Next.js standalone output, the multi-stage Dockerfile pattern is:
+
+```dockerfile
+FROM node:22-slim AS builder
+# ... build steps ...
+
+FROM node:22-slim AS runner
+COPY --from=builder /app/.next/standalone ./
+COPY --from=builder /app/.next/static ./.next/static
+COPY --from=builder /app/public ./public
+CMD ["node", "server.js"]
+```
+
+**⚠️ Implementation Risk:** None — this is a baseline configuration decision. Node.js 22 is stable and fully compatible with Next.js 15.
+
+---
+
+### 6. Configure output: 'standalone' in next.config for Docker
+
+`PROJECT_SCOPE.md §7 Deployment` — Without `output: 'standalone'`, a Dockerized Next.js build requires copying the entire `node_modules` tree into the image, producing images of 500MB–1GB+. The standalone output mode traces all imported modules and copies only what is needed, typically reducing the final image to under 100MB.
+
+**Fix:** Add to `next.config.ts` (or `next.config.js`):
+
+```typescript
+const nextConfig = {
+  output: "standalone",
+  // ... other config
+};
+```
+
+Then structure the Dockerfile to copy from `.next/standalone`. Remember that `public/` and `.next/static/` must be copied separately — they are NOT included in the standalone output.
+
+**⚠️ Implementation Risk:** Low. One known gotcha: any `node_modules` that use dynamic `require()` with runtime-resolved paths may not be traced correctly. Test the Docker build early in Phase 1 (Step 1.7) before adding many dependencies.
+
+---
+
+### 7. TMDB Search Caching — Prevent Free Tier Rate Limit Hits
+
+`PROJECT_SCOPE.md §6 Usability Concerns` — The scope notes "Cache recent search results client-side to reduce API calls and stay within free tier rate limits." TanStack Query is already selected, and its query cache satisfies this requirement — but only if `staleTime` is configured explicitly. Without a `staleTime`, TanStack Query considers all cached results stale immediately and refetches on every component mount, which defeats the purpose.
+
+**Fix:** Configure `staleTime` on TMDB query hooks:
+
+```typescript
+// Search results — cache for 60 seconds (user likely to revisit same query in session)
+useQuery({
+  queryKey: ["tmdb-search", query],
+  queryFn: () => fetchTMDBSearch(query),
+  staleTime: 60_000,
+  enabled: query.length > 2,
+});
+
+// Popular/trending for landing page — cache for 10 minutes
+useQuery({
+  queryKey: ["tmdb-popular"],
+  queryFn: fetchTMDBPopular,
+  staleTime: 10 * 60_000,
+});
+```
+
+Additionally, implement TMDB search through a Next.js Route Handler (`/api/tmdb/search`) rather than calling TMDB directly from the browser. This keeps the TMDB API key server-side only (it never appears in the client bundle), and enables server-side response caching with `Cache-Control` headers as an additional layer.
+
+**⚠️ Implementation Risk:** Low — TanStack Query staleTime is a one-line configuration per query. The Route Handler proxy adds one network hop but is the correct security posture for any API key.
+
+---
+
+### 8. Reverse Proxy Required for HTTPS and WebSocket in Docker
+
+`PROJECT_SCOPE.md §7 Deployment` — The scope describes no infrastructure around the Docker container. A bare Next.js container listens on port 3000 over HTTP. For production: (a) a browser-accessible app requires HTTPS — service workers (PWA), Supabase Realtime's `wss://`, and secure cookies all require TLS; (b) if any future load balancing or multiple containers are added, a reverse proxy is essential.
+
+**Fix:** Place a reverse proxy in front of the Next.js container. The recommended options in order of simplicity:
+
+1. **Caddy** (simplest): Automatic HTTPS via Let's Encrypt, zero config for WebSocket proxying. One `Caddyfile`:
+   ```
+   moviedice.example.com {
+     reverse_proxy localhost:3000
+   }
+   ```
+2. **Traefik**: Good if already using Docker Compose with labels; auto-discovers containers.
+3. **nginx**: More configuration but widely understood. Must explicitly add WebSocket upgrade headers.
+
+WebSocket note: Supabase Realtime connections are client-browser → `wss://supabase.co` — they do not pass through the Next.js container at all. So WebSocket proxying is only needed if the app itself opens WebSocket connections through the container (not currently planned).
+
+**⚠️ Implementation Risk:** Low for Caddy/Traefik. DNS and certificate provisioning adds ~30 minutes of setup. Plan for this in Phase 1.7 ("Deploy skeleton").
+
+---
+
+### 9. Invite Code Entropy Is Low — Scale Risk
+
+`PROJECT_SCOPE.md §7 Data Model` — The proposed invite code format is `WOLF-42` (one English word + 2-digit number). Assuming a dictionary of ~1000 common English words and numbers 00–99, this yields approximately 100,000 unique codes. At MVP scale (dozens of groups) this is fine. However, the codes are human-guessable — someone who knows the format can enumerate valid codes with ~100K attempts (trivial for any script). Since an invite code grants full group membership, a guessable code allows unauthorized group joining.
+
+**Fix (two levels):**
+
+1. **Short-term (MVP)**: Add a rate limit on the join-by-code endpoint (`/api/groups/join`). Limit to 5–10 failed attempts per IP per hour. This makes brute force impractical.
+
+2. **Medium-term**: Expand the code format to `WORD-WORD` or `WORD-4DIGITS` to increase entropy to millions of combinations, reducing collision probability as group count grows.
+
+**⚠️ Implementation Risk:** Rate limiting is a straightforward middleware addition. The code format change is a data migration on the `groups` table and a UI tweak — best done before MVP launch if the format change is desired.
+
+---
+
+### 10. Admin Session Management — Needs Explicit Implementation
+
+`PROJECT_SCOPE.md §7 Data Model` — The scope notes `admin_sessions` as a "secure server-side token store" but does not specify the mechanism. A naive implementation (e.g., a session token in a plain cookie, or a localStorage value for admin state) would be insecure. The admin panel controls global list and user deletion.
+
+**Fix:** Use an HttpOnly, Secure, SameSite=Strict cookie containing a signed session token. Two implementation options:
+
+1. **`iron-session`**: Simple encrypted cookie session for Next.js. No DB required — the session data is encrypted in the cookie itself. Easy to implement in Route Handlers.
+
+   ```
+   npm install iron-session
+   ```
+
+2. **`jose` + DB-backed sessions**: Sign a JWT with a server-side secret, store the session record in `admin_sessions` table (token hash, created_at, expires_at). Allows explicit session revocation.
+
+For a single admin user with TOTP authentication, `iron-session` is sufficient and simpler. The session should expire after a fixed duration (e.g., 8 hours) and include a `role: 'admin'` claim. All `/admin` route handlers must validate the session on every request.
+
+**⚠️ Implementation Risk:** Low — `iron-session` is well-documented for Next.js App Router. The risk is in under-specifying this and building the admin UI before the session layer, leading to a retrofit.
+
+---
+
+## STRATEGIC RECOMMENDATIONS
+
+---
+
+### 11. Consider Supabase Auth (Anonymous Sign-In) as the Complete Auth Stack
+
+This is an extension of Finding 3 above, but worth addressing as a strategic choice. The current scope proposes building a custom auth layer: client-side UUID generation, localStorage persistence, hashed recovery codes, and manual session management. Supabase already provides all of this natively through its Anonymous Sign-In + Auth SDK:
+
+- `signInAnonymously()` — UUID-backed anonymous account, JWT issued immediately
+- Supabase Auth persists the session (access + refresh token) in localStorage automatically
+- `supabase.auth.getSession()` — works on return visits (equivalent to reading the stored user ID)
+- JWT auto-refresh — handled by the Supabase client library
+- Recovery: link recovery identity to the anonymous account, or use a custom claims approach
+
+The custom auth approach in the scope requires writing security-critical code (UUID generation, recovery code hashing, session management) that Supabase already handles correctly. The only custom component needed is the recovery code lookup logic, which is a single Supabase RPC function.
+
+**Benefit:** Dramatically simpler implementation, correct RLS support, no bespoke security surface. Estimated savings: 2–3 days of Phase 1 work.
+
+**⚠️ Implementation Risk:** The scope was written assuming no Supabase Auth dependency. Adopting it changes Phase 1.3–1.6 substantially. If the team is already familiar with Supabase Auth, this is low-risk. If not, budget a half-day to read the anonymous sign-in docs before starting.
+
+---
+
+### 12. Pre-Render the Landing Page as a Static Page
+
+The landing page (root `/`) has no user-specific content before login detection. It is entirely static: logo, tagline, Roll buttons, About section, how-it-works steps. The slot-machine reel posters come from the `landing_reel_posters` DB table and could be fetched at build time (ISR) or at request time. The "login state detection" (redirect to home page if user ID found) is a client-side check against localStorage.
+
+In Next.js App Router, the landing page root layout should be a Server Component that generates a static shell, with a thin Client Component wrapper that handles the localStorage redirect. This means:
+
+- The page skeleton is served instantly as static HTML
+- The "should I redirect?" check happens client-side after hydration (no server round-trip needed, since localStorage is client-only)
+- TMDB reel posters can be embedded at build time via ISR (revalidate every ~12 hours) rather than fetching from the DB on every page load
+
+This approach gives near-instant perceived load times on the landing page, which is the first impression for all new users.
+
+**⚠️ Implementation Risk:** Medium — requires careful separation of Server and Client Components, particularly around the login detection redirect. The rule: anything touching localStorage must be in a Client Component. The risk of getting this wrong is a hydration mismatch error, which is caught quickly in development.
+
+---
+
+### 13. Define a TMDB Image Strategy Before Building the Grid
+
+The movie poster grid is the core visual element of the app. Getting the image strategy wrong results in either: (a) slow loads because Next.js is optimizing every TMDB image in-container, or (b) blurry/wrong-size images because sizes are not specified. This decision should be made before Phase 3 (Movie List Core).
+
+**Recommended strategy:**
+
+- Use TMDB's native sized URLs directly: `w185` for grid thumbnails on mobile, `w342` for the expanded panel full-size poster.
+- Use a standard `<img>` tag (not `next/image`) for poster grid cards. This bypasses the optimization pipeline entirely and uses TMDB's CDN directly.
+- Reserve `next/image` for locally-served assets where optimization provides clear value (logo, hero images).
+- Add `image.tmdb.org` to `next.config.js` `remotePatterns` regardless (required if `next/image` is ever used for TMDB images).
+
+For the landing page reel animation (spinning poster images), use the smallest TMDB size that looks acceptable in the reel frame (`w185` or `w342`) to minimize data transfer on mobile.
+
+**⚠️ Implementation Risk:** Low — this is a configuration and component design decision made once. Inconsistent image strategies (some `next/image`, some `<img>`) are fine as long as TMDB posters consistently use the direct URL approach.
+
+---
+
+### 14. Emotion-to-Genre Mapping Belongs in Application Code, Not a DB Table
+
+`PROJECT_SCOPE.md §10` — The scope provides a static emotion-to-genre mapping table. This is correctly implemented as a static TypeScript object in the application code (a `const` map), not a database table or API call. It never changes without a code deploy. Storing it in the DB adds a query on every Genre Roll with no benefit.
+
+```typescript
+// lib/emotionGenreMap.ts
+export const EMOTION_GENRE_MAP: Record<string, number[]> = {
+  happy: [35, 16, 10751], // Comedy, Animation, Family
+  cheerful: [35, 16, 10751],
+  // ...
+};
+```
+
+Genre IDs should be stored as TMDB numeric IDs (not genre label strings) since TMDB's filter endpoints accept numeric IDs. The mapping in the scope uses genre labels — translate these to numeric IDs during implementation.
+
+**⚠️ Implementation Risk:** None — this is a straightforward implementation choice. The only action required is mapping the genre labels in the scope document to TMDB's numeric genre IDs (e.g., Action = 28, Comedy = 35, Drama = 18).
+
+---
+
+## POSITIVES
+
+The following aspects of the proposed scope are well-designed and require no changes:
+
+- **TanStack Query selection**: Correct choice for this app. Server state management with built-in caching, loading states, and background refetching aligns well with the TMDB search and list data requirements.
+- **Supabase Postgres Changes for Realtime**: Using Postgres row-level change events for real-time list sync is the right Supabase feature. More reliable than Broadcast for persistent state.
+- **TMDB data stored in DB at add-time**: Storing `poster_path`, `genres`, `title`, `year`, and `trailer_url` in the `movies` table at add-time (rather than fetching from TMDB on every render) is correct. Reduces TMDB API calls, eliminates dependency on TMDB uptime for existing list items.
+- **trailer_url_refreshed_at timestamp**: Tracking when trailer URLs were last fetched is good hygiene. The scope notes this should be extended to refresh stale (not just null) URLs post-launch — that is the right call.
+- **TOTP-only admin auth with env var credentials**: No UI for credential management is the right call for a single-admin panel. Env vars mean no credential storage in the DB.
+- **Recovery code hashed before storage**: Correct security practice. The recovery code is a credential and must be stored as a hash.
+- **Infinite scroll with initial batch of 12**: Correct approach for a poster grid. 12 items is a natural 2-column grid height on mobile screens. Pagination by scroll is appropriate for this interaction model.
+- **Debounce on TMDB search (300ms)**: Standard and correct. Prevents flooding TMDB on every keystroke.
+- **Two-tap delete confirmation**: Good UX pattern for a destructive action in a list — prevents accidental deletes while remaining accessible.
+- **Separate landing page and in-app roll animations**: Architecturally correct to keep these as distinct components. They have different data sources, different visual goals, and will evolve independently.
+
+---
+
+## NO ACTION NEEDED
+
+| Package / Feature  | Version  | Status  | Notes                                                            |
+| ------------------ | -------- | ------- | ---------------------------------------------------------------- |
+| Supabase (hosted)  | current  | Active  | Hosted service; no version management needed                     |
+| TMDB API v3        | v3       | Active  | No v4 migration needed; v3 covers all required endpoints         |
+| otplib             | v12.x    | Active  | Actively maintained; standard TOTP/HOTP library                  |
+| TanStack Query     | v5       | Current | Use v5 for new project; v5 is stable and the documented standard |
+| Tailwind CSS       | v3 or v4 | Current | v3 is stable; v4 is available and forward-looking; either works  |
+| Supabase pg_cron   | built-in | Active  | Available on free tier; no additional service needed             |
+| Next.js App Router | 15.x     | Current | App Router is stable and the correct choice for new projects     |
+
+---
+
+## Summary
+
+| Severity             | Count | Items                                                                                                                               |
+| -------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| Critical             | 3     | Vercel Cron unavailable in Docker; Image optimization performance risk; No RLS without Supabase Auth                                |
+| Recommended Upgrades | 7     | next-pwa → serwist; Node.js 22; standalone output; TMDB staleTime; reverse proxy; invite code entropy; admin session implementation |
+| Strategic            | 4     | Full Supabase Auth adoption; static landing page pre-render; TMDB image strategy; emotion map in code                               |
+| No Action            | 7     | Supabase, TMDB API, otplib, TanStack Query v5, Tailwind CSS, pg_cron, Next.js App Router                                            |
+
+**Top priority before writing any code:**
+
+1. Adopt Supabase Anonymous Sign-In (resolves Critical #3 and Strategic #11 simultaneously)
+2. Decide on Docker deployment infrastructure (reverse proxy, Node.js version, standalone output config)
+3. Replace Vercel Cron references in the implementation plan with Supabase pg_cron
+
+---
+
+## Files Reviewed
+
+| #   | File                                                | Type                                   |
+| --- | --------------------------------------------------- | -------------------------------------- |
+| 1   | `/home/user/moviedice/PROJECT_SCOPE.md`             | Project specification                  |
+| 2   | `/home/user/moviedice/research/PROJECT_INFO.md`     | Project context (created this session) |
+| 3   | `/home/user/moviedice/research/SUPPORT-RESEARCH.md` | Research cache (created this session)  |
+
+---
+
+## Second Review — Technology Verification
+
+_Analyzed: 2026-04-05 | Scope version: updated (post-first-review) | Web search unavailable — findings based on training knowledge through August 2025. All items verified against official documentation, source repositories, and known implementation patterns as of that date._
+
+The updated scope has correctly adopted the recommendations from the first review: Supabase Anonymous Sign-In, self-hosted Supabase Docker, Caddy reverse proxy, pg_cron, serwist, iron-session, Node.js 22, standalone output, and direct TMDB CDN URLs. This review verifies whether those choices actually work together correctly in a self-hosted Docker context and identifies implementation traps the scope does not yet address.
+
+---
+
+### NEW CRITICAL
+
+---
+
+#### C1. Supabase Anonymous Sign-In Requires an Explicit GoTrue Environment Variable — Off by Default in Self-Hosted
+
+`PROJECT_SCOPE.md §7 Deployment / Phase 1.2` — The scope specifies `supabase.auth.signInAnonymously()` as the auth mechanism, which is correct. However, in self-hosted Supabase, anonymous sign-in is **disabled by default** in the GoTrue service. It must be explicitly enabled via an environment variable in the GoTrue container configuration. In the official self-hosted `docker-compose.yml`, GoTrue's config is controlled by environment variables passed to the `auth` service. The required variable is:
+
+```
+GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=true
+```
+
+If this variable is not set, `signInAnonymously()` will return a `400` error with the message "Anonymous sign-ins are disabled." This will silently break the entire onboarding flow (Phase 1.4) with no indication in the Next.js app that configuration is missing — it just looks like a failed auth call.
+
+This is distinct from managed Supabase, where anonymous sign-in is toggled in the dashboard UI. In self-hosted mode there is no persistent dashboard — the Studio UI setting does not survive container recreation; the environment variable in `docker-compose.yml` is the only durable configuration.
+
+**Fix:** Add to the `auth` service environment in `docker-compose.yml`:
+
+```yaml
+auth:
+  environment:
+    GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: "true"
+```
+
+Add this to the documented environment variable list in `PROJECT_SCOPE.md §7 Deployment` alongside the existing `SUPABASE_*` variables. Verify this is set before running Phase 1.4.
+
+**⚠️ Implementation Risk:** Zero effort to add, but high-impact if missed. Recommend adding a startup check: the `api/health` endpoint (already planned) should attempt a `supabase.auth.getSession()` call and verify the GoTrue service is reachable and correctly configured before reporting healthy.
+
+---
+
+#### C2. Supabase Edge Functions Do NOT Run Automatically in the Self-Hosted Docker Stack — Require Separate Deno Container
+
+`PROJECT_SCOPE.md §7 Deployment, Phase 5.2, Phase 6.2` — The scope states: "Background jobs: Supabase pg_cron triggering Edge Functions (local Deno runtime in Docker)." This plan has a critical gap: the standard self-hosted Supabase Docker Compose stack (`supabase/supabase` on GitHub) does **not** include an Edge Function runtime container by default. Edge Functions in self-hosted Supabase require the `supabase/edge-runtime` container (a Deno-based service), which must be added manually to the docker-compose setup and exposed correctly to the other Supabase services.
+
+The `supabase/edge-runtime` container is a separate image (`supabase/edge-runtime:v1.x`). It must be configured with the path to the functions directory, a service role key, and network access to the Postgres and GoTrue containers. Without it, calling a function URL (`http://supabase_kong:8000/functions/v1/refresh-reels`) returns a `404` — pg_cron's `net.http_post()` call silently does nothing, the background jobs never run, and the landing reel posters are never populated.
+
+Importantly, `pg_cron` using `net.http_post()` to call an Edge Function also requires the `pg_net` extension to be enabled (separate from `pg_cron`). The scope mentions pg_cron but not pg_net.
+
+**Fix (two parts):**
+
+1. Add the `supabase/edge-runtime` container to `docker-compose.yml`:
+
+```yaml
+functions:
+  image: supabase/edge-runtime:v1.x
+  volumes:
+    - ./supabase/functions:/home/deno/functions:ro
+  environment:
+    SUPABASE_URL: http://supabase_kong:8000
+    SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}
+    SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
+    VERIFY_JWT: "true"
+```
+
+Wire it into the Kong API gateway so function URLs resolve correctly.
+
+2. Enable `pg_net` alongside `pg_cron` in the Supabase Postgres extensions. Both are required for the `pg_cron` → `net.http_post()` → Edge Function invocation pattern described in the first review.
+
+**Alternative (simpler):** Replace Edge Functions with pure SQL stored procedures. For the landing reel refresh and trailer URL refresh jobs, the operations are: fetch from TMDB (HTTP) → upsert into Postgres. If the SQL stored procedure approach is used for the DB writes and a lightweight HTTP-capable cron mechanism does the TMDB call, this avoids the Deno runtime entirely. For example, use a small Node.js cron container (`node:22-alpine` + `node-cron`) that calls the TMDB API and writes directly to Postgres via the Supabase service role key. This is simpler to operate and debug than the pg_cron + Edge Function chain.
+
+**⚠️ Implementation Risk:** High if not addressed before Phase 5.2. The Phase 5.2 task is "Build and seed landing_reel_posters table: implement periodic refresh job via Supabase pg_cron + Edge Function" — this is an MVP deliverable. If the Edge Function runtime is not running, this entire phase stalls. Recommend deciding on the cron architecture (Edge Function vs. Node.js cron container) at Phase 1.7 when the Docker infrastructure is being built.
+
+---
+
+### NEW RECOMMENDED
+
+---
+
+#### R1. argon2 npm Package Requires Native Build Tools in Docker — Complicates Multi-Stage Builds
+
+`PROJECT_SCOPE.md §7 Data Model / Phase 1.5` — The scope specifies "hashed with Argon2id before storage" for recovery codes. The canonical npm package for this is `argon2` (by ranisalt, ~2M weekly downloads). It is the correct choice — it provides Argon2id natively and is actively maintained with Node.js 22 support.
+
+The complication is that `argon2` uses native bindings (it compiles a C extension at install time via `node-gyp`). In a multi-stage Docker build with `node:22-slim`, this requires build tools in the builder stage that must not leak into the final runner stage. The `node:22-slim` image does not include `python3`, `make`, or `gcc` — these must be explicitly installed in the builder stage, or the `npm install` of `argon2` will fail with a `node-gyp` build error.
+
+The correct multi-stage Dockerfile pattern:
+
+```dockerfile
+FROM node:22-slim AS builder
+# Install native build tools for argon2 (and any other native modules)
+RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
+WORKDIR /app
+COPY package*.json ./
+RUN npm ci
+# ... rest of build
+
+FROM node:22-slim AS runner
+# Do NOT install build tools here — copy only the compiled output
+COPY --from=builder /app/.next/standalone ./
+# argon2's compiled .node file is included in standalone trace — verify with:
+# ls .next/standalone/node_modules/argon2/
+```
+
+The `output: 'standalone'` trace should include the compiled `argon2` binary, but this must be verified — standalone tracing does not always follow native `.node` file references correctly. If `argon2` is missing from the standalone output, add it explicitly:
+
+```dockerfile
+COPY --from=builder /app/node_modules/argon2 ./node_modules/argon2
+```
+
+There is a pure-JS alternative (`@node-rs/argon2`, which ships pre-compiled binaries for common platforms), but `argon2` remains the battle-tested choice. If Docker build complexity is a concern, `@node-rs/argon2` eliminates the `node-gyp` issue by using pre-built NAPI binaries.
+
+**Fix:** In Phase 1.7 when the Dockerfile is built, add `python3 make g++` to the builder stage's apt install line. Test the full multi-stage build early — do not defer this to Phase 5.8 smoke testing.
+
+**⚠️ Implementation Risk:** Medium. If discovered late in Phase 3-4, the Dockerfile needs revision and all Docker builds retested. Low risk if addressed in Phase 1.7 as specified.
+
+---
+
+#### R2. iron-session v8 Has a Breaking Configuration Change from v7 — Wrong Docs Are Everywhere
+
+`PROJECT_SCOPE.md §7 Tech Stack, Phase 7.3` — The scope correctly selects iron-session for admin session management. The current version is v8.x (released 2023), which supports Next.js App Router and Route Handlers natively. However, iron-session v8 introduced a breaking change from v7 that catches many developers: the session wrapper API changed substantially.
+
+In v8, the correct App Router pattern uses `getIronSession()` directly in a Route Handler or Server Action:
+
+```typescript
+import { getIronSession } from "iron-session";
+import { cookies } from "next/headers";
+
+export async function POST(request: Request) {
+  const session = await getIronSession<AdminSessionData>(await cookies(), {
+    password: process.env.IRON_SESSION_SECRET!,
+    cookieName: "admin-session",
+    cookieOptions: {
+      secure: process.env.NODE_ENV === "production",
+      httpOnly: true,
+      sameSite: "strict",
+      maxAge: 60 * 60 * 8, // 8 hours in seconds
+    },
+  });
+  // ...
+}
+```
+
+The common mistake — importing `withIronSessionApiRoute` or `withIronSessionSsr` from v7 patterns — produces a runtime error in v8 because those exports no longer exist. Many Stack Overflow answers and blog posts still reference the v7 API. The official v8 README on GitHub is the authoritative source.
+
+One additional gotcha specific to this project: `sameSite: 'strict'` is the correct setting for the admin cookie. However, strict SameSite means the cookie is **not sent on the first request following a cross-site navigation** (e.g., if an admin clicks a link from another site to `/admin`). For an admin panel, this is acceptable — the admin will just be redirected to login. Document this behavior so it is not misdiagnosed as a bug.
+
+**Fix:** When implementing Phase 7.3, read the iron-session v8 README directly (`github.com/vvo/iron-session`) rather than relying on tutorials or blog posts, which are likely to reference the v7 API.
+
+**⚠️ Implementation Risk:** Low with awareness. High without — v7 patterns used with v8 produce silent session failures (the session appears empty) or runtime import errors, which can be time-consuming to diagnose.
+
+---
+
+#### R3. TanStack Query persistQueryClient Does Not Work With IndexedDB Out of the Box — Requires an Adapter Package
+
+`PROJECT_SCOPE.md §8 Phase 8.2` — The scope states: "Implement offline graceful degradation: cached list data via TanStack Query `persistQueryClient` (IndexedDB)." The `persistQueryClient` plugin does exist in TanStack Query v5, but it does not ship with an IndexedDB adapter. It ships with a `createSyncStoragePersister` (localStorage) and `createAsyncStoragePersister` (async key-value stores). IndexedDB requires a separate adapter package.
+
+The standard approach is to use `idb-keyval` (a lightweight IndexedDB wrapper) as the storage backend for `createAsyncStoragePersister`:
+
+```typescript
+import { persistQueryClient } from "@tanstack/react-query-persist-client";
+import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
+import { get, set, del } from "idb-keyval";
+
+const idbPersister = createAsyncStoragePersister({
+  storage: {
+    getItem: (key) => get(key),
+    setItem: (key, value) => set(key, value),
+    removeItem: (key) => del(key),
+  },
+});
+```
+
+The packages required (in addition to `@tanstack/react-query`):
+
+- `@tanstack/react-query-persist-client`
+- `@tanstack/query-async-storage-persister`
+- `idb-keyval`
+
+Note that `persistQueryClient` in v5 uses a `PersistQueryClientProvider` component wrapper, not a plugin passed to `QueryClient`. The API changed between v4 and v5. Additionally, persisted cache is stored serialized — ensure no non-serializable data (class instances, functions) ends up in query data, or `JSON.parse` will silently drop it on restore.
+
+**Fix:** In Phase 8.2, install all three packages above. Use `PersistQueryClientProvider` from `@tanstack/react-query-persist-client` rather than calling `persistQueryClient()` imperatively.
+
+**⚠️ Implementation Risk:** Low in isolation, but Phase 8.2 is already post-MVP. The risk is underestimating the scope of this task — it is 3 packages, not a one-line config, and requires testing the serialization of the query cache. Budget 2-4 hours rather than 30 minutes.
+
+---
+
+#### R4. @serwist/next Requires a Custom Service Worker Source File — Auto-Generation Is Not Supported
+
+`PROJECT_SCOPE.md §8 Phase 8.1` — The scope correctly specifies `@serwist/next`. One implementation gotcha that is not obvious from a quick read of the docs: unlike the old `next-pwa` which could auto-generate a service worker with zero config, `@serwist/next` requires you to write the service worker source file yourself (`app/sw.ts`). The build process compiles it, but you must author it.
+
+The minimal service worker for this app:
+
+```typescript
+// app/sw.ts
+import { defaultCache } from "@serwist/next/worker";
+import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
+import { Serwist } from "serwist";
+
+declare global {
+  interface WorkerGlobalScope extends SerwistGlobalConfig {
+    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
+  }
+}
+
+declare const self: ServiceWorkerGlobalScope;
+
+const serwist = new Serwist({
+  precacheEntries: self.__SW_MANIFEST,
+  skipWaiting: true,
+  clientsClaim: true,
+  navigationPreload: true,
+  runtimeCaching: defaultCache,
+});
+
+serwist.addEventListeners();
+```
+
+The `next.config.ts` wrapper and the `tsconfig.json` for the service worker file also need adjustments (the sw file needs `lib: ['WebWorker']` in TypeScript config, which conflicts with the main app's `lib: ['DOM']` — this requires a separate `tsconfig.worker.json`).
+
+**Fix:** Do not assume Phase 8.1 is a quick config step. It requires: (1) authoring `app/sw.ts`, (2) configuring `next.config.ts` with `withSerwist()`, (3) adding a `tsconfig.worker.json` for the service worker TypeScript compilation, (4) adding the `manifest.json` to `public/`. Budget a half-day for Phase 8.1, not an hour.
+
+**⚠️ Implementation Risk:** Low functionally — serwist is well-documented. The risk is schedule underestimation. The `tsconfig.worker.json` separation is the most common stumbling block.
+
+---
+
+#### R5. t3-env Is the Better Choice Than Raw zod for This Stack
+
+`PROJECT_SCOPE.md §7 Tech Stack` — The scope lists "zod (or t3-env)" for env validation and picks "zod" in Phase 1.3. Both work, but for a Next.js project with the distinction between `NEXT_PUBLIC_` (client-side) and server-only environment variables, `t3-env` (`@t3-oss/env-nextjs`) is meaningfully better:
+
+- It enforces the client/server split structurally — server-only vars in a `server` block, public vars in a `client` block. Putting `TMDB_API_KEY` in the wrong block causes a build-time error, not a runtime leak.
+- The `NEXT_PUBLIC_` prefix requirement is validated at build time.
+- It generates a type-safe `env` object that TypeScript narrows correctly throughout the codebase.
+- The boilerplate is nearly identical to raw zod validation.
+
+Using raw zod for env validation requires manually enforcing the server/client split by convention (and code review) rather than by type-system constraint. Given that the scope explicitly calls out `TMDB_API_KEY` must never be `NEXT_PUBLIC_`, the structural enforcement of `t3-env` is directly relevant.
+
+```typescript
+// env.ts
+import { createEnv } from "@t3-oss/env-nextjs";
+import { z } from "zod";
+
+export const env = createEnv({
+  server: {
+    TMDB_API_KEY: z.string().min(1),
+    SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
+    MASTER_ADMIN_USERNAME: z.string().min(1),
+    MASTER_ADMIN_TOTP_SECRET: z.string().min(1),
+    IRON_SESSION_SECRET: z.string().min(32),
+  },
+  client: {
+    NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
+    NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
+  },
+  runtimeEnv: {
+    TMDB_API_KEY: process.env.TMDB_API_KEY,
+    // ...
+  },
+});
+```
+
+Note from the scope: `SUPABASE_URL` and `SUPABASE_ANON_KEY` are listed as server-side env vars, but the Supabase client-side SDK needs them in the browser (for `signInAnonymously()` and real-time subscriptions). They should be `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` — the anon key is public by design (Supabase's security model relies on RLS, not key secrecy). Only `SUPABASE_SERVICE_ROLE_KEY` must remain server-side. This is a minor naming inconsistency in the scope's env var list that should be resolved before Phase 1.3.
+
+**Fix:** Use `t3-env` (`@t3-oss/env-nextjs`) in Phase 1.3 instead of raw zod. Rename `SUPABASE_URL` → `NEXT_PUBLIC_SUPABASE_URL` and `SUPABASE_ANON_KEY` → `NEXT_PUBLIC_SUPABASE_ANON_KEY` in the env var list, keeping only `SUPABASE_SERVICE_ROLE_KEY` as a server-only secret.
+
+**⚠️ Implementation Risk:** Near zero — `t3-env` is a thin wrapper around zod. Migration from raw zod to t3-env later has negligible cost, but fixing an accidentally-public service role key has very high cost.
+
+---
+
+#### R6. tini Is Still Warranted in Node.js 22 Docker Containers
+
+`PROJECT_SCOPE.md §7 Deployment` — The scope correctly specifies `tini` for PID 1 signal handling. This is the right call and remains necessary despite Node.js 22 improvements. The issue is not Node.js's signal handling per se — it is the zombie process reaping problem. When a process runs as PID 1 in a container, it is responsible for reaping orphaned child processes. Node.js does not implement zombie reaping. If the app spawns any child processes (even transiently — for example, `sharp` spawning worker threads, or any `exec()` call), orphaned processes accumulate and Docker `stop` commands may time out waiting for cleanup.
+
+`tini` as PID 1 correctly handles `SIGTERM` → graceful shutdown forwarding and zombie reaping. The Docker Compose `stop_grace_period` and `stop_signal` options depend on this working correctly. The overhead is negligible (< 1MB binary, zero runtime cost).
+
+The scope already specifies this correctly. This finding is confirmatory: do not remove `tini` based on advice that "Node.js 22 handles signals." It handles signals; it does not reap zombies.
+
+```dockerfile
+FROM node:22-slim AS runner
+RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
+# ...
+ENTRYPOINT ["/usr/bin/tini", "--"]
+CMD ["node", "server.js"]
+```
+
+**Fix:** No change needed — the scope is correct. This is a confirmation to not be talked out of it.
+
+**⚠️ Implementation Risk:** None.
+
+---
+
+#### R7. Caddy + Next.js Standalone + Self-Hosted Supabase — One docker-compose Networking Gotcha
+
+`PROJECT_SCOPE.md §7 Deployment, Phase 1.7` — The scope describes a docker-compose setup with three major components: the Next.js app container, the self-hosted Supabase stack, and Caddy. There is a networking trap in this configuration that commonly causes hours of debugging.
+
+The self-hosted Supabase stack uses Kong as its API gateway (port 8000 internally, the entry point for all Supabase services including GoTrue, PostgREST, and Realtime). When the **Next.js server** makes requests to Supabase (Server Components, Route Handlers), it should use the internal Docker network hostname (`http://supabase_kong:8000` or whatever the Kong service name is in the compose file) rather than the public-facing URL. This is faster (no round-trip through Caddy and back) and avoids TLS termination issues on internal traffic.
+
+When the **browser** makes requests to Supabase (the Supabase JS client calling GoTrue for auth, Realtime for subscriptions), it uses the public URL (e.g., `https://yourdomain.com/supabase` or a subdomain). Caddy must proxy this correctly to the Kong container.
+
+The critical gotcha: `NEXT_PUBLIC_SUPABASE_URL` must be the **public-facing URL** (for the browser client), not the internal Docker hostname. But for server-side Supabase client initialization using the service role key, you want to use the internal hostname. This means the Supabase client must be initialized differently for server vs. client contexts — using different URLs.
+
+The standard pattern:
+
+```typescript
+// lib/supabase/server.ts — uses internal URL for server-side calls
+import { createClient } from "@supabase/supabase-js";
+const supabase = createClient(
+  process.env.SUPABASE_INTERNAL_URL!, // http://supabase_kong:8000
+  process.env.SUPABASE_SERVICE_ROLE_KEY!,
+);
+
+// lib/supabase/client.ts — uses public URL for browser
+import { createBrowserClient } from "@supabase/ssr";
+const supabase = createBrowserClient(
+  process.env.NEXT_PUBLIC_SUPABASE_URL!, // https://yourdomain.com (or subdomain)
+  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+);
+```
+
+Add `SUPABASE_INTERNAL_URL` as a server-only env var in the scope's env var list.
+
+**Fix:** Add `SUPABASE_INTERNAL_URL` (the internal Docker network URL for Kong) to the deployment env var list. Initialize the Supabase server client using the internal URL and the service role key. Use `@supabase/ssr` for the browser client factory (`createBrowserClient`/`createServerClient`) rather than the base `@supabase/supabase-js` — the SSR package handles cookie-based session persistence correctly for Next.js App Router.
+
+**⚠️ Implementation Risk:** Medium. Getting this wrong means server-side Supabase calls either fail (if the public URL is not routable from inside Docker) or incur unnecessary latency (if they round-trip through Caddy). Caught in Phase 1.7 smoke testing, not catastrophic. Missed until Phase 3, it causes confusing auth failures on server-rendered pages.
+
+---
+
+#### R8. Vitest + Playwright Setup With Next.js App Router — Two Non-Obvious Configuration Steps
+
+`PROJECT_SCOPE.md §7 Tech Stack, Phase 1.1` — The scope adds Vitest in Phase 1.1 and Playwright in Phase 9.16. Both work with Next.js App Router, but each has a non-obvious setup step that is commonly overlooked.
+
+**Vitest:** Next.js App Router uses React Server Components, which cannot be rendered in a Vitest test environment (Vitest uses jsdom, which is a browser simulator and does not support RSC). Tests for Server Components must either: (a) test the underlying data-fetching logic in isolation (not the component itself), or (b) use Playwright for full-stack E2E rendering. Unit tests with Vitest are best scoped to pure utility functions, business logic (randomizer algorithm, emotion-genre mapping, recovery code generation, invite code generation), and Client Components only.
+
+The Vitest config requires the `@vitejs/plugin-react` plugin and explicit path aliases matching `tsconfig.json`:
+
+```typescript
+// vitest.config.ts
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+export default defineConfig({
+  plugins: [react()],
+  test: {
+    environment: "jsdom",
+    setupFiles: "./vitest.setup.ts",
+    alias: {
+      "@": path.resolve(__dirname, "./"),
+    },
+  },
+});
+```
+
+**Playwright:** For E2E tests against a Next.js App Router app in Docker, Playwright needs the app running before tests execute. In Phase 9 (QA), Playwright should run against the actual Docker production stack, not a `next dev` instance, because several behaviors differ (standalone output, real Supabase connection, real cookies). The `playwright.config.ts` `webServer` option can start a local build, but for testing against the Docker-composed stack, the `baseURL` should point to the running container (or Caddy endpoint).
+
+**Fix:** In Phase 1.1, add the Vitest config file and constrain the scope of unit tests to logic and Client Components only — do not attempt to unit-test Server Components or Route Handlers with Vitest. For Phase 9.16, run Playwright against the full Docker stack, not against `next dev`.
+
+**⚠️ Implementation Risk:** Low functionally. The risk is wasted time trying to unit-test Server Components in Vitest and getting cryptic errors about `async` component rendering, RSC context, or missing Node.js modules.
+
+---
+
+### SECOND REVIEW POSITIVES
+
+The updated scope correctly addresses all three Critical findings and all seven Recommended Upgrades from the first review. Specific improvements noted:
+
+- Anonymous Sign-In via `signInAnonymously()` — adopted. Closes the RLS gap.
+- Supabase self-hosted Docker — specified. Closes the managed-vs-self-hosted ambiguity.
+- Caddy reverse proxy — specified. Correct for HTTPS, PWA, and wss:// requirements.
+- pg_cron + Edge Functions — specified to replace Vercel Cron. See C2 above for a gap in this plan.
+- `@serwist/next` — adopted. Correct replacement for next-pwa.
+- `output: 'standalone'` + `node:22-slim` — specified. Correct.
+- TMDB native CDN URLs with TMDB sizes — specified. Correct.
+- `iron-session` for admin sessions — specified. Correct.
+- Invite code format changed to `WORD-WORD` — adopted. Entropy issue resolved.
+- Recovery code entropy (24 alphanumeric / 128-bit) — well-specified.
+- Argon2id for recovery code hashing — correct algorithm selection. See R1 for Docker build gotcha.
+- `tini` for PID 1 — correctly specified. See R6 confirming this should be kept.
+- Security headers section added — comprehensive and correct.
+- RLS policies defined per-table — correctly scoped.
+- Data retention policy (12-month inactivity deletion) — well-specified and privacy-policy-documented.
+
+---
+
+### SECOND REVIEW SUMMARY
+
+| Severity      | Count | New Items                                                                                                                                                                                                                                        |
+| ------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Critical      | 2     | GoTrue anonymous auth env var; Edge Functions not in default self-hosted stack                                                                                                                                                                   |
+| Recommended   | 8     | argon2 Docker native build; iron-session v8 API; persistQueryClient adapter packages; serwist sw.ts authoring; t3-env over raw zod + SUPABASE_URL naming fix; tini confirmation; Caddy/Docker internal URL networking; Vitest/Playwright scoping |
+| No New Action | —     | All first-review findings confirmed adopted in updated scope                                                                                                                                                                                     |

BIN
src/app/favicon.ico


+ 26 - 0
src/app/globals.css

@@ -0,0 +1,26 @@
+@import "tailwindcss";
+
+:root {
+  --background: #ffffff;
+  --foreground: #171717;
+}
+
+@theme inline {
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --font-sans: var(--font-geist-sans);
+  --font-mono: var(--font-geist-mono);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --background: #0a0a0a;
+    --foreground: #ededed;
+  }
+}
+
+body {
+  background: var(--background);
+  color: var(--foreground);
+  font-family: Arial, Helvetica, sans-serif;
+}

+ 39 - 0
src/app/layout.tsx

@@ -0,0 +1,39 @@
+import type { Metadata, Viewport } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import { Providers } from "@/components/providers";
+import "./globals.css";
+
+const geistSans = Geist({
+  variable: "--font-geist-sans",
+  subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+  variable: "--font-geist-mono",
+  subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+  title: "MovieDice",
+  description: "One shared list. One button to decide. No arguments.",
+};
+
+export const viewport: Viewport = {
+  width: "device-width",
+  initialScale: 1,
+  themeColor: "#0a0a0a",
+};
+
+export default function RootLayout({
+  children,
+}: Readonly<{
+  children: React.ReactNode;
+}>) {
+  return (
+    <html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}>
+      <body className="min-h-full flex flex-col">
+        <Providers>{children}</Providers>
+      </body>
+    </html>
+  );
+}

+ 8 - 0
src/app/page.tsx

@@ -0,0 +1,8 @@
+export default function RootPage() {
+  return (
+    <main className="flex min-h-screen flex-col items-center justify-center">
+      <h1 className="text-4xl font-bold">MovieDice</h1>
+      <p className="mt-4 text-lg text-foreground/60">One shared list. One button to decide.</p>
+    </main>
+  );
+}

+ 20 - 0
src/components/providers.tsx

@@ -0,0 +1,20 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { useState, type ReactNode } from "react";
+
+export function Providers({ children }: { children: ReactNode }) {
+  const [queryClient] = useState(
+    () =>
+      new QueryClient({
+        defaultOptions: {
+          queries: {
+            staleTime: 60 * 1000,
+            retry: 1,
+          },
+        },
+      }),
+  );
+
+  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
+}

+ 30 - 0
src/env.ts

@@ -0,0 +1,30 @@
+import { createEnv } from "@t3-oss/env-nextjs";
+import { z } from "zod";
+
+export const env = createEnv({
+  server: {
+    TMDB_API_KEY: z.string().min(1, "TMDB_API_KEY is required"),
+    SUPABASE_INTERNAL_URL: z.string().url("SUPABASE_INTERNAL_URL must be a valid URL"),
+    SUPABASE_SERVICE_ROLE_KEY: z.string().min(1, "SUPABASE_SERVICE_ROLE_KEY is required"),
+    MASTER_ADMIN_USERNAME: z.string().min(1, "MASTER_ADMIN_USERNAME is required"),
+    MASTER_ADMIN_TOTP_SECRET: z.string().min(1, "MASTER_ADMIN_TOTP_SECRET is required"),
+    IRON_SESSION_SECRET: z.string().min(32, "IRON_SESSION_SECRET must be at least 32 characters"),
+  },
+  client: {
+    NEXT_PUBLIC_SUPABASE_URL: z.string().url("NEXT_PUBLIC_SUPABASE_URL must be a valid URL"),
+    NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1, "NEXT_PUBLIC_SUPABASE_ANON_KEY is required"),
+    NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
+  },
+  runtimeEnv: {
+    TMDB_API_KEY: process.env.TMDB_API_KEY,
+    SUPABASE_INTERNAL_URL: process.env.SUPABASE_INTERNAL_URL,
+    SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,
+    MASTER_ADMIN_USERNAME: process.env.MASTER_ADMIN_USERNAME,
+    MASTER_ADMIN_TOTP_SECRET: process.env.MASTER_ADMIN_TOTP_SECRET,
+    IRON_SESSION_SECRET: process.env.IRON_SESSION_SECRET,
+    NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
+    NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
+    NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
+  },
+  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
+});

+ 60 - 0
src/lib/constants.ts

@@ -0,0 +1,60 @@
+export const TMDB_API_BASE_URL = "https://api.themoviedb.org/3";
+
+export const TRAILER_DOMAIN_ALLOWLIST = [
+  "youtube.com",
+  "www.youtube.com",
+  "themoviedb.org",
+  "imdb.com",
+  "www.imdb.com",
+];
+
+export const EMOTION_TO_GENRE_MAP: Record<string, { primary: number[]; secondary: number[] }> = {
+  happy: { primary: [35, 16, 10751], secondary: [12, 10402] },
+  cheerful: { primary: [35, 16, 10751], secondary: [12, 10402] },
+  upbeat: { primary: [35, 16, 10751], secondary: [12, 10402] },
+  fun: { primary: [35, 16, 10751], secondary: [12, 10402] },
+  sad: { primary: [18, 10749], secondary: [10752, 36] },
+  emotional: { primary: [18, 10749], secondary: [10752, 36] },
+  cry: { primary: [18, 10749], secondary: [10752, 36] },
+  tearjerker: { primary: [18, 10749], secondary: [10752, 36] },
+  excited: { primary: [28, 12], secondary: [878, 53] },
+  hyped: { primary: [28, 12], secondary: [878, 53] },
+  energetic: { primary: [28, 12], secondary: [878, 53] },
+  pumped: { primary: [28, 12], secondary: [878, 53] },
+  scared: { primary: [27, 53], secondary: [9648] },
+  tense: { primary: [27, 53], secondary: [9648] },
+  nervous: { primary: [27, 53], secondary: [9648] },
+  creepy: { primary: [27, 53], secondary: [9648] },
+  calm: { primary: [99, 18], secondary: [16] },
+  relaxed: { primary: [99, 18], secondary: [16] },
+  chill: { primary: [99, 18], secondary: [16] },
+  cozy: { primary: [99, 18], secondary: [16] },
+  romantic: { primary: [10749, 35], secondary: [18] },
+  lovey: { primary: [10749, 35], secondary: [18] },
+  "date night": { primary: [10749, 35], secondary: [18] },
+  thoughtful: { primary: [99, 18], secondary: [36] },
+  reflective: { primary: [99, 18], secondary: [36] },
+  deep: { primary: [99, 18], secondary: [36] },
+  funny: { primary: [35, 16], secondary: [10751] },
+  silly: { primary: [35, 16], secondary: [10751] },
+  goofy: { primary: [35, 16], secondary: [10751] },
+  laugh: { primary: [35, 16], secondary: [10751] },
+  dark: { primary: [80, 53], secondary: [18, 10752] },
+  gritty: { primary: [80, 53], secondary: [18, 10752] },
+  intense: { primary: [80, 53], secondary: [18, 10752] },
+  serious: { primary: [80, 53], secondary: [18, 10752] },
+  nostalgic: { primary: [], secondary: [] },
+  classic: { primary: [], secondary: [] },
+  retro: { primary: [], secondary: [] },
+};
+
+export const NOSTALGIC_KEYWORDS = new Set(["nostalgic", "classic", "retro"]);
+export const NOSTALGIC_YEAR_CUTOFF = 2000;
+
+export const INVITE_CODE_SEPARATOR = "-";
+export const DISPLAY_NAME_MAX_LENGTH = 30;
+export const GROUP_NAME_MAX_LENGTH = 50;
+export const RECOVERY_CODE_LENGTH = 24;
+export const MOVIES_PER_PAGE = 12;
+export const SEARCH_DEBOUNCE_MS = 300;
+export const REEL_POSTER_COUNT = 20;

+ 20 - 0
src/lib/supabase/admin.ts

@@ -0,0 +1,20 @@
+import { createClient } from "@supabase/supabase-js";
+import type { Database } from "@/types/database";
+
+let adminClient: ReturnType<typeof createClient<Database>> | null = null;
+
+export function getSupabaseAdminClient() {
+  if (adminClient) return adminClient;
+
+  const url = process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!;
+  const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
+
+  adminClient = createClient<Database>(url, serviceKey, {
+    auth: {
+      autoRefreshToken: false,
+      persistSession: false,
+    },
+  });
+
+  return adminClient;
+}

+ 17 - 0
src/lib/supabase/client.ts

@@ -0,0 +1,17 @@
+"use client";
+
+import { createBrowserClient } from "@supabase/ssr";
+import type { Database } from "@/types/database";
+
+let client: ReturnType<typeof createBrowserClient<Database>> | null = null;
+
+export function getSupabaseBrowserClient() {
+  if (client) return client;
+
+  client = createBrowserClient<Database>(
+    process.env.NEXT_PUBLIC_SUPABASE_URL!,
+    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+  );
+
+  return client;
+}

+ 28 - 0
src/lib/supabase/server.ts

@@ -0,0 +1,28 @@
+import { createServerClient, type CookieOptions } from "@supabase/ssr";
+import { cookies } from "next/headers";
+import type { Database } from "@/types/database";
+
+export async function getSupabaseServerClient() {
+  const cookieStore = await cookies();
+
+  return createServerClient<Database>(
+    process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!,
+    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+    {
+      cookies: {
+        getAll() {
+          return cookieStore.getAll();
+        },
+        setAll(cookiesToSet: Array<{ name: string; value: string; options: CookieOptions }>) {
+          try {
+            cookiesToSet.forEach(({ name, value, options }) => {
+              cookieStore.set(name, value, options);
+            });
+          } catch {
+            // Called from a Server Component — ignore
+          }
+        },
+      },
+    },
+  );
+}

+ 153 - 0
src/types/database.ts

@@ -0,0 +1,153 @@
+export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];
+
+export interface Database {
+  public: {
+    Tables: {
+      users: {
+        Row: {
+          id: string;
+          display_name: string;
+          avatar_color: string | null;
+          recovery_code: string | null;
+          last_active_at: string;
+          created_at: string;
+        };
+        Insert: {
+          id: string;
+          display_name: string;
+          avatar_color?: string | null;
+          recovery_code?: string | null;
+          last_active_at?: string;
+          created_at?: string;
+        };
+        Update: {
+          id?: string;
+          display_name?: string;
+          avatar_color?: string | null;
+          recovery_code?: string | null;
+          last_active_at?: string;
+          created_at?: string;
+        };
+      };
+      groups: {
+        Row: {
+          id: string;
+          name: string;
+          invite_code: string;
+          created_by: string;
+          created_at: string;
+        };
+        Insert: {
+          id?: string;
+          name: string;
+          invite_code: string;
+          created_by: string;
+          created_at?: string;
+        };
+        Update: {
+          id?: string;
+          name?: string;
+          invite_code?: string;
+          created_by?: string;
+          created_at?: string;
+        };
+      };
+      group_members: {
+        Row: {
+          group_id: string;
+          user_id: string;
+          role: "admin" | "member";
+          joined_at: string;
+        };
+        Insert: {
+          group_id: string;
+          user_id: string;
+          role: "admin" | "member";
+          joined_at?: string;
+        };
+        Update: {
+          group_id?: string;
+          user_id?: string;
+          role?: "admin" | "member";
+          joined_at?: string;
+        };
+      };
+      movies: {
+        Row: {
+          id: string;
+          group_id: string;
+          tmdb_id: number;
+          title: string;
+          year: number;
+          poster_path: string | null;
+          genres: string[];
+          trailer_url: string | null;
+          trailer_url_refreshed_at: string | null;
+          metadata_refreshed_at: string | null;
+          added_by: string | null;
+          watched: boolean;
+          watched_at: string | null;
+          added_at: string;
+        };
+        Insert: {
+          id?: string;
+          group_id: string;
+          tmdb_id: number;
+          title: string;
+          year: number;
+          poster_path?: string | null;
+          genres?: string[];
+          trailer_url?: string | null;
+          trailer_url_refreshed_at?: string | null;
+          metadata_refreshed_at?: string | null;
+          added_by: string;
+          watched?: boolean;
+          watched_at?: string | null;
+          added_at?: string;
+        };
+        Update: {
+          id?: string;
+          group_id?: string;
+          tmdb_id?: number;
+          title?: string;
+          year?: number;
+          poster_path?: string | null;
+          genres?: string[];
+          trailer_url?: string | null;
+          trailer_url_refreshed_at?: string | null;
+          metadata_refreshed_at?: string | null;
+          added_by?: string | null;
+          watched?: boolean;
+          watched_at?: string | null;
+          added_at?: string;
+        };
+      };
+      landing_reel_posters: {
+        Row: {
+          id: number;
+          tmdb_id: number;
+          poster_path: string;
+          title: string;
+          refreshed_at: string;
+        };
+        Insert: {
+          id?: number;
+          tmdb_id: number;
+          poster_path: string;
+          title: string;
+          refreshed_at?: string;
+        };
+        Update: {
+          id?: number;
+          tmdb_id?: number;
+          poster_path?: string;
+          title?: string;
+          refreshed_at?: string;
+        };
+      };
+    };
+    Views: Record<string, never>;
+    Functions: Record<string, never>;
+    Enums: Record<string, never>;
+  };
+}

+ 84 - 0
src/types/tmdb.ts

@@ -0,0 +1,84 @@
+export interface TMDBMovie {
+  id: number;
+  title: string;
+  overview: string;
+  poster_path: string | null;
+  backdrop_path: string | null;
+  release_date: string;
+  genre_ids: number[];
+  vote_average: number;
+  vote_count: number;
+  adult: boolean;
+  popularity: number;
+}
+
+export interface TMDBMovieDetails extends Omit<TMDBMovie, "genre_ids"> {
+  genres: TMDBGenre[];
+  runtime: number | null;
+  tagline: string | null;
+  imdb_id: string | null;
+}
+
+export interface TMDBSearchResponse {
+  page: number;
+  results: TMDBMovie[];
+  total_pages: number;
+  total_results: number;
+}
+
+export interface TMDBGenre {
+  id: number;
+  name: string;
+}
+
+export interface TMDBVideo {
+  id: string;
+  key: string;
+  name: string;
+  site: string;
+  type: string;
+  official: boolean;
+}
+
+export interface TMDBVideosResponse {
+  id: number;
+  results: TMDBVideo[];
+}
+
+export const TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p";
+
+export const TMDB_IMAGE_SIZES = {
+  grid: "w342",
+  reel: "w185",
+  panel: "w500",
+} as const;
+
+export function getTMDBImageUrl(
+  posterPath: string | null,
+  size: keyof typeof TMDB_IMAGE_SIZES,
+): string | null {
+  if (!posterPath) return null;
+  return `${TMDB_IMAGE_BASE_URL}/${TMDB_IMAGE_SIZES[size]}${posterPath}`;
+}
+
+export const TMDB_GENRE_MAP: Record<number, string> = {
+  28: "Action",
+  12: "Adventure",
+  16: "Animation",
+  35: "Comedy",
+  80: "Crime",
+  99: "Documentary",
+  18: "Drama",
+  10751: "Family",
+  14: "Fantasy",
+  36: "History",
+  27: "Horror",
+  10402: "Music",
+  9648: "Mystery",
+  10749: "Romance",
+  878: "Science Fiction",
+  10770: "TV Movie",
+  53: "Thriller",
+  10752: "War",
+  37: "Western",
+};

+ 34 - 0
tsconfig.json

@@ -0,0 +1,34 @@
+{
+  "compilerOptions": {
+    "target": "ES2017",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "react-jsx",
+    "incremental": true,
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ],
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": [
+    "next-env.d.ts",
+    "**/*.ts",
+    "**/*.tsx",
+    ".next/types/**/*.ts",
+    ".next/dev/types/**/*.ts",
+    "**/*.mts"
+  ],
+  "exclude": ["node_modules"]
+}