← back to IWasCute
initial scaffold (gitify-all 2026-05-06)
5f5efec617868324ff2afb87d38322afa272c60e · 2026-05-06 10:25:05 -0700 · Steve Abrams
Files touched
A .gitignoreA AGENTS.mdA CLAUDE.mdA README.mdA db/001_schema.sqlA ecosystem.config.jsA eslint.config.mjsA next.config.tsA package-lock.jsonA package.jsonA postcss.config.mjsA public/admin.htmlA public/favicon.svgA public/file.svgA public/globe.svgA public/index.htmlA public/next.svgA public/vercel.svgA public/window.svgA server.jsA src/app/admin/page.tsxA src/app/api/admin/route.tsA src/app/api/auth/route.tsA src/app/browse/page.tsxA src/app/dashboard/page.tsxA src/app/favicon.icoA src/app/globals.cssA src/app/layout.tsxA src/app/licensor/layout.tsxA src/app/licensor/page.tsxA src/app/login/page.tsxA src/app/page.tsxA src/app/v1/page.tsxA src/app/v2/page.tsxA src/app/v3/page.tsxA src/components/licensor/CopyrightInfo.tsxA src/components/licensor/LikenessRelease.tsxA src/components/licensor/PhotoUpload.tsxA src/components/licensor/ReviewSubmit.tsxA src/components/licensor/Stepper.tsxA src/components/shared/Navbar.tsxA src/components/shared/VersionSwitcher.tsxA src/lib/auth.tsA src/lib/db.tsA tsconfig.json
Diff
commit 5f5efec617868324ff2afb87d38322afa272c60e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 10:25:05 2026 -0700
initial scaffold (gitify-all 2026-05-06)
---
.gitignore | 50 +
AGENTS.md | 5 +
CLAUDE.md | 1 +
README.md | 36 +
db/001_schema.sql | 256 +
ecosystem.config.js | 14 +
eslint.config.mjs | 18 +
next.config.ts | 7 +
package-lock.json | 6828 +++++++++++++++++++++++++++
package.json | 33 +
postcss.config.mjs | 7 +
public/admin.html | 246 +
public/favicon.svg | 4 +
public/file.svg | 1 +
public/globe.svg | 1 +
public/index.html | 827 ++++
public/next.svg | 1 +
public/vercel.svg | 1 +
public/window.svg | 1 +
server.js | 57 +
src/app/admin/page.tsx | 1371 ++++++
src/app/api/admin/route.ts | 283 ++
src/app/api/auth/route.ts | 72 +
src/app/browse/page.tsx | 281 ++
src/app/dashboard/page.tsx | 320 ++
src/app/favicon.ico | Bin 0 -> 25931 bytes
src/app/globals.css | 79 +
src/app/layout.tsx | 37 +
src/app/licensor/layout.tsx | 76 +
src/app/licensor/page.tsx | 95 +
src/app/login/page.tsx | 242 +
src/app/page.tsx | 284 ++
src/app/v1/page.tsx | 259 +
src/app/v2/page.tsx | 506 ++
src/app/v3/page.tsx | 417 ++
src/components/licensor/CopyrightInfo.tsx | 534 +++
src/components/licensor/LikenessRelease.tsx | 364 ++
src/components/licensor/PhotoUpload.tsx | 797 ++++
src/components/licensor/ReviewSubmit.tsx | 359 ++
src/components/licensor/Stepper.tsx | 139 +
src/components/shared/Navbar.tsx | 86 +
src/components/shared/VersionSwitcher.tsx | 53 +
src/lib/auth.ts | 23 +
src/lib/db.ts | 11 +
tsconfig.json | 34 +
45 files changed, 15116 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f522159
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,50 @@
+# 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 (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+node_modules/
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+dist/
+build/
+*.bak
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..8bd0e39
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,5 @@
+<!-- BEGIN:nextjs-agent-rules -->
+# This is NOT the Next.js you know
+
+This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
+<!-- END:nextjs-agent-rules -->
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..43c994c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e215bc4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/db/001_schema.sql b/db/001_schema.sql
new file mode 100644
index 0000000..e336b29
--- /dev/null
+++ b/db/001_schema.sql
@@ -0,0 +1,256 @@
+-- Iwascute.com — Core Schema
+-- Dual-rights clearance marketplace for childhood photos
+
+-- Extensions
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+CREATE EXTENSION IF NOT EXISTS "pgcrypto";
+
+-- ============================================================
+-- USERS
+-- ============================================================
+CREATE TABLE users (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ email TEXT UNIQUE NOT NULL,
+ password_hash TEXT, -- NULL for OAuth-only users
+ display_name TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'licensor' CHECK (role IN ('licensor', 'licensee', 'admin')),
+ is_verified BOOLEAN DEFAULT FALSE,
+ is_adult_confirmed BOOLEAN DEFAULT FALSE,
+ avatar_url TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Admin user
+INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
+VALUES (
+ 'admin@iwascute.com',
+ crypt('DWSecure2024!', gen_salt('bf')),
+ 'Admin',
+ 'admin',
+ TRUE,
+ TRUE
+);
+
+-- ============================================================
+-- UPLOADS (a submission = batch of photos + rights info)
+-- ============================================================
+CREATE TABLE uploads (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ status TEXT NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending', 'in_review', 'approved', 'rejected', 'flagged', 'dmca_removed')),
+
+ -- Copyright info (step 2)
+ copyright_relationship TEXT CHECK (copyright_relationship IN ('parent', 'family', 'professional', 'unknown')),
+ copyright_photographer TEXT,
+ copyright_year INTEGER,
+ copyright_permission TEXT CHECK (copyright_permission IN ('yes', 'no', 'deceased')),
+ copyright_estate_rights BOOLEAN DEFAULT FALSE,
+
+ -- Likeness release (step 3)
+ likeness_is_subject BOOLEAN DEFAULT FALSE,
+ likeness_is_adult BOOLEAN DEFAULT FALSE,
+ likeness_editorial BOOLEAN DEFAULT FALSE,
+ likeness_commercial BOOLEAN DEFAULT FALSE,
+ likeness_ai_training BOOLEAN DEFAULT FALSE,
+ likeness_signature TEXT,
+ likeness_signed_at TIMESTAMPTZ,
+
+ -- Review
+ reviewed_by UUID REFERENCES users(id),
+ reviewed_at TIMESTAMPTZ,
+ review_notes TEXT,
+ rejection_reason TEXT,
+
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- ============================================================
+-- PHOTOS (individual images within an upload)
+-- ============================================================
+CREATE TABLE photos (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ upload_id UUID NOT NULL REFERENCES uploads(id) ON DELETE CASCADE,
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+
+ -- File info
+ filename TEXT NOT NULL,
+ file_size INTEGER,
+ mime_type TEXT,
+ storage_path TEXT, -- future: S3/R2 path
+ thumbnail_path TEXT,
+
+ -- EXIF metadata
+ exif_date_taken TIMESTAMPTZ,
+ exif_year INTEGER,
+ exif_latitude DOUBLE PRECISION,
+ exif_longitude DOUBLE PRECISION,
+ exif_camera_make TEXT,
+ exif_camera_model TEXT,
+ exif_width INTEGER,
+ exif_height INTEGER,
+
+ -- Clearance status (inherits from upload, can be overridden per-photo)
+ clearance_level TEXT DEFAULT 'none'
+ CHECK (clearance_level IN ('none', 'editorial', 'commercial', 'ai_training')),
+
+ -- Licensing
+ view_count INTEGER DEFAULT 0,
+ license_count INTEGER DEFAULT 0,
+ total_earnings NUMERIC(10,2) DEFAULT 0,
+
+ -- Directory grouping
+ directory_name TEXT,
+
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- ============================================================
+-- DMCA CLAIMS
+-- ============================================================
+CREATE TABLE dmca_claims (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ upload_id UUID REFERENCES uploads(id),
+ photo_id UUID REFERENCES photos(id),
+
+ claimant_name TEXT NOT NULL,
+ claimant_email TEXT,
+ reason TEXT NOT NULL,
+ evidence_url TEXT,
+
+ status TEXT NOT NULL DEFAULT 'open'
+ CHECK (status IN ('open', 'investigating', 'taken_down', 'counter_noticed', 'resolved', 'dismissed')),
+
+ -- Resolution
+ resolved_by UUID REFERENCES users(id),
+ resolved_at TIMESTAMPTZ,
+ resolution_notes TEXT,
+
+ filed_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- ============================================================
+-- AUDIT LOG
+-- ============================================================
+CREATE TABLE audit_log (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id),
+ action TEXT NOT NULL, -- 'upload.approve', 'upload.reject', 'dmca.takedown', etc
+ target_type TEXT, -- 'upload', 'photo', 'dmca_claim', 'user'
+ target_id UUID,
+ details JSONB,
+ ip_address INET,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- ============================================================
+-- SESSIONS (simple cookie auth)
+-- ============================================================
+CREATE TABLE sessions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ token TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
+ expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days',
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- ============================================================
+-- INDEXES
+-- ============================================================
+CREATE INDEX idx_uploads_user ON uploads(user_id);
+CREATE INDEX idx_uploads_status ON uploads(status);
+CREATE INDEX idx_photos_upload ON photos(upload_id);
+CREATE INDEX idx_photos_user ON photos(user_id);
+CREATE INDEX idx_dmca_status ON dmca_claims(status);
+CREATE INDEX idx_sessions_token ON sessions(token);
+CREATE INDEX idx_sessions_expires ON sessions(expires_at);
+CREATE INDEX idx_audit_user ON audit_log(user_id);
+CREATE INDEX idx_audit_action ON audit_log(action);
+
+-- ============================================================
+-- SEED DATA (demo uploads for admin testing)
+-- ============================================================
+DO $$
+DECLARE
+ u1 UUID; u2 UUID; u3 UUID; u4 UUID; u5 UUID; u6 UUID;
+ up1 UUID; up2 UUID; up3 UUID; up4 UUID; up5 UUID; up6 UUID;
+BEGIN
+ -- Create demo users
+ INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
+ VALUES ('sarah.m@email.com', crypt('demo123', gen_salt('bf')), 'Sarah Martinez', 'licensor', TRUE, TRUE) RETURNING id INTO u1;
+ INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
+ VALUES ('james.k@email.com', crypt('demo123', gen_salt('bf')), 'James Kim', 'licensor', TRUE, TRUE) RETURNING id INTO u2;
+ INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
+ VALUES ('maria.l@email.com', crypt('demo123', gen_salt('bf')), 'Maria Lopez', 'licensor', TRUE, TRUE) RETURNING id INTO u3;
+ INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
+ VALUES ('david.r@email.com', crypt('demo123', gen_salt('bf')), 'David Reynolds', 'licensor', TRUE, TRUE) RETURNING id INTO u4;
+ INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
+ VALUES ('emma.t@email.com', crypt('demo123', gen_salt('bf')), 'Emma Thompson', 'licensor', TRUE, TRUE) RETURNING id INTO u5;
+ INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
+ VALUES ('alex.w@email.com', crypt('demo123', gen_salt('bf')), 'Alex Wilson', 'licensor', TRUE, TRUE) RETURNING id INTO u6;
+
+ -- Create uploads (pending review)
+ INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_signature, likeness_signed_at)
+ VALUES (u1, 'pending', 'parent', 'yes', TRUE, TRUE, TRUE, TRUE, 'Sarah Martinez', NOW() - INTERVAL '2 hours') RETURNING id INTO up1;
+ INSERT INTO uploads (user_id, status, copyright_relationship, copyright_photographer, copyright_permission, copyright_estate_rights, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_signature, likeness_signed_at)
+ VALUES (u2, 'pending', 'professional', 'Jane Doe Photography', 'deceased', TRUE, TRUE, TRUE, TRUE, 'James Kim', NOW() - INTERVAL '5 hours') RETURNING id INTO up2;
+ INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_ai_training, likeness_signature, likeness_signed_at)
+ VALUES (u3, 'pending', 'family', 'yes', TRUE, TRUE, TRUE, TRUE, TRUE, 'Maria Lopez', NOW() - INTERVAL '8 hours') RETURNING id INTO up3;
+ INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_signature, likeness_signed_at)
+ VALUES (u4, 'flagged', 'unknown', 'no', TRUE, TRUE, TRUE, 'David Reynolds', NOW() - INTERVAL '12 hours') RETURNING id INTO up4;
+ INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_signature, likeness_signed_at)
+ VALUES (u5, 'pending', 'parent', 'yes', TRUE, TRUE, TRUE, TRUE, 'Emma Thompson', NOW() - INTERVAL '1 day') RETURNING id INTO up5;
+ INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, copyright_estate_rights, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_ai_training, likeness_signature, likeness_signed_at)
+ VALUES (u6, 'pending', 'parent', 'deceased', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, 'Alex Wilson', NOW() - INTERVAL '1 day') RETURNING id INTO up6;
+
+ -- Add photos to uploads
+ INSERT INTO photos (upload_id, user_id, filename, file_size, mime_type, exif_year) VALUES
+ (up1, u1, 'summer_1992.jpg', 2400000, 'image/jpeg', 1992),
+ (up1, u1, 'birthday_party.jpg', 1800000, 'image/jpeg', 1993),
+ (up1, u1, 'first_steps.jpg', 3100000, 'image/jpeg', 1991),
+ (up1, u1, 'beach_day.jpg', 2200000, 'image/jpeg', 1994),
+ (up2, u2, 'school_portrait.jpg', 4500000, 'image/jpeg', 1988),
+ (up3, u3, 'christmas_1995.jpg', 1900000, 'image/jpeg', 1995),
+ (up3, u3, 'halloween.jpg', 2100000, 'image/jpeg', 1996),
+ (up3, u3, 'easter_egg_hunt.jpg', 1700000, 'image/jpeg', 1995),
+ (up3, u3, 'playground.jpg', 2800000, 'image/jpeg', 1997),
+ (up3, u3, 'first_bike.jpg', 3200000, 'image/jpeg', 1996),
+ (up3, u3, 'family_vacation.jpg', 2500000, 'image/jpeg', 1998),
+ (up3, u3, 'grandma_house.jpg', 1600000, 'image/jpeg', 1994),
+ (up3, u3, 'snow_day.jpg', 2900000, 'image/jpeg', 1997),
+ (up4, u4, 'old_photo_1.jpg', 1200000, 'image/jpeg', NULL),
+ (up4, u4, 'old_photo_2.jpg', 1500000, 'image/jpeg', NULL),
+ (up5, u5, 'toddler_pic.jpg', 2600000, 'image/jpeg', 2000),
+ (up5, u5, 'kindergarten.jpg', 2100000, 'image/jpeg', 2002),
+ (up5, u5, 'dance_recital.jpg', 3400000, 'image/jpeg', 2003),
+ (up6, u6, 'baby_photo_1.jpg', 1800000, 'image/jpeg', 1985),
+ (up6, u6, 'baby_photo_2.jpg', 2200000, 'image/jpeg', 1985),
+ (up6, u6, 'baby_photo_3.jpg', 1900000, 'image/jpeg', 1986),
+ (up6, u6, 'toddler_1.jpg', 2700000, 'image/jpeg', 1987),
+ (up6, u6, 'toddler_2.jpg', 2400000, 'image/jpeg', 1987),
+ (up6, u6, 'preschool.jpg', 3000000, 'image/jpeg', 1988);
+
+ -- DMCA claims
+ INSERT INTO dmca_claims (upload_id, claimant_name, claimant_email, reason, status, filed_at) VALUES
+ (up1, 'John Smith Photography', 'john@smithphoto.com', 'Copyright infringement — claims to be the original photographer of these images', 'open', NOW() - INTERVAL '3 days'),
+ (up3, 'Getty Images Legal', 'legal@gettyimages.com', 'Stock photo mistakenly uploaded as personal childhood photo', 'resolved', NOW() - INTERVAL '7 days'),
+ (up5, 'Anonymous', NULL, 'Subject requests removal — disputes likeness consent was given', 'open', NOW() - INTERVAL '2 days');
+
+ -- Some approved uploads (historical)
+ FOR i IN 1..20 LOOP
+ INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_signature, likeness_signed_at, reviewed_at)
+ VALUES (
+ (ARRAY[u1, u2, u3, u5, u6])[1 + (i % 5)],
+ 'approved',
+ (ARRAY['parent', 'family', 'professional'])[1 + (i % 3)],
+ 'yes', TRUE, TRUE, TRUE, TRUE,
+ 'Demo User ' || i,
+ NOW() - (i || ' days')::INTERVAL,
+ NOW() - ((i - 1) || ' days')::INTERVAL
+ );
+ END LOOP;
+
+END $$;
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..41f6e79
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,14 @@
+module.exports = {
+ apps: [
+ {
+ name: 'iwascute',
+ cwd: '/root/Projects/iwascute',
+ script: 'node_modules/.bin/next',
+ args: 'start -p 7450',
+ env: {
+ NODE_ENV: 'production',
+ PORT: 7450,
+ },
+ },
+ ],
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..05e726d
--- /dev/null
+++ b/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;
diff --git a/next.config.ts b/next.config.ts
new file mode 100644
index 0000000..e9ffa30
--- /dev/null
+++ b/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+};
+
+export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e21fa3b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6828 @@
+{
+ "name": "iwascute",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "iwascute",
+ "version": "0.1.0",
+ "dependencies": {
+ "@tabler/icons-react": "^3.41.1",
+ "clsx": "^2.1.1",
+ "exifr": "^7.1.3",
+ "framer-motion": "^12.38.0",
+ "lucide-react": "^1.8.0",
+ "next": "16.2.3",
+ "pg": "^8.20.0",
+ "react": "19.2.4",
+ "react-dom": "19.2.4"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/pg": "^8.20.0",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "16.2.3",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ }
+ },
+ "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/@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/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-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-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/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/@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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "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-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/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.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/@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/@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/@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/@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/@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/@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.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/@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/@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/@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/@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/@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/@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.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/@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/@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/@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/@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/@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.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/@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/@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==",
+ "dev": true,
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz",
+ "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==",
+ "license": "MIT"
+ },
+ "node_modules/@next/eslint-plugin-next": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.3.tgz",
+ "integrity": "sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "3.3.1"
+ }
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz",
+ "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz",
+ "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz",
+ "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz",
+ "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz",
+ "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz",
+ "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz",
+ "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "16.2.3",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz",
+ "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==",
+ "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/@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/@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/@tabler/icons": {
+ "version": "3.41.1",
+ "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.41.1.tgz",
+ "integrity": "sha512-OaRnVbRmH2nHtFeg+RmMJ/7m2oBIF9XCJAUD5gQnMrpK9f05ydj8MZrAf3NZQqOXyxGN1UBL0D5IKLLEUfr74Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ }
+ },
+ "node_modules/@tabler/icons-react": {
+ "version": "3.41.1",
+ "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.41.1.tgz",
+ "integrity": "sha512-kUgweE+DJtAlMZVIns1FTDdcbpRVnkK7ZpUOXmoxy3JAF0rSHj0TcP4VHF14+gMJGnF+psH2Zt26BLT6owetBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tabler/icons": "3.41.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ },
+ "peerDependencies": {
+ "react": ">= 16"
+ }
+ },
+ "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/@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==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "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/node": {
+ "version": "20.19.39",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
+ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "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/@typescript-eslint/eslint-plugin": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
+ "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/type-utils": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
+ "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.1",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
+ "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
+ "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.58.1",
+ "@typescript-eslint/types": "^8.58.1",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
+ "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
+ "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
+ "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
+ "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
+ "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.58.1",
+ "@typescript-eslint/tsconfig-utils": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
+ "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/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
+ "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
+ "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.58.1",
+ "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/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-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/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-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/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/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.18",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz",
+ "integrity": "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "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/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "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.30001787",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+ "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/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/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/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "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/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/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-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/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==",
+ "devOptional": true,
+ "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/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.335",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
+ "integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==",
+ "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/es-abstract": {
+ "version": "1.24.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz",
+ "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "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"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/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.3",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.3.tgz",
+ "integrity": "sha512-Dnkrylzjof/Az7iNoIQJqD18zTxQZcngir19KJaiRsMnnjpQSVoa6aEg/1Q4hQC+cW90uTlgQYadwL1CYNwFWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@next/eslint-plugin-next": "16.2.3",
+ "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-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/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-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-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-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/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/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/exifr": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
+ "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==",
+ "license": "MIT"
+ },
+ "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/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/framer-motion": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz",
+ "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.38.0",
+ "motion-utils": "^12.36.0",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "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-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-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/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/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/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/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-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-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-bun-module/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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-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-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/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/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/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/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/lucide-react": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.8.0.tgz",
+ "integrity": "sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "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/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/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/motion-dom": {
+ "version": "12.38.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz",
+ "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.36.0"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.36.0",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
+ "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
+ "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.3",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz",
+ "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "16.2.3",
+ "@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.3",
+ "@next/swc-darwin-x64": "16.2.3",
+ "@next/swc-linux-arm64-gnu": "16.2.3",
+ "@next/swc-linux-arm64-musl": "16.2.3",
+ "@next/swc-linux-x64-gnu": "16.2.3",
+ "@next/swc-linux-x64-musl": "16.2.3",
+ "@next/swc-win32-arm64-msvc": "16.2.3",
+ "@next/swc-win32-x64-msvc": "16.2.3",
+ "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/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/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-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/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/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/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/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/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.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==",
+ "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==",
+ "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/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "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": "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/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.9",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+ "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==",
+ "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==",
+ "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==",
+ "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==",
+ "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/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/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/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/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/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/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/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/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": "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/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.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/sharp/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "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/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.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "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/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/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "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/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.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-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/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/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/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/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/tinyglobby/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/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/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/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.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz",
+ "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.58.1",
+ "@typescript-eslint/parser": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1"
+ },
+ "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/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/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/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/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "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/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": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "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"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6e282df
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "iwascute",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint"
+ },
+ "dependencies": {
+ "@tabler/icons-react": "^3.41.1",
+ "clsx": "^2.1.1",
+ "exifr": "^7.1.3",
+ "framer-motion": "^12.38.0",
+ "lucide-react": "^1.8.0",
+ "next": "16.2.3",
+ "pg": "^8.20.0",
+ "react": "19.2.4",
+ "react-dom": "19.2.4"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/pg": "^8.20.0",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "16.2.3",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ }
+}
diff --git a/postcss.config.mjs b/postcss.config.mjs
new file mode 100644
index 0000000..61e3684
--- /dev/null
+++ b/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+
+export default config;
diff --git a/public/admin.html b/public/admin.html
new file mode 100644
index 0000000..74f8992
--- /dev/null
+++ b/public/admin.html
@@ -0,0 +1,246 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Admin — I was Cute</title>
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+:root{--bg:#1a1714;--bg2:#221f1a;--bg3:#2a2620;--cream:#f5ede0;--amber:#c4763a;--amber2:#d4894e;--rose:#d4827a;--green:#34d399;--text:#f5ede0;--text2:#b8ad9e;--text3:#8a7f72;--sans:'DM Sans',sans-serif;--serif:'Playfair Display',serif;--radius:10px}
+body{font-family:var(--sans);background:var(--bg);color:var(--text);min-height:100vh}
+
+.admin-header{padding:20px 32px;border-bottom:1px solid rgba(245,237,224,.08);display:flex;align-items:center;justify-content:space-between}
+.admin-header h1{font-family:var(--serif);font-size:22px}
+.admin-header h1 em{color:var(--amber2);font-style:italic}
+.admin-header .badge{font-size:12px;background:var(--amber);color:#fff;padding:4px 12px;border-radius:99px;font-weight:600}
+
+.stats-bar{display:flex;gap:16px;padding:20px 32px;flex-wrap:wrap}
+.stat-card{background:var(--bg2);border:1px solid rgba(245,237,224,.06);border-radius:var(--radius);padding:16px 20px;flex:1;min-width:140px}
+.stat-card .stat-num{font-size:28px;font-weight:700;font-family:var(--serif)}
+.stat-card .stat-label{font-size:12px;color:var(--text3);margin-top:2px;text-transform:uppercase;letter-spacing:.08em}
+.stat-card.pending .stat-num{color:var(--amber2)}
+.stat-card.approved .stat-num{color:var(--green)}
+.stat-card.rejected .stat-num{color:var(--rose)}
+
+.queue-header{padding:20px 32px 12px;display:flex;align-items:center;justify-content:space-between}
+.queue-header h2{font-size:16px;font-weight:600}
+.queue-header .filter-pills{display:flex;gap:6px}
+.queue-header .pill{background:var(--bg3);border:1px solid rgba(245,237,224,.08);color:var(--text2);padding:6px 14px;border-radius:99px;font-size:12px;cursor:pointer;font-family:var(--sans);border:none}
+.queue-header .pill.active{background:var(--amber);color:#fff}
+
+.queue{padding:0 32px 32px}
+.queue-item{display:flex;gap:16px;padding:16px;background:var(--bg2);border:1px solid rgba(245,237,224,.06);border-radius:var(--radius);margin-bottom:10px;align-items:center;transition:border-color .2s}
+.queue-item:hover{border-color:var(--amber)}
+.queue-item .qi-img{width:80px;height:80px;border-radius:6px;object-fit:cover;background:var(--bg3);flex-shrink:0}
+.queue-item .qi-info{flex:1;min-width:0}
+.queue-item .qi-title{font-weight:600;font-size:14px;margin-bottom:2px}
+.queue-item .qi-meta{font-size:12px;color:var(--text3)}
+.queue-item .qi-meta span{margin-right:12px}
+.queue-item .qi-desc{font-size:12px;color:var(--text2);margin-top:4px;font-style:italic;line-height:1.4}
+.queue-item .qi-actions{display:flex;gap:8px;flex-shrink:0}
+.qi-btn{padding:8px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:none;font-family:var(--sans);transition:all .2s}
+.qi-btn.approve{background:var(--green);color:#000}
+.qi-btn.approve:hover{filter:brightness(1.1);transform:translateY(-1px)}
+.qi-btn.reject{background:var(--rose);color:#fff}
+.qi-btn.reject:hover{filter:brightness(1.1);transform:translateY(-1px)}
+.qi-btn.view{background:var(--bg3);color:var(--text2);border:1px solid rgba(245,237,224,.1)}
+.qi-btn.view:hover{border-color:var(--amber)}
+
+.qi-status{padding:4px 10px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em}
+.qi-status.pending{background:rgba(196,118,58,.15);color:var(--amber2)}
+.qi-status.approved{background:rgba(52,211,153,.15);color:var(--green)}
+.qi-status.rejected{background:rgba(212,130,122,.15);color:var(--rose)}
+
+.empty-queue{text-align:center;padding:60px 20px;color:var(--text3);font-size:14px}
+.empty-queue .eq-icon{font-size:48px;margin-bottom:12px;opacity:.4}
+
+/* Demo banner */
+.demo-banner{background:linear-gradient(135deg,rgba(196,118,58,.1),rgba(212,130,122,.1));border:1px solid rgba(196,118,58,.2);border-radius:var(--radius);padding:16px 20px;margin:0 32px 0;font-size:13px;color:var(--amber2);display:flex;align-items:center;gap:12px}
+.demo-banner strong{color:var(--cream)}
+</style>
+</head>
+<body>
+
+<div class="admin-header">
+ <h1>I was <em>Cute</em> — Admin</h1>
+ <span class="badge">Admin Panel</span>
+</div>
+
+<div class="stats-bar" id="stats-bar">
+ <div class="stat-card"><div class="stat-num" id="stat-total">-</div><div class="stat-label">Total Submissions</div></div>
+ <div class="stat-card pending"><div class="stat-num" id="stat-pending">-</div><div class="stat-label">Pending Review</div></div>
+ <div class="stat-card approved"><div class="stat-num" id="stat-approved">-</div><div class="stat-label">Approved</div></div>
+ <div class="stat-card rejected"><div class="stat-num" id="stat-rejected">-</div><div class="stat-label">Rejected</div></div>
+</div>
+
+<div class="demo-banner" id="demo-banner" style="margin-top:16px">
+ <span style="font-size:20px">💡</span>
+ <span><strong>Demo mode</strong> — Submit photos from the main app to see them appear here. <a href="/" style="color:var(--amber);text-decoration:underline">Go to main app</a></span>
+</div>
+
+<div class="queue-header">
+ <h2>Submission Queue</h2>
+ <div class="filter-pills">
+ <button class="pill active" data-filter="all">All</button>
+ <button class="pill" data-filter="pending">Pending</button>
+ <button class="pill" data-filter="approved">Approved</button>
+ <button class="pill" data-filter="rejected">Rejected</button>
+ </div>
+</div>
+
+<div class="queue" id="queue"></div>
+
+<script>
+var AUTH = 'Basic ' + btoa('admin:DWSecure2024!');
+var currentFilter = 'all';
+
+document.querySelectorAll('.pill').forEach(function(p) {
+ p.addEventListener('click', function() {
+ document.querySelectorAll('.pill').forEach(function(x) { x.classList.remove('active'); });
+ p.classList.add('active');
+ currentFilter = p.dataset.filter;
+ renderQueue();
+ });
+});
+
+var allSubmissions = [];
+
+function loadSubmissions() {
+ fetch('/api/admin/submissions', { headers: { Authorization: AUTH } })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ allSubmissions = data.submissions || [];
+ updateStats();
+ renderQueue();
+ if (allSubmissions.length > 0) document.getElementById('demo-banner').style.display = 'none';
+ })
+ .catch(function(e) { console.error('Load error:', e); });
+}
+
+function updateStats() {
+ var total = allSubmissions.length;
+ var pending = allSubmissions.filter(function(s) { return s.status === 'pending'; }).length;
+ var approved = allSubmissions.filter(function(s) { return s.status === 'approved'; }).length;
+ var rejected = allSubmissions.filter(function(s) { return s.status === 'rejected'; }).length;
+ document.getElementById('stat-total').textContent = total;
+ document.getElementById('stat-pending').textContent = pending;
+ document.getElementById('stat-approved').textContent = approved;
+ document.getElementById('stat-rejected').textContent = rejected;
+}
+
+function renderQueue() {
+ var queue = document.getElementById('queue');
+ queue.textContent = '';
+ var filtered = currentFilter === 'all' ? allSubmissions : allSubmissions.filter(function(s) { return s.status === currentFilter; });
+
+ if (filtered.length === 0) {
+ var empty = document.createElement('div');
+ empty.className = 'empty-queue';
+ var icon = document.createElement('div');
+ icon.className = 'eq-icon';
+ icon.textContent = '\uD83D\uDCED';
+ empty.appendChild(icon);
+ var msg = document.createElement('p');
+ msg.textContent = currentFilter === 'all' ? 'No submissions yet. Photos submitted from the main app will appear here.' : 'No ' + currentFilter + ' submissions.';
+ empty.appendChild(msg);
+ queue.appendChild(empty);
+ return;
+ }
+
+ filtered.forEach(function(sub) {
+ var item = document.createElement('div');
+ item.className = 'queue-item';
+
+ // Thumbnail placeholder
+ var img = document.createElement('div');
+ img.className = 'qi-img';
+ img.style.background = 'linear-gradient(135deg, var(--amber), var(--bg3))';
+ img.style.display = 'flex';
+ img.style.alignItems = 'center';
+ img.style.justifyContent = 'center';
+ img.style.fontSize = '24px';
+ img.textContent = '\uD83D\uDDBC';
+ item.appendChild(img);
+
+ // Info
+ var info = document.createElement('div');
+ info.className = 'qi-info';
+
+ var titleRow = document.createElement('div');
+ titleRow.style.cssText = 'display:flex;align-items:center;gap:8px';
+ var title = document.createElement('span');
+ title.className = 'qi-title';
+ title.textContent = sub.title || 'Untitled Photo';
+ titleRow.appendChild(title);
+ var status = document.createElement('span');
+ status.className = 'qi-status ' + sub.status;
+ status.textContent = sub.status;
+ titleRow.appendChild(status);
+ info.appendChild(titleRow);
+
+ var meta = document.createElement('div');
+ meta.className = 'qi-meta';
+ var catSpan = document.createElement('span');
+ catSpan.textContent = sub.category || 'Uncategorized';
+ meta.appendChild(catSpan);
+ var yearSpan = document.createElement('span');
+ yearSpan.textContent = sub.year ? 'Year: ' + sub.year : '';
+ meta.appendChild(yearSpan);
+ var ageSpan = document.createElement('span');
+ ageSpan.textContent = sub.age ? 'Age: ' + sub.age : '';
+ meta.appendChild(ageSpan);
+ var dateSpan = document.createElement('span');
+ dateSpan.textContent = new Date(sub.submittedAt).toLocaleString();
+ meta.appendChild(dateSpan);
+ info.appendChild(meta);
+
+ if (sub.description) {
+ var desc = document.createElement('div');
+ desc.className = 'qi-desc';
+ desc.textContent = '"' + sub.description.substring(0, 120) + (sub.description.length > 120 ? '...' : '') + '"';
+ info.appendChild(desc);
+ }
+
+ item.appendChild(info);
+
+ // Actions
+ var actions = document.createElement('div');
+ actions.className = 'qi-actions';
+
+ if (sub.status === 'pending') {
+ var approveBtn = document.createElement('button');
+ approveBtn.className = 'qi-btn approve';
+ approveBtn.textContent = 'Approve';
+ approveBtn.addEventListener('click', function() { reviewAction(sub.id, 'approve'); });
+ actions.appendChild(approveBtn);
+
+ var rejectBtn = document.createElement('button');
+ rejectBtn.className = 'qi-btn reject';
+ rejectBtn.textContent = 'Reject';
+ rejectBtn.addEventListener('click', function() { reviewAction(sub.id, 'reject'); });
+ actions.appendChild(rejectBtn);
+ }
+
+ item.appendChild(actions);
+ queue.appendChild(item);
+ });
+}
+
+function reviewAction(id, action) {
+ fetch('/api/admin/review', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Authorization: AUTH },
+ body: JSON.stringify({ id: id, action: action })
+ })
+ .then(function(r) { return r.json(); })
+ .then(function() { loadSubmissions(); })
+ .catch(function(e) { console.error('Review error:', e); });
+}
+
+// Poll every 5s for new submissions
+loadSubmissions();
+setInterval(loadSubmissions, 5000);
+</script>
+</body>
+</html>
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..24b9870
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
+<rect width="32" height="32" rx="6" fill="#10b981"/>
+<text x="50%" y="55%" text-anchor="middle" dominant-baseline="middle" font-size="20" font-family="Apple Color Emoji, Segoe UI Emoji, sans-serif" fill="white">I</text>
+</svg>
\ No newline at end of file
diff --git a/public/file.svg b/public/file.svg
new file mode 100644
index 0000000..004145c
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/public/globe.svg b/public/globe.svg
new file mode 100644
index 0000000..567f17b
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..21cab56
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,827 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>I was Cute — Image Licensing + Then vs Now</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
+<style>
+*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
+:root{
+ --bg:#1a1714;--bg2:#221f1a;--bg3:#2a2620;
+ --cream:#f5ede0;--cream2:#e8ddd0;--cream3:#d4c9b8;
+ --amber:#c4763a;--amber2:#d4894e;--rose:#d4827a;--rose2:#e09a93;
+ --text:#f5ede0;--text2:#b8ad9e;--text3:#8a7f72;
+ --polaroid:#faf6ef;--shadow:rgba(0,0,0,.4);
+ --serif:'Playfair Display',Georgia,serif;
+ --sans:'DM Sans',-apple-system,sans-serif;
+ --radius:12px;--radius-sm:8px;
+}
+html{scroll-behavior:smooth}
+body{font-family:var(--sans);background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
+a{color:var(--amber2);text-decoration:none}
+::selection{background:var(--amber);color:#fff}
+
+/* ── Navigation ───────────────────────────────── */
+nav{position:fixed;top:0;left:0;right:0;z-index:100;padding:16px 32px;display:flex;align-items:center;justify-content:space-between;background:linear-gradient(180deg,var(--bg) 60%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
+nav .logo{font-family:var(--serif);font-size:22px;font-weight:700;cursor:pointer}
+nav .logo em{font-style:italic;color:var(--amber2)}
+nav .nav-links{display:flex;gap:28px;align-items:center}
+nav .nav-links a{color:var(--text2);font-size:14px;font-weight:500;transition:color .2s;letter-spacing:.02em}
+nav .nav-links a:hover{color:var(--cream)}
+nav .nav-links a.active{color:var(--amber2)}
+.nav-cta{background:var(--amber);color:#fff;padding:8px 20px;border-radius:99px;font-size:13px;font-weight:600;border:none;cursor:pointer;transition:all .2s}
+.nav-cta:hover{background:var(--amber2);transform:translateY(-1px)}
+
+/* ── Hero ─────────────────────────────────────── */
+.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:120px 24px 80px;position:relative;overflow:hidden}
+.hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 30% 40%,rgba(196,118,58,.08) 0%,transparent 60%),radial-gradient(ellipse at 70% 60%,rgba(212,130,122,.06) 0%,transparent 50%);pointer-events:none}
+.hero::after{content:'';position:absolute;inset:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence baseFrequency='.7' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.03'/%3E%3C/svg%3E");opacity:.5;pointer-events:none}
+
+.hero h1{font-family:var(--serif);font-size:clamp(48px,8vw,96px);font-weight:700;line-height:1.05;margin-bottom:24px;position:relative}
+.hero h1 .line1{display:block;color:var(--cream3)}
+.hero h1 .line2{display:block;font-style:italic;color:var(--amber2)}
+.hero p{font-size:clamp(16px,2vw,20px);color:var(--text2);max-width:520px;line-height:1.6;margin-bottom:40px;position:relative}
+.hero-actions{display:flex;gap:16px;position:relative;flex-wrap:wrap;justify-content:center}
+.btn-primary{background:var(--amber);color:#fff;padding:14px 36px;border-radius:99px;font-size:15px;font-weight:600;border:none;cursor:pointer;transition:all .25s;font-family:var(--sans)}
+.btn-primary:hover{background:var(--amber2);transform:translateY(-2px);box-shadow:0 8px 32px rgba(196,118,58,.3)}
+.btn-secondary{background:transparent;color:var(--cream);padding:14px 36px;border-radius:99px;font-size:15px;font-weight:600;border:1px solid var(--cream3);cursor:pointer;transition:all .25s;font-family:var(--sans)}
+.btn-secondary:hover{border-color:var(--amber);color:var(--amber2);transform:translateY(-2px)}
+
+/* ── Floating Polaroids (hero decoration) ─────── */
+.floating-polaroids{position:absolute;inset:0;pointer-events:none;overflow:hidden}
+.fpol{position:absolute;background:var(--polaroid);padding:8px 8px 32px;box-shadow:0 8px 40px var(--shadow);border-radius:2px;transform:rotate(var(--rot));animation:floatPol 20s ease-in-out infinite;opacity:.15}
+.fpol .fimg{width:100%;aspect-ratio:1;background:var(--bg3);border-radius:1px}
+@keyframes floatPol{0%,100%{transform:rotate(var(--rot)) translateY(0)}50%{transform:rotate(var(--rot)) translateY(-20px)}}
+
+/* ── Sections ─────────────────────────────────── */
+section{padding:100px 32px;max-width:1200px;margin:0 auto}
+.section-label{font-size:12px;text-transform:uppercase;letter-spacing:.15em;color:var(--amber);font-weight:600;margin-bottom:12px}
+.section-title{font-family:var(--serif);font-size:clamp(32px,4vw,48px);font-weight:700;margin-bottom:16px;line-height:1.15}
+.section-desc{color:var(--text2);font-size:16px;max-width:500px;line-height:1.6;margin-bottom:48px}
+
+/* ── Browse Grid ──────────────────────────────── */
+.browse-bar{display:flex;gap:12px;margin-bottom:32px;flex-wrap:wrap;align-items:center}
+.browse-bar input{background:var(--bg3);border:1px solid rgba(245,237,224,.1);color:var(--text);padding:10px 16px;border-radius:99px;font-size:14px;flex:1;min-width:240px;font-family:var(--sans)}
+.browse-bar input::placeholder{color:var(--text3)}
+.browse-bar select{background:var(--bg3);border:1px solid rgba(245,237,224,.1);color:var(--text2);padding:10px 16px;border-radius:99px;font-size:13px;font-family:var(--sans);cursor:pointer}
+.browse-bar .filter-pill{background:var(--bg3);border:1px solid rgba(245,237,224,.08);color:var(--text2);padding:8px 16px;border-radius:99px;font-size:12px;cursor:pointer;transition:all .2s;font-family:var(--sans)}
+.browse-bar .filter-pill:hover,.browse-bar .filter-pill.active{background:var(--amber);color:#fff;border-color:var(--amber)}
+
+.image-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:20px}
+.image-card{position:relative;background:var(--polaroid);padding:10px 10px 44px;border-radius:3px;cursor:pointer;transition:all .3s;box-shadow:0 4px 20px rgba(0,0,0,.2)}
+.image-card:hover{transform:translateY(-4px) rotate(-1deg);box-shadow:0 12px 40px rgba(0,0,0,.3)}
+.image-card .card-img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:1px;background:var(--bg3)}
+.image-card .card-meta{position:absolute;bottom:12px;left:14px;right:14px;display:flex;justify-content:space-between;align-items:center}
+.image-card .card-title{font-size:11px;color:var(--bg);font-weight:500;font-family:var(--sans)}
+.image-card .card-price{font-size:11px;color:var(--amber);font-weight:700;font-family:var(--sans)}
+
+/* Polaroid develop animation */
+.image-card .card-img{animation:develop 1.2s ease-out forwards}
+@keyframes develop{0%{opacity:0;filter:sepia(1) brightness(1.3) contrast(.7)}40%{opacity:1;filter:sepia(.6) brightness(1.1) contrast(.85)}100%{filter:sepia(0) brightness(1) contrast(1)}}
+
+/* ── Then vs Now ──────────────────────────────── */
+.tvn-container{display:flex;gap:48px;align-items:flex-start;flex-wrap:wrap}
+.tvn-preview{flex:1;min-width:320px;position:relative;border-radius:var(--radius);overflow:hidden;aspect-ratio:3/2;background:var(--bg3)}
+.tvn-preview .tvn-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
+.tvn-preview .tvn-overlay{position:absolute;inset:0;width:50%;overflow:hidden;border-right:3px solid var(--amber)}
+.tvn-preview .tvn-overlay img{position:absolute;inset:0;width:200%;height:100%;object-fit:cover}
+.tvn-slider{position:absolute;top:0;bottom:0;left:50%;width:40px;margin-left:-20px;cursor:ew-resize;display:flex;align-items:center;justify-content:center;z-index:5}
+.tvn-slider .slider-handle{width:32px;height:32px;background:var(--amber);border-radius:50%;border:3px solid var(--polaroid);box-shadow:0 2px 12px var(--shadow);display:flex;align-items:center;justify-content:center}
+.tvn-slider .slider-handle::before{content:'\2194';color:#fff;font-size:14px}
+.tvn-label{position:absolute;bottom:12px;padding:4px 12px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;border-radius:4px;font-weight:600;letter-spacing:.05em;text-transform:uppercase}
+.tvn-label.then-label{left:12px}
+.tvn-label.now-label{right:12px}
+
+.tvn-sidebar{flex:0 0 320px;display:flex;flex-direction:column;gap:16px}
+.tvn-upload-box{border:2px dashed rgba(245,237,224,.15);border-radius:var(--radius);padding:32px;text-align:center;cursor:pointer;transition:all .25s}
+.tvn-upload-box:hover{border-color:var(--amber);background:rgba(196,118,58,.05)}
+.tvn-upload-box .upload-icon{font-size:36px;margin-bottom:8px;opacity:.5}
+.tvn-upload-box p{font-size:13px;color:var(--text3)}
+.tvn-upload-box p strong{color:var(--amber2)}
+
+/* ── Upload Page ──────────────────────────────── */
+.upload-zone{border:2px dashed rgba(245,237,224,.12);border-radius:var(--radius);padding:64px 32px;text-align:center;transition:all .3s;cursor:pointer}
+.upload-zone:hover{border-color:var(--amber);background:rgba(196,118,58,.04)}
+.upload-zone .uz-icon{font-size:64px;opacity:.3;margin-bottom:16px}
+.upload-zone h3{font-family:var(--serif);font-size:24px;margin-bottom:8px}
+.upload-zone p{color:var(--text3);font-size:14px}
+.upload-form{margin-top:32px;display:grid;grid-template-columns:1fr 1fr;gap:16px}
+.upload-form .form-group{display:flex;flex-direction:column;gap:6px}
+.upload-form .form-group.full{grid-column:1/-1}
+.upload-form label{font-size:12px;color:var(--text3);font-weight:500;text-transform:uppercase;letter-spacing:.08em}
+.upload-form input,.upload-form textarea,.upload-form select{background:var(--bg3);border:1px solid rgba(245,237,224,.08);color:var(--text);padding:12px 16px;border-radius:var(--radius-sm);font-family:var(--sans);font-size:14px}
+.upload-form textarea{min-height:80px;resize:vertical}
+
+/* ── License Modal ────────────────────────────── */
+.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(8px);z-index:200;display:none;align-items:center;justify-content:center}
+.modal-overlay.open{display:flex}
+.modal{background:var(--bg2);border:1px solid rgba(245,237,224,.08);border-radius:16px;padding:32px;max-width:520px;width:90%;max-height:90vh;overflow-y:auto}
+.modal h2{font-family:var(--serif);font-size:24px;margin-bottom:4px}
+.modal .modal-sub{color:var(--text3);font-size:13px;margin-bottom:24px}
+.license-options{display:flex;flex-direction:column;gap:12px;margin-bottom:24px}
+.license-opt{border:1px solid rgba(245,237,224,.1);border-radius:var(--radius-sm);padding:16px;cursor:pointer;transition:all .2s;display:flex;justify-content:space-between;align-items:center}
+.license-opt:hover{border-color:var(--amber)}
+.license-opt.selected{border-color:var(--amber);background:rgba(196,118,58,.08)}
+.license-opt .lo-name{font-weight:600;font-size:14px}
+.license-opt .lo-desc{font-size:12px;color:var(--text3);margin-top:2px}
+.license-opt .lo-price{font-family:var(--serif);font-size:20px;font-weight:700;color:var(--amber2)}
+.modal-close{position:absolute;top:16px;right:16px;background:none;border:none;color:var(--text3);font-size:20px;cursor:pointer}
+
+/* ── Footer ───────────────────────────────────── */
+footer{border-top:1px solid rgba(245,237,224,.06);padding:48px 32px;text-align:center;color:var(--text3);font-size:13px}
+footer .footer-brand{font-family:var(--serif);font-size:18px;color:var(--cream3);margin-bottom:8px}
+
+/* ── Page transitions ─────────────────────────── */
+.page{display:none;opacity:0;transition:opacity .3s}
+.page.active{display:block;opacity:1}
+
+/* ── Photo Editor ─────────────────────────────── */
+.edit-panel{background:var(--bg3);border:1px solid rgba(245,237,224,.08);border-radius:var(--radius);padding:14px}
+.edit-panel-title{font-size:13px;font-weight:600;color:var(--cream);margin-bottom:10px}
+.slider-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
+.slider-row label{font-size:11px;color:var(--text3);min-width:70px;text-transform:uppercase;letter-spacing:.04em}
+.slider-row input[type=range]{flex:1;accent-color:var(--amber);height:4px;cursor:pointer}
+.slider-row span{font-size:11px;color:var(--amber2);min-width:32px;text-align:right;font-family:var(--sans)}
+.edit-btn{background:var(--bg2);border:1px solid rgba(245,237,224,.1);color:var(--cream);padding:8px 14px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;cursor:pointer;transition:all .2s;font-family:var(--sans)}
+.edit-btn:hover{border-color:var(--amber);color:var(--amber2)}
+.edit-btn.full{width:100%;margin-bottom:6px;text-align:center;padding:10px}
+.edit-btn.active{background:var(--amber);color:#fff;border-color:var(--amber)}
+
+/* ── Responsive ───────────────────────────────── */
+@media(max-width:768px){
+ nav{padding:12px 16px}
+ nav .nav-links{gap:16px}
+ section{padding:60px 16px}
+ .tvn-container{flex-direction:column}
+ .tvn-sidebar{flex:none;width:100%}
+ .upload-form{grid-template-columns:1fr}
+}
+
+/* ── Stagger animations ───────────────────────── */
+.stagger > *{opacity:0;transform:translateY(20px);animation:staggerIn .5s ease-out forwards}
+.stagger > *:nth-child(1){animation-delay:.1s}
+.stagger > *:nth-child(2){animation-delay:.2s}
+.stagger > *:nth-child(3){animation-delay:.25s}
+.stagger > *:nth-child(4){animation-delay:.3s}
+.stagger > *:nth-child(5){animation-delay:.35s}
+.stagger > *:nth-child(6){animation-delay:.4s}
+@keyframes staggerIn{to{opacity:1;transform:translateY(0)}}
+</style>
+<script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script>
+</head>
+<body>
+
+<!-- Navigation -->
+<nav>
+ <div class="logo" onclick="showPage('home')">I was <em>Cute</em></div>
+ <div class="nav-links">
+ <a href="#" onclick="showPage('home')" class="active" id="nav-home">Browse</a>
+ <a href="#" onclick="showPage('thenvsnow')" id="nav-tvn">Then vs Now</a>
+ <a href="#" onclick="showPage('upload')" id="nav-upload">Upload</a>
+ <button class="nav-cta" onclick="showPage('upload')">Sell Your Photos</button>
+ </div>
+</nav>
+
+<!-- ═══════════ HOME PAGE ═══════════ -->
+<div class="page active" id="page-home">
+
+ <!-- Hero -->
+ <section class="hero">
+ <div class="floating-polaroids">
+ <div class="fpol" style="width:120px;top:15%;left:8%;--rot:-12deg;animation-delay:-3s"><div class="fimg" style="background:linear-gradient(135deg,var(--rose),var(--amber))"></div></div>
+ <div class="fpol" style="width:100px;top:25%;right:10%;--rot:8deg;animation-delay:-8s"><div class="fimg" style="background:linear-gradient(135deg,var(--amber2),var(--cream3))"></div></div>
+ <div class="fpol" style="width:90px;bottom:20%;left:12%;--rot:15deg;animation-delay:-12s"><div class="fimg" style="background:linear-gradient(135deg,var(--cream3),var(--rose2))"></div></div>
+ <div class="fpol" style="width:110px;bottom:25%;right:15%;--rot:-6deg;animation-delay:-5s"><div class="fimg" style="background:linear-gradient(135deg,var(--bg3),var(--amber))"></div></div>
+ </div>
+ <div class="stagger">
+ <h1><span class="line1">I was</span><span class="line2">Cute</span></h1>
+ <p>License nostalgic, authentic, and beautifully imperfect images. Or create your own Then vs Now story.</p>
+ <div class="hero-actions">
+ <button class="btn-primary" onclick="showPage('home');document.getElementById('browse-section').scrollIntoView({behavior:'smooth'})">Browse Images</button>
+ <button class="btn-secondary" onclick="showPage('thenvsnow')">Make a Then vs Now</button>
+ </div>
+ </div>
+ </section>
+
+ <!-- Browse Section -->
+ <section id="browse-section">
+ <div class="section-label">Image Library</div>
+ <div class="section-title">Find the perfect shot</div>
+ <div class="section-desc">Thousands of authentic, nostalgic photos available for licensing. Every image tells a story.</div>
+
+ <div class="browse-bar">
+ <input type="text" placeholder="Search photos... try 'childhood', '90s', 'family'" id="search-input">
+ <select>
+ <option>All Categories</option>
+ <option>Childhood</option>
+ <option>Family</option>
+ <option>Vintage</option>
+ <option>Pets</option>
+ <option>School</option>
+ <option>Holidays</option>
+ </select>
+ <div class="filter-pill active">All</div>
+ <div class="filter-pill">Editorial</div>
+ <div class="filter-pill">Commercial</div>
+ <div class="filter-pill">Personal</div>
+ </div>
+
+ <div class="image-grid stagger" id="image-grid"></div>
+ </section>
+</div>
+
+<!-- ═══════════ THEN VS NOW PAGE ═══════════ -->
+<div class="page" id="page-thenvsnow">
+ <section style="padding-top:120px">
+ <div class="section-label">Create</div>
+ <div class="section-title">Then vs Now</div>
+ <div class="section-desc">Upload an old photo and a new one. Drag the slider to reveal the passage of time.</div>
+
+ <div class="tvn-container">
+ <div class="tvn-preview" id="tvn-preview">
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:14px">Upload two photos to begin</div>
+ </div>
+ <div class="tvn-sidebar">
+ <div class="tvn-upload-box" id="then-upload">
+ <div class="upload-icon">📷</div>
+ <p><strong>Then</strong> — Upload your old photo</p>
+ </div>
+ <div class="tvn-upload-box" id="now-upload">
+ <div class="upload-icon">📸</div>
+ <p><strong>Now</strong> — Upload your current photo</p>
+ </div>
+ <button class="btn-primary" style="width:100%;margin-top:8px" disabled id="tvn-create-btn">Create Comparison</button>
+ <p style="font-size:12px;color:var(--text3);text-align:center">Share your story or license the result</p>
+ </div>
+ </div>
+ </section>
+</div>
+
+<!-- ═══════════ UPLOAD PAGE ═══════════ -->
+<div class="page" id="page-upload">
+ <section style="padding-top:120px">
+ <div class="section-label">Upload</div>
+ <div class="section-title">Share Your Memory</div>
+ <div class="section-desc">Upload a photo, edit it, and let AI guess your story.</div>
+
+ <!-- Step 1: Upload Zone (visible initially) -->
+ <div id="upload-step1">
+ <div class="upload-zone" id="upload-zone">
+ <div class="uz-icon">🖼</div>
+ <h3>Drop your photo here</h3>
+ <p>or click to browse. JPG, PNG up to 50MB.</p>
+ </div>
+ </div>
+
+ <!-- Step 2: Photo Editor (hidden until upload) -->
+ <div id="upload-step2" style="display:none">
+ <div style="display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start">
+
+ <!-- Photo preview with canvas -->
+ <div style="flex:1;min-width:320px">
+ <div style="position:relative;background:var(--bg3);border-radius:var(--radius);overflow:hidden;border:1px solid rgba(245,237,224,.1)">
+ <canvas id="photo-canvas" style="width:100%;display:block;cursor:crosshair"></canvas>
+ </div>
+ <!-- Crop controls -->
+ <div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
+ <button class="edit-btn" id="btn-crop-free" title="Free crop">✂ Crop</button>
+ <button class="edit-btn" id="btn-crop-square" title="Square crop">⬜ Square</button>
+ <button class="edit-btn" id="btn-crop-4x3" title="4:3 crop">▭ 4:3</button>
+ <button class="edit-btn" id="btn-reset" title="Reset all edits">↺ Reset</button>
+ </div>
+ </div>
+
+ <!-- Controls sidebar -->
+ <div style="flex:0 0 300px;display:flex;flex-direction:column;gap:16px">
+
+ <!-- Color adjustments -->
+ <div class="edit-panel">
+ <div class="edit-panel-title">🎨 Adjustments</div>
+ <div class="slider-row">
+ <label>Brightness</label>
+ <input type="range" id="sl-brightness" min="-100" max="100" value="0">
+ <span id="val-brightness">0</span>
+ </div>
+ <div class="slider-row">
+ <label>Contrast</label>
+ <input type="range" id="sl-contrast" min="-100" max="100" value="0">
+ <span id="val-contrast">0</span>
+ </div>
+ <div class="slider-row">
+ <label>Saturation</label>
+ <input type="range" id="sl-saturation" min="-100" max="100" value="0">
+ <span id="val-saturation">0</span>
+ </div>
+ <div class="slider-row">
+ <label>Hue Rotate</label>
+ <input type="range" id="sl-hue" min="0" max="360" value="0">
+ <span id="val-hue">0°</span>
+ </div>
+ <div class="slider-row">
+ <label>Exposure</label>
+ <input type="range" id="sl-exposure" min="-100" max="100" value="0">
+ <span id="val-exposure">0</span>
+ </div>
+ <div class="slider-row">
+ <label>Sepia (vintage)</label>
+ <input type="range" id="sl-sepia" min="0" max="100" value="0">
+ <span id="val-sepia">0%</span>
+ </div>
+ </div>
+
+ <!-- AI magic -->
+ <div class="edit-panel">
+ <div class="edit-panel-title">🤖 AI Magic</div>
+ <button class="edit-btn full" id="btn-ai-describe">✨ Generate AI Description</button>
+ <button class="edit-btn full" id="btn-ai-guess-year">📅 Guess the Year</button>
+ <div id="ai-output" style="font-size:13px;color:var(--cream3);line-height:1.5;min-height:20px;margin-top:4px"></div>
+ </div>
+
+ <!-- Age calculator -->
+ <div class="edit-panel">
+ <div class="edit-panel-title">👶 How old were you?</div>
+ <div class="slider-row">
+ <label>Born in</label>
+ <input type="number" id="inp-born" placeholder="1990" min="1900" max="2026" style="width:80px;background:var(--bg3);border:1px solid rgba(245,237,224,.1);color:var(--text);padding:6px 8px;border-radius:6px;font-size:14px;text-align:center">
+ </div>
+ <div class="slider-row">
+ <label>Photo taken</label>
+ <input type="number" id="inp-year" placeholder="2005" min="1900" max="2026" style="width:80px;background:var(--bg3);border:1px solid rgba(245,237,224,.1);color:var(--text);padding:6px 8px;border-radius:6px;font-size:14px;text-align:center">
+ </div>
+ <div id="age-result" style="font-size:15px;color:var(--amber2);font-weight:600;margin-top:4px;min-height:22px"></div>
+ </div>
+
+ <!-- Details form -->
+ <div class="edit-panel">
+ <div class="edit-panel-title">📝 Details</div>
+ <div class="slider-row" style="flex-direction:column;align-items:stretch">
+ <label>Title</label>
+ <input type="text" id="inp-title" placeholder="e.g. Summer of '92" style="background:var(--bg3);border:1px solid rgba(245,237,224,.1);color:var(--text);padding:8px 12px;border-radius:6px;font-size:14px;width:100%">
+ </div>
+ <div class="slider-row" style="flex-direction:column;align-items:stretch">
+ <label>AI Description</label>
+ <textarea id="inp-desc" rows="3" placeholder="AI will fill this in..." style="background:var(--bg3);border:1px solid rgba(245,237,224,.1);color:var(--text);padding:8px 12px;border-radius:6px;font-size:13px;width:100%;resize:vertical;font-family:var(--sans)"></textarea>
+ </div>
+ <div class="slider-row">
+ <label>Category</label>
+ <select id="inp-cat" style="background:var(--bg3);border:1px solid rgba(245,237,224,.1);color:var(--text);padding:6px 10px;border-radius:6px;font-size:13px">
+ <option>Childhood</option><option>Family</option><option>School</option><option>Pets</option><option>Holidays</option><option>Vintage</option>
+ </select>
+ </div>
+ </div>
+
+ <button class="btn-primary" style="width:100%" id="btn-submit-photo">Submit for Review</button>
+ </div>
+ </div>
+ </div>
+ </section>
+</div>
+
+<!-- License Modal -->
+<div class="modal-overlay" id="license-modal">
+ <div class="modal">
+ <h2>License This Image</h2>
+ <div class="modal-sub" id="modal-image-title">Summer of '92 — Childhood</div>
+ <div class="license-options">
+ <div class="license-opt selected" onclick="selectLicense(this)">
+ <div><div class="lo-name">Personal Use</div><div class="lo-desc">Blog, social media, personal projects</div></div>
+ <div class="lo-price">$9</div>
+ </div>
+ <div class="license-opt" onclick="selectLicense(this)">
+ <div><div class="lo-name">Editorial</div><div class="lo-desc">News, magazines, educational content</div></div>
+ <div class="lo-price">$29</div>
+ </div>
+ <div class="license-opt" onclick="selectLicense(this)">
+ <div><div class="lo-name">Commercial</div><div class="lo-desc">Advertising, products, merchandise</div></div>
+ <div class="lo-price">$99</div>
+ </div>
+ <div class="license-opt" onclick="selectLicense(this)">
+ <div><div class="lo-name">Extended / Exclusive</div><div class="lo-desc">Unlimited use, exclusivity period</div></div>
+ <div class="lo-price">$299</div>
+ </div>
+ </div>
+ <button class="btn-primary" style="width:100%">Purchase License</button>
+ <p style="text-align:center;margin-top:12px;font-size:12px;color:var(--text3);cursor:pointer" onclick="closeLicenseModal()">Cancel</p>
+ </div>
+</div>
+
+<footer>
+ <div class="footer-brand">I was <em style="font-style:italic">Cute</em></div>
+ <p>Authentic nostalgia, beautifully licensed.</p>
+</footer>
+
+<script>
+// ── Page Router ────────────────────────────────
+function showPage(page) {
+ document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
+ document.querySelectorAll('.nav-links a').forEach(function(a) { a.classList.remove('active'); });
+ var target = document.getElementById('page-' + page);
+ if (target) { target.classList.add('active'); window.scrollTo(0, 0); }
+ var navLink = document.getElementById('nav-' + (page === 'thenvsnow' ? 'tvn' : page));
+ if (navLink) navLink.classList.add('active');
+}
+
+// ── Demo Image Data ────────────────────────────
+var demoImages = [
+ { title: "Summer of '92", cat: "Childhood", year: "1992", price: 29, color: "var(--amber)" },
+ { title: "First Day of School", cat: "School", year: "1988", price: 19, color: "var(--rose)" },
+ { title: "Grandma's Kitchen", cat: "Family", year: "1975", price: 39, color: "var(--cream3)" },
+ { title: "The Old Treehouse", cat: "Childhood", year: "1995", price: 29, color: "var(--amber2)" },
+ { title: "Christmas Morning", cat: "Holidays", year: "1983", price: 49, color: "var(--rose2)" },
+ { title: "Beach Day", cat: "Family", year: "1990", price: 19, color: "var(--amber)" },
+ { title: "Prom Night", cat: "School", year: "1998", price: 29, color: "var(--rose)" },
+ { title: "Road Trip '85", cat: "Family", year: "1985", price: 39, color: "var(--cream3)" },
+ { title: "Birthday Party", cat: "Childhood", year: "1991", price: 19, color: "var(--amber2)" },
+ { title: "Snow Day", cat: "Holidays", year: "1987", price: 29, color: "var(--rose2)" },
+ { title: "First Bike", cat: "Childhood", year: "1993", price: 39, color: "var(--amber)" },
+ { title: "Family Portrait", cat: "Family", year: "1979", price: 49, color: "var(--cream3)" }
+];
+
+// ── Render Image Grid ──────────────────────────
+function renderGrid() {
+ var grid = document.getElementById('image-grid');
+ grid.textContent = '';
+ demoImages.forEach(function(img, i) {
+ var card = document.createElement('div');
+ card.className = 'image-card';
+ card.style.animationDelay = (i * .06) + 's';
+ card.onclick = function() { openLicenseModal(img); };
+
+ var imgDiv = document.createElement('div');
+ imgDiv.className = 'card-img';
+ imgDiv.style.background = 'linear-gradient(135deg, ' + img.color + ', var(--bg3))';
+ card.appendChild(imgDiv);
+
+ var meta = document.createElement('div');
+ meta.className = 'card-meta';
+
+ var titleSpan = document.createElement('span');
+ titleSpan.className = 'card-title';
+ titleSpan.textContent = img.title + ' \u00b7 ' + img.year;
+ meta.appendChild(titleSpan);
+
+ var priceSpan = document.createElement('span');
+ priceSpan.className = 'card-price';
+ priceSpan.textContent = 'from $' + img.price;
+ meta.appendChild(priceSpan);
+
+ card.appendChild(meta);
+ grid.appendChild(card);
+ });
+}
+renderGrid();
+
+// ── License Modal ──────────────────────────────
+function openLicenseModal(img) {
+ document.getElementById('modal-image-title').textContent = img.title + ' \u2014 ' + img.cat;
+ document.getElementById('license-modal').classList.add('open');
+}
+function closeLicenseModal() {
+ document.getElementById('license-modal').classList.remove('open');
+}
+function selectLicense(el) {
+ document.querySelectorAll('.license-opt').forEach(function(o) { o.classList.remove('selected'); });
+ el.classList.add('selected');
+}
+document.getElementById('license-modal').addEventListener('click', function(e) {
+ if (e.target === this) closeLicenseModal();
+});
+
+// ── Filter pills ───────────────────────────────
+document.querySelectorAll('.filter-pill').forEach(function(pill) {
+ pill.addEventListener('click', function() {
+ document.querySelectorAll('.filter-pill').forEach(function(p) { p.classList.remove('active'); });
+ this.classList.add('active');
+ });
+});
+
+// ── Hidden file inputs ─────────────────────────
+function createFileInput(accept, cb) {
+ var inp = document.createElement('input');
+ inp.type = 'file';
+ inp.accept = accept || 'image/*';
+ inp.style.display = 'none';
+ inp.onchange = function() { if (inp.files[0]) cb(inp.files[0]); };
+ document.body.appendChild(inp);
+ return inp;
+}
+
+// ── Then vs Now ────────────────────────────────
+var thenImg = null, nowImg = null;
+
+document.getElementById('then-upload').addEventListener('click', function() {
+ var inp = createFileInput('image/*', function(file) {
+ thenImg = URL.createObjectURL(file);
+ document.getElementById('then-upload').querySelector('p').textContent = file.name;
+ document.getElementById('then-upload').style.borderColor = 'var(--amber)';
+ if (thenImg && nowImg) enableTvn();
+ });
+ inp.click();
+});
+
+document.getElementById('now-upload').addEventListener('click', function() {
+ var inp = createFileInput('image/*', function(file) {
+ nowImg = URL.createObjectURL(file);
+ document.getElementById('now-upload').querySelector('p').textContent = file.name;
+ document.getElementById('now-upload').style.borderColor = 'var(--amber)';
+ if (thenImg && nowImg) enableTvn();
+ });
+ inp.click();
+});
+
+function enableTvn() {
+ document.getElementById('tvn-create-btn').disabled = false;
+ document.getElementById('tvn-create-btn').onclick = buildTvnSlider;
+}
+
+function buildTvnSlider() {
+ var preview = document.getElementById('tvn-preview');
+ preview.textContent = '';
+ preview.style.cursor = 'default';
+
+ // Now image (full background)
+ var nowEl = document.createElement('img');
+ nowEl.className = 'tvn-img';
+ nowEl.src = nowImg;
+ preview.appendChild(nowEl);
+
+ // Then overlay (clipped left side)
+ var overlay = document.createElement('div');
+ overlay.className = 'tvn-overlay';
+ overlay.id = 'tvn-overlay';
+ var thenEl = document.createElement('img');
+ thenEl.src = thenImg;
+ thenEl.style.cssText = 'position:absolute;top:0;left:0;width:200%;height:100%;object-fit:cover';
+ overlay.appendChild(thenEl);
+ preview.appendChild(overlay);
+
+ // Slider handle
+ var slider = document.createElement('div');
+ slider.className = 'tvn-slider';
+ slider.id = 'tvn-slider';
+ var handle = document.createElement('div');
+ handle.className = 'slider-handle';
+ slider.appendChild(handle);
+ preview.appendChild(slider);
+
+ // Labels
+ var thenLabel = document.createElement('div');
+ thenLabel.className = 'tvn-label then-label';
+ thenLabel.textContent = 'THEN';
+ preview.appendChild(thenLabel);
+ var nowLabel = document.createElement('div');
+ nowLabel.className = 'tvn-label now-label';
+ nowLabel.textContent = 'NOW';
+ preview.appendChild(nowLabel);
+
+ // Drag logic
+ var dragging = false;
+ slider.addEventListener('mousedown', function(e) { dragging = true; e.preventDefault(); });
+ slider.addEventListener('touchstart', function(e) { dragging = true; });
+ window.addEventListener('mouseup', function() { dragging = false; });
+ window.addEventListener('touchend', function() { dragging = false; });
+
+ function moveSlider(clientX) {
+ var rect = preview.getBoundingClientRect();
+ var pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
+ overlay.style.width = (pct * 100) + '%';
+ thenEl.style.width = (100 / pct) + '%';
+ slider.style.left = (pct * 100) + '%';
+ }
+
+ preview.addEventListener('mousemove', function(e) { if (dragging) moveSlider(e.clientX); });
+ preview.addEventListener('touchmove', function(e) { if (dragging) moveSlider(e.touches[0].clientX); });
+ preview.addEventListener('click', function(e) { moveSlider(e.clientX); });
+
+ document.getElementById('tvn-create-btn').textContent = 'Share / Download';
+ document.getElementById('tvn-create-btn').onclick = function() {
+ alert('Sharing and download coming soon!');
+ };
+}
+
+// ── Upload zone drag & drop ────────────────────
+var uploadZone = document.getElementById('upload-zone');
+if (uploadZone) {
+ uploadZone.addEventListener('click', function() {
+ var inp = createFileInput('image/*,.tiff', function(file) {
+ uploadZone.querySelector('h3').textContent = file.name;
+ uploadZone.querySelector('p').textContent = (file.size / 1024 / 1024).toFixed(1) + ' MB — Ready to submit';
+ uploadZone.style.borderColor = 'var(--amber)';
+ uploadZone.style.background = 'rgba(196,118,58,.06)';
+ });
+ inp.click();
+ });
+
+ uploadZone.addEventListener('dragover', function(e) {
+ e.preventDefault();
+ uploadZone.style.borderColor = 'var(--amber)';
+ uploadZone.style.background = 'rgba(196,118,58,.08)';
+ });
+
+ uploadZone.addEventListener('dragleave', function() {
+ uploadZone.style.borderColor = 'rgba(245,237,224,.12)';
+ uploadZone.style.background = 'transparent';
+ });
+
+ uploadZone.addEventListener('drop', function(e) {
+ e.preventDefault();
+ var file = e.dataTransfer.files[0];
+ if (file) {
+ uploadZone.querySelector('h3').textContent = file.name;
+ uploadZone.querySelector('p').textContent = (file.size / 1024 / 1024).toFixed(1) + ' MB — Ready to submit';
+ uploadZone.style.borderColor = 'var(--amber)';
+ uploadZone.style.background = 'rgba(196,118,58,.06)';
+ }
+ });
+}
+
+// ── Search filter ──────────────────────────────
+document.getElementById('search-input').addEventListener('input', function() {
+ var q = this.value.toLowerCase();
+ var cards = document.querySelectorAll('.image-card');
+ cards.forEach(function(card) {
+ var title = card.querySelector('.card-title').textContent.toLowerCase();
+ card.style.display = title.indexOf(q) >= 0 ? '' : 'none';
+ });
+});
+
+// ── Purchase button ────────────────────────────
+document.querySelector('.modal .btn-primary').addEventListener('click', function() {
+ this.textContent = 'Processing...';
+ this.disabled = true;
+ var btn = this;
+ setTimeout(function() {
+ btn.textContent = 'Licensed!';
+ btn.style.background = 'var(--rose)';
+ setTimeout(function() { closeLicenseModal(); btn.textContent = 'Purchase License'; btn.disabled = false; btn.style.background = ''; }, 1500);
+ }, 1200);
+});
+
+// ═══════════ PHOTO EDITOR ═════════════════════
+var photoCanvas = document.getElementById('photo-canvas');
+var photoCtx = photoCanvas ? photoCanvas.getContext('2d') : null;
+var originalImage = null;
+var uploadZoneEl = document.getElementById('upload-zone');
+if (uploadZoneEl) {
+ function handleFile(file) {
+ if (!file || !file.type.startsWith('image/')) return;
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var img = new Image();
+ img.onload = function() {
+ originalImage = img;
+ photoCanvas.width = img.width;
+ photoCanvas.height = img.height;
+ photoCtx.drawImage(img, 0, 0);
+ document.getElementById('upload-step1').style.display = 'none';
+ document.getElementById('upload-step2').style.display = 'block';
+ applyFilters();
+ };
+ img.src = e.target.result;
+ };
+ reader.readAsDataURL(file);
+ }
+ uploadZoneEl.addEventListener('click', function() {
+ var inp = document.createElement('input'); inp.type='file'; inp.accept='image/*'; inp.style.display='none';
+ inp.onchange = function() { if (inp.files[0]) handleFile(inp.files[0]); };
+ document.body.appendChild(inp); inp.click();
+ });
+ uploadZoneEl.addEventListener('dragover', function(e) { e.preventDefault(); uploadZoneEl.style.borderColor='var(--amber)'; });
+ uploadZoneEl.addEventListener('dragleave', function() { uploadZoneEl.style.borderColor='rgba(245,237,224,.12)'; });
+ uploadZoneEl.addEventListener('drop', function(e) { e.preventDefault(); if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); });
+}
+
+var sliderNames = ['brightness','contrast','saturation','hue','exposure','sepia'];
+sliderNames.forEach(function(name) {
+ var sl = document.getElementById('sl-' + name);
+ if (sl) sl.addEventListener('input', function() {
+ var val = document.getElementById('val-' + name);
+ if (name === 'hue') val.textContent = sl.value + '\u00b0';
+ else if (name === 'sepia') val.textContent = sl.value + '%';
+ else val.textContent = sl.value;
+ applyFilters();
+ });
+});
+
+function applyFilters() {
+ if (!originalImage || !photoCtx) return;
+ var b = parseInt(document.getElementById('sl-brightness').value);
+ var c = parseInt(document.getElementById('sl-contrast').value);
+ var s = parseInt(document.getElementById('sl-saturation').value);
+ var h = parseInt(document.getElementById('sl-hue').value);
+ var ex = parseInt(document.getElementById('sl-exposure').value);
+ var sep = parseInt(document.getElementById('sl-sepia').value);
+ photoCtx.clearRect(0, 0, photoCanvas.width, photoCanvas.height);
+ photoCtx.filter = 'brightness('+(1+(b+ex)/100)+') contrast('+(1+c/100)+') saturate('+(1+s/100)+') hue-rotate('+h+'deg) sepia('+sep/100+')';
+ photoCtx.drawImage(originalImage, 0, 0);
+ photoCtx.filter = 'none';
+}
+
+var btnReset = document.getElementById('btn-reset');
+if (btnReset) btnReset.addEventListener('click', function() {
+ sliderNames.forEach(function(n) {
+ var sl = document.getElementById('sl-'+n); var val = document.getElementById('val-'+n);
+ sl.value = 0; val.textContent = n==='hue'?'0\u00b0':n==='sepia'?'0%':'0';
+ }); applyFilters();
+});
+
+function applyCrop(ratio) {
+ if (!originalImage) return;
+ var w=originalImage.width, h=originalImage.height, cw, ch;
+ if (ratio==='square') { cw=ch=Math.min(w,h); }
+ else if (ratio==='4x3') { cw=Math.min(w,h*4/3); ch=cw*3/4; }
+ else return;
+ var tc=document.createElement('canvas'); tc.width=cw; tc.height=ch;
+ tc.getContext('2d').drawImage(originalImage,(w-cw)/2,(h-ch)/2,cw,ch,0,0,cw,ch);
+ var ci=new Image(); ci.onload=function(){ originalImage=ci; photoCanvas.width=cw; photoCanvas.height=ch; applyFilters(); };
+ ci.src=tc.toDataURL();
+}
+var bs=document.getElementById('btn-crop-square'); if(bs) bs.addEventListener('click',function(){applyCrop('square')});
+var b43=document.getElementById('btn-crop-4x3'); if(b43) b43.addEventListener('click',function(){applyCrop('4x3')});
+
+// Age Calculator
+var inpBorn=document.getElementById('inp-born'), inpYear=document.getElementById('inp-year'), ageResult=document.getElementById('age-result');
+function calcAge() {
+ var born=parseInt(inpBorn.value), year=parseInt(inpYear.value);
+ if (born && year && year>=born) {
+ var age=year-born;
+ ageResult.textContent = (age<=5?'\uD83D\uDC76':age<=12?'\uD83D\uDC66':age<=18?'\uD83E\uDDD2':age<=30?'\uD83D\uDC68':'\uD83E\uDDD3') + ' You were ' + age + ' years old in ' + year + '!';
+ } else { ageResult.textContent=''; }
+}
+if(inpBorn) inpBorn.addEventListener('input',calcAge);
+if(inpYear) inpYear.addEventListener('input',calcAge);
+
+// AI Description Generator
+var aiDescs = [
+ 'A sun-drenched afternoon frozen in time. The colors have faded but the joy hasn\'t.',
+ 'This photo smells like fresh-cut grass and screen doors slamming. Pure unfiltered nostalgia.',
+ 'The photographer had zero formal training but captured something a professional never could.',
+ 'Somewhere between the awkward haircut and questionable fashion lies genuine happiness.',
+ 'This image radiates "we didn\'t have smartphones so we actually talked to each other" energy.',
+ 'A snapshot that proves life was better when we didn\'t know what we looked like 24/7.',
+ 'The composition is accidental, the lighting is terrible, and that makes it perfect.',
+ 'Future historians will study this photo to understand what "vibes" meant.',
+ 'WARNING: Looking at this image may cause sudden waves of nostalgia and urges to call your mom.',
+];
+var yearGuesses = [
+ { range: 'early 1970s', detail: 'The warm orange tint screams Kodachrome. Bell-bottoms probably just out of frame.' },
+ { range: 'late 1970s', detail: 'That sun-faded yellow only existed between 1976 and 1982. Science is clear on this.' },
+ { range: 'mid 1980s', detail: 'Hair was BIG. Probably shot on a disposable camera from a drugstore.' },
+ { range: 'early 1990s', detail: 'Distinctly post-Cold-War optimism. Grunge hadn\'t ruined everything yet.' },
+ { range: 'late 1990s', detail: 'Strong "Y2K was coming and nobody cared" energy.' },
+ { range: 'early 2000s', detail: 'Early point-and-shoot digital camera. Resolution terrible. Memories priceless.' },
+];
+var titles = ['Summer Vibes','The Good Old Days','Before Smartphones','Pure Joy','Throwback Gold','Vintage Moment','Time Capsule','Memory Lane','Golden Hour','Blurry But Beautiful'];
+
+var btnAiDesc=document.getElementById('btn-ai-describe');
+if(btnAiDesc) btnAiDesc.addEventListener('click',function(){
+ var out=document.getElementById('ai-output'); out.textContent='Analyzing image...'; btnAiDesc.disabled=true;
+ setTimeout(function(){
+ var d=aiDescs[Math.floor(Math.random()*aiDescs.length)];
+ out.textContent='\u2728 "'+d+'"';
+ var da=document.getElementById('inp-desc'); if(da) da.value=d;
+ var ti=document.getElementById('inp-title'); if(ti&&!ti.value) ti.value=titles[Math.floor(Math.random()*titles.length)];
+ btnAiDesc.disabled=false;
+ },1500);
+});
+
+var btnGuess=document.getElementById('btn-ai-guess-year');
+if(btnGuess) btnGuess.addEventListener('click',function(){
+ var out=document.getElementById('ai-output'); out.textContent='Examining film grain, color temperature, fashion clues...'; btnGuess.disabled=true;
+ setTimeout(function(){
+ var g=yearGuesses[Math.floor(Math.random()*yearGuesses.length)];
+ out.textContent='\uD83D\uDCC5 AI Guess: '+g.range+' \u2014 '+g.detail;
+ btnGuess.disabled=false;
+ var yi=document.getElementById('inp-year');
+ if(yi&&!yi.value){ var m=g.range.match(/(\d{4})/); if(m){yi.value=m[1]; calcAge();} }
+ },2000);
+});
+
+var btnSubmit=document.getElementById('btn-submit-photo');
+if(btnSubmit) btnSubmit.addEventListener('click',function(){
+ btnSubmit.textContent='Submitting...'; btnSubmit.disabled=true;
+ var born=document.getElementById('inp-born').value;
+ var year=document.getElementById('inp-year').value;
+ var payload = {
+ title: document.getElementById('inp-title').value || 'Untitled',
+ description: document.getElementById('inp-desc').value || '',
+ category: document.getElementById('inp-cat').value || 'Childhood',
+ year: year || null,
+ born: born || null,
+ age: (born && year && parseInt(year) >= parseInt(born)) ? (parseInt(year) - parseInt(born)) : null
+ };
+ fetch('/api/submit', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) })
+ .then(function(r){return r.json()})
+ .then(function(d){
+ btnSubmit.textContent='Submitted for Review!'; btnSubmit.style.background='var(--rose)';
+ setTimeout(function(){ btnSubmit.textContent='Submit for Review'; btnSubmit.disabled=false; btnSubmit.style.background=''; },2500);
+ })
+ .catch(function(){
+ btnSubmit.textContent='Error — try again'; btnSubmit.disabled=false;
+ });
+});
+</script>
+</body>
+</html>
diff --git a/public/next.svg b/public/next.svg
new file mode 100644
index 0000000..5174b28
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/public/vercel.svg b/public/vercel.svg
new file mode 100644
index 0000000..7705396
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/public/window.svg b/public/window.svg
new file mode 100644
index 0000000..b2b2a44
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..6d02890
--- /dev/null
+++ b/server.js
@@ -0,0 +1,57 @@
+const express = require('express');
+const path = require('path');
+const app = express();
+const PORT = 7350;
+
+// Basic auth for admin
+function adminAuth(req, res, next) {
+ const auth = req.headers.authorization;
+ if (!auth || auth !== 'Basic ' + Buffer.from('admin:DWSecure2024!').toString('base64')) {
+ res.set('WWW-Authenticate', 'Basic realm="IWasCute Admin"');
+ return res.status(401).send('Auth required');
+ }
+ next();
+}
+
+app.use(express.json());
+
+// In-memory submissions store (would be DB in production)
+const submissions = [];
+
+// API: submit a photo
+app.post('/api/submit', (req, res) => {
+ const sub = { id: Date.now(), ...req.body, status: 'pending', submittedAt: new Date().toISOString() };
+ submissions.push(sub);
+ res.json({ ok: true, id: sub.id });
+});
+
+// API: list submissions (admin)
+app.get('/api/admin/submissions', adminAuth, (req, res) => {
+ res.json({ submissions: submissions.slice().reverse() });
+});
+
+// API: approve/reject (admin)
+app.post('/api/admin/review', adminAuth, (req, res) => {
+ const { id, action } = req.body;
+ const sub = submissions.find(s => s.id === id);
+ if (!sub) return res.status(404).json({ error: 'Not found' });
+ sub.status = action === 'approve' ? 'approved' : 'rejected';
+ sub.reviewedAt = new Date().toISOString();
+ res.json({ ok: true, status: sub.status });
+});
+
+// Admin dashboard
+app.get('/admin', adminAuth, (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'admin.html'));
+});
+
+app.use(express.static(path.join(__dirname, 'public')));
+
+// SPA fallback
+app.use((req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});
+
+app.listen(PORT, '0.0.0.0', () => {
+ console.log(`[IWasCute] Running on http://45.61.58.125:${PORT}`);
+});
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
new file mode 100644
index 0000000..7a746d8
--- /dev/null
+++ b/src/app/admin/page.tsx
@@ -0,0 +1,1371 @@
+'use client'
+
+import { useState, useEffect, useCallback } from 'react'
+import { motion } from 'framer-motion'
+import {
+ Shield,
+ CheckCircle2,
+ XCircle,
+ Clock,
+ AlertTriangle,
+ Users,
+ ImageIcon,
+ DollarSign,
+ Search,
+ Eye,
+ Scale,
+ FileWarning,
+ Ban,
+ BarChart3,
+ Settings,
+ LogOut,
+ Loader2,
+ Flag,
+ RefreshCw,
+} from 'lucide-react'
+import Link from 'next/link'
+
+/* ------------------------------------------------------------------ */
+/* Types */
+/* ------------------------------------------------------------------ */
+
+type AdminTab = 'overview' | 'queue' | 'dmca' | 'users'
+
+interface AuthUser {
+ id: string
+ email: string
+ display_name: string
+ role: string
+}
+
+interface Stats {
+ pending: number
+ approved: number
+ rejected: number
+ users: number
+ revenue: number
+ dmca_open: number
+}
+
+interface QueueItem {
+ id: string
+ status: string
+ created_at: string
+ copyright_relationship: string
+ copyright_permission: string
+ display_name: string
+ email: string
+ photo_count: number
+ photos: { id: string; filename: string; exif_year: number | null }[] | null
+}
+
+interface DMCAClaim {
+ id: string
+ claimant_name: string
+ claimant_email: string
+ reason: string
+ status: string
+ filed_at: string
+ upload_uuid: string | null
+ upload_user: string | null
+}
+
+interface UserRow {
+ id: string
+ email: string
+ display_name: string
+ role: string
+ is_verified: boolean
+ created_at: string
+ upload_count: number
+ photo_count: number
+ total_earnings: number
+ approved_uploads: number
+}
+
+interface OverviewData {
+ stats: Stats
+ recent_queue: QueueItem[]
+ recent_dmca: DMCAClaim[]
+}
+
+/* ------------------------------------------------------------------ */
+/* Helpers */
+/* ------------------------------------------------------------------ */
+
+function timeAgo(dateStr: string): string {
+ const now = new Date()
+ const date = new Date(dateStr)
+ const diffMs = now.getTime() - date.getTime()
+ const diffMin = Math.floor(diffMs / 60000)
+ if (diffMin < 1) return 'just now'
+ if (diffMin < 60) return `${diffMin}m ago`
+ const diffHours = Math.floor(diffMin / 60)
+ if (diffHours < 24) return `${diffHours}h ago`
+ const diffDays = Math.floor(diffHours / 24)
+ if (diffDays < 30) return `${diffDays}d ago`
+ const diffMonths = Math.floor(diffDays / 30)
+ return `${diffMonths}mo ago`
+}
+
+function formatDate(dateStr: string): string {
+ return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
+}
+
+function formatCurrency(amount: number): string {
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
+}
+
+/* ------------------------------------------------------------------ */
+/* Alert Banner */
+/* ------------------------------------------------------------------ */
+
+interface AlertState {
+ type: 'success' | 'error'
+ message: string
+}
+
+function AlertBanner({ alert, onDismiss }: { alert: AlertState; onDismiss: () => void }) {
+ useEffect(() => {
+ const timer = setTimeout(onDismiss, 4000)
+ return () => clearTimeout(timer)
+ }, [onDismiss])
+
+ return (
+ <div
+ className="fixed top-4 right-4 z-50 px-5 py-3 rounded-xl shadow-lg flex items-center gap-3 text-sm font-semibold"
+ style={{
+ background: alert.type === 'success' ? '#8BBCAA' : '#F4846F',
+ color: 'white',
+ }}
+ >
+ {alert.type === 'success' ? <CheckCircle2 size={16} /> : <XCircle size={16} />}
+ {alert.message}
+ <button onClick={onDismiss} className="ml-2 opacity-70 hover:opacity-100">
+ <XCircle size={14} />
+ </button>
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* StatusBadge */
+/* ------------------------------------------------------------------ */
+
+function StatusBadge({ status }: { status: string }) {
+ const config: Record<string, { bg: string; color: string }> = {
+ pending: { bg: 'rgba(255, 181, 167, 0.2)', color: '#F4846F' },
+ flagged: { bg: 'rgba(244, 132, 111, 0.25)', color: '#F4846F' },
+ in_review: { bg: 'rgba(255, 224, 203, 0.4)', color: '#6B705C' },
+ approved: { bg: 'rgba(139, 188, 170, 0.25)', color: '#8BBCAA' },
+ rejected: { bg: 'rgba(107, 112, 92, 0.2)', color: '#6B705C' },
+ open: { bg: 'rgba(244, 132, 111, 0.2)', color: '#F4846F' },
+ investigating: { bg: 'rgba(255, 224, 203, 0.4)', color: '#6B705C' },
+ taken_down: { bg: 'rgba(107, 112, 92, 0.2)', color: '#6B705C' },
+ counter_noticed: { bg: 'rgba(139, 188, 170, 0.25)', color: '#8BBCAA' },
+ dismissed: { bg: 'rgba(139, 188, 170, 0.25)', color: '#8BBCAA' },
+ resolved: { bg: 'rgba(139, 188, 170, 0.25)', color: '#8BBCAA' },
+ active: { bg: 'rgba(139, 188, 170, 0.25)', color: '#8BBCAA' },
+ suspended: { bg: 'rgba(244, 132, 111, 0.2)', color: '#F4846F' },
+ dmca_removed: { bg: 'rgba(107, 112, 92, 0.2)', color: '#6B705C' },
+ }
+ const c = config[status] ?? config.pending
+ return (
+ <span
+ className="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider whitespace-nowrap"
+ style={{ background: c.bg, color: c.color }}
+ >
+ {status.replace(/_/g, ' ')}
+ </span>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* Loading Spinner */
+/* ------------------------------------------------------------------ */
+
+function LoadingPane({ text }: { text?: string }) {
+ return (
+ <div className="flex flex-col items-center justify-center py-20 gap-3">
+ <Loader2 size={28} className="animate-spin" style={{ color: '#F4846F' }} />
+ <p className="text-xs font-semibold" style={{ color: '#6B705C' }}>{text || 'Loading...'}</p>
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* Empty State */
+/* ------------------------------------------------------------------ */
+
+function EmptyState({ icon: Icon, message }: { icon: typeof Shield; message: string }) {
+ return (
+ <div className="flex flex-col items-center justify-center py-16 gap-3">
+ <Icon size={32} style={{ color: '#B5D5C5' }} />
+ <p className="text-sm font-semibold" style={{ color: '#6B705C' }}>{message}</p>
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* Login Form */
+/* ------------------------------------------------------------------ */
+
+function LoginForm({ onLogin }: { onLogin: (user: AuthUser) => void }) {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [error, setError] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError('')
+ setLoading(true)
+
+ try {
+ const res = await fetch('/api/auth', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action: 'login', email, password }),
+ })
+ const data = await res.json()
+
+ if (!res.ok) {
+ setError(data.error || 'Login failed')
+ setLoading(false)
+ return
+ }
+
+ if (data.user?.role !== 'admin') {
+ setError('Admin access required')
+ setLoading(false)
+ return
+ }
+
+ onLogin(data.user)
+ } catch {
+ setError('Network error. Please try again.')
+ setLoading(false)
+ }
+ }
+
+ return (
+ <div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--color-cream)' }}>
+ <div
+ className="w-full max-w-sm rounded-3xl p-8 shadow-xl"
+ style={{ background: 'rgba(255,255,255,0.7)', border: '1px solid rgba(255,224,203,0.5)' }}
+ >
+ <div className="flex items-center gap-2 mb-6 justify-center">
+ <Shield size={22} style={{ color: '#F4846F' }} />
+ <span className="text-lg font-black tracking-tighter" style={{ color: '#3D405B' }}>
+ i was cute
+ </span>
+ <span className="text-[9px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(255,181,167,0.2)', color: '#FFB5A7' }}>
+ ADMIN
+ </span>
+ </div>
+
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
+ <div>
+ <label className="block text-[10px] font-bold uppercase tracking-wider mb-1.5" style={{ color: '#6B705C' }}>
+ Email
+ </label>
+ <input
+ type="email"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ className="w-full px-4 py-3 rounded-xl text-sm border outline-none"
+ style={{ background: 'rgba(255,255,255,0.8)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ placeholder="admin@iwascute.com"
+ required
+ />
+ </div>
+ <div>
+ <label className="block text-[10px] font-bold uppercase tracking-wider mb-1.5" style={{ color: '#6B705C' }}>
+ Password
+ </label>
+ <input
+ type="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ className="w-full px-4 py-3 rounded-xl text-sm border outline-none"
+ style={{ background: 'rgba(255,255,255,0.8)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ placeholder="Enter password"
+ required
+ />
+ </div>
+
+ {error && (
+ <p className="text-xs font-semibold px-3 py-2 rounded-lg" style={{ background: 'rgba(244,132,111,0.15)', color: '#F4846F' }}>
+ {error}
+ </p>
+ )}
+
+ <motion.button
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ type="submit"
+ disabled={loading}
+ className="w-full py-3 rounded-xl text-sm font-bold flex items-center justify-center gap-2"
+ style={{ background: '#F4846F', color: 'white', opacity: loading ? 0.7 : 1 }}
+ >
+ {loading ? <Loader2 size={16} className="animate-spin" /> : <Shield size={16} />}
+ {loading ? 'Signing in...' : 'Sign In'}
+ </motion.button>
+ </form>
+ </div>
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* Overview Tab */
+/* ------------------------------------------------------------------ */
+
+function OverviewTab({
+ data,
+ loading,
+ onSwitchTab,
+}: {
+ data: OverviewData | null
+ loading: boolean
+ onSwitchTab: (tab: AdminTab) => void
+}) {
+ if (loading || !data) return <LoadingPane text="Loading overview..." />
+
+ const { stats, recent_queue, recent_dmca } = data
+
+ const statCards = [
+ { label: 'Pending Review', value: String(stats.pending), icon: Clock, color: '#F4846F' },
+ { label: 'Approved', value: String(stats.approved), icon: CheckCircle2, color: '#8BBCAA' },
+ { label: 'Rejected', value: String(stats.rejected), icon: XCircle, color: '#6B705C' },
+ { label: 'Total Users', value: String(stats.users), icon: Users, color: '#FFB5A7' },
+ { label: 'Revenue', value: formatCurrency(stats.revenue), icon: DollarSign, color: '#F4846F' },
+ { label: 'DMCA Claims', value: String(stats.dmca_open), icon: AlertTriangle, color: '#FFB5A7' },
+ ]
+
+ return (
+ <div className="flex flex-col gap-6">
+ {/* Stats */}
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
+ {statCards.map((s) => {
+ const Icon = s.icon
+ return (
+ <div
+ key={s.label}
+ className="p-4 rounded-2xl"
+ style={{ background: 'rgba(255,255,255,0.5)', border: '1px solid rgba(255,224,203,0.5)' }}
+ >
+ <div className="flex items-center gap-2 mb-2">
+ <Icon size={14} style={{ color: s.color }} />
+ <span className="text-[10px] font-semibold uppercase tracking-wider" style={{ color: '#6B705C' }}>
+ {s.label}
+ </span>
+ </div>
+ <p className="text-2xl font-black" style={{ color: '#3D405B' }}>{s.value}</p>
+ </div>
+ )
+ })}
+ </div>
+
+ {/* Quick actions */}
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ {/* Recent queue */}
+ <div
+ className="rounded-2xl p-5"
+ style={{ background: 'rgba(255,255,255,0.5)', border: '1px solid rgba(255,224,203,0.5)' }}
+ >
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-sm font-bold" style={{ color: '#3D405B' }}>Review Queue</h3>
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={() => onSwitchTab('queue')}
+ className="text-xs font-semibold px-2 py-1 rounded-full cursor-pointer"
+ style={{ background: 'rgba(244,132,111,0.15)', color: '#F4846F' }}
+ >
+ {stats.pending} pending
+ </motion.button>
+ </div>
+ {recent_queue.length === 0 ? (
+ <p className="text-xs py-4 text-center" style={{ color: '#6B705C' }}>No pending uploads</p>
+ ) : (
+ recent_queue.slice(0, 5).map((item) => (
+ <div
+ key={item.id}
+ className="flex items-center justify-between py-3 border-b last:border-b-0"
+ style={{ borderColor: 'rgba(255,224,203,0.4)' }}
+ >
+ <div className="flex items-center gap-3">
+ <div className="w-8 h-8 rounded-lg flex items-center justify-center" style={{ background: 'rgba(255,181,167,0.2)' }}>
+ <ImageIcon size={14} style={{ color: '#F4846F' }} />
+ </div>
+ <div>
+ <p className="text-xs font-semibold" style={{ color: '#3D405B' }}>{item.display_name || item.email}</p>
+ <p className="text-[10px]" style={{ color: '#6B705C' }}>
+ {item.photo_count} photo{Number(item.photo_count) !== 1 ? 's' : ''} · {timeAgo(item.created_at)}
+ </p>
+ </div>
+ </div>
+ <StatusBadge status={item.status} />
+ </div>
+ ))
+ )}
+ </div>
+
+ {/* DMCA alerts */}
+ <div
+ className="rounded-2xl p-5"
+ style={{ background: 'rgba(255,255,255,0.5)', border: '1px solid rgba(255,224,203,0.5)' }}
+ >
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-sm font-bold" style={{ color: '#3D405B' }}>DMCA Claims</h3>
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={() => onSwitchTab('dmca')}
+ className="text-xs font-semibold px-2 py-1 rounded-full cursor-pointer"
+ style={{ background: 'rgba(244,132,111,0.15)', color: '#F4846F' }}
+ >
+ {stats.dmca_open} open
+ </motion.button>
+ </div>
+ {recent_dmca.length === 0 ? (
+ <p className="text-xs py-4 text-center" style={{ color: '#6B705C' }}>No DMCA claims</p>
+ ) : (
+ recent_dmca.slice(0, 5).map((claim) => (
+ <div
+ key={claim.id}
+ className="flex items-center justify-between py-3 border-b last:border-b-0"
+ style={{ borderColor: 'rgba(255,224,203,0.4)' }}
+ >
+ <div className="flex items-center gap-3">
+ <div
+ className="w-8 h-8 rounded-lg flex items-center justify-center"
+ style={{ background: claim.status === 'open' ? 'rgba(244,132,111,0.2)' : 'rgba(139,188,170,0.2)' }}
+ >
+ <FileWarning size={14} style={{ color: claim.status === 'open' ? '#F4846F' : '#8BBCAA' }} />
+ </div>
+ <div>
+ <p className="text-xs font-semibold" style={{ color: '#3D405B' }}>{claim.claimant_name}</p>
+ <p className="text-[10px]" style={{ color: '#6B705C' }}>{timeAgo(claim.filed_at)}</p>
+ </div>
+ </div>
+ <StatusBadge status={claim.status} />
+ </div>
+ ))
+ )}
+ </div>
+ </div>
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* Queue Tab */
+/* ------------------------------------------------------------------ */
+
+function QueueTab({
+ onAction,
+ actionLoading,
+}: {
+ onAction: (action: string, payload: Record<string, string>) => Promise<void>
+ actionLoading: string | null
+}) {
+ const [queue, setQueue] = useState<QueueItem[]>([])
+ const [loading, setLoading] = useState(true)
+ const [search, setSearch] = useState('')
+ const [statusFilter, setStatusFilter] = useState('all')
+ const [rejectId, setRejectId] = useState<string | null>(null)
+ const [rejectReason, setRejectReason] = useState('')
+ const [flagId, setFlagId] = useState<string | null>(null)
+ const [flagNotes, setFlagNotes] = useState('')
+
+ const fetchQueue = useCallback(async () => {
+ setLoading(true)
+ try {
+ const params = new URLSearchParams({ view: 'queue' })
+ if (statusFilter !== 'all') params.set('status', statusFilter)
+ if (search) params.set('search', search)
+ const res = await fetch(`/api/admin?${params}`)
+ if (res.ok) {
+ const data = await res.json()
+ setQueue(data.queue || [])
+ }
+ } catch {
+ // silently fail, user can retry
+ } finally {
+ setLoading(false)
+ }
+ }, [statusFilter, search])
+
+ useEffect(() => {
+ fetchQueue()
+ }, [fetchQueue])
+
+ const handleApprove = async (uploadId: string) => {
+ await onAction('approve_upload', { upload_id: uploadId })
+ fetchQueue()
+ }
+
+ const handleReject = async () => {
+ if (!rejectId) return
+ await onAction('reject_upload', { upload_id: rejectId, reason: rejectReason || 'Rights verification failed' })
+ setRejectId(null)
+ setRejectReason('')
+ fetchQueue()
+ }
+
+ const handleFlag = async () => {
+ if (!flagId) return
+ await onAction('flag_upload', { upload_id: flagId, notes: flagNotes })
+ setFlagId(null)
+ setFlagNotes('')
+ fetchQueue()
+ }
+
+ return (
+ <div className="flex flex-col gap-4">
+ {/* Search / Filter */}
+ <div className="flex items-center gap-3">
+ <div className="relative flex-1">
+ <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: '#6B705C' }} />
+ <input
+ type="text"
+ placeholder="Search by user or submission ID..."
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ className="w-full pl-9 pr-4 py-2.5 rounded-xl text-sm border outline-none"
+ style={{ background: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ />
+ </div>
+ <select
+ value={statusFilter}
+ onChange={(e) => setStatusFilter(e.target.value)}
+ className="px-3 py-2.5 rounded-xl text-xs font-semibold border"
+ style={{ background: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ >
+ <option value="all">All status</option>
+ <option value="pending">Pending</option>
+ <option value="flagged">Flagged</option>
+ <option value="in_review">In Review</option>
+ </select>
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={fetchQueue}
+ className="p-2.5 rounded-xl border"
+ style={{ background: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,224,203,0.5)', color: '#6B705C' }}
+ >
+ <RefreshCw size={14} />
+ </motion.button>
+ </div>
+
+ {loading ? (
+ <LoadingPane text="Loading review queue..." />
+ ) : queue.length === 0 ? (
+ <EmptyState icon={CheckCircle2} message="No uploads matching this filter" />
+ ) : (
+ queue.map((item) => {
+ const photoCount = Number(item.photo_count)
+ const isActing = actionLoading === item.id
+
+ return (
+ <div
+ key={item.id}
+ className="rounded-2xl p-5 flex flex-col md:flex-row md:items-center gap-4"
+ style={{
+ background: 'rgba(255,255,255,0.5)',
+ border: `1px solid ${item.status === 'flagged' ? 'rgba(244,132,111,0.4)' : 'rgba(255,224,203,0.5)'}`,
+ opacity: isActing ? 0.6 : 1,
+ }}
+ >
+ {/* Thumbnails placeholder */}
+ <div className="flex gap-1 shrink-0">
+ {Array.from({ length: Math.min(photoCount, 3) }).map((_, i) => (
+ <div
+ key={i}
+ className="w-12 h-12 rounded-lg"
+ style={{ background: ['#FFB5A7', '#B5D5C5', '#FFE0CB'][i % 3] }}
+ />
+ ))}
+ {photoCount > 3 && (
+ <div
+ className="w-12 h-12 rounded-lg flex items-center justify-center text-xs font-bold"
+ style={{ background: 'rgba(255,224,203,0.5)', color: '#6B705C' }}
+ >
+ +{photoCount - 3}
+ </div>
+ )}
+ {photoCount === 0 && (
+ <div
+ className="w-12 h-12 rounded-lg flex items-center justify-center"
+ style={{ background: 'rgba(255,224,203,0.3)' }}
+ >
+ <ImageIcon size={14} style={{ color: '#6B705C' }} />
+ </div>
+ )}
+ </div>
+
+ {/* Info */}
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center gap-2 flex-wrap">
+ <p className="text-sm font-bold" style={{ color: '#3D405B' }}>
+ {item.display_name || 'Anonymous'}
+ </p>
+ <StatusBadge status={item.status} />
+ {item.status === 'flagged' && <AlertTriangle size={12} style={{ color: '#F4846F' }} />}
+ </div>
+ <p className="text-xs mt-1" style={{ color: '#6B705C' }}>
+ {item.id.slice(0, 8)}... · {photoCount} photo{photoCount !== 1 ? 's' : ''} ·
+ Copyright: {item.copyright_relationship || 'n/a'} ·
+ Permission: {item.copyright_permission || 'n/a'} · {timeAgo(item.created_at)}
+ </p>
+ {item.email && (
+ <p className="text-[10px] mt-0.5" style={{ color: '#6B705C' }}>{item.email}</p>
+ )}
+ </div>
+
+ {/* Actions */}
+ <div className="flex items-center gap-2 shrink-0 flex-wrap">
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={() => handleApprove(item.id)}
+ disabled={isActing}
+ className="flex items-center gap-1 px-3 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: 'rgba(139,188,170,0.25)', color: '#8BBCAA' }}
+ >
+ {isActing ? <Loader2 size={12} className="animate-spin" /> : <CheckCircle2 size={12} />} Approve
+ </motion.button>
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={() => setRejectId(item.id)}
+ disabled={isActing}
+ className="flex items-center gap-1 px-3 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: 'rgba(244,132,111,0.15)', color: '#F4846F' }}
+ >
+ <XCircle size={12} /> Reject
+ </motion.button>
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={() => setFlagId(item.id)}
+ disabled={isActing}
+ className="flex items-center gap-1 px-3 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: 'rgba(255,255,255,0.6)', color: '#6B705C' }}
+ >
+ <Flag size={12} /> Flag
+ </motion.button>
+ </div>
+ </div>
+ )
+ })
+ )}
+
+ {/* Reject Modal */}
+ {rejectId && (
+ <div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4" onClick={() => setRejectId(null)}>
+ <div
+ className="w-full max-w-md rounded-2xl p-6 shadow-xl"
+ style={{ background: 'var(--color-cream)', border: '1px solid rgba(255,224,203,0.5)' }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <h3 className="text-sm font-bold mb-3" style={{ color: '#3D405B' }}>Reject Upload</h3>
+ <p className="text-xs mb-3" style={{ color: '#6B705C' }}>Upload: {rejectId.slice(0, 8)}...</p>
+ <textarea
+ value={rejectReason}
+ onChange={(e) => setRejectReason(e.target.value)}
+ placeholder="Reason for rejection (optional)..."
+ rows={3}
+ className="w-full px-3 py-2 rounded-xl text-sm border outline-none resize-none mb-4"
+ style={{ background: 'rgba(255,255,255,0.8)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ />
+ <div className="flex gap-2 justify-end">
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={() => setRejectId(null)}
+ className="px-4 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: 'rgba(255,255,255,0.6)', color: '#6B705C' }}
+ >
+ Cancel
+ </motion.button>
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={handleReject}
+ className="px-4 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: '#F4846F', color: 'white' }}
+ >
+ Confirm Reject
+ </motion.button>
+ </div>
+ </div>
+ </div>
+ )}
+
+ {/* Flag Modal */}
+ {flagId && (
+ <div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4" onClick={() => setFlagId(null)}>
+ <div
+ className="w-full max-w-md rounded-2xl p-6 shadow-xl"
+ style={{ background: 'var(--color-cream)', border: '1px solid rgba(255,224,203,0.5)' }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <h3 className="text-sm font-bold mb-3" style={{ color: '#3D405B' }}>Flag Upload for Review</h3>
+ <p className="text-xs mb-3" style={{ color: '#6B705C' }}>Upload: {flagId.slice(0, 8)}...</p>
+ <textarea
+ value={flagNotes}
+ onChange={(e) => setFlagNotes(e.target.value)}
+ placeholder="Notes about why this is flagged..."
+ rows={3}
+ className="w-full px-3 py-2 rounded-xl text-sm border outline-none resize-none mb-4"
+ style={{ background: 'rgba(255,255,255,0.8)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ />
+ <div className="flex gap-2 justify-end">
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={() => setFlagId(null)}
+ className="px-4 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: 'rgba(255,255,255,0.6)', color: '#6B705C' }}
+ >
+ Cancel
+ </motion.button>
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={handleFlag}
+ className="px-4 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: '#F4846F', color: 'white' }}
+ >
+ Confirm Flag
+ </motion.button>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* DMCA Tab */
+/* ------------------------------------------------------------------ */
+
+function DMCATab({
+ onAction,
+ actionLoading,
+}: {
+ onAction: (action: string, payload: Record<string, string>) => Promise<void>
+ actionLoading: string | null
+}) {
+ const [claims, setClaims] = useState<DMCAClaim[]>([])
+ const [loading, setLoading] = useState(true)
+ const [statusFilter, setStatusFilter] = useState('all')
+ const [counterModal, setCounterModal] = useState<string | null>(null)
+ const [counterNotes, setCounterNotes] = useState('')
+ const [dismissModal, setDismissModal] = useState<string | null>(null)
+ const [dismissNotes, setDismissNotes] = useState('')
+
+ const fetchClaims = useCallback(async () => {
+ setLoading(true)
+ try {
+ const params = new URLSearchParams({ view: 'dmca' })
+ if (statusFilter !== 'all') params.set('status', statusFilter)
+ const res = await fetch(`/api/admin?${params}`)
+ if (res.ok) {
+ const data = await res.json()
+ setClaims(data.claims || [])
+ }
+ } catch {
+ // silently fail
+ } finally {
+ setLoading(false)
+ }
+ }, [statusFilter])
+
+ useEffect(() => {
+ fetchClaims()
+ }, [fetchClaims])
+
+ const handleTakedown = async (claimId: string) => {
+ await onAction('dmca_takedown', { claim_id: claimId })
+ fetchClaims()
+ }
+
+ const handleCounter = async () => {
+ if (!counterModal) return
+ await onAction('dmca_counter', { claim_id: counterModal, notes: counterNotes })
+ setCounterModal(null)
+ setCounterNotes('')
+ fetchClaims()
+ }
+
+ const handleDismiss = async () => {
+ if (!dismissModal) return
+ await onAction('dmca_dismiss', { claim_id: dismissModal, notes: dismissNotes })
+ setDismissModal(null)
+ setDismissNotes('')
+ fetchClaims()
+ }
+
+ return (
+ <div className="flex flex-col gap-4">
+ {/* Filter */}
+ <div className="flex items-center gap-3">
+ <select
+ value={statusFilter}
+ onChange={(e) => setStatusFilter(e.target.value)}
+ className="px-3 py-2.5 rounded-xl text-xs font-semibold border"
+ style={{ background: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ >
+ <option value="all">All status</option>
+ <option value="open">Open</option>
+ <option value="investigating">Investigating</option>
+ <option value="taken_down">Taken Down</option>
+ <option value="counter_noticed">Counter Noticed</option>
+ <option value="dismissed">Dismissed</option>
+ </select>
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={fetchClaims}
+ className="p-2.5 rounded-xl border"
+ style={{ background: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,224,203,0.5)', color: '#6B705C' }}
+ >
+ <RefreshCw size={14} />
+ </motion.button>
+ </div>
+
+ {loading ? (
+ <LoadingPane text="Loading DMCA claims..." />
+ ) : claims.length === 0 ? (
+ <EmptyState icon={FileWarning} message="No DMCA claims matching this filter" />
+ ) : (
+ claims.map((claim) => {
+ const isOpen = claim.status === 'open' || claim.status === 'investigating'
+ const isActing = actionLoading === claim.id
+
+ return (
+ <div
+ key={claim.id}
+ className="rounded-2xl p-6"
+ style={{
+ background: 'rgba(255,255,255,0.5)',
+ border: `1px solid ${isOpen ? 'rgba(244,132,111,0.3)' : 'rgba(255,224,203,0.5)'}`,
+ opacity: isActing ? 0.6 : 1,
+ }}
+ >
+ <div className="flex items-center justify-between mb-3">
+ <div className="flex items-center gap-3">
+ <div
+ className="w-10 h-10 rounded-xl flex items-center justify-center"
+ style={{ background: isOpen ? 'rgba(244,132,111,0.15)' : 'rgba(139,188,170,0.15)' }}
+ >
+ <FileWarning size={18} style={{ color: isOpen ? '#F4846F' : '#8BBCAA' }} />
+ </div>
+ <div>
+ <p className="text-sm font-bold" style={{ color: '#3D405B' }}>DMCA-{claim.id.slice(0, 8)}</p>
+ <p className="text-xs" style={{ color: '#6B705C' }}>
+ Filed {timeAgo(claim.filed_at)}
+ {claim.upload_uuid && <> · Upload: {claim.upload_uuid.slice(0, 8)}...</>}
+ </p>
+ </div>
+ </div>
+ <StatusBadge status={claim.status} />
+ </div>
+ <p className="text-sm mb-2" style={{ color: '#6B705C' }}>
+ <strong style={{ color: '#3D405B' }}>Claimant:</strong> {claim.claimant_name}
+ {claim.claimant_email && <span className="ml-2 text-xs">({claim.claimant_email})</span>}
+ </p>
+ {claim.upload_user && (
+ <p className="text-sm mb-2" style={{ color: '#6B705C' }}>
+ <strong style={{ color: '#3D405B' }}>Upload by:</strong> {claim.upload_user}
+ </p>
+ )}
+ <p className="text-sm mb-4" style={{ color: '#6B705C' }}>
+ <strong style={{ color: '#3D405B' }}>Reason:</strong> {claim.reason}
+ </p>
+ {isOpen && (
+ <div className="flex gap-2 flex-wrap">
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={() => handleTakedown(claim.id)}
+ disabled={isActing}
+ className="px-4 py-2 rounded-xl text-xs font-semibold flex items-center gap-1"
+ style={{ background: 'rgba(244,132,111,0.15)', color: '#F4846F' }}
+ >
+ {isActing ? <Loader2 size={12} className="animate-spin" /> : <Ban size={12} />} Take Down
+ </motion.button>
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={() => setCounterModal(claim.id)}
+ disabled={isActing}
+ className="px-4 py-2 rounded-xl text-xs font-semibold flex items-center gap-1"
+ style={{ background: 'rgba(139,188,170,0.2)', color: '#8BBCAA' }}
+ >
+ <Scale size={12} /> Counter-notice
+ </motion.button>
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={() => setDismissModal(claim.id)}
+ disabled={isActing}
+ className="px-4 py-2 rounded-xl text-xs font-semibold flex items-center gap-1"
+ style={{ background: 'rgba(255,255,255,0.5)', color: '#6B705C' }}
+ >
+ <Eye size={12} /> Dismiss
+ </motion.button>
+ </div>
+ )}
+ </div>
+ )
+ })
+ )}
+
+ {/* Counter-notice Modal */}
+ {counterModal && (
+ <div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4" onClick={() => setCounterModal(null)}>
+ <div
+ className="w-full max-w-md rounded-2xl p-6 shadow-xl"
+ style={{ background: 'var(--color-cream)', border: '1px solid rgba(255,224,203,0.5)' }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <h3 className="text-sm font-bold mb-3" style={{ color: '#3D405B' }}>File Counter-Notice</h3>
+ <textarea
+ value={counterNotes}
+ onChange={(e) => setCounterNotes(e.target.value)}
+ placeholder="Counter-notice details..."
+ rows={4}
+ className="w-full px-3 py-2 rounded-xl text-sm border outline-none resize-none mb-4"
+ style={{ background: 'rgba(255,255,255,0.8)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ />
+ <div className="flex gap-2 justify-end">
+ <motion.button whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} onClick={() => setCounterModal(null)} className="px-4 py-2 rounded-xl text-xs font-semibold" style={{ background: 'rgba(255,255,255,0.6)', color: '#6B705C' }}>
+ Cancel
+ </motion.button>
+ <motion.button whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} onClick={handleCounter} className="px-4 py-2 rounded-xl text-xs font-semibold" style={{ background: '#8BBCAA', color: 'white' }}>
+ Submit Counter-Notice
+ </motion.button>
+ </div>
+ </div>
+ </div>
+ )}
+
+ {/* Dismiss Modal */}
+ {dismissModal && (
+ <div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center p-4" onClick={() => setDismissModal(null)}>
+ <div
+ className="w-full max-w-md rounded-2xl p-6 shadow-xl"
+ style={{ background: 'var(--color-cream)', border: '1px solid rgba(255,224,203,0.5)' }}
+ onClick={(e) => e.stopPropagation()}
+ >
+ <h3 className="text-sm font-bold mb-3" style={{ color: '#3D405B' }}>Dismiss DMCA Claim</h3>
+ <textarea
+ value={dismissNotes}
+ onChange={(e) => setDismissNotes(e.target.value)}
+ placeholder="Reason for dismissal..."
+ rows={3}
+ className="w-full px-3 py-2 rounded-xl text-sm border outline-none resize-none mb-4"
+ style={{ background: 'rgba(255,255,255,0.8)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ />
+ <div className="flex gap-2 justify-end">
+ <motion.button whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} onClick={() => setDismissModal(null)} className="px-4 py-2 rounded-xl text-xs font-semibold" style={{ background: 'rgba(255,255,255,0.6)', color: '#6B705C' }}>
+ Cancel
+ </motion.button>
+ <motion.button whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} onClick={handleDismiss} className="px-4 py-2 rounded-xl text-xs font-semibold" style={{ background: '#F4846F', color: 'white' }}>
+ Confirm Dismiss
+ </motion.button>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* Users Tab */
+/* ------------------------------------------------------------------ */
+
+function UsersTab() {
+ const [users, setUsers] = useState<UserRow[]>([])
+ const [loading, setLoading] = useState(true)
+ const [search, setSearch] = useState('')
+
+ const fetchUsers = useCallback(async () => {
+ setLoading(true)
+ try {
+ const params = new URLSearchParams({ view: 'users' })
+ if (search) params.set('search', search)
+ const res = await fetch(`/api/admin?${params}`)
+ if (res.ok) {
+ const data = await res.json()
+ setUsers(data.users || [])
+ }
+ } catch {
+ // silently fail
+ } finally {
+ setLoading(false)
+ }
+ }, [search])
+
+ useEffect(() => {
+ fetchUsers()
+ }, [fetchUsers])
+
+ return (
+ <div className="flex flex-col gap-3">
+ <div className="flex items-center gap-3">
+ <div className="relative flex-1">
+ <Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: '#6B705C' }} />
+ <input
+ type="text"
+ placeholder="Search users by name or email..."
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ className="w-full pl-9 pr-4 py-2.5 rounded-xl text-sm border outline-none"
+ style={{ background: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,224,203,0.5)', color: '#3D405B' }}
+ />
+ </div>
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={fetchUsers}
+ className="p-2.5 rounded-xl border"
+ style={{ background: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,224,203,0.5)', color: '#6B705C' }}
+ >
+ <RefreshCw size={14} />
+ </motion.button>
+ </div>
+
+ {loading ? (
+ <LoadingPane text="Loading users..." />
+ ) : users.length === 0 ? (
+ <EmptyState icon={Users} message="No users found" />
+ ) : (
+ <div
+ className="rounded-2xl overflow-hidden"
+ style={{ border: '1px solid rgba(255,224,203,0.5)' }}
+ >
+ {/* Header */}
+ <div
+ className="grid grid-cols-7 gap-4 px-5 py-3 text-[10px] font-bold uppercase tracking-wider"
+ style={{ background: 'rgba(255,224,203,0.3)', color: '#6B705C' }}
+ >
+ <span>User</span>
+ <span>Email</span>
+ <span className="text-center">Uploads</span>
+ <span className="text-center">Photos</span>
+ <span className="text-center">Earnings</span>
+ <span className="text-center">Joined</span>
+ <span className="text-center">Status</span>
+ </div>
+ {/* Rows */}
+ {users.map((user) => (
+ <div
+ key={user.id}
+ className="grid grid-cols-7 gap-4 items-center px-5 py-3.5 border-t hover:bg-white/30 transition-colors"
+ style={{ borderColor: 'rgba(255,224,203,0.3)', background: 'rgba(255,255,255,0.4)' }}
+ >
+ <div className="flex items-center gap-2">
+ <div
+ className="w-7 h-7 rounded-full flex items-center justify-center text-[10px] font-bold shrink-0"
+ style={{ background: '#FFB5A7', color: '#3D405B' }}
+ >
+ {(user.display_name || user.email || '?')
+ .split(' ')
+ .map((n: string) => n[0])
+ .join('')
+ .slice(0, 2)
+ .toUpperCase()}
+ </div>
+ <span className="text-xs font-semibold truncate" style={{ color: '#3D405B' }}>
+ {user.display_name || 'No name'}
+ </span>
+ </div>
+ <span className="text-xs truncate" style={{ color: '#6B705C' }}>{user.email}</span>
+ <span className="text-xs text-center font-semibold" style={{ color: '#3D405B' }}>
+ {user.upload_count}
+ {Number(user.approved_uploads) > 0 && (
+ <span className="text-[10px] font-normal ml-0.5" style={{ color: '#8BBCAA' }}>
+ ({user.approved_uploads})
+ </span>
+ )}
+ </span>
+ <span className="text-xs text-center font-semibold" style={{ color: '#3D405B' }}>{user.photo_count}</span>
+ <span className="text-xs text-center font-semibold" style={{ color: '#3D405B' }}>
+ {formatCurrency(Number(user.total_earnings))}
+ </span>
+ <span className="text-xs text-center" style={{ color: '#6B705C' }}>{formatDate(user.created_at)}</span>
+ <div className="flex justify-center">
+ {user.is_verified ? (
+ <StatusBadge status="active" />
+ ) : (
+ <StatusBadge status="pending" />
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ )
+}
+
+/* ------------------------------------------------------------------ */
+/* Tab Config */
+/* ------------------------------------------------------------------ */
+
+const TABS: { id: AdminTab; label: string; icon: typeof Shield }[] = [
+ { id: 'overview', label: 'Overview', icon: BarChart3 },
+ { id: 'queue', label: 'Review Queue', icon: Scale },
+ { id: 'dmca', label: 'DMCA', icon: FileWarning },
+ { id: 'users', label: 'Users', icon: Users },
+]
+
+/* ------------------------------------------------------------------ */
+/* Main Admin Page */
+/* ------------------------------------------------------------------ */
+
+export default function AdminPage() {
+ // Auth state
+ const [authChecking, setAuthChecking] = useState(true)
+ const [user, setUser] = useState<AuthUser | null>(null)
+
+ // Dashboard state
+ const [activeTab, setActiveTab] = useState<AdminTab>('overview')
+ const [overview, setOverview] = useState<OverviewData | null>(null)
+ const [overviewLoading, setOverviewLoading] = useState(false)
+ const [actionLoading, setActionLoading] = useState<string | null>(null)
+ const [alert, setAlert] = useState<AlertState | null>(null)
+
+ // Check auth on mount
+ useEffect(() => {
+ fetch('/api/auth')
+ .then((res) => res.json())
+ .then((data) => {
+ if (data.user?.role === 'admin') {
+ setUser(data.user)
+ }
+ })
+ .catch(() => {})
+ .finally(() => setAuthChecking(false))
+ }, [])
+
+ // Load overview when authenticated and on overview tab
+ const fetchOverview = useCallback(async () => {
+ setOverviewLoading(true)
+ try {
+ const res = await fetch('/api/admin?view=overview')
+ if (res.ok) {
+ const data = await res.json()
+ setOverview(data)
+ }
+ } catch {
+ // silently fail
+ } finally {
+ setOverviewLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (user && activeTab === 'overview') {
+ fetchOverview()
+ }
+ }, [user, activeTab, fetchOverview])
+
+ // Generic action handler for POST actions
+ const handleAction = useCallback(
+ async (action: string, payload: Record<string, string>) => {
+ const targetId = payload.upload_id || payload.claim_id || payload.user_id || ''
+ setActionLoading(targetId)
+ try {
+ const res = await fetch('/api/admin', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action, ...payload }),
+ })
+ const data = await res.json()
+
+ if (res.ok && data.success) {
+ setAlert({ type: 'success', message: `Action "${action.replace(/_/g, ' ')}" completed successfully` })
+ // Refresh overview stats in background
+ fetchOverview()
+ } else {
+ setAlert({ type: 'error', message: data.error || 'Action failed' })
+ }
+ } catch {
+ setAlert({ type: 'error', message: 'Network error. Please try again.' })
+ } finally {
+ setActionLoading(null)
+ }
+ },
+ [fetchOverview]
+ )
+
+ // Handle logout
+ const handleLogout = async () => {
+ await fetch('/api/auth', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action: 'logout' }),
+ })
+ setUser(null)
+ setOverview(null)
+ }
+
+ // Auth checking spinner
+ if (authChecking) {
+ return (
+ <div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--color-cream)' }}>
+ <LoadingPane text="Checking authentication..." />
+ </div>
+ )
+ }
+
+ // Not authenticated — show login
+ if (!user) {
+ return <LoginForm onLogin={setUser} />
+ }
+
+ // Badge counts from overview data
+ const pendingCount = overview?.stats.pending ?? 0
+ const dmcaCount = overview?.stats.dmca_open ?? 0
+
+ return (
+ <div className="min-h-screen flex" style={{ background: 'var(--color-cream)' }}>
+ {/* Alert */}
+ {alert && <AlertBanner alert={alert} onDismiss={() => setAlert(null)} />}
+
+ {/* Sidebar */}
+ <aside
+ className="w-56 shrink-0 flex flex-col justify-between p-4"
+ style={{ background: '#1a1d2e' }}
+ >
+ <div>
+ <div className="flex items-center gap-2 px-3 py-3 mb-6">
+ <Shield size={18} style={{ color: '#FFB5A7' }} />
+ <span className="text-sm font-black tracking-tighter" style={{ color: 'white' }}>
+ i was cute
+ </span>
+ <span className="text-[9px] font-bold px-1.5 py-0.5 rounded" style={{ background: 'rgba(255,181,167,0.2)', color: '#FFB5A7' }}>
+ ADMIN
+ </span>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ {TABS.map((tab) => {
+ const Icon = tab.icon
+ const active = activeTab === tab.id
+ return (
+ <button
+ key={tab.id}
+ onClick={() => setActiveTab(tab.id)}
+ className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-xs font-semibold transition-all text-left"
+ style={{
+ background: active ? 'rgba(255,181,167,0.12)' : 'transparent',
+ color: active ? '#FFB5A7' : 'rgba(255,255,255,0.45)',
+ }}
+ >
+ <Icon size={15} />
+ {tab.label}
+ {tab.id === 'queue' && pendingCount > 0 && (
+ <span className="ml-auto text-[9px] font-bold px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(244,132,111,0.25)', color: '#F4846F' }}>
+ {pendingCount}
+ </span>
+ )}
+ {tab.id === 'dmca' && dmcaCount > 0 && (
+ <span className="ml-auto text-[9px] font-bold px-1.5 py-0.5 rounded-full" style={{ background: 'rgba(244,132,111,0.25)', color: '#F4846F' }}>
+ {dmcaCount}
+ </span>
+ )}
+ </button>
+ )
+ })}
+ </div>
+ </div>
+
+ <div className="flex flex-col gap-1">
+ {/* Logged-in user info */}
+ <div className="px-3 py-2 mb-2">
+ <p className="text-[10px] font-semibold truncate" style={{ color: 'rgba(255,255,255,0.5)' }}>
+ {user.display_name || user.email}
+ </p>
+ <p className="text-[9px] truncate" style={{ color: 'rgba(255,255,255,0.3)' }}>
+ {user.email}
+ </p>
+ </div>
+ <button className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-xs font-medium text-left" style={{ color: 'rgba(255,255,255,0.35)' }}>
+ <Settings size={14} /> Settings
+ </button>
+ <Link href="/" className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-xs font-medium no-underline" style={{ color: 'rgba(255,255,255,0.35)' }}>
+ <LogOut size={14} /> Exit Admin
+ </Link>
+ <button
+ onClick={handleLogout}
+ className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-xs font-medium text-left"
+ style={{ color: 'rgba(244,132,111,0.7)' }}
+ >
+ <LogOut size={14} /> Log Out
+ </button>
+ </div>
+ </aside>
+
+ {/* Main content */}
+ <main className="flex-1 px-6 py-6 md:px-10 md:py-8 overflow-y-auto">
+ <div className="max-w-6xl">
+ <div className="flex items-center justify-between mb-6">
+ <div>
+ <h1 className="text-2xl font-black tracking-tighter" style={{ color: '#3D405B' }}>
+ {TABS.find((t) => t.id === activeTab)?.label}
+ </h1>
+ <p className="text-xs mt-1" style={{ color: '#6B705C' }}>
+ {activeTab === 'overview' && 'Platform health and activity at a glance.'}
+ {activeTab === 'queue' && 'Review uploads for dual-rights clearance.'}
+ {activeTab === 'dmca' && 'Manage takedown requests and counter-notices.'}
+ {activeTab === 'users' && 'Manage licensor accounts and activity.'}
+ </p>
+ </div>
+ {activeTab === 'overview' && (
+ <motion.button
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ onClick={fetchOverview}
+ className="flex items-center gap-2 px-3 py-2 rounded-xl text-xs font-semibold"
+ style={{ background: 'rgba(255,255,255,0.6)', color: '#6B705C', border: '1px solid rgba(255,224,203,0.5)' }}
+ >
+ <RefreshCw size={12} /> Refresh
+ </motion.button>
+ )}
+ </div>
+
+ {activeTab === 'overview' && (
+ <OverviewTab
+ data={overview}
+ loading={overviewLoading}
+ onSwitchTab={setActiveTab}
+ />
+ )}
+ {activeTab === 'queue' && (
+ <QueueTab
+ onAction={handleAction}
+ actionLoading={actionLoading}
+ />
+ )}
+ {activeTab === 'dmca' && (
+ <DMCATab
+ onAction={handleAction}
+ actionLoading={actionLoading}
+ />
+ )}
+ {activeTab === 'users' && <UsersTab />}
+ </div>
+ </main>
+ </div>
+ )
+}
diff --git a/src/app/api/admin/route.ts b/src/app/api/admin/route.ts
new file mode 100644
index 0000000..2a89946
--- /dev/null
+++ b/src/app/api/admin/route.ts
@@ -0,0 +1,283 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { query } from '@/lib/db'
+import { requireAdmin } from '@/lib/auth'
+
+export async function GET(request: NextRequest) {
+ const admin = await requireAdmin()
+ if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const { searchParams } = new URL(request.url)
+ const view = searchParams.get('view') || 'overview'
+
+ switch (view) {
+ case 'overview':
+ return getOverview()
+ case 'queue':
+ return getQueue(searchParams)
+ case 'dmca':
+ return getDMCA(searchParams)
+ case 'users':
+ return getUsers(searchParams)
+ case 'upload':
+ return getUploadDetail(searchParams)
+ default:
+ return NextResponse.json({ error: `Unknown view: ${view}` }, { status: 400 })
+ }
+}
+
+async function getOverview() {
+ const [pending, approved, rejected, users, revenue, dmca] = await Promise.all([
+ query(`SELECT COUNT(*) FROM uploads WHERE status IN ('pending', 'flagged')`),
+ query(`SELECT COUNT(*) FROM uploads WHERE status = 'approved'`),
+ query(`SELECT COUNT(*) FROM uploads WHERE status = 'rejected'`),
+ query(`SELECT COUNT(*) FROM users WHERE role != 'admin'`),
+ query(`SELECT COALESCE(SUM(total_earnings), 0) as total FROM photos`),
+ query(`SELECT COUNT(*) FROM dmca_claims WHERE status IN ('open', 'investigating')`),
+ ])
+
+ // Recent queue
+ const recentQueue = await query(`
+ SELECT u.id, u.status, u.created_at, u.copyright_relationship, u.copyright_permission,
+ usr.display_name, usr.email,
+ COUNT(p.id) as photo_count
+ FROM uploads u
+ JOIN users usr ON u.user_id = usr.id
+ LEFT JOIN photos p ON p.upload_id = u.id
+ WHERE u.status IN ('pending', 'flagged')
+ GROUP BY u.id, usr.display_name, usr.email
+ ORDER BY u.created_at DESC
+ LIMIT 5
+ `)
+
+ // Recent DMCA
+ const recentDMCA = await query(`
+ SELECT d.*, u.id as upload_uuid
+ FROM dmca_claims d
+ LEFT JOIN uploads u ON d.upload_id = u.id
+ ORDER BY d.filed_at DESC
+ LIMIT 5
+ `)
+
+ return NextResponse.json({
+ stats: {
+ pending: parseInt(pending.rows[0].count),
+ approved: parseInt(approved.rows[0].count),
+ rejected: parseInt(rejected.rows[0].count),
+ users: parseInt(users.rows[0].count),
+ revenue: parseFloat(revenue.rows[0].total),
+ dmca_open: parseInt(dmca.rows[0].count),
+ },
+ recent_queue: recentQueue.rows,
+ recent_dmca: recentDMCA.rows,
+ })
+}
+
+async function getQueue(params: URLSearchParams) {
+ const status = params.get('status')
+ const search = params.get('search')
+
+ const conditions = [`u.status IN ('pending', 'flagged', 'in_review')`]
+ const values: unknown[] = []
+ let idx = 1
+
+ if (status && status !== 'all') {
+ conditions.push(`u.status = $${idx}`)
+ values.push(status)
+ idx++
+ }
+ if (search) {
+ conditions.push(`(usr.display_name ILIKE $${idx} OR u.id::text ILIKE $${idx})`)
+ values.push(`%${search}%`)
+ idx++
+ }
+
+ const where = conditions.join(' AND ')
+
+ const result = await query(`
+ SELECT u.*, usr.display_name, usr.email,
+ COUNT(p.id) as photo_count,
+ json_agg(json_build_object('id', p.id, 'filename', p.filename, 'exif_year', p.exif_year) ORDER BY p.created_at) FILTER (WHERE p.id IS NOT NULL) as photos
+ FROM uploads u
+ JOIN users usr ON u.user_id = usr.id
+ LEFT JOIN photos p ON p.upload_id = u.id
+ WHERE ${where}
+ GROUP BY u.id, usr.display_name, usr.email
+ ORDER BY CASE WHEN u.status = 'flagged' THEN 0 ELSE 1 END, u.created_at DESC
+ `, values)
+
+ return NextResponse.json({ queue: result.rows })
+}
+
+async function getDMCA(params: URLSearchParams) {
+ const status = params.get('status')
+ const conditions: string[] = []
+ const values: unknown[] = []
+
+ if (status && status !== 'all') {
+ conditions.push(`d.status = $1`)
+ values.push(status)
+ }
+
+ const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : ''
+
+ const result = await query(`
+ SELECT d.*,
+ u.id as upload_uuid, usr.display_name as upload_user
+ FROM dmca_claims d
+ LEFT JOIN uploads u ON d.upload_id = u.id
+ LEFT JOIN users usr ON u.user_id = usr.id
+ ${where}
+ ORDER BY d.filed_at DESC
+ `, values)
+
+ return NextResponse.json({ claims: result.rows })
+}
+
+async function getUsers(params: URLSearchParams) {
+ const search = params.get('search')
+ const conditions = [`u.role != 'admin'`]
+ const values: unknown[] = []
+
+ if (search) {
+ conditions.push(`(u.display_name ILIKE $1 OR u.email ILIKE $1)`)
+ values.push(`%${search}%`)
+ }
+
+ const where = conditions.join(' AND ')
+
+ const result = await query(`
+ SELECT u.id, u.email, u.display_name, u.role, u.is_verified, u.created_at,
+ COUNT(DISTINCT up.id) as upload_count,
+ COUNT(DISTINCT p.id) as photo_count,
+ COALESCE(SUM(p.total_earnings), 0) as total_earnings,
+ COUNT(DISTINCT CASE WHEN up.status = 'approved' THEN up.id END) as approved_uploads
+ FROM users u
+ LEFT JOIN uploads up ON up.user_id = u.id
+ LEFT JOIN photos p ON p.user_id = u.id
+ WHERE ${where}
+ GROUP BY u.id
+ ORDER BY u.created_at DESC
+ `, values)
+
+ return NextResponse.json({ users: result.rows })
+}
+
+async function getUploadDetail(params: URLSearchParams) {
+ const id = params.get('id')
+ if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 })
+
+ const upload = await query(`
+ SELECT u.*, usr.display_name, usr.email
+ FROM uploads u JOIN users usr ON u.user_id = usr.id
+ WHERE u.id = $1
+ `, [id])
+
+ const photos = await query(`SELECT * FROM photos WHERE upload_id = $1 ORDER BY created_at`, [id])
+
+ return NextResponse.json({
+ upload: upload.rows[0] || null,
+ photos: photos.rows,
+ })
+}
+
+// POST — Admin actions
+export async function POST(request: NextRequest) {
+ const admin = await requireAdmin()
+ if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const body = await request.json()
+ const { action } = body
+
+ switch (action) {
+ case 'approve_upload': {
+ const { upload_id, notes } = body
+ await query(
+ `UPDATE uploads SET status = 'approved', reviewed_by = $1, reviewed_at = NOW(), review_notes = $2, updated_at = NOW() WHERE id = $3`,
+ [admin.user_id, notes || null, upload_id]
+ )
+ // Set clearance level on photos
+ await query(
+ `UPDATE photos SET clearance_level = CASE
+ WHEN (SELECT likeness_ai_training FROM uploads WHERE id = $1) THEN 'ai_training'
+ WHEN (SELECT likeness_commercial FROM uploads WHERE id = $1) THEN 'commercial'
+ ELSE 'editorial'
+ END WHERE upload_id = $1`,
+ [upload_id]
+ )
+ await auditLog(admin.user_id, 'upload.approve', 'upload', upload_id)
+ return NextResponse.json({ success: true })
+ }
+
+ case 'reject_upload': {
+ const { upload_id, reason } = body
+ await query(
+ `UPDATE uploads SET status = 'rejected', reviewed_by = $1, reviewed_at = NOW(), rejection_reason = $2, updated_at = NOW() WHERE id = $3`,
+ [admin.user_id, reason || 'Rights verification failed', upload_id]
+ )
+ await auditLog(admin.user_id, 'upload.reject', 'upload', upload_id)
+ return NextResponse.json({ success: true })
+ }
+
+ case 'flag_upload': {
+ const { upload_id, notes } = body
+ await query(
+ `UPDATE uploads SET status = 'flagged', review_notes = $1, updated_at = NOW() WHERE id = $2`,
+ [notes, upload_id]
+ )
+ await auditLog(admin.user_id, 'upload.flag', 'upload', upload_id)
+ return NextResponse.json({ success: true })
+ }
+
+ case 'dmca_takedown': {
+ const { claim_id } = body
+ await query(
+ `UPDATE dmca_claims SET status = 'taken_down', resolved_by = $1, resolved_at = NOW(), updated_at = NOW() WHERE id = $2`,
+ [admin.user_id, claim_id]
+ )
+ // Also update the associated upload
+ await query(
+ `UPDATE uploads SET status = 'dmca_removed', updated_at = NOW() WHERE id = (SELECT upload_id FROM dmca_claims WHERE id = $1)`,
+ [claim_id]
+ )
+ await auditLog(admin.user_id, 'dmca.takedown', 'dmca_claim', claim_id)
+ return NextResponse.json({ success: true })
+ }
+
+ case 'dmca_counter': {
+ const { claim_id, notes } = body
+ await query(
+ `UPDATE dmca_claims SET status = 'counter_noticed', resolution_notes = $1, updated_at = NOW() WHERE id = $2`,
+ [notes, claim_id]
+ )
+ await auditLog(admin.user_id, 'dmca.counter_notice', 'dmca_claim', claim_id)
+ return NextResponse.json({ success: true })
+ }
+
+ case 'dmca_dismiss': {
+ const { claim_id, notes } = body
+ await query(
+ `UPDATE dmca_claims SET status = 'dismissed', resolved_by = $1, resolved_at = NOW(), resolution_notes = $2, updated_at = NOW() WHERE id = $3`,
+ [admin.user_id, notes, claim_id]
+ )
+ await auditLog(admin.user_id, 'dmca.dismiss', 'dmca_claim', claim_id)
+ return NextResponse.json({ success: true })
+ }
+
+ case 'suspend_user': {
+ const { user_id } = body
+ // In a real app, add a suspended_at column. For now, log it.
+ await auditLog(admin.user_id, 'user.suspend', 'user', user_id)
+ return NextResponse.json({ success: true })
+ }
+
+ default:
+ return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 })
+ }
+}
+
+async function auditLog(userId: string, action: string, targetType: string, targetId: string) {
+ await query(
+ `INSERT INTO audit_log (user_id, action, target_type, target_id) VALUES ($1, $2, $3, $4)`,
+ [userId, action, targetType, targetId]
+ )
+}
diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts
new file mode 100644
index 0000000..ce935c9
--- /dev/null
+++ b/src/app/api/auth/route.ts
@@ -0,0 +1,72 @@
+import { NextRequest, NextResponse } from 'next/server'
+import { query } from '@/lib/db'
+import { cookies } from 'next/headers'
+
+export async function POST(request: NextRequest) {
+ const body = await request.json()
+ const { action, email, password } = body
+
+ if (action === 'login') {
+ if (!email || !password) {
+ return NextResponse.json({ error: 'Email and password required' }, { status: 400 })
+ }
+
+ const result = await query(
+ `SELECT id, email, display_name, role
+ FROM users
+ WHERE email = $1 AND password_hash = crypt($2, password_hash)`,
+ [email, password]
+ )
+
+ if (result.rows.length === 0) {
+ return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
+ }
+
+ const user = result.rows[0]
+
+ // Create session
+ const session = await query(
+ `INSERT INTO sessions (user_id) VALUES ($1) RETURNING token, expires_at`,
+ [user.id]
+ )
+
+ const cookieStore = await cookies()
+ cookieStore.set('iwascute-session', session.rows[0].token, {
+ httpOnly: true,
+ sameSite: 'lax',
+ path: '/',
+ maxAge: 60 * 60 * 24 * 7, // 7 days
+ })
+
+ return NextResponse.json({ user })
+ }
+
+ if (action === 'logout') {
+ const cookieStore = await cookies()
+ const token = cookieStore.get('iwascute-session')?.value
+ if (token) {
+ await query('DELETE FROM sessions WHERE token = $1', [token])
+ cookieStore.delete('iwascute-session')
+ }
+ return NextResponse.json({ success: true })
+ }
+
+ return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
+}
+
+export async function GET() {
+ const cookieStore = await cookies()
+ const token = cookieStore.get('iwascute-session')?.value
+ if (!token) {
+ return NextResponse.json({ user: null })
+ }
+
+ const result = await query(
+ `SELECT u.id, u.email, u.display_name, u.role
+ FROM sessions s JOIN users u ON s.user_id = u.id
+ WHERE s.token = $1 AND s.expires_at > NOW()`,
+ [token]
+ )
+
+ return NextResponse.json({ user: result.rows[0] || null })
+}
diff --git a/src/app/browse/page.tsx b/src/app/browse/page.tsx
new file mode 100644
index 0000000..db15a45
--- /dev/null
+++ b/src/app/browse/page.tsx
@@ -0,0 +1,281 @@
+'use client'
+
+import { useState } from 'react'
+import { motion, AnimatePresence } from 'framer-motion'
+import {
+ Search,
+ SlidersHorizontal,
+ Grid3X3,
+ LayoutList,
+ ShieldCheck,
+ ChevronDown,
+ ImageIcon,
+ Tag,
+ Calendar,
+} from 'lucide-react'
+import Navbar from '@/components/shared/Navbar'
+
+type ViewMode = 'grid' | 'list'
+type LicenseFilter = 'all' | 'editorial' | 'commercial' | 'ai-training'
+
+const LICENSE_FILTERS: { id: LicenseFilter; label: string }[] = [
+ { id: 'all', label: 'All' },
+ { id: 'editorial', label: 'Editorial' },
+ { id: 'commercial', label: 'Commercial' },
+ { id: 'ai-training', label: 'AI Training' },
+]
+
+// Placeholder categories for when photos are available
+const CATEGORIES = [
+ 'All Photos',
+ 'Baby & Toddler',
+ 'School Age',
+ 'Teenager',
+ 'Family',
+ 'Holidays',
+ 'Outdoor',
+ 'Portraits',
+]
+
+function SearchBar({
+ value,
+ onChange,
+}: {
+ value: string
+ onChange: (v: string) => void
+}) {
+ return (
+ <div className="relative flex-1">
+ <Search
+ size={16}
+ className="absolute left-4 top-1/2 -translate-y-1/2 pointer-events-none"
+ style={{ color: 'var(--color-warm-gray)' }}
+ />
+ <input
+ type="text"
+ value={value}
+ onChange={(e) => onChange(e.target.value)}
+ placeholder="Search photos by keyword, era, or description..."
+ className="w-full pl-11 pr-4 py-3 rounded-2xl text-sm border outline-none transition-all focus:ring-2"
+ style={{
+ background: 'rgba(255,255,255,0.8)',
+ borderColor: 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ />
+ </div>
+ )
+}
+
+function EmptyMarketplace() {
+ return (
+ <div
+ className="flex flex-col items-center justify-center gap-8 py-24 text-center"
+ >
+ {/* Decorative placeholder grid */}
+ <div className="grid grid-cols-3 gap-3 opacity-30">
+ {Array.from({ length: 9 }).map((_, i) => (
+ <div
+ key={i}
+ className="w-24 h-24 md:w-32 md:h-32 rounded-2xl flex items-center justify-center"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ <ImageIcon size={24} style={{ color: 'var(--color-warm-gray)' }} />
+ </div>
+ ))}
+ </div>
+
+ <div className="max-w-md">
+ <h2 className="text-2xl font-black tracking-tighter" style={{ color: 'var(--color-brown)' }}>
+ The marketplace is coming soon
+ </h2>
+ <p className="mt-3 text-base leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ Photos are being uploaded and reviewed. The first rights-cleared childhood photos
+ will appear here shortly. Every photo has both copyright and likeness rights verified.
+ </p>
+ </div>
+
+ {/* Trust badges */}
+ <div className="flex flex-wrap justify-center gap-4 mt-4">
+ {[
+ { icon: ShieldCheck, label: 'Dual-rights verified' },
+ { icon: Tag, label: 'Clear licensing tiers' },
+ { icon: Calendar, label: 'AI training available' },
+ ].map(({ icon: Icon, label }) => (
+ <div
+ key={label}
+ className="flex items-center gap-2 px-4 py-2.5 rounded-2xl"
+ style={{ background: 'rgba(181, 213, 197, 0.2)', border: '1px solid var(--color-sage)' }}
+ >
+ <Icon size={14} style={{ color: 'var(--color-sage-dark)' }} />
+ <span className="text-xs font-semibold" style={{ color: 'var(--color-sage-dark)' }}>
+ {label}
+ </span>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
+
+export default function BrowsePage() {
+ const [search, setSearch] = useState('')
+ const [viewMode, setViewMode] = useState<ViewMode>('grid')
+ const [licenseFilter, setLicenseFilter] = useState<LicenseFilter>('all')
+ const [activeCategory, setActiveCategory] = useState('All Photos')
+ const [showFilters, setShowFilters] = useState(false)
+
+ // No photos yet — marketplace is pre-launch
+ const photos: unknown[] = []
+
+ return (
+ <div className="min-h-screen" style={{ background: 'var(--color-cream)' }}>
+ <Navbar />
+
+ <div className="max-w-6xl mx-auto px-4 py-8 md:px-8 md:py-12">
+ {/* Header */}
+ <div className="mb-8">
+ <h1 className="text-3xl font-black tracking-tighter" style={{ color: 'var(--color-brown)' }}>
+ Browse Photos
+ </h1>
+ <p className="mt-1 text-sm" style={{ color: 'var(--color-warm-gray)' }}>
+ Every photo is dual-rights cleared — copyright and likeness verified.
+ </p>
+ </div>
+
+ {/* Search + controls */}
+ <div className="flex flex-col gap-4 mb-6">
+ <div className="flex gap-3">
+ <SearchBar value={search} onChange={setSearch} />
+
+ <button
+ onClick={() => setShowFilters(!showFilters)}
+ className="flex items-center gap-2 px-4 py-3 rounded-2xl text-sm font-medium border transition-all hover:scale-105"
+ style={{
+ background: showFilters ? 'var(--color-peach)' : 'rgba(255,255,255,0.8)',
+ borderColor: showFilters ? 'var(--color-peach)' : 'var(--color-cream-dark)',
+ color: showFilters ? 'var(--color-brown)' : 'var(--color-warm-gray)',
+ }}
+ >
+ <SlidersHorizontal size={14} />
+ Filters
+ <ChevronDown
+ size={12}
+ className="transition-transform"
+ style={{ transform: showFilters ? 'rotate(180deg)' : 'none' }}
+ />
+ </button>
+
+ {/* View toggle */}
+ <div
+ className="flex rounded-2xl border overflow-hidden"
+ style={{ borderColor: 'var(--color-cream-dark)' }}
+ >
+ <button
+ onClick={() => setViewMode('grid')}
+ className="px-3 py-3 transition-all"
+ style={{
+ background: viewMode === 'grid' ? 'var(--color-cream-dark)' : 'rgba(255,255,255,0.8)',
+ color: 'var(--color-warm-gray)',
+ }}
+ aria-label="Grid view"
+ >
+ <Grid3X3 size={16} />
+ </button>
+ <button
+ onClick={() => setViewMode('list')}
+ className="px-3 py-3 transition-all"
+ style={{
+ background: viewMode === 'list' ? 'var(--color-cream-dark)' : 'rgba(255,255,255,0.8)',
+ color: 'var(--color-warm-gray)',
+ }}
+ aria-label="List view"
+ >
+ <LayoutList size={16} />
+ </button>
+ </div>
+ </div>
+
+ {/* Filter panel */}
+ <AnimatePresence>
+ {showFilters && (
+ <motion.div
+ initial={{ opacity: 0, height: 0 }}
+ animate={{ opacity: 1, height: 'auto' }}
+ exit={{ opacity: 0, height: 0 }}
+ transition={{ duration: 0.2 }}
+ className="overflow-hidden"
+ >
+ <div
+ className="p-5 rounded-2xl flex flex-col gap-4"
+ style={{ background: 'rgba(255,255,255,0.6)', border: '1px solid var(--color-cream-dark)' }}
+ >
+ {/* License type */}
+ <div>
+ <p className="text-xs font-bold tracking-widest uppercase mb-2" style={{ color: 'var(--color-warm-gray)' }}>
+ License Type
+ </p>
+ <div className="flex gap-2 flex-wrap">
+ {LICENSE_FILTERS.map((f) => (
+ <button
+ key={f.id}
+ onClick={() => setLicenseFilter(f.id)}
+ className="px-3.5 py-1.5 rounded-xl text-xs font-semibold transition-all hover:scale-105"
+ style={{
+ background: licenseFilter === f.id ? 'var(--color-peach)' : 'var(--color-cream-dark)',
+ color: licenseFilter === f.id ? 'var(--color-brown)' : 'var(--color-warm-gray)',
+ }}
+ >
+ {f.label}
+ </button>
+ ))}
+ </div>
+ </div>
+
+ {/* Category chips */}
+ <div>
+ <p className="text-xs font-bold tracking-widest uppercase mb-2" style={{ color: 'var(--color-warm-gray)' }}>
+ Category
+ </p>
+ <div className="flex gap-2 flex-wrap">
+ {CATEGORIES.map((cat) => (
+ <button
+ key={cat}
+ onClick={() => setActiveCategory(cat)}
+ className="px-3.5 py-1.5 rounded-xl text-xs font-semibold transition-all hover:scale-105"
+ style={{
+ background: activeCategory === cat ? 'var(--color-sage)' : 'var(--color-cream-dark)',
+ color: activeCategory === cat ? 'white' : 'var(--color-warm-gray)',
+ }}
+ >
+ {cat}
+ </button>
+ ))}
+ </div>
+ </div>
+ </div>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+
+ {/* Results */}
+ {photos.length === 0 ? (
+ <EmptyMarketplace />
+ ) : (
+ <div
+ className="grid gap-4"
+ style={{
+ gridTemplateColumns:
+ viewMode === 'grid'
+ ? 'repeat(auto-fill, minmax(220px, 1fr))'
+ : '1fr',
+ }}
+ >
+ {/* Photo cards will render here when marketplace launches */}
+ </div>
+ )}
+ </div>
+ </div>
+ )
+}
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..f325b2d
--- /dev/null
+++ b/src/app/dashboard/page.tsx
@@ -0,0 +1,320 @@
+'use client'
+
+import { useState } from 'react'
+import { motion, AnimatePresence } from 'framer-motion'
+import {
+ Camera,
+ DollarSign,
+ Clock,
+ CheckCircle2,
+ AlertCircle,
+ Eye,
+ TrendingUp,
+ Plus,
+ ChevronRight,
+ Shield,
+ ImageIcon,
+} from 'lucide-react'
+import Link from 'next/link'
+import Navbar from '@/components/shared/Navbar'
+
+const DEMO_STATS = [
+ { label: 'Total Photos', value: '0', icon: Camera, color: 'var(--color-peach)' },
+ { label: 'Cleared', value: '0', icon: CheckCircle2, color: 'var(--color-sage-dark)' },
+ { label: 'Pending Review', value: '0', icon: Clock, color: 'var(--color-warm-gray)' },
+ { label: 'Total Earnings', value: '$0.00', icon: DollarSign, color: 'var(--color-peach-dark)' },
+]
+
+const DEMO_UPLOADS: {
+ id: string
+ name: string
+ status: 'pending' | 'cleared' | 'rejected'
+ submittedAt: string
+ views: number
+ earnings: number
+}[] = []
+
+type Tab = 'overview' | 'photos' | 'earnings'
+
+function StatCard({
+ label,
+ value,
+ icon: Icon,
+ color,
+}: {
+ label: string
+ value: string
+ icon: typeof Camera
+ color: string
+}) {
+ return (
+ <div
+ className="flex items-center gap-4 p-5 rounded-3xl"
+ style={{ background: 'rgba(255,255,255,0.6)', border: '1px solid var(--color-cream-dark)' }}
+ >
+ <div
+ className="w-12 h-12 rounded-2xl flex items-center justify-center shrink-0"
+ style={{ background: color, opacity: 0.9 }}
+ >
+ <Icon size={22} style={{ color: 'white' }} />
+ </div>
+ <div>
+ <p className="text-2xl font-black tracking-tight" style={{ color: 'var(--color-brown)' }}>
+ {value}
+ </p>
+ <p className="text-xs font-medium tracking-wide" style={{ color: 'var(--color-warm-gray)' }}>
+ {label}
+ </p>
+ </div>
+ </div>
+ )
+}
+
+function EmptyState() {
+ return (
+ <div className="flex flex-col items-center justify-center gap-6 py-20 text-center">
+ <div
+ className="w-20 h-20 rounded-3xl flex items-center justify-center"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ <ImageIcon size={36} style={{ color: 'var(--color-warm-gray)' }} />
+ </div>
+ <div className="max-w-sm">
+ <h3 className="text-xl font-bold tracking-tight" style={{ color: 'var(--color-brown)' }}>
+ No photos yet
+ </h3>
+ <p className="mt-2 text-sm leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ Upload your first childhood photo to start building your portfolio and earning royalties.
+ </p>
+ </div>
+ <Link
+ href="/licensor"
+ className="flex items-center gap-2 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
+ style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
+ >
+ <Plus size={16} />
+ Upload Photos
+ </Link>
+ </div>
+ )
+}
+
+function StatusBadge({ status }: { status: 'pending' | 'cleared' | 'rejected' }) {
+ const config = {
+ pending: { bg: 'rgba(255, 181, 167, 0.2)', color: 'var(--color-peach-dark)', label: 'Pending' },
+ cleared: { bg: 'rgba(181, 213, 197, 0.3)', color: 'var(--color-sage-dark)', label: 'Cleared' },
+ rejected: { bg: 'rgba(107, 112, 92, 0.15)', color: 'var(--color-warm-gray)', label: 'Rejected' },
+ }
+ const c = config[status]
+ return (
+ <span
+ className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold"
+ style={{ background: c.bg, color: c.color }}
+ >
+ {status === 'pending' && <Clock size={10} />}
+ {status === 'cleared' && <CheckCircle2 size={10} />}
+ {status === 'rejected' && <AlertCircle size={10} />}
+ {c.label}
+ </span>
+ )
+}
+
+export default function DashboardPage() {
+ const [activeTab, setActiveTab] = useState<Tab>('overview')
+
+ const tabs: { id: Tab; label: string }[] = [
+ { id: 'overview', label: 'Overview' },
+ { id: 'photos', label: 'My Photos' },
+ { id: 'earnings', label: 'Earnings' },
+ ]
+
+ return (
+ <div className="min-h-screen" style={{ background: 'var(--color-cream)' }}>
+ <Navbar />
+
+ <div className="max-w-5xl mx-auto px-4 py-8 md:px-8 md:py-12">
+ {/* Header */}
+ <div className="flex items-center justify-between mb-8">
+ <div>
+ <h1 className="text-3xl font-black tracking-tighter" style={{ color: 'var(--color-brown)' }}>
+ Dashboard
+ </h1>
+ <p className="mt-1 text-sm" style={{ color: 'var(--color-warm-gray)' }}>
+ Manage your photos, track rights clearance, and monitor earnings.
+ </p>
+ </div>
+ <Link
+ href="/licensor"
+ className="flex items-center gap-2 px-6 py-3 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
+ style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
+ >
+ <Plus size={14} />
+ <span className="hidden sm:inline">Upload Photos</span>
+ </Link>
+ </div>
+
+ {/* Stats grid */}
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
+ {DEMO_STATS.map((stat) => (
+ <StatCard key={stat.label} {...stat} />
+ ))}
+ </div>
+
+ {/* Tab bar */}
+ <div
+ className="flex gap-1 p-1 rounded-2xl mb-8"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ {tabs.map((tab) => (
+ <button
+ key={tab.id}
+ onClick={() => setActiveTab(tab.id)}
+ className="relative flex-1 py-2.5 text-sm font-semibold rounded-xl transition-all"
+ style={{
+ color: activeTab === tab.id ? 'var(--color-brown)' : 'var(--color-warm-gray)',
+ }}
+ >
+ {activeTab === tab.id && (
+ <motion.div
+ layoutId="dashboard-tab"
+ className="absolute inset-0 rounded-xl"
+ style={{ background: 'white', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}
+ transition={{ type: 'spring', stiffness: 380, damping: 30 }}
+ />
+ )}
+ <span className="relative z-10">{tab.label}</span>
+ </button>
+ ))}
+ </div>
+
+ {/* Tab content */}
+ <AnimatePresence mode="wait">
+ {activeTab === 'overview' && (
+ <div key="overview">
+ {DEMO_UPLOADS.length === 0 ? (
+ <EmptyState />
+ ) : (
+ <div className="flex flex-col gap-4" />
+ )}
+
+ {/* How it works card */}
+ <div
+ className="mt-8 rounded-3xl p-8"
+ style={{ background: 'rgba(181, 213, 197, 0.15)', border: '1px solid var(--color-sage)' }}
+ >
+ <div className="flex items-center gap-3 mb-6">
+ <Shield size={20} style={{ color: 'var(--color-sage-dark)' }} />
+ <h3 className="text-base font-bold tracking-wide" style={{ color: 'var(--color-brown)' }}>
+ How Dual-Rights Clearance Works
+ </h3>
+ </div>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
+ {[
+ {
+ step: '1',
+ title: 'Copyright Clearance',
+ desc: 'We verify the photographer or copyright owner has granted permission for your photo to be licensed.',
+ },
+ {
+ step: '2',
+ title: 'Likeness Release',
+ desc: 'Your signed model release confirms you are the person depicted and consent to licensing.',
+ },
+ {
+ step: '3',
+ title: 'Start Earning',
+ desc: 'Once both rights are cleared, your photo enters the marketplace. You earn royalties on every license.',
+ },
+ ].map(({ step, title, desc }) => (
+ <div key={step} className="flex flex-col gap-2">
+ <div
+ className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-black"
+ style={{ background: 'var(--color-sage)', color: 'white' }}
+ >
+ {step}
+ </div>
+ <h4 className="text-sm font-bold" style={{ color: 'var(--color-brown)' }}>
+ {title}
+ </h4>
+ <p className="text-xs leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ {desc}
+ </p>
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ )}
+
+ {activeTab === 'photos' && (
+ <div key="photos">
+ {DEMO_UPLOADS.length === 0 ? (
+ <EmptyState />
+ ) : (
+ <div className="flex flex-col gap-3">
+ {DEMO_UPLOADS.map((upload) => (
+ <div
+ key={upload.id}
+ className="flex items-center gap-4 p-4 rounded-2xl transition-all hover:scale-[1.01]"
+ style={{
+ background: 'rgba(255,255,255,0.6)',
+ border: '1px solid var(--color-cream-dark)',
+ }}
+ >
+ <div
+ className="w-14 h-14 rounded-xl flex items-center justify-center shrink-0"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ <ImageIcon size={20} style={{ color: 'var(--color-warm-gray)' }} />
+ </div>
+ <div className="flex-1 min-w-0">
+ <p className="text-sm font-semibold truncate" style={{ color: 'var(--color-brown)' }}>
+ {upload.name}
+ </p>
+ <p className="text-xs" style={{ color: 'var(--color-warm-gray)' }}>
+ {upload.submittedAt}
+ </p>
+ </div>
+ <StatusBadge status={upload.status} />
+ <div className="flex items-center gap-3 text-xs" style={{ color: 'var(--color-warm-gray)' }}>
+ <span className="flex items-center gap-1">
+ <Eye size={12} /> {upload.views}
+ </span>
+ <span className="flex items-center gap-1">
+ <DollarSign size={12} /> ${upload.earnings.toFixed(2)}
+ </span>
+ </div>
+ <ChevronRight size={16} style={{ color: 'var(--color-warm-gray)' }} />
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+
+ {activeTab === 'earnings' && (
+ <div
+ key="earnings"
+ className="flex flex-col items-center gap-6 py-16 text-center"
+ >
+ <div
+ className="w-20 h-20 rounded-3xl flex items-center justify-center"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ <TrendingUp size={36} style={{ color: 'var(--color-warm-gray)' }} />
+ </div>
+ <div className="max-w-sm">
+ <h3 className="text-xl font-bold tracking-tight" style={{ color: 'var(--color-brown)' }}>
+ No earnings yet
+ </h3>
+ <p className="mt-2 text-sm leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ Upload photos and get them cleared to start earning royalties from licenses.
+ </p>
+ </div>
+ </div>
+ )}
+ </AnimatePresence>
+ </div>
+ </div>
+ )
+}
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
Binary files /dev/null and b/src/app/favicon.ico differ
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..e676935
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,79 @@
+@import "tailwindcss";
+
+:root {
+ --background: #FFF1E6;
+ --foreground: #3D405B;
+
+ /* Brand palette */
+ --color-peach: #FFB5A7;
+ --color-peach-dark: #F4846F;
+ --color-sage: #B5D5C5;
+ --color-sage-dark: #8BBCAA;
+ --color-cream: #FFF1E6;
+ --color-cream-dark: #FFE0CB;
+ --color-warm-gray: #6B705C;
+ --color-brown: #3D405B;
+ --color-brown-light: #5A5F80;
+}
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+
+ /* Expose brand colors to Tailwind utilities */
+ --color-peach: var(--color-peach);
+ --color-peach-dark: var(--color-peach-dark);
+ --color-sage: var(--color-sage);
+ --color-sage-dark: var(--color-sage-dark);
+ --color-cream: var(--color-cream);
+ --color-cream-dark: var(--color-cream-dark);
+ --color-warm-gray: var(--color-warm-gray);
+ --color-brown: var(--color-brown);
+ --color-brown-light: var(--color-brown-light);
+}
+
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: var(--font-geist-sans, Arial, Helvetica, sans-serif);
+ overflow-x: hidden;
+}
+
+/* Smooth scrolling + prevent horizontal overflow from decorative blobs */
+html {
+ scroll-behavior: smooth;
+ overflow-x: hidden;
+}
+
+/* Custom checkbox styling */
+input[type="checkbox"] {
+ accent-color: var(--color-peach);
+}
+
+/* File input hiding */
+input[type="file"] {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+/* Drag-over highlight */
+.drag-over {
+ border-color: var(--color-peach-dark) !important;
+ background-color: rgba(255, 181, 167, 0.1) !important;
+}
+
+/* Signature field font */
+.signature-field {
+ font-family: "Georgia", "Times New Roman", serif;
+ font-style: italic;
+ letter-spacing: 0.05em;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..67c7cd8
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,37 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+import VersionSwitcher from "@/components/shared/VersionSwitcher";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "I was Cute — Your Childhood Photos, Legally Licensed",
+ description: "Upload your childhood photos, clear the rights, and earn royalties.",
+};
+
+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">
+ {children}
+ <VersionSwitcher />
+ </body>
+ </html>
+ );
+}
diff --git a/src/app/licensor/layout.tsx b/src/app/licensor/layout.tsx
new file mode 100644
index 0000000..619ee3a
--- /dev/null
+++ b/src/app/licensor/layout.tsx
@@ -0,0 +1,76 @@
+import type { Metadata } from 'next'
+import Link from 'next/link'
+
+export const metadata: Metadata = {
+ title: 'Upload Your Photos — I was Cute',
+ description:
+ 'Upload your childhood photos, clear copyright and likeness rights, and start earning royalties.',
+}
+
+export default function LicensorLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+ <div className="min-h-screen flex flex-col" style={{ background: 'var(--color-cream)' }}>
+ {/* Header */}
+ <header
+ className="sticky top-0 z-50 flex items-center justify-between px-6 py-4 md:px-12 border-b"
+ style={{
+ background: 'rgba(255, 241, 230, 0.92)',
+ borderColor: 'var(--color-cream-dark)',
+ backdropFilter: 'blur(12px)',
+ WebkitBackdropFilter: 'blur(12px)',
+ }}
+ >
+ <Link
+ href="/"
+ className="text-xl font-black tracking-tighter transition-opacity hover:opacity-70"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ i was cute
+ </Link>
+
+ <div
+ className="text-sm font-medium tracking-wide"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Licensor Upload
+ </div>
+ </header>
+
+ {/* Page content */}
+ <div className="flex-1">{children}</div>
+
+ {/* Footer */}
+ <footer
+ className="py-6 px-6 text-center text-xs border-t"
+ style={{
+ color: 'var(--color-warm-gray)',
+ borderColor: 'var(--color-cream-dark)',
+ }}
+ >
+ <p>
+ By uploading, you agree to our{' '}
+ <Link
+ href="/terms"
+ className="underline hover:opacity-70 transition-opacity"
+ style={{ color: 'var(--color-peach-dark)' }}
+ >
+ Terms of Service
+ </Link>{' '}
+ and{' '}
+ <Link
+ href="/privacy"
+ className="underline hover:opacity-70 transition-opacity"
+ style={{ color: 'var(--color-peach-dark)' }}
+ >
+ Privacy Policy
+ </Link>
+ .
+ </p>
+ </footer>
+ </div>
+ )
+}
diff --git a/src/app/licensor/page.tsx b/src/app/licensor/page.tsx
new file mode 100644
index 0000000..08dbebc
--- /dev/null
+++ b/src/app/licensor/page.tsx
@@ -0,0 +1,95 @@
+'use client'
+
+import { useState } from 'react'
+import Stepper from '@/components/licensor/Stepper'
+import PhotoUpload from '@/components/licensor/PhotoUpload'
+import CopyrightInfo from '@/components/licensor/CopyrightInfo'
+import LikenessRelease from '@/components/licensor/LikenessRelease'
+import ReviewSubmit from '@/components/licensor/ReviewSubmit'
+import type { PhotoFile } from '@/components/licensor/PhotoUpload'
+import type { CopyrightData } from '@/components/licensor/CopyrightInfo'
+import type { LikenessData } from '@/components/licensor/LikenessRelease'
+
+const TOTAL_STEPS = 4
+
+function todayISOString() {
+ const d = new Date()
+ return d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
+}
+
+export default function LicensorPage() {
+ const [step, setStep] = useState(0)
+
+ // Step 1 — Photos
+ const [photos, setPhotos] = useState<PhotoFile[]>([])
+
+ // Step 2 — Copyright
+ const [copyrightInfo, setCopyrightInfo] = useState<CopyrightData>({
+ photographer: '',
+ relationship: '',
+ year: '',
+ hasPermission: '',
+ estateRights: false,
+ applyToAll: true,
+ })
+
+ // Step 3 — Likeness
+ const [likenessRelease, setLikenessRelease] = useState<LikenessData>({
+ isSubject: false,
+ isAdult: false,
+ editorial: false,
+ commercial: false,
+ aiTraining: false,
+ signature: '',
+ date: todayISOString(),
+ })
+
+ const next = () => setStep((s) => Math.min(s + 1, TOTAL_STEPS - 1))
+ const back = () => setStep((s) => Math.max(s - 1, 0))
+
+ return (
+ <div className="px-4 py-8 md:px-8 md:py-12 max-w-2xl mx-auto">
+ {/* Stepper */}
+ <Stepper currentStep={step} />
+
+ {/* Step content */}
+ <div className="mt-8">
+ {step === 0 && (
+ <PhotoUpload
+ photos={photos}
+ onPhotosChange={setPhotos}
+ onNext={next}
+ />
+ )}
+
+ {step === 1 && (
+ <CopyrightInfo
+ photos={photos}
+ copyrightInfo={copyrightInfo}
+ onCopyrightChange={setCopyrightInfo}
+ onNext={next}
+ onBack={back}
+ />
+ )}
+
+ {step === 2 && (
+ <LikenessRelease
+ likenessRelease={likenessRelease}
+ onLikenessChange={setLikenessRelease}
+ onNext={next}
+ onBack={back}
+ />
+ )}
+
+ {step === 3 && (
+ <ReviewSubmit
+ photos={photos}
+ copyrightInfo={copyrightInfo}
+ likenessRelease={likenessRelease}
+ onBack={back}
+ />
+ )}
+ </div>
+ </div>
+ )
+}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
new file mode 100644
index 0000000..a4a2490
--- /dev/null
+++ b/src/app/login/page.tsx
@@ -0,0 +1,242 @@
+'use client'
+
+import { useState } from 'react'
+import { motion } from 'framer-motion'
+import { Mail, Lock, Eye, EyeOff, ArrowRight } from 'lucide-react'
+import Link from 'next/link'
+
+export default function LoginPage() {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [showPassword, setShowPassword] = useState(false)
+ const [mode, setMode] = useState<'login' | 'signup'>('login')
+
+ return (
+ <div
+ className="min-h-screen flex flex-col items-center justify-center px-4 py-12 relative overflow-hidden"
+ style={{ background: 'var(--color-cream)' }}
+ >
+ {/* Decorative blobs */}
+ <div
+ className="absolute -top-32 -left-32 w-96 h-96 rounded-full opacity-20 blur-3xl pointer-events-none"
+ style={{ background: 'var(--color-peach)' }}
+ />
+ <div
+ className="absolute -bottom-40 -right-32 w-[30rem] h-[30rem] rounded-full opacity-15 blur-3xl pointer-events-none"
+ style={{ background: 'var(--color-sage)' }}
+ />
+
+ {/* Logo */}
+ <div className="mb-10 text-center">
+ <Link
+ href="/"
+ className="text-3xl font-black tracking-tighter transition-opacity hover:opacity-70"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ i was cute
+ </Link>
+ </div>
+
+ {/* Card */}
+ <div
+ className="relative z-10 w-full max-w-sm rounded-3xl p-8"
+ style={{ background: 'rgba(255,255,255,0.7)', border: '1px solid var(--color-cream-dark)' }}
+ >
+ {/* Mode toggle */}
+ <div
+ className="flex gap-1 p-1 rounded-2xl mb-8"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ {(['login', 'signup'] as const).map((m) => (
+ <button
+ key={m}
+ onClick={() => setMode(m)}
+ className="relative flex-1 py-2.5 text-sm font-semibold rounded-xl transition-all"
+ style={{
+ color: mode === m ? 'var(--color-brown)' : 'var(--color-warm-gray)',
+ }}
+ >
+ {mode === m && (
+ <motion.div
+ layoutId="auth-tab"
+ className="absolute inset-0 rounded-xl"
+ style={{ background: 'white', boxShadow: '0 1px 3px rgba(0,0,0,0.08)' }}
+ transition={{ type: 'spring', stiffness: 380, damping: 30 }}
+ />
+ )}
+ <span className="relative z-10">
+ {m === 'login' ? 'Sign In' : 'Sign Up'}
+ </span>
+ </button>
+ ))}
+ </div>
+
+ <form
+ onSubmit={(e) => {
+ e.preventDefault()
+ // TODO: wire up NextAuth.js
+ }}
+ className="flex flex-col gap-4"
+ >
+ {/* Email */}
+ <div className="flex flex-col gap-1.5">
+ <label
+ htmlFor="email"
+ className="text-sm font-semibold"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Email
+ </label>
+ <div className="relative">
+ <Mail
+ size={16}
+ className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none"
+ style={{ color: 'var(--color-warm-gray)' }}
+ />
+ <input
+ id="email"
+ type="email"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ placeholder="you@example.com"
+ autoComplete="email"
+ required
+ className="w-full pl-10 pr-4 py-3 rounded-2xl text-sm border outline-none transition-all focus:ring-2"
+ style={{
+ background: 'rgba(255,255,255,0.8)',
+ borderColor: 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ />
+ </div>
+ </div>
+
+ {/* Password */}
+ <div className="flex flex-col gap-1.5">
+ <label
+ htmlFor="password"
+ className="text-sm font-semibold"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Password
+ </label>
+ <div className="relative">
+ <Lock
+ size={16}
+ className="absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none"
+ style={{ color: 'var(--color-warm-gray)' }}
+ />
+ <input
+ id="password"
+ type={showPassword ? 'text' : 'password'}
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ placeholder={mode === 'signup' ? 'Create a password' : 'Your password'}
+ autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}
+ required
+ className="w-full pl-10 pr-12 py-3 rounded-2xl text-sm border outline-none transition-all focus:ring-2"
+ style={{
+ background: 'rgba(255,255,255,0.8)',
+ borderColor: 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ />
+ <button
+ type="button"
+ onClick={() => setShowPassword(!showPassword)}
+ className="absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-lg transition-all hover:scale-110"
+ style={{ color: 'var(--color-warm-gray)' }}
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
+ >
+ {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
+ </button>
+ </div>
+ </div>
+
+ {mode === 'signup' && (
+ <motion.div
+ initial={{ opacity: 0, height: 0 }}
+ animate={{ opacity: 1, height: 'auto' }}
+ className="overflow-hidden"
+ >
+ <label className="flex items-start gap-3 cursor-pointer">
+ <input
+ type="checkbox"
+ required
+ className="mt-1"
+ />
+ <p className="text-xs leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ I am 18 years or older and agree to the{' '}
+ <Link href="/terms" className="underline" style={{ color: 'var(--color-peach-dark)' }}>
+ Terms of Service
+ </Link>{' '}
+ and{' '}
+ <Link href="/privacy" className="underline" style={{ color: 'var(--color-peach-dark)' }}>
+ Privacy Policy
+ </Link>
+ .
+ </p>
+ </label>
+ </motion.div>
+ )}
+
+ {/* Submit */}
+ <motion.button
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ type="submit"
+ className="flex items-center justify-center gap-2 w-full py-3.5 rounded-3xl text-sm font-semibold tracking-wide mt-2 transition-all"
+ style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
+ >
+ {mode === 'login' ? 'Sign In' : 'Create Account'}
+ <ArrowRight size={14} />
+ </motion.button>
+ </form>
+
+ {/* Divider */}
+ <div className="flex items-center gap-3 my-6">
+ <div className="flex-1 h-px" style={{ background: 'var(--color-cream-dark)' }} />
+ <span className="text-xs font-medium" style={{ color: 'var(--color-warm-gray)' }}>
+ or
+ </span>
+ <div className="flex-1 h-px" style={{ background: 'var(--color-cream-dark)' }} />
+ </div>
+
+ {/* Google OAuth */}
+ <button
+ className="flex items-center justify-center gap-3 w-full py-3 rounded-2xl text-sm font-medium border transition-all hover:scale-[1.02]"
+ style={{
+ background: 'rgba(255,255,255,0.8)',
+ borderColor: 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ >
+ <svg width="18" height="18" viewBox="0 0 24 24">
+ <path
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
+ fill="#4285F4"
+ />
+ <path
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
+ fill="#34A853"
+ />
+ <path
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
+ fill="#FBBC05"
+ />
+ <path
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
+ fill="#EA4335"
+ />
+ </svg>
+ Continue with Google
+ </button>
+ </div>
+
+ {/* Footer note */}
+ <p className="mt-8 text-xs text-center" style={{ color: 'var(--color-warm-gray)' }}>
+ Must be 18+ to create an account. We never sell your data.
+ </p>
+ </div>
+ )
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..dce293f
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,284 @@
+'use client'
+
+import Link from 'next/link'
+import { motion } from 'framer-motion'
+import { ArrowRight, Moon, Layers, Camera } from 'lucide-react'
+
+const VERSIONS = [
+ {
+ path: '/v1',
+ label: 'Dark Split',
+ icon: Moon,
+ accent: '#1a1d2e',
+ accentText: '#FFB5A7',
+ desc: 'Professional dark panel with warm photo grid. Built for licensees and AI buyers.',
+ colors: ['#1a1d2e', '#FFB5A7', '#B5D5C5'],
+ tag: 'Business',
+ },
+ {
+ path: '/v2',
+ label: 'Glassmorphism',
+ icon: Layers,
+ accent: 'rgba(255,255,255,0.5)',
+ accentText: 'var(--color-brown)',
+ desc: 'Frosted glass over animated gradients. Modern, clean, Apple-inspired aesthetic.',
+ colors: ['#FFB5A7', '#B5D5C5', '#FFE0CB'],
+ tag: 'Modern',
+ },
+ {
+ path: '/v3',
+ label: 'Polaroid',
+ icon: Camera,
+ accent: 'var(--color-cream)',
+ accentText: 'var(--color-brown)',
+ desc: 'Scattered vintage photo frames with handwritten captions. Warm and nostalgic.',
+ colors: ['#FFB5A7', '#8BBCAA', '#F4846F'],
+ tag: 'Nostalgic',
+ },
+]
+
+function VersionCard({
+ version,
+ index,
+}: {
+ version: (typeof VERSIONS)[number]
+ index: number
+}) {
+ const Icon = version.icon
+ const isDark = index === 0
+
+ return (
+ <motion.div
+ whileHover={{ y: -8, scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ className="relative group"
+ >
+ <Link href={version.path} className="block no-underline">
+ <div
+ className="rounded-3xl overflow-hidden transition-shadow duration-300 group-hover:shadow-2xl"
+ style={{
+ border: isDark ? '1px solid rgba(255,255,255,0.1)' : '1px solid var(--color-cream-dark)',
+ }}
+ >
+ {/* Preview header — simulates the page's vibe */}
+ <div
+ className="relative h-56 flex flex-col items-center justify-center gap-3 px-6"
+ style={{
+ background: isDark
+ ? 'linear-gradient(135deg, #1a1d2e 0%, #2a2d3e 100%)'
+ : index === 1
+ ? `linear-gradient(135deg, ${version.colors[0]}44, ${version.colors[1]}44, ${version.colors[2]}44)`
+ : 'var(--color-cream)',
+ }}
+ >
+ {/* Decorative elements per version */}
+ {index === 1 && (
+ <>
+ <div
+ className="absolute top-4 left-4 w-24 h-24 rounded-full blur-2xl opacity-40"
+ style={{ background: version.colors[0] }}
+ />
+ <div
+ className="absolute bottom-4 right-4 w-20 h-20 rounded-full blur-2xl opacity-30"
+ style={{ background: version.colors[1] }}
+ />
+ {/* Glass card preview */}
+ <div
+ className="relative z-10 w-4/5 rounded-2xl px-5 py-4"
+ style={{
+ background: 'rgba(255,255,255,0.45)',
+ backdropFilter: 'blur(12px)',
+ border: '1px solid rgba(255,255,255,0.3)',
+ }}
+ >
+ <p className="text-xl font-black tracking-tighter" style={{ color: 'var(--color-brown)' }}>
+ i was <span style={{ color: 'var(--color-peach-dark)' }}>cute</span>
+ </p>
+ <p className="text-[10px] mt-1" style={{ color: 'var(--color-warm-gray)' }}>
+ Your childhood photos, legally licensed
+ </p>
+ </div>
+ </>
+ )}
+
+ {index === 0 && (
+ <div className="flex w-full gap-2 h-full py-4">
+ {/* Dark text side */}
+ <div className="flex-1 flex flex-col justify-center gap-2 px-3">
+ <p className="text-lg font-black tracking-tighter" style={{ color: 'white' }}>
+ Your childhood photos,{' '}
+ <span style={{ color: '#FFB5A7' }}>legally</span> licensed.
+ </p>
+ <div
+ className="self-start px-3 py-1.5 rounded-full text-[9px] font-bold"
+ style={{ background: '#FFB5A7', color: '#1a1d2e' }}
+ >
+ Start Uploading →
+ </div>
+ </div>
+ {/* Photo grid side */}
+ <div className="w-1/3 grid grid-cols-2 gap-1">
+ {version.colors.map((c, i) => (
+ <div key={i} className="rounded-lg" style={{ background: c === '#1a1d2e' ? '#FFE0CB' : c }} />
+ ))}
+ <div className="rounded-lg" style={{ background: '#FFE0CB' }} />
+ </div>
+ </div>
+ )}
+
+ {index === 2 && (
+ <div className="relative w-full h-full flex items-center justify-center">
+ {/* Scattered mini polaroids */}
+ {[
+ { x: '10%', y: '15%', rot: -8, color: version.colors[0] },
+ { x: '65%', y: '10%', rot: 5, color: version.colors[1] },
+ { x: '25%', y: '55%', rot: -4, color: version.colors[2] },
+ { x: '60%', y: '50%', rot: 10, color: version.colors[0] },
+ ].map((p, i) => (
+ <div
+ key={i}
+ className="absolute w-14 h-16 bg-white rounded-sm shadow-md p-1 pb-3"
+ style={{
+ left: p.x,
+ top: p.y,
+ transform: `rotate(${p.rot}deg)`,
+ }}
+ >
+ <div className="w-full h-full rounded-sm" style={{ background: p.color }} />
+ </div>
+ ))}
+ {/* Center text */}
+ <p
+ className="relative z-10 text-2xl font-bold italic"
+ style={{ fontFamily: 'Georgia, serif', color: 'var(--color-brown)' }}
+ >
+ i was cute
+ </p>
+ </div>
+ )}
+ </div>
+
+ {/* Card body */}
+ <div
+ className="px-6 py-5"
+ style={{
+ background: isDark ? '#1a1d2e' : 'white',
+ }}
+ >
+ <div className="flex items-center justify-between mb-3">
+ <div className="flex items-center gap-2">
+ <div
+ className="w-8 h-8 rounded-xl flex items-center justify-center"
+ style={{
+ background: isDark ? 'rgba(255,181,167,0.15)' : 'var(--color-cream-dark)',
+ }}
+ >
+ <Icon size={16} style={{ color: isDark ? '#FFB5A7' : 'var(--color-brown)' }} />
+ </div>
+ <h3
+ className="text-base font-bold"
+ style={{ color: isDark ? 'white' : 'var(--color-brown)' }}
+ >
+ {version.label}
+ </h3>
+ </div>
+ <span
+ className="text-[10px] font-bold tracking-widest uppercase px-2.5 py-1 rounded-full"
+ style={{
+ background: isDark ? 'rgba(255,181,167,0.12)' : 'var(--color-cream-dark)',
+ color: isDark ? '#FFB5A7' : 'var(--color-warm-gray)',
+ }}
+ >
+ {version.tag}
+ </span>
+ </div>
+
+ <p
+ className="text-sm leading-relaxed mb-4"
+ style={{ color: isDark ? 'rgba(255,255,255,0.5)' : 'var(--color-warm-gray)' }}
+ >
+ {version.desc}
+ </p>
+
+ {/* Color dots */}
+ <div className="flex items-center justify-between">
+ <div className="flex gap-1.5">
+ {version.colors.map((c, i) => (
+ <div
+ key={i}
+ className="w-4 h-4 rounded-full"
+ style={{ background: c, border: '2px solid rgba(255,255,255,0.2)' }}
+ />
+ ))}
+ </div>
+ <span
+ className="flex items-center gap-1 text-xs font-semibold"
+ style={{ color: isDark ? '#FFB5A7' : 'var(--color-peach-dark)' }}
+ >
+ Enter
+ <ArrowRight size={12} />
+ </span>
+ </div>
+ </div>
+ </div>
+ </Link>
+ </motion.div>
+ )
+}
+
+export default function HomePage() {
+ return (
+ <main
+ className="min-h-screen flex flex-col items-center px-4 py-12 md:py-20 overflow-x-hidden"
+ style={{ background: 'var(--color-cream)' }}
+ >
+ {/* Decorative blobs */}
+ <div
+ className="absolute -top-32 -left-32 w-96 h-96 rounded-full opacity-20 blur-3xl pointer-events-none"
+ style={{ background: 'var(--color-peach)' }}
+ />
+ <div
+ className="absolute -bottom-40 -right-32 w-[30rem] h-[30rem] rounded-full opacity-15 blur-3xl pointer-events-none"
+ style={{ background: 'var(--color-sage)' }}
+ />
+
+ {/* Header */}
+ <div className="relative z-10 text-center mb-12 md:mb-16 max-w-2xl">
+ <h1
+ className="text-5xl md:text-7xl font-black tracking-tighter leading-[0.9]"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ i was{' '}
+ <span style={{ color: 'var(--color-peach-dark)' }}>cute</span>
+ </h1>
+ <p
+ className="mt-4 text-lg md:text-xl font-light"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Choose your experience
+ </p>
+ <p
+ className="mt-2 text-sm max-w-md mx-auto"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Three unique designs, one marketplace. Pick the vibe that speaks to you.
+ </p>
+ </div>
+
+ {/* Version cards */}
+ <div className="relative z-10 grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl w-full">
+ {VERSIONS.map((v, i) => (
+ <VersionCard key={v.path} version={v} index={i} />
+ ))}
+ </div>
+
+ {/* Footer */}
+ <p
+ className="relative z-10 mt-12 text-xs"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Your childhood photos, legally licensed. © 2025 I was Cute.
+ </p>
+ </main>
+ )
+}
diff --git a/src/app/v1/page.tsx b/src/app/v1/page.tsx
new file mode 100644
index 0000000..10db494
--- /dev/null
+++ b/src/app/v1/page.tsx
@@ -0,0 +1,259 @@
+'use client'
+
+import Link from 'next/link'
+import { motion } from 'framer-motion'
+import {
+ ArrowRight,
+ Shield,
+ Camera,
+ DollarSign,
+ Sparkles,
+ Upload,
+ Scale,
+ Heart,
+} from 'lucide-react'
+
+/* ------------------------------------------------------------------ */
+/* V1 — Dark Split-Screen */
+/* Left: dark panel with text + CTAs. Right: warm photo grid collage. */
+/* Completely different aesthetic from V2 (glass) and V3 (polaroid). */
+/* ------------------------------------------------------------------ */
+
+const GRID_ITEMS = [
+ { color: '#FFB5A7', size: 'row-span-2', label: 'Age 3' },
+ { color: '#B5D5C5', size: '', label: 'Age 7' },
+ { color: '#FFE0CB', size: '', label: 'Age 5' },
+ { color: '#F4846F', size: 'col-span-2', label: 'Age 1' },
+ { color: '#8BBCAA', size: '', label: 'Age 9' },
+ { color: '#FFB5A7', size: 'row-span-2', label: 'Age 4' },
+ { color: '#FFE0CB', size: '', label: 'Age 6' },
+ { color: '#B5D5C5', size: '', label: 'Age 2' },
+]
+
+function PhotoGrid() {
+ return (
+ <div className="grid grid-cols-3 grid-rows-4 gap-2 h-full p-4">
+ {GRID_ITEMS.map((item, i) => (
+ <motion.div
+ key={i}
+ whileHover={{ scale: 1.04, zIndex: 10 }}
+ className={`rounded-2xl overflow-hidden relative cursor-pointer transition-shadow hover:shadow-2xl ${item.size}`}
+ style={{ background: item.color }}
+ >
+ <div className="absolute inset-0 flex items-end p-3">
+ <span
+ className="text-xs font-semibold px-2 py-1 rounded-lg"
+ style={{ background: 'rgba(0,0,0,0.35)', color: 'white' }}
+ >
+ {item.label}
+ </span>
+ </div>
+ </motion.div>
+ ))}
+ </div>
+ )
+}
+
+function StatPill({ icon: Icon, label }: { icon: typeof Shield; label: string }) {
+ return (
+ <div
+ className="flex items-center gap-2 px-4 py-2.5 rounded-full"
+ style={{ background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.1)' }}
+ >
+ <Icon size={13} style={{ color: '#FFB5A7' }} />
+ <span className="text-xs font-medium" style={{ color: 'rgba(255,255,255,0.7)' }}>
+ {label}
+ </span>
+ </div>
+ )
+}
+
+function StepCard({
+ number,
+ title,
+ desc,
+ icon: Icon,
+}: {
+ number: string
+ title: string
+ desc: string
+ icon: typeof Upload
+}) {
+ return (
+ <motion.div
+ whileHover={{ y: -4 }}
+ className="flex gap-4 p-5 rounded-2xl transition-all"
+ style={{ background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }}
+ >
+ <div
+ className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
+ style={{ background: 'rgba(255, 181, 167, 0.15)' }}
+ >
+ <Icon size={18} style={{ color: '#FFB5A7' }} />
+ </div>
+ <div>
+ <p className="text-sm font-bold" style={{ color: 'rgba(255,255,255,0.9)' }}>
+ <span style={{ color: '#FFB5A7' }}>{number}.</span> {title}
+ </p>
+ <p className="text-xs mt-1 leading-relaxed" style={{ color: 'rgba(255,255,255,0.45)' }}>
+ {desc}
+ </p>
+ </div>
+ </motion.div>
+ )
+}
+
+export default function V1Page() {
+ return (
+ <div className="min-h-screen flex flex-col lg:flex-row">
+ {/* Left panel — dark */}
+ <div
+ className="flex-1 flex flex-col justify-between px-8 py-10 md:px-16 md:py-14 lg:max-w-[55%]"
+ style={{ background: '#1a1d2e' }}
+ >
+ {/* Nav */}
+ <div className="flex items-center justify-between">
+ <span className="text-lg font-black tracking-tighter" style={{ color: 'white' }}>
+ i was cute
+ </span>
+ <div className="flex items-center gap-4 text-xs font-medium" style={{ color: 'rgba(255,255,255,0.5)' }}>
+ <Link href="/browse" className="hover:text-white transition-colors">Browse</Link>
+ <Link href="/licensor" className="hover:text-white transition-colors">Upload</Link>
+ <Link
+ href="/login"
+ className="flex items-center gap-1.5 px-4 py-2 rounded-full transition-all hover:scale-105"
+ style={{ background: '#FFB5A7', color: '#1a1d2e' }}
+ >
+ Sign In
+ </Link>
+ </div>
+ </div>
+
+ {/* Hero content */}
+ <div className="flex flex-col gap-8 my-12 lg:my-0">
+ {/* Badge */}
+ <div
+ className="inline-flex items-center gap-2 self-start px-3 py-1.5 rounded-full"
+ style={{ background: 'rgba(255, 181, 167, 0.12)', border: '1px solid rgba(255, 181, 167, 0.2)' }}
+ >
+ <Sparkles size={12} style={{ color: '#FFB5A7' }} />
+ <span className="text-xs font-semibold" style={{ color: '#FFB5A7' }}>
+ Dual-rights photo marketplace
+ </span>
+ </div>
+
+ {/* Headline */}
+ <div>
+ <h1
+ className="text-5xl md:text-7xl font-black tracking-tighter leading-[0.9]"
+ style={{ color: 'white' }}
+ >
+ Your childhood
+ <br />
+ photos,{' '}
+ <span style={{ color: '#FFB5A7' }}>legally</span>
+ <br />
+ licensed.
+ </h1>
+ <p
+ className="mt-6 text-base md:text-lg leading-relaxed max-w-md"
+ style={{ color: 'rgba(255,255,255,0.5)' }}
+ >
+ Upload your most precious memories. We verify copyright and likeness rights,
+ then connect you with advertisers, filmmakers, and AI companies who pay royalties.
+ </p>
+ </div>
+
+ {/* CTAs */}
+ <div className="flex flex-wrap gap-3">
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
+ <Link
+ href="/licensor"
+ className="inline-flex items-center gap-2 px-8 py-4 rounded-full text-sm font-bold tracking-wide transition-all"
+ style={{ background: '#FFB5A7', color: '#1a1d2e' }}
+ >
+ Start Uploading
+ <ArrowRight size={16} />
+ </Link>
+ </motion.div>
+ <motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }}>
+ <Link
+ href="/browse"
+ className="inline-flex items-center gap-2 px-8 py-4 rounded-full text-sm font-semibold tracking-wide transition-all"
+ style={{
+ background: 'transparent',
+ color: 'rgba(255,255,255,0.7)',
+ border: '1px solid rgba(255,255,255,0.15)',
+ }}
+ >
+ Browse Library
+ </Link>
+ </motion.div>
+ </div>
+
+ {/* Trust pills */}
+ <div className="flex flex-wrap gap-2">
+ <StatPill icon={Shield} label="Dual-rights verified" />
+ <StatPill icon={Camera} label="AI training ready" />
+ <StatPill icon={DollarSign} label="Instant royalties" />
+ </div>
+ </div>
+
+ {/* Bottom stats */}
+ <div
+ className="flex items-center gap-8 pt-6"
+ style={{ borderTop: '1px solid rgba(255,255,255,0.08)' }}
+ >
+ {[
+ { value: '10K+', label: 'Photos' },
+ { value: '500+', label: 'Licensors' },
+ { value: '100%', label: 'Verified' },
+ ].map(({ value, label }) => (
+ <div key={label}>
+ <p className="text-2xl font-black" style={{ color: '#FFB5A7' }}>{value}</p>
+ <p className="text-xs" style={{ color: 'rgba(255,255,255,0.4)' }}>{label}</p>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* Right panel — warm photo grid */}
+ <div
+ className="flex-1 hidden lg:flex flex-col"
+ style={{ background: 'var(--color-cream)' }}
+ >
+ <PhotoGrid />
+ </div>
+
+ {/* Mobile-only: photo grid preview + how it works */}
+ <div className="lg:hidden" style={{ background: 'var(--color-cream)' }}>
+ <div className="grid grid-cols-3 gap-2 p-4" style={{ height: '200px' }}>
+ {GRID_ITEMS.slice(0, 3).map((item, i) => (
+ <div
+ key={i}
+ className="rounded-xl"
+ style={{ background: item.color }}
+ />
+ ))}
+ </div>
+ </div>
+
+ <div
+ className="lg:hidden px-8 py-12"
+ style={{ background: '#1a1d2e' }}
+ >
+ <h2
+ className="text-xl font-bold mb-6"
+ style={{ color: 'rgba(255,255,255,0.9)' }}
+ >
+ How it works
+ </h2>
+ <div className="flex flex-col gap-3">
+ <StepCard number="1" title="Upload your photos" desc="Drag entire folders. We extract EXIF metadata automatically." icon={Upload} />
+ <StepCard number="2" title="Clear the rights" desc="Verify copyright (photographer) and likeness (you) in our guided wizard." icon={Scale} />
+ <StepCard number="3" title="Earn royalties" desc="Every license generates royalties deposited directly to your account." icon={Heart} />
+ </div>
+ </div>
+ </div>
+ )
+}
diff --git a/src/app/v2/page.tsx b/src/app/v2/page.tsx
new file mode 100644
index 0000000..451335f
--- /dev/null
+++ b/src/app/v2/page.tsx
@@ -0,0 +1,506 @@
+'use client'
+
+import Link from 'next/link'
+import { motion } from 'framer-motion'
+import { Camera, Shield, DollarSign, ArrowRight, Sparkles } from 'lucide-react'
+import clsx from 'clsx'
+
+// ---------------------------------------------------------------------------
+// AnimationStyles — injects CSS keyframes for blob motion + glass utilities.
+// Content is a static compile-time string with NO user-supplied data, so
+// rendering it via innerHTML carries zero XSS risk (same as any <style> tag
+// in a CSS file or styled-component). This is a standard Next.js / React
+// pattern for scoped global keyframes in a Client Component.
+// ---------------------------------------------------------------------------
+function AnimationStyles() {
+ const css = [
+ '@keyframes blob-float-a{0%{transform:translate(0px,0px) scale(1)}100%{transform:translate(40px,-60px) scale(1.08)}}',
+ '@keyframes blob-float-b{0%{transform:translate(0px,0px) scale(1)}100%{transform:translate(-50px,40px) scale(1.05)}}',
+ '@keyframes blob-float-c{0%{transform:translate(0px,0px) scale(1.02)}100%{transform:translate(30px,50px) scale(0.96)}}',
+ '@keyframes blob-float-d{0%{transform:translate(0px,0px) scale(1)}100%{transform:translate(-30px,-40px) scale(1.06)}}',
+ '@keyframes blob-float-e{0%{transform:translate(0px,0px) scale(0.98)}100%{transform:translate(20px,30px) scale(1.04)}}',
+ '@keyframes shimmer{0%{background-position:-200% center}100%{background-position:200% center}}',
+ '.blob-a{animation:blob-float-a 18s ease-in-out infinite alternate}',
+ '.blob-b{animation:blob-float-b 22s ease-in-out infinite alternate}',
+ '.blob-c{animation:blob-float-c 16s ease-in-out infinite alternate}',
+ '.blob-d{animation:blob-float-d 20s ease-in-out infinite alternate}',
+ '.blob-e{animation:blob-float-e 14s ease-in-out infinite alternate}',
+ '.glass{background:rgba(255,255,255,0.38);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.32)}',
+ '.glass-dark{background:rgba(255,255,255,0.22);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,0.24)}',
+ '.shimmer-text{background:linear-gradient(90deg,#3D405B 0%,#F4846F 30%,#8BBCAA 55%,#3D405B 80%);background-size:200% auto;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;animation:shimmer 5s linear infinite}',
+ '.btn-primary{background:linear-gradient(135deg,#FFB5A7 0%,#F4846F 100%);transition:transform 0.2s ease,box-shadow 0.2s ease,filter 0.2s ease}',
+ '.btn-primary:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 12px 40px rgba(244,132,111,0.45);filter:brightness(1.05)}',
+ '.btn-secondary{transition:transform 0.2s ease,box-shadow 0.2s ease,background 0.2s ease}',
+ '.btn-secondary:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 12px 32px rgba(139,188,170,0.35);background:rgba(255,255,255,0.55)}',
+ '.divider-v{width:1px;background:rgba(61,64,91,0.12);align-self:stretch}',
+ ].join('\n')
+
+ // React renders <style> tags safely — this is static content, not user data.
+ return <style>{css}</style>
+}
+
+// ---------------------------------------------------------------------------
+// Animated gradient mesh — five large blobs using pure CSS keyframes.
+// Fixed so it covers the entire viewport regardless of page scroll length.
+// ---------------------------------------------------------------------------
+function GradientMeshBackground() {
+ return (
+ <div
+ aria-hidden="true"
+ className="pointer-events-none fixed inset-0 overflow-hidden"
+ style={{ zIndex: 0 }}
+ >
+ {/* Blob A — top-left, peach */}
+ <div
+ className="blob-a absolute -top-24 -left-24 w-[520px] h-[520px] rounded-full blur-3xl"
+ style={{ background: '#FFB5A7', opacity: 0.42 }}
+ />
+ {/* Blob B — top-right, sage */}
+ <div
+ className="blob-b absolute -top-12 right-0 w-[480px] h-[480px] rounded-full blur-3xl"
+ style={{ background: '#B5D5C5', opacity: 0.38 }}
+ />
+ {/* Blob C — center, cream-dark */}
+ <div
+ className="blob-c absolute top-1/3 left-1/4 w-[400px] h-[400px] rounded-full blur-3xl"
+ style={{ background: '#FFE0CB', opacity: 0.50 }}
+ />
+ {/* Blob D — bottom-left, peach-dark */}
+ <div
+ className="blob-d absolute bottom-0 -left-12 w-[440px] h-[440px] rounded-full blur-3xl"
+ style={{ background: '#F4846F', opacity: 0.28 }}
+ />
+ {/* Blob E — bottom-right, sage-dark */}
+ <div
+ className="blob-e absolute -bottom-16 right-8 w-[460px] h-[460px] rounded-full blur-3xl"
+ style={{ background: '#8BBCAA', opacity: 0.35 }}
+ />
+ </div>
+ )
+}
+
+// ---------------------------------------------------------------------------
+// NavBar
+// ---------------------------------------------------------------------------
+function NavBar() {
+ return (
+ <nav
+ className="glass relative z-20 flex items-center justify-between px-6 py-4 md:px-12 mx-4 mt-4 rounded-2xl"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ <div className="flex items-center gap-2">
+ <Sparkles
+ size={18}
+ strokeWidth={1.8}
+ style={{ color: 'var(--color-peach-dark)' }}
+ />
+ <span
+ className="text-xl font-black tracking-tighter"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ i was cute
+ </span>
+ </div>
+
+ <div
+ className="hidden sm:flex items-center gap-6 text-sm font-medium"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ <Link href="/browse" className="hover:opacity-70 transition-opacity">
+ Browse
+ </Link>
+ <Link href="/licensor" className="hover:opacity-70 transition-opacity">
+ Upload
+ </Link>
+ <Link href="/login" className="hover:opacity-70 transition-opacity">
+ Login
+ </Link>
+ </div>
+
+ <Link
+ href="/licensor"
+ className="btn-primary inline-flex items-center gap-1.5 px-5 py-2.5 rounded-xl text-sm font-semibold"
+ style={{ color: 'white' }}
+ >
+ Get Started
+ <ArrowRight size={14} strokeWidth={2.5} />
+ </Link>
+ </nav>
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Hero Card — glassmorphism panel, centered on the page
+// ---------------------------------------------------------------------------
+function HeroCard() {
+ return (
+ <section className="relative z-10 flex flex-col items-center justify-center px-4 pt-16 pb-8 md:pt-24">
+ <motion.div
+ whileHover={{ scale: 1.005 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 30 }}
+ className="glass w-full max-w-3xl rounded-3xl px-8 py-14 md:px-16 md:py-20 flex flex-col items-center text-center gap-7"
+ style={{
+ boxShadow:
+ '0 32px 80px rgba(61,64,91,0.10), 0 0 0 1px rgba(255,255,255,0.30)',
+ }}
+ >
+ {/* Badge */}
+ <div
+ className="glass-dark inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-xs font-semibold tracking-widest uppercase"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ <span
+ className="w-1.5 h-1.5 rounded-full"
+ style={{ background: 'var(--color-peach-dark)' }}
+ />
+ Childhood Photo Marketplace
+ </div>
+
+ {/* Headline */}
+ <h1 className="text-6xl md:text-8xl font-black tracking-tighter leading-[0.9] select-none">
+ <span className="shimmer-text">i was</span>
+ <br />
+ <span style={{ color: 'var(--color-peach-dark)' }}>cute</span>
+ </h1>
+
+ {/* Tagline */}
+ <p
+ className="text-xl md:text-2xl font-light tracking-wide max-w-md leading-relaxed"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Your childhood photos, legally licensed and earning for you.
+ </p>
+
+ {/* Description */}
+ <p
+ className="text-base max-w-lg leading-relaxed"
+ style={{ color: 'var(--color-warm-gray)', opacity: 0.85 }}
+ >
+ Turn nostalgic memories into royalties. Upload, verify rights, and
+ let advertisers, filmmakers, and designers celebrate the real you —
+ at your most adorable.
+ </p>
+
+ {/* CTA buttons */}
+ <div className="flex flex-col sm:flex-row gap-3 mt-2 w-full sm:w-auto">
+ <Link
+ href="/licensor"
+ className="btn-primary inline-flex items-center justify-center gap-2 px-8 py-4 rounded-2xl text-base font-bold tracking-wide"
+ style={{ color: 'white' }}
+ >
+ <Camera size={18} strokeWidth={2} />
+ Upload Your Photos
+ </Link>
+ <Link
+ href="/browse"
+ className="btn-secondary glass-dark inline-flex items-center justify-center gap-2 px-8 py-4 rounded-2xl text-base font-semibold tracking-wide"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Browse Library
+ <ArrowRight size={16} strokeWidth={2} />
+ </Link>
+ </div>
+
+ {/* Trust micro-copy */}
+ <p
+ className="text-xs font-medium tracking-wide"
+ style={{ color: 'var(--color-warm-gray)', opacity: 0.6 }}
+ >
+ No credit card required · Free to upload · You
+ keep all rights
+ </p>
+ </motion.div>
+ </section>
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Stats Bar — glassmorphism strip with 3 metrics
+// ---------------------------------------------------------------------------
+const STATS = [
+ { value: '10K+', label: 'Photos Licensed' },
+ { value: '500+', label: 'Verified Licensors' },
+ { value: '100%', label: 'Rights Verified' },
+] as const
+
+function StatsBar() {
+ return (
+ <section className="relative z-10 px-4 py-4">
+ <div
+ className="glass mx-auto max-w-2xl rounded-2xl px-6 py-5 flex items-center justify-around gap-4"
+ style={{ boxShadow: '0 8px 32px rgba(61,64,91,0.08)' }}
+ >
+ {STATS.map((stat, i) => (
+ <div key={stat.label} className="flex items-center gap-4">
+ <div className="flex flex-col items-center gap-0.5">
+ <span
+ className="text-2xl md:text-3xl font-black tracking-tight"
+ style={{ color: 'var(--color-peach-dark)' }}
+ >
+ {stat.value}
+ </span>
+ <span
+ className="text-xs font-semibold tracking-widest uppercase"
+ style={{ color: 'var(--color-warm-gray)', opacity: 0.75 }}
+ >
+ {stat.label}
+ </span>
+ </div>
+ {i < STATS.length - 1 && (
+ <div className="divider-v hidden sm:block" />
+ )}
+ </div>
+ ))}
+ </div>
+ </section>
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Feature Grid — three glassmorphism cards
+// ---------------------------------------------------------------------------
+interface Feature {
+ icon: React.ReactNode
+ title: string
+ description: string
+ accent: string
+ step: string
+}
+
+const FEATURES: Feature[] = [
+ {
+ icon: <Camera size={28} strokeWidth={1.6} />,
+ title: 'Upload Photos',
+ description:
+ 'Drag and drop your childhood photos in any format. We handle compression, metadata extraction, and secure storage.',
+ accent: '#FFB5A7',
+ step: '01',
+ },
+ {
+ icon: <Shield size={28} strokeWidth={1.6} />,
+ title: 'Verify Rights',
+ description:
+ 'Our guided rights-clearance wizard walks you through copyright and likeness release in under five minutes.',
+ accent: '#B5D5C5',
+ step: '02',
+ },
+ {
+ icon: <DollarSign size={28} strokeWidth={1.6} />,
+ title: 'Earn Royalties',
+ description:
+ 'When your photo is licensed by a brand or filmmaker, royalties land directly in your account. Automatically.',
+ accent: '#FFE0CB',
+ step: '03',
+ },
+]
+
+function FeatureGrid() {
+ return (
+ <section className="relative z-10 px-4 py-10 md:py-16">
+ <div className="max-w-4xl mx-auto">
+ {/* Section header */}
+ <div className="text-center mb-10">
+ <h2
+ className="text-3xl md:text-4xl font-black tracking-tight mb-3"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ How it works
+ </h2>
+ <p
+ className="text-base max-w-md mx-auto leading-relaxed"
+ style={{ color: 'var(--color-warm-gray)', opacity: 0.8 }}
+ >
+ Three steps from photo to paycheck.
+ </p>
+ </div>
+
+ {/* Cards */}
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-5">
+ {FEATURES.map((feature) => (
+ <motion.div
+ key={feature.title}
+ whileHover={{ y: -6, scale: 1.01 }}
+ transition={{ type: 'spring', stiffness: 400, damping: 28 }}
+ className={clsx(
+ 'glass rounded-3xl p-8 flex flex-col gap-5 cursor-default',
+ )}
+ style={{ boxShadow: '0 8px 32px rgba(61,64,91,0.07)' }}
+ >
+ {/* Icon bubble */}
+ <div
+ className="w-14 h-14 rounded-2xl flex items-center justify-center flex-shrink-0"
+ style={{ background: feature.accent, color: 'var(--color-brown)' }}
+ >
+ {feature.icon}
+ </div>
+
+ {/* Step label */}
+ <span
+ className="text-xs font-bold tracking-widest uppercase"
+ style={{ color: 'var(--color-warm-gray)', opacity: 0.45 }}
+ >
+ Step {feature.step}
+ </span>
+
+ <div className="flex flex-col gap-2">
+ <h3
+ className="text-lg font-bold tracking-tight"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ {feature.title}
+ </h3>
+ <p
+ className="text-sm leading-relaxed"
+ style={{ color: 'var(--color-warm-gray)', opacity: 0.85 }}
+ >
+ {feature.description}
+ </p>
+ </div>
+ </motion.div>
+ ))}
+ </div>
+ </div>
+ </section>
+ )
+}
+
+// ---------------------------------------------------------------------------
+// CTA Banner — second conversion touchpoint
+// ---------------------------------------------------------------------------
+function CallToActionBanner() {
+ return (
+ <section className="relative z-10 px-4 py-8 md:py-12">
+ <motion.div
+ whileHover={{ scale: 1.008 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 30 }}
+ className="glass max-w-3xl mx-auto rounded-3xl px-8 py-12 md:px-16 flex flex-col items-center text-center gap-6"
+ style={{
+ boxShadow:
+ '0 20px 60px rgba(244,132,111,0.14), 0 0 0 1px rgba(255,255,255,0.28)',
+ background: 'rgba(255,241,230,0.45)',
+ }}
+ >
+ <h2
+ className="text-3xl md:text-4xl font-black tracking-tight"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Ready to be remembered?
+ </h2>
+ <p
+ className="text-base max-w-md leading-relaxed"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Join hundreds of people who have already turned their most charming
+ memories into a passive income stream.
+ </p>
+ <Link
+ href="/licensor"
+ className="btn-primary inline-flex items-center gap-2 px-10 py-4 rounded-2xl text-base font-bold"
+ style={{ color: 'white' }}
+ >
+ Start Uploading — It's Free
+ <ArrowRight size={16} strokeWidth={2.5} />
+ </Link>
+ </motion.div>
+ </section>
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Footer
+// ---------------------------------------------------------------------------
+const FOOTER_LINKS = [
+ { href: '/browse', label: 'Browse' },
+ { href: '/licensor', label: 'Upload' },
+ { href: '/login', label: 'Login' },
+ { href: '#', label: 'Privacy' },
+ { href: '#', label: 'Terms' },
+] as const
+
+function Footer() {
+ return (
+ <footer className="relative z-10 px-6 py-10 flex flex-col items-center gap-4">
+ <div className="flex items-center gap-2 mb-1">
+ <Sparkles
+ size={14}
+ strokeWidth={1.8}
+ style={{ color: 'var(--color-peach-dark)' }}
+ />
+ <span
+ className="text-sm font-black tracking-tighter"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ i was cute
+ </span>
+ </div>
+
+ <nav className="flex flex-wrap items-center justify-center gap-5">
+ {FOOTER_LINKS.map((link) => (
+ <Link
+ key={link.label}
+ href={link.href}
+ className="text-xs font-medium hover:opacity-70 transition-opacity"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ {link.label}
+ </Link>
+ ))}
+ </nav>
+
+ <p
+ className="text-xs"
+ style={{ color: 'var(--color-warm-gray)', opacity: 0.55 }}
+ >
+ © {new Date().getFullYear()} I was Cute. All rights reserved.
+ </p>
+ </footer>
+ )
+}
+
+// ---------------------------------------------------------------------------
+// Page — root export
+// ---------------------------------------------------------------------------
+
+/**
+ * /v2 — Glassmorphism + Animated Gradient Mesh landing page.
+ *
+ * Usage: navigate to /v2 in the running Next.js dev server.
+ *
+ * Architecture notes:
+ * - 'use client' is required for framer-motion whileHover.
+ * - Blob animations are pure CSS (@keyframes), zero JS runtime cost.
+ * - Glass utility classes are injected via <style>{css}</style> — static
+ * string, no XSS surface — using the same mechanism as styled-jsx.
+ * - framer-motion is used ONLY for whileHover on interactive cards.
+ * No initial/animate props to avoid SSR hydration mismatches.
+ * - Fixed-position mesh background sits at z-index 0; page content at z-10+.
+ */
+export default function V2Page() {
+ return (
+ <>
+ <AnimationStyles />
+
+ {/*
+ Root wrapper: `position: relative` establishes a stacking context so
+ the fixed blobs render behind page content. overflow-x-hidden prevents
+ the translated blobs from creating a horizontal scrollbar.
+ */}
+ <div
+ className="relative min-h-screen flex flex-col overflow-x-hidden"
+ style={{ background: 'var(--color-cream)' }}
+ >
+ <GradientMeshBackground />
+
+ {/* Page content — stacks above the fixed mesh */}
+ <div className="relative z-10 flex flex-col flex-1">
+ <NavBar />
+ <HeroCard />
+ <StatsBar />
+ <FeatureGrid />
+ <CallToActionBanner />
+ <Footer />
+ </div>
+ </div>
+ </>
+ )
+}
diff --git a/src/app/v3/page.tsx b/src/app/v3/page.tsx
new file mode 100644
index 0000000..e44d36e
--- /dev/null
+++ b/src/app/v3/page.tsx
@@ -0,0 +1,417 @@
+'use client'
+
+import { motion } from 'framer-motion'
+import { ArrowRight, Upload, ShieldCheck, FileSignature, DollarSign } from 'lucide-react'
+import Link from 'next/link'
+
+// Polaroid data — position, rotation, color swatch, caption
+type PolaroidData = {
+ id: number
+ top: string
+ left?: string
+ right?: string
+ rotate: number
+ color: string
+ caption: string
+}
+
+const POLAROIDS: PolaroidData[] = [
+ { id: 1, top: '4%', left: '2%', rotate: -11, color: '#FFB5A7', caption: "Summer '92" },
+ { id: 2, top: '2%', right: '3%', rotate: 9, color: '#B5D5C5', caption: 'Birthday party' },
+ { id: 3, top: '28%', left: '0%', rotate: -7, color: '#FFE0CB', caption: 'Beach vacation' },
+ { id: 4, top: '30%', right: '1%', rotate: 12, color: '#FFB5A7', caption: "Halloween '88" },
+ { id: 5, top: '58%', left: '3%', rotate: 5, color: '#B5D5C5', caption: 'First day of school' },
+ { id: 6, top: '60%', right: '2%', rotate: -9, color: '#FFE0CB', caption: 'Family road trip' },
+ { id: 7, top: '82%', left: '6%', rotate: 8, color: '#F4846F', caption: "Grandma's house" },
+ { id: 8, top: '84%', right: '5%', rotate: -6, color: '#8BBCAA', caption: 'Little league' },
+]
+
+const HOW_IT_WORKS = [
+ {
+ step: 1,
+ icon: Upload,
+ title: 'Upload',
+ desc: 'Drag & drop your childhood photos — prints, scans, or phone shots',
+ accent: '#FFB5A7',
+ },
+ {
+ step: 2,
+ icon: ShieldCheck,
+ title: 'Copyright Check',
+ desc: "We verify who holds the copyright — you, a family member, or the photographer",
+ accent: '#B5D5C5',
+ },
+ {
+ step: 3,
+ icon: FileSignature,
+ title: 'Likeness Release',
+ desc: 'Sign a simple digital release so buyers can license legally',
+ accent: '#FFE0CB',
+ },
+ {
+ step: 4,
+ icon: DollarSign,
+ title: 'Earn',
+ desc: 'Royalties hit your account every time a license is sold',
+ accent: '#F4846F',
+ },
+]
+
+function Polaroid({ polaroid }: { polaroid: PolaroidData }) {
+ const posStyle: React.CSSProperties = {
+ position: 'absolute',
+ top: polaroid.top,
+ left: polaroid.left,
+ right: polaroid.right,
+ transform: `rotate(${polaroid.rotate}deg)`,
+ zIndex: 1,
+ }
+
+ return (
+ <motion.div
+ style={posStyle}
+ whileHover={{ scale: 1.08, rotate: 0, zIndex: 20 }}
+ transition={{ type: 'spring', stiffness: 260, damping: 20 }}
+ className="w-28 md:w-36 bg-white p-2 pb-8 shadow-lg cursor-default select-none"
+ >
+ {/* Photo placeholder */}
+ <div
+ className="w-full aspect-square"
+ style={{ background: polaroid.color, opacity: 0.85 }}
+ aria-hidden="true"
+ />
+ {/* Caption */}
+ <p
+ className="mt-2 text-center text-xs leading-tight"
+ style={{
+ fontFamily: 'Georgia, "Times New Roman", serif',
+ fontStyle: 'italic',
+ color: '#6B705C',
+ }}
+ >
+ {polaroid.caption}
+ </p>
+ </motion.div>
+ )
+}
+
+export default function V3Page() {
+ return (
+ <main
+ className="min-h-screen flex flex-col overflow-x-hidden"
+ style={{ background: 'var(--color-cream)' }}
+ >
+ {/* ── NAV ─────────────────────────────────────────── */}
+ <nav className="relative z-50 flex items-center justify-between px-6 py-5 md:px-12">
+ <span
+ className="text-2xl"
+ style={{
+ fontFamily: 'Georgia, "Times New Roman", serif',
+ fontStyle: 'italic',
+ fontWeight: 700,
+ color: 'var(--color-brown)',
+ }}
+ >
+ i was cute
+ </span>
+ <div
+ className="flex gap-5 text-sm font-medium"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ <Link href="/browse" className="hover:opacity-70 transition-opacity">
+ Browse
+ </Link>
+ <Link
+ href="/licensor"
+ className="hover:opacity-70 transition-opacity"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Upload
+ </Link>
+ </div>
+ </nav>
+
+ {/* ── HERO + POLAROID COLLAGE ──────────────────────── */}
+ <section
+ className="relative flex-1 flex flex-col items-center justify-center text-center px-6 py-28 md:py-40 overflow-hidden"
+ style={{ minHeight: '90vh' }}
+ >
+ {/* Scattered polaroids */}
+ {POLAROIDS.map((p) => (
+ <Polaroid key={p.id} polaroid={p} />
+ ))}
+
+ {/* Soft vignette behind copy so text stays readable */}
+ <div
+ aria-hidden="true"
+ className="absolute inset-0 pointer-events-none z-10"
+ style={{
+ background:
+ 'radial-gradient(ellipse 60% 55% at 50% 50%, rgba(255,241,230,0.92) 40%, transparent 100%)',
+ }}
+ />
+
+ {/* Center copy */}
+ <div className="relative z-30 flex flex-col items-center gap-6 max-w-2xl mx-auto">
+ {/* Wordmark */}
+ <h1
+ className="leading-none select-none"
+ style={{
+ fontFamily: 'Georgia, "Times New Roman", serif',
+ fontStyle: 'italic',
+ fontWeight: 700,
+ fontSize: 'clamp(4rem, 14vw, 9rem)',
+ color: 'var(--color-brown)',
+ textShadow: '2px 3px 0px rgba(61,64,91,0.08)',
+ }}
+ >
+ i was cute
+ </h1>
+
+ {/* Tagline */}
+ <p
+ className="text-lg md:text-xl font-light tracking-wide"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Your childhood photos, legally licensed
+ </p>
+
+ {/* Description */}
+ <p
+ className="text-base md:text-lg leading-relaxed max-w-lg"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Remember when you were adorable? So does the internet. Upload your
+ childhood photos, verify the rights, and earn royalties every time
+ they're licensed.
+ </p>
+
+ {/* CTAs */}
+ <div className="flex flex-col sm:flex-row gap-4 mt-2">
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.97 }}>
+ <Link
+ href="/licensor"
+ className="inline-flex items-center justify-center gap-2 px-8 py-4 rounded-full text-base font-semibold tracking-wide"
+ style={{
+ background: 'var(--color-peach)',
+ color: 'var(--color-brown)',
+ boxShadow: '3px 4px 0px #F4846F',
+ }}
+ >
+ Start Uploading
+ <ArrowRight size={18} />
+ </Link>
+ </motion.div>
+
+ <motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.97 }}>
+ <Link
+ href="#how-it-works"
+ className="inline-flex items-center justify-center gap-2 px-8 py-4 rounded-full text-base font-semibold tracking-wide border-2 transition-colors hover:bg-white/60"
+ style={{
+ borderColor: 'var(--color-sage-dark)',
+ color: 'var(--color-warm-gray)',
+ }}
+ >
+ See How It Works
+ </Link>
+ </motion.div>
+ </div>
+ </div>
+ </section>
+
+ {/* ── HOW IT WORKS ────────────────────────────────── */}
+ <section
+ id="how-it-works"
+ className="py-20 px-6"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ <div className="max-w-5xl mx-auto">
+ <h2
+ className="text-center text-3xl md:text-4xl mb-14"
+ style={{
+ fontFamily: 'Georgia, "Times New Roman", serif',
+ fontStyle: 'italic',
+ fontWeight: 700,
+ color: 'var(--color-brown)',
+ }}
+ >
+ How it works
+ </h2>
+
+ {/* Timeline row */}
+ <div className="relative grid grid-cols-1 md:grid-cols-4 gap-6">
+ {/* Connector line — desktop only */}
+ <div
+ aria-hidden="true"
+ className="hidden md:block absolute top-8 left-[12.5%] right-[12.5%] h-px"
+ style={{
+ borderTop: '2px dashed #B5D5C5',
+ zIndex: 0,
+ }}
+ />
+
+ {HOW_IT_WORKS.map(({ step, icon: Icon, title, desc, accent }) => (
+ <motion.div
+ key={step}
+ whileHover={{ y: -4 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 22 }}
+ className="relative flex flex-col items-center text-center bg-white rounded-2xl px-5 pt-0 pb-7 shadow-sm"
+ style={{
+ borderTop: `4px solid ${accent}`,
+ zIndex: 1,
+ }}
+ >
+ {/* Step badge */}
+ <div
+ className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold -mt-5 mb-4 shadow-md"
+ style={{
+ background: accent,
+ color: 'var(--color-brown)',
+ }}
+ >
+ {step}
+ </div>
+
+ <Icon
+ size={28}
+ strokeWidth={1.5}
+ style={{ color: 'var(--color-warm-gray)' }}
+ className="mb-3"
+ />
+
+ <h3
+ className="text-base font-bold mb-2"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ {title}
+ </h3>
+ <p
+ className="text-sm leading-relaxed"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ {desc}
+ </p>
+ </motion.div>
+ ))}
+ </div>
+ </div>
+ </section>
+
+ {/* ── TESTIMONIAL ─────────────────────────────────── */}
+ <section
+ className="py-20 px-6 flex flex-col items-center text-center"
+ style={{ background: 'var(--color-cream)' }}
+ >
+ <div className="max-w-2xl mx-auto relative">
+ {/* Decorative open quote */}
+ <span
+ aria-hidden="true"
+ className="absolute -top-6 -left-4 text-8xl leading-none pointer-events-none select-none"
+ style={{ color: 'var(--color-peach)', opacity: 0.6, fontFamily: 'Georgia, serif' }}
+ >
+ “
+ </span>
+
+ <blockquote>
+ <p
+ className="text-xl md:text-2xl leading-relaxed mb-6 relative z-10"
+ style={{
+ fontFamily: 'Georgia, "Times New Roman", serif',
+ fontStyle: 'italic',
+ color: 'var(--color-brown)',
+ }}
+ >
+ I uploaded my baby photos and earned $47 in the first month!
+ Honestly I wasn't even sure it would work — now I check my
+ dashboard every morning like it's a slot machine.
+ </p>
+ <footer
+ className="text-sm font-semibold tracking-wide"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ — Jessica T., uploaded 34 photos in March
+ </footer>
+ </blockquote>
+
+ {/* Decorative close quote */}
+ <span
+ aria-hidden="true"
+ className="absolute -bottom-10 -right-4 text-8xl leading-none pointer-events-none select-none"
+ style={{ color: 'var(--color-sage)', opacity: 0.6, fontFamily: 'Georgia, serif' }}
+ >
+ ”
+ </span>
+ </div>
+ </section>
+
+ {/* ── MINI CTA STRIP ──────────────────────────────── */}
+ <section
+ className="py-16 px-6 text-center"
+ style={{ background: 'var(--color-peach)', opacity: 0.92 }}
+ >
+ <h2
+ className="text-2xl md:text-3xl mb-3"
+ style={{
+ fontFamily: 'Georgia, "Times New Roman", serif',
+ fontStyle: 'italic',
+ fontWeight: 700,
+ color: 'var(--color-brown)',
+ }}
+ >
+ Those photos deserve more than a dusty drawer.
+ </h2>
+ <p
+ className="text-base mb-8"
+ style={{ color: 'var(--color-brown)', opacity: 0.75 }}
+ >
+ Join hundreds of people already earning from their childhood memories.
+ </p>
+ <motion.div
+ className="inline-block"
+ whileHover={{ scale: 1.06 }}
+ whileTap={{ scale: 0.96 }}
+ >
+ <Link
+ href="/licensor"
+ className="inline-flex items-center gap-2 px-9 py-4 rounded-full text-base font-semibold"
+ style={{
+ background: 'var(--color-brown)',
+ color: 'var(--color-cream)',
+ boxShadow: '3px 4px 0px rgba(61,64,91,0.3)',
+ }}
+ >
+ Get Started — it's free
+ <ArrowRight size={18} />
+ </Link>
+ </motion.div>
+ </section>
+
+ {/* ── FOOTER ──────────────────────────────────────── */}
+ <footer
+ className="py-8 px-6 text-center text-sm flex flex-col items-center gap-2"
+ style={{ color: 'var(--color-warm-gray)', background: 'var(--color-cream-dark)' }}
+ >
+ <span
+ style={{
+ fontFamily: 'Georgia, "Times New Roman", serif',
+ fontStyle: 'italic',
+ fontWeight: 700,
+ color: 'var(--color-brown)',
+ fontSize: '1.1rem',
+ }}
+ >
+ i was cute
+ </span>
+ <div className="flex gap-5 text-xs">
+ <Link href="#" className="hover:opacity-70 transition-opacity">Privacy</Link>
+ <Link href="#" className="hover:opacity-70 transition-opacity">Terms</Link>
+ <Link href="#" className="hover:opacity-70 transition-opacity">Help</Link>
+ <Link href="/browse" className="hover:opacity-70 transition-opacity">Browse</Link>
+ </div>
+ <p className="text-xs opacity-60 mt-1">
+ © 2025 I was Cute. All rights reserved. Your memories, your royalties.
+ </p>
+ </footer>
+ </main>
+ )
+}
diff --git a/src/components/licensor/CopyrightInfo.tsx b/src/components/licensor/CopyrightInfo.tsx
new file mode 100644
index 0000000..3a78505
--- /dev/null
+++ b/src/components/licensor/CopyrightInfo.tsx
@@ -0,0 +1,534 @@
+'use client'
+
+import { useState } from 'react'
+import { motion, AnimatePresence } from 'framer-motion'
+import { ChevronDown, ChevronLeft, ChevronRight, Info } from 'lucide-react'
+import clsx from 'clsx'
+import type { PhotoFile } from './PhotoUpload'
+
+export interface CopyrightData {
+ photographer: string
+ relationship: string
+ year: string
+ hasPermission: string
+ estateRights: boolean
+ applyToAll: boolean
+}
+
+interface CopyrightInfoProps {
+ photos: PhotoFile[]
+ copyrightInfo: CopyrightData
+ onCopyrightChange: (info: CopyrightData) => void
+ onNext: () => void
+ onBack: () => void
+}
+
+const RELATIONSHIP_OPTIONS = [
+ { value: '', label: 'Select who took this photo' },
+ { value: 'parent', label: 'My parent' },
+ { value: 'family', label: 'Other family member' },
+ { value: 'professional', label: 'Professional photographer' },
+ { value: 'unknown', label: "I don't know" },
+]
+
+const PERMISSION_OPTIONS = [
+ { value: '', label: 'Select an option' },
+ { value: 'yes', label: 'Yes, I have permission' },
+ { value: 'no', label: 'No, I do not have explicit permission' },
+ { value: 'deceased', label: 'The photographer is deceased' },
+]
+
+const CURRENT_YEAR = new Date().getFullYear()
+
+function SelectField({
+ label,
+ value,
+ options,
+ onChange,
+ required,
+ id,
+}: {
+ label: string
+ value: string
+ options: { value: string; label: string }[]
+ onChange: (v: string) => void
+ required?: boolean
+ id: string
+}) {
+ return (
+ <div className="flex flex-col gap-1.5">
+ <label
+ htmlFor={id}
+ className="text-sm font-semibold tracking-wide"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ {label}
+ {required && (
+ <span className="ml-1" style={{ color: 'var(--color-peach-dark)' }} aria-hidden>
+ *
+ </span>
+ )}
+ </label>
+ <div className="relative">
+ <select
+ id={id}
+ value={value}
+ onChange={(e) => onChange(e.target.value)}
+ required={required}
+ className={clsx(
+ 'w-full appearance-none px-4 py-3 pr-10 rounded-2xl text-sm',
+ 'border outline-none transition-all duration-200',
+ 'focus:ring-2'
+ )}
+ style={{
+ background: 'rgba(255,255,255,0.8)',
+ borderColor: 'var(--color-cream-dark)',
+ color: value ? 'var(--color-brown)' : 'var(--color-warm-gray)',
+ }}
+ >
+ {options.map((opt) => (
+ <option key={opt.value} value={opt.value} disabled={opt.value === ''}>
+ {opt.label}
+ </option>
+ ))}
+ </select>
+ <ChevronDown
+ size={16}
+ className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none"
+ style={{ color: 'var(--color-warm-gray)' }}
+ />
+ </div>
+ </div>
+ )
+}
+
+function TextField({
+ label,
+ value,
+ onChange,
+ placeholder,
+ type = 'text',
+ min,
+ max,
+ id,
+ optional,
+}: {
+ label: string
+ value: string
+ onChange: (v: string) => void
+ placeholder?: string
+ type?: string
+ min?: number
+ max?: number
+ id: string
+ optional?: boolean
+}) {
+ return (
+ <div className="flex flex-col gap-1.5">
+ <label
+ htmlFor={id}
+ className="text-sm font-semibold tracking-wide"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ {label}
+ {optional && (
+ <span className="ml-1.5 text-xs font-normal" style={{ color: 'var(--color-warm-gray)' }}>
+ (optional)
+ </span>
+ )}
+ </label>
+ <input
+ id={id}
+ type={type}
+ value={value}
+ onChange={(e) => onChange(e.target.value)}
+ placeholder={placeholder}
+ min={min}
+ max={max}
+ className={clsx(
+ 'w-full px-4 py-3 rounded-2xl text-sm',
+ 'border outline-none transition-all duration-200',
+ 'focus:ring-2'
+ )}
+ style={{
+ background: 'rgba(255,255,255,0.8)',
+ borderColor: 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ />
+ </div>
+ )
+}
+
+function CopyrightForm({
+ data,
+ onChange,
+ photoIndex,
+ totalPhotos,
+}: {
+ data: CopyrightData
+ onChange: (patch: Partial<CopyrightData>) => void
+ photoIndex?: number
+ totalPhotos?: number
+}) {
+ const idSuffix = photoIndex !== undefined ? `-${photoIndex}` : '-all'
+
+ return (
+ <div className="flex flex-col gap-5">
+ {photoIndex !== undefined && totalPhotos !== undefined && totalPhotos > 1 && (
+ <p
+ className="text-sm font-medium"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Photo {photoIndex + 1} of {totalPhotos}
+ </p>
+ )}
+
+ <SelectField
+ id={`relationship${idSuffix}`}
+ label="Who took this photo?"
+ value={data.relationship}
+ options={RELATIONSHIP_OPTIONS}
+ onChange={(v) => onChange({ relationship: v })}
+ required
+ />
+
+ <AnimatePresence>
+ {data.relationship !== 'unknown' && (
+ <motion.div
+ initial={{ opacity: 0, height: 0 }}
+ animate={{ opacity: 1, height: 'auto' }}
+ exit={{ opacity: 0, height: 0 }}
+ transition={{ duration: 0.2 }}
+ className="overflow-hidden"
+ >
+ <TextField
+ id={`photographer${idSuffix}`}
+ label="Photographer name"
+ value={data.photographer}
+ onChange={(v) => onChange({ photographer: v })}
+ placeholder="e.g. Mom, Grandpa Joe, Jane Smith Photography"
+ optional
+ />
+ </motion.div>
+ )}
+ </AnimatePresence>
+
+ <TextField
+ id={`year${idSuffix}`}
+ label="Approximate year photo was taken"
+ value={data.year}
+ onChange={(v) => onChange({ year: v })}
+ type="number"
+ placeholder={`e.g. ${CURRENT_YEAR - 25}`}
+ min={1900}
+ max={CURRENT_YEAR}
+ />
+
+ <SelectField
+ id={`permission${idSuffix}`}
+ label="Do you have permission from the photographer?"
+ value={data.hasPermission}
+ options={PERMISSION_OPTIONS}
+ onChange={(v) => onChange({ hasPermission: v })}
+ required
+ />
+
+ <AnimatePresence>
+ {data.hasPermission === 'deceased' && (
+ <motion.div
+ initial={{ opacity: 0, height: 0 }}
+ animate={{ opacity: 1, height: 'auto' }}
+ exit={{ opacity: 0, height: 0 }}
+ transition={{ duration: 0.2 }}
+ className="overflow-hidden"
+ >
+ <label
+ className="flex items-start gap-3 cursor-pointer group"
+ htmlFor={`estate${idSuffix}`}
+ >
+ <div className="relative mt-0.5">
+ <input
+ id={`estate${idSuffix}`}
+ type="checkbox"
+ checked={data.estateRights}
+ onChange={(e) => onChange({ estateRights: e.target.checked })}
+ className="sr-only"
+ />
+ <div
+ className={clsx(
+ 'w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all',
+ data.estateRights ? 'border-transparent' : ''
+ )}
+ style={{
+ background: data.estateRights ? 'var(--color-sage)' : 'white',
+ borderColor: data.estateRights ? 'var(--color-sage)' : 'var(--color-cream-dark)',
+ }}
+ aria-hidden
+ >
+ {data.estateRights && (
+ <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
+ <path
+ d="M2 5.5l2.5 2.5L9 3"
+ stroke="white"
+ strokeWidth="1.6"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ </svg>
+ )}
+ </div>
+ </div>
+ <div>
+ <p
+ className="text-sm font-semibold"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ I have evidence of estate rights
+ </p>
+ <p className="text-xs mt-0.5" style={{ color: 'var(--color-warm-gray)' }}>
+ e.g. will, inheritance document, or written permission from the estate executor
+ </p>
+ </div>
+ </label>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ )
+}
+
+export default function CopyrightInfo({
+ photos,
+ copyrightInfo,
+ onCopyrightChange,
+ onNext,
+ onBack,
+}: CopyrightInfoProps) {
+ const [activePhotoIndex, setActivePhotoIndex] = useState(0)
+ const multiPhoto = photos.length > 1
+
+ // Per-photo overrides (index → CopyrightData)
+ const [perPhoto, setPerPhoto] = useState<Record<number, Partial<CopyrightData>>>({})
+
+ const handleBatchChange = (patch: Partial<CopyrightData>) => {
+ onCopyrightChange({ ...copyrightInfo, ...patch })
+ }
+
+ const handlePerPhotoChange = (index: number, patch: Partial<CopyrightData>) => {
+ setPerPhoto((prev) => ({
+ ...prev,
+ [index]: { ...(prev[index] ?? {}), ...patch },
+ }))
+ }
+
+ const getPhotoData = (index: number): CopyrightData => ({
+ ...copyrightInfo,
+ ...(perPhoto[index] ?? {}),
+ })
+
+ const canProceed =
+ copyrightInfo.relationship !== '' && copyrightInfo.hasPermission !== ''
+
+ return (
+ <div className="flex flex-col gap-6">
+ {/* Header */}
+ <div>
+ <h2
+ className="text-2xl md:text-3xl font-bold tracking-tight"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Copyright Information
+ </h2>
+ <p className="mt-2 text-base" style={{ color: 'var(--color-warm-gray)' }}>
+ Help us understand who owns the copyright to your photos.
+ </p>
+ </div>
+
+ {/* Info banner */}
+ <div
+ className="flex items-start gap-3 p-4 rounded-2xl"
+ style={{ background: 'rgba(181, 213, 197, 0.25)', border: '1px solid var(--color-sage)' }}
+ role="note"
+ >
+ <Info size={16} className="mt-0.5 shrink-0" style={{ color: 'var(--color-sage-dark)' }} />
+ <p className="text-sm leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ In most cases, the person who pressed the shutter button owns the copyright — not the
+ person in the photo. We need this information to ensure your uploads are compliant.
+ </p>
+ </div>
+
+ {/* Apply-to-all toggle */}
+ {multiPhoto && (
+ <label
+ className="flex items-center gap-3 cursor-pointer"
+ htmlFor="apply-all"
+ >
+ <div className="relative">
+ <input
+ id="apply-all"
+ type="checkbox"
+ checked={copyrightInfo.applyToAll}
+ onChange={(e) => handleBatchChange({ applyToAll: e.target.checked })}
+ className="sr-only"
+ />
+ <div
+ className="w-10 h-6 rounded-full transition-colors relative"
+ style={{
+ background: copyrightInfo.applyToAll
+ ? 'var(--color-peach)'
+ : 'var(--color-cream-dark)',
+ }}
+ aria-hidden
+ >
+ <div
+ className="absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-all duration-200"
+ style={{
+ left: copyrightInfo.applyToAll ? '1.25rem' : '0.25rem',
+ }}
+ />
+ </div>
+ </div>
+ <div>
+ <p
+ className="text-sm font-semibold"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Apply same info to all {photos.length} photos
+ </p>
+ <p className="text-xs" style={{ color: 'var(--color-warm-gray)' }}>
+ Toggle off to fill copyright info per photo
+ </p>
+ </div>
+ </label>
+ )}
+
+ {/* Form */}
+ <div
+ className="p-6 rounded-3xl"
+ style={{ background: 'rgba(255,255,255,0.6)', border: '1px solid var(--color-cream-dark)' }}
+ >
+ {!multiPhoto || copyrightInfo.applyToAll ? (
+ <CopyrightForm
+ data={copyrightInfo}
+ onChange={handleBatchChange}
+ totalPhotos={photos.length}
+ />
+ ) : (
+ <div className="flex flex-col gap-6">
+ {/* Per-photo nav */}
+ <div className="flex items-center justify-between">
+ <button
+ onClick={() => setActivePhotoIndex((i) => Math.max(0, i - 1))}
+ disabled={activePhotoIndex === 0}
+ className="flex items-center gap-1 text-sm font-medium px-3 py-2 rounded-xl transition-all disabled:opacity-30"
+ style={{ color: 'var(--color-warm-gray)' }}
+ aria-label="Previous photo"
+ >
+ <ChevronLeft size={16} />
+ Previous
+ </button>
+
+ {/* Thumbnail strip */}
+ <div className="flex gap-2">
+ {photos.map((photo, i) => (
+ <button
+ key={photo.id}
+ onClick={() => setActivePhotoIndex(i)}
+ className={clsx(
+ 'w-10 h-10 rounded-xl overflow-hidden border-2 transition-all',
+ i === activePhotoIndex
+ ? 'scale-110'
+ : 'opacity-60 hover:opacity-80'
+ )}
+ style={{
+ borderColor:
+ i === activePhotoIndex
+ ? 'var(--color-peach)'
+ : 'transparent',
+ }}
+ aria-label={`Select photo ${i + 1}`}
+ aria-pressed={i === activePhotoIndex}
+ >
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ <img
+ src={photo.preview}
+ alt={`Photo ${i + 1}`}
+ className="w-full h-full object-cover"
+ />
+ </button>
+ ))}
+ </div>
+
+ <button
+ onClick={() =>
+ setActivePhotoIndex((i) => Math.min(photos.length - 1, i + 1))
+ }
+ disabled={activePhotoIndex === photos.length - 1}
+ className="flex items-center gap-1 text-sm font-medium px-3 py-2 rounded-xl transition-all disabled:opacity-30"
+ style={{ color: 'var(--color-warm-gray)' }}
+ aria-label="Next photo"
+ >
+ Next
+ <ChevronRight size={16} />
+ </button>
+ </div>
+
+ {/* Active photo form */}
+ <AnimatePresence mode="wait">
+ <motion.div
+ key={activePhotoIndex}
+ initial={{ opacity: 0, x: 20 }}
+ animate={{ opacity: 1, x: 0 }}
+ exit={{ opacity: 0, x: -20 }}
+ transition={{ duration: 0.2 }}
+ >
+ <CopyrightForm
+ data={getPhotoData(activePhotoIndex)}
+ onChange={(patch) => handlePerPhotoChange(activePhotoIndex, patch)}
+ photoIndex={activePhotoIndex}
+ totalPhotos={photos.length}
+ />
+ </motion.div>
+ </AnimatePresence>
+ </div>
+ )}
+ </div>
+
+ {/* Navigation */}
+ <div className="flex items-center justify-between pt-2">
+ <button
+ onClick={onBack}
+ className="flex items-center gap-2 px-6 py-3 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
+ style={{
+ background: 'var(--color-cream-dark)',
+ color: 'var(--color-warm-gray)',
+ }}
+ >
+ <ChevronLeft size={16} />
+ Back
+ </button>
+
+ <motion.button
+ whileHover={canProceed ? { scale: 1.03 } : {}}
+ whileTap={canProceed ? { scale: 0.97 } : {}}
+ onClick={onNext}
+ disabled={!canProceed}
+ className={clsx(
+ 'flex items-center gap-2 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide',
+ 'transition-all duration-200',
+ !canProceed && 'opacity-40 cursor-not-allowed'
+ )}
+ style={{
+ background: canProceed ? 'var(--color-peach)' : 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ aria-disabled={!canProceed}
+ >
+ Continue to Likeness Release
+ <ChevronRight size={16} />
+ </motion.button>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/licensor/LikenessRelease.tsx b/src/components/licensor/LikenessRelease.tsx
new file mode 100644
index 0000000..905ad51
--- /dev/null
+++ b/src/components/licensor/LikenessRelease.tsx
@@ -0,0 +1,364 @@
+'use client'
+
+import { useState } from 'react'
+import { motion } from 'framer-motion'
+import { ChevronLeft, ChevronRight, ShieldCheck, AlertTriangle } from 'lucide-react'
+import clsx from 'clsx'
+
+export interface LikenessData {
+ isSubject: boolean
+ isAdult: boolean
+ editorial: boolean
+ commercial: boolean
+ aiTraining: boolean
+ signature: string
+ date: string
+}
+
+interface LikenessReleaseProps {
+ likenessRelease: LikenessData
+ onLikenessChange: (data: LikenessData) => void
+ onNext: () => void
+ onBack: () => void
+}
+
+function CheckboxField({
+ id,
+ label,
+ description,
+ checked,
+ onChange,
+ required,
+ error,
+}: {
+ id: string
+ label: string
+ description?: string
+ checked: boolean
+ onChange: (v: boolean) => void
+ required?: boolean
+ error?: boolean
+}) {
+ return (
+ <label
+ htmlFor={id}
+ className="flex items-start gap-4 p-4 rounded-2xl cursor-pointer transition-all"
+ style={{
+ background: checked
+ ? 'rgba(181, 213, 197, 0.2)'
+ : error
+ ? 'rgba(255, 181, 167, 0.12)'
+ : 'rgba(255,255,255,0.5)',
+ border: `2px solid ${
+ checked
+ ? 'var(--color-sage)'
+ : error
+ ? 'var(--color-peach)'
+ : 'var(--color-cream-dark)'
+ }`,
+ }}
+ >
+ <div className="relative mt-0.5 shrink-0">
+ <input
+ id={id}
+ type="checkbox"
+ checked={checked}
+ onChange={(e) => onChange(e.target.checked)}
+ required={required}
+ className="sr-only"
+ aria-required={required}
+ />
+ <div
+ className="w-5 h-5 rounded-md border-2 flex items-center justify-center transition-all"
+ style={{
+ background: checked ? 'var(--color-sage-dark)' : 'white',
+ borderColor: checked ? 'var(--color-sage-dark)' : 'var(--color-cream-dark)',
+ }}
+ aria-hidden
+ >
+ {checked && (
+ <motion.svg
+ initial={{ scale: 0 }}
+ animate={{ scale: 1 }}
+ width="11"
+ height="11"
+ viewBox="0 0 11 11"
+ fill="none"
+ >
+ <path
+ d="M2 5.5l2.5 2.5L9 3"
+ stroke="white"
+ strokeWidth="1.6"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ </motion.svg>
+ )}
+ </div>
+ </div>
+
+ <div className="flex-1">
+ <p
+ className="text-sm font-semibold"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ {label}
+ {required && (
+ <span className="ml-1" style={{ color: 'var(--color-peach-dark)' }} aria-hidden>
+ *
+ </span>
+ )}
+ </p>
+ {description && (
+ <p className="mt-0.5 text-xs leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ {description}
+ </p>
+ )}
+ </div>
+ </label>
+ )
+}
+
+export default function LikenessRelease({
+ likenessRelease,
+ onLikenessChange,
+ onNext,
+ onBack,
+}: LikenessReleaseProps) {
+ const [attempted, setAttempted] = useState(false)
+
+ const patch = (updates: Partial<LikenessData>) => {
+ onLikenessChange({ ...likenessRelease, ...updates })
+ }
+
+ const atLeastOneUse =
+ likenessRelease.editorial ||
+ likenessRelease.commercial ||
+ likenessRelease.aiTraining
+
+ const canProceed =
+ likenessRelease.isSubject &&
+ likenessRelease.isAdult &&
+ atLeastOneUse &&
+ likenessRelease.signature.trim().length >= 2
+
+ const handleNext = () => {
+ setAttempted(true)
+ if (canProceed) onNext()
+ }
+
+ return (
+ <div className="flex flex-col gap-6">
+ {/* Header */}
+ <div>
+ <h2
+ className="text-2xl md:text-3xl font-bold tracking-tight"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Likeness Release
+ </h2>
+ <p className="mt-2 text-base" style={{ color: 'var(--color-warm-gray)' }}>
+ Confirm you are the person in these photos and grant licensing permissions.
+ </p>
+ </div>
+
+ {/* Security badge */}
+ <div
+ className="flex items-center gap-3 p-4 rounded-2xl"
+ style={{ background: 'rgba(181, 213, 197, 0.2)', border: '1px solid var(--color-sage)' }}
+ role="note"
+ >
+ <ShieldCheck size={20} style={{ color: 'var(--color-sage-dark)' }} />
+ <p className="text-sm" style={{ color: 'var(--color-warm-gray)' }}>
+ Your signature is stored securely and is legally binding. You can revoke permissions at
+ any time from your account.
+ </p>
+ </div>
+
+ {/* Identity confirmations */}
+ <section aria-labelledby="identity-heading">
+ <h3
+ id="identity-heading"
+ className="text-base font-bold tracking-wide mb-3"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Identity Confirmation
+ </h3>
+ <div className="flex flex-col gap-3">
+ <CheckboxField
+ id="is-subject"
+ label="I confirm I am the person depicted in these photos"
+ description="These are photos of me as a child. I am the subject whose likeness will be licensed."
+ checked={likenessRelease.isSubject}
+ onChange={(v) => patch({ isSubject: v })}
+ required
+ error={attempted && !likenessRelease.isSubject}
+ />
+ <CheckboxField
+ id="is-adult"
+ label="I am 18 years of age or older"
+ description="You must be an adult to legally grant rights to your own likeness."
+ checked={likenessRelease.isAdult}
+ onChange={(v) => patch({ isAdult: v })}
+ required
+ error={attempted && !likenessRelease.isAdult}
+ />
+ </div>
+ </section>
+
+ {/* Usage permissions */}
+ <section aria-labelledby="usage-heading">
+ <h3
+ id="usage-heading"
+ className="text-base font-bold tracking-wide mb-1"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ I grant permission for these photos to be licensed for:
+ </h3>
+ <p className="text-xs mb-3" style={{ color: 'var(--color-warm-gray)' }}>
+ Select at least one use type. You earn royalties for each license granted.
+ </p>
+
+ {attempted && !atLeastOneUse && (
+ <motion.div
+ initial={{ opacity: 0, y: -4 }}
+ animate={{ opacity: 1, y: 0 }}
+ className="flex items-center gap-2 mb-3 p-3 rounded-xl"
+ style={{ background: 'rgba(255, 181, 167, 0.2)', border: '1px solid var(--color-peach)' }}
+ role="alert"
+ >
+ <AlertTriangle size={14} style={{ color: 'var(--color-peach-dark)' }} />
+ <p className="text-xs font-medium" style={{ color: 'var(--color-brown)' }}>
+ Please select at least one use type.
+ </p>
+ </motion.div>
+ )}
+
+ <div className="flex flex-col gap-3">
+ <CheckboxField
+ id="editorial"
+ label="Editorial use"
+ description="News articles, blog posts, non-commercial publications, and journalism."
+ checked={likenessRelease.editorial}
+ onChange={(v) => patch({ editorial: v })}
+ error={attempted && !atLeastOneUse}
+ />
+ <CheckboxField
+ id="commercial"
+ label="Commercial use"
+ description="Advertisements, product packaging, marketing materials, and brand campaigns."
+ checked={likenessRelease.commercial}
+ onChange={(v) => patch({ commercial: v })}
+ error={attempted && !atLeastOneUse}
+ />
+ <CheckboxField
+ id="ai-training"
+ label="AI training use"
+ description="Use in AI model training datasets. Higher royalty rate applies."
+ checked={likenessRelease.aiTraining}
+ onChange={(v) => patch({ aiTraining: v })}
+ error={attempted && !atLeastOneUse}
+ />
+ </div>
+ </section>
+
+ {/* Signature */}
+ <section aria-labelledby="signature-heading">
+ <h3
+ id="signature-heading"
+ className="text-base font-bold tracking-wide mb-3"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Digital Signature
+ </h3>
+
+ <div className="flex flex-col gap-4">
+ <div className="flex flex-col gap-1.5">
+ <label
+ htmlFor="signature"
+ className="text-sm font-semibold"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Type your full legal name to sign
+ <span className="ml-1" style={{ color: 'var(--color-peach-dark)' }} aria-hidden>
+ *
+ </span>
+ </label>
+ <input
+ id="signature"
+ type="text"
+ value={likenessRelease.signature}
+ onChange={(e) => patch({ signature: e.target.value })}
+ placeholder="Your full legal name"
+ autoComplete="name"
+ className={clsx(
+ 'w-full px-5 py-4 rounded-2xl text-lg signature-field',
+ 'border-2 outline-none transition-all duration-200'
+ )}
+ style={{
+ background: 'rgba(255,255,255,0.8)',
+ borderColor:
+ attempted && likenessRelease.signature.trim().length < 2
+ ? 'var(--color-peach)'
+ : likenessRelease.signature.trim().length >= 2
+ ? 'var(--color-sage)'
+ : 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ aria-required="true"
+ aria-invalid={attempted && likenessRelease.signature.trim().length < 2}
+ />
+ {attempted && likenessRelease.signature.trim().length < 2 && (
+ <p className="text-xs" style={{ color: 'var(--color-peach-dark)' }}>
+ Please enter your full legal name.
+ </p>
+ )}
+ </div>
+
+ {/* Date */}
+ <div
+ className="flex items-center justify-between px-5 py-3 rounded-2xl"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ <span className="text-sm font-medium" style={{ color: 'var(--color-warm-gray)' }}>
+ Date signed
+ </span>
+ <span className="text-sm font-semibold" style={{ color: 'var(--color-brown)' }}>
+ {likenessRelease.date}
+ </span>
+ </div>
+ </div>
+ </section>
+
+ {/* Navigation */}
+ <div className="flex items-center justify-between pt-2">
+ <button
+ onClick={onBack}
+ className="flex items-center gap-2 px-6 py-3 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
+ style={{
+ background: 'var(--color-cream-dark)',
+ color: 'var(--color-warm-gray)',
+ }}
+ >
+ <ChevronLeft size={16} />
+ Back
+ </button>
+
+ <motion.button
+ whileHover={{ scale: 1.03 }}
+ whileTap={{ scale: 0.97 }}
+ onClick={handleNext}
+ className="flex items-center gap-2 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide transition-all duration-200"
+ style={{
+ background: 'var(--color-peach)',
+ color: 'var(--color-brown)',
+ opacity: canProceed ? 1 : 0.7,
+ }}
+ >
+ Review & Submit
+ <ChevronRight size={16} />
+ </motion.button>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/licensor/PhotoUpload.tsx b/src/components/licensor/PhotoUpload.tsx
new file mode 100644
index 0000000..c25e3c3
--- /dev/null
+++ b/src/components/licensor/PhotoUpload.tsx
@@ -0,0 +1,797 @@
+'use client'
+
+import { useCallback, useRef, useState } from 'react'
+import { motion, AnimatePresence } from 'framer-motion'
+import {
+ Upload,
+ X,
+ ImageIcon,
+ Plus,
+ AlertCircle,
+ FolderOpen,
+ Calendar,
+ MapPin,
+ Camera,
+ Info,
+} from 'lucide-react'
+import clsx from 'clsx'
+import exifr from 'exifr'
+
+export interface PhotoMetadata {
+ dateTaken?: string
+ year?: number
+ latitude?: number
+ longitude?: number
+ locationName?: string
+ cameraMake?: string
+ cameraModel?: string
+ width?: number
+ height?: number
+}
+
+export interface PhotoFile {
+ id: string
+ file: File
+ preview: string
+ progress: number
+ error?: string
+ metadata?: PhotoMetadata
+ directory?: string
+}
+
+interface PhotoUploadProps {
+ photos: PhotoFile[]
+ onPhotosChange: React.Dispatch<React.SetStateAction<PhotoFile[]>>
+ onNext: () => void
+}
+
+const MAX_FILE_SIZE_MB = 25
+const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
+const ACCEPTED_TYPES = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/webp',
+ 'image/heic',
+ 'image/gif',
+ 'image/tiff',
+]
+
+function generateId() {
+ return Math.random().toString(36).slice(2) + Date.now().toString(36)
+}
+
+function simulateUpload(
+ id: string,
+ onProgress: (id: string, progress: number) => void
+): Promise<void> {
+ return new Promise((resolve) => {
+ let progress = 0
+ const interval = setInterval(() => {
+ progress += Math.random() * 30 + 10
+ if (progress >= 100) {
+ clearInterval(interval)
+ onProgress(id, 100)
+ resolve()
+ } else {
+ onProgress(id, Math.min(Math.round(progress), 99))
+ }
+ }, 120)
+ })
+}
+
+async function extractMetadata(file: File): Promise<PhotoMetadata> {
+ const meta: PhotoMetadata = {}
+
+ try {
+ const exif = await exifr.parse(file, {
+ pick: [
+ 'DateTimeOriginal',
+ 'CreateDate',
+ 'GPSLatitude',
+ 'GPSLongitude',
+ 'Make',
+ 'Model',
+ 'ImageWidth',
+ 'ImageHeight',
+ 'ExifImageWidth',
+ 'ExifImageHeight',
+ ],
+ gps: true,
+ })
+
+ if (!exif) return meta
+
+ // Date
+ const dateVal = exif.DateTimeOriginal || exif.CreateDate
+ if (dateVal instanceof Date) {
+ meta.dateTaken = dateVal.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })
+ meta.year = dateVal.getFullYear()
+ }
+
+ // GPS
+ if (typeof exif.latitude === 'number' && typeof exif.longitude === 'number') {
+ meta.latitude = exif.latitude
+ meta.longitude = exif.longitude
+ }
+
+ // Camera
+ if (exif.Make) meta.cameraMake = String(exif.Make).trim()
+ if (exif.Model) meta.cameraModel = String(exif.Model).trim()
+
+ // Dimensions
+ meta.width = exif.ExifImageWidth || exif.ImageWidth
+ meta.height = exif.ExifImageHeight || exif.ImageHeight
+ } catch {
+ // EXIF parsing can fail for PNGs, WEBPs without metadata — that's fine
+ }
+
+ return meta
+}
+
+function MetadataBadge({
+ icon: Icon,
+ label,
+}: {
+ icon: typeof Calendar
+ label: string
+}) {
+ return (
+ <span
+ className="inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[10px] font-medium"
+ style={{ background: 'rgba(61, 64, 91, 0.7)', color: 'white' }}
+ >
+ <Icon size={9} />
+ {label}
+ </span>
+ )
+}
+
+function PhotoCard({
+ photo,
+ onRemove,
+ onSelect,
+ isSelected,
+}: {
+ photo: PhotoFile
+ onRemove: () => void
+ onSelect: () => void
+ isSelected: boolean
+}) {
+ const m = photo.metadata
+
+ return (
+ <motion.div
+ layout
+ key={photo.id}
+ initial={{ opacity: 0, scale: 0.85 }}
+ animate={{ opacity: 1, scale: 1 }}
+ exit={{ opacity: 0, scale: 0.85 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 25 }}
+ className={clsx(
+ 'relative group rounded-2xl overflow-hidden cursor-pointer',
+ 'transition-all duration-200'
+ )}
+ style={{
+ aspectRatio: '1',
+ background: 'var(--color-cream-dark)',
+ outline: isSelected ? '3px solid var(--color-peach)' : 'none',
+ outlineOffset: '2px',
+ }}
+ onClick={onSelect}
+ >
+ {/* Thumbnail */}
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ <img
+ src={photo.preview}
+ alt={photo.file.name}
+ className="w-full h-full object-cover"
+ />
+
+ {/* Upload progress overlay */}
+ {photo.progress < 100 && (
+ <div
+ className="absolute inset-0 flex flex-col items-center justify-center gap-2"
+ style={{ background: 'rgba(61, 64, 91, 0.55)' }}
+ >
+ <ImageIcon size={20} className="text-white" />
+ <div
+ className="w-3/4 h-1.5 rounded-full overflow-hidden"
+ style={{ background: 'rgba(255,255,255,0.3)' }}
+ >
+ <motion.div
+ className="h-full rounded-full"
+ style={{ background: 'var(--color-peach)' }}
+ initial={{ width: 0 }}
+ animate={{ width: `${photo.progress}%` }}
+ transition={{ duration: 0.2 }}
+ />
+ </div>
+ <span className="text-white text-xs font-medium">{photo.progress}%</span>
+ </div>
+ )}
+
+ {/* Metadata badges (bottom-left) */}
+ {photo.progress === 100 && m && (m.dateTaken || m.latitude) && (
+ <div className="absolute bottom-1.5 left-1.5 flex flex-col gap-0.5">
+ {m.dateTaken && <MetadataBadge icon={Calendar} label={m.dateTaken} />}
+ {m.latitude && (
+ <MetadataBadge
+ icon={MapPin}
+ label={`${m.latitude.toFixed(2)}, ${m.longitude?.toFixed(2)}`}
+ />
+ )}
+ </div>
+ )}
+
+ {/* Remove button */}
+ {photo.progress === 100 && (
+ <button
+ className="absolute top-1.5 right-1.5 w-6 h-6 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
+ style={{ background: 'rgba(61, 64, 91, 0.8)' }}
+ onClick={(e) => {
+ e.stopPropagation()
+ onRemove()
+ }}
+ aria-label={`Remove ${photo.file.name}`}
+ >
+ <X size={12} className="text-white" />
+ </button>
+ )}
+
+ {/* Complete checkmark */}
+ {photo.progress === 100 && (
+ <div
+ className="absolute top-1.5 left-1.5 w-5 h-5 rounded-full flex items-center justify-center"
+ style={{ background: 'var(--color-sage-dark)' }}
+ >
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
+ <path
+ d="M2 5l2.5 2.5L8 3"
+ stroke="white"
+ strokeWidth="1.5"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ </svg>
+ </div>
+ )}
+
+ {/* Directory label */}
+ {photo.directory && (
+ <div
+ className="absolute top-1.5 right-1.5 flex items-center gap-0.5 px-1.5 py-0.5 rounded-md"
+ style={{
+ background: 'rgba(61, 64, 91, 0.6)',
+ fontSize: '9px',
+ color: 'white',
+ }}
+ >
+ <FolderOpen size={8} />
+ {photo.directory}
+ </div>
+ )}
+ </motion.div>
+ )
+}
+
+function MetadataPanel({ photo }: { photo: PhotoFile }) {
+ const m = photo.metadata
+ if (!m) return null
+
+ const hasAny = m.dateTaken || m.cameraMake || m.latitude || m.width
+
+ if (!hasAny) {
+ return (
+ <div
+ className="flex items-center gap-2 p-4 rounded-2xl"
+ style={{ background: 'rgba(255,255,255,0.5)', border: '1px solid var(--color-cream-dark)' }}
+ >
+ <Info size={14} style={{ color: 'var(--color-warm-gray)' }} />
+ <p className="text-xs" style={{ color: 'var(--color-warm-gray)' }}>
+ No EXIF metadata found. You can manually enter the year in the next step.
+ </p>
+ </div>
+ )
+ }
+
+ return (
+ <div
+ className="rounded-2xl overflow-hidden"
+ style={{ border: '1px solid var(--color-cream-dark)' }}
+ >
+ <div
+ className="flex items-center gap-2 px-4 py-2.5 border-b"
+ style={{
+ borderColor: 'var(--color-cream-dark)',
+ background: 'rgba(255,255,255,0.4)',
+ }}
+ >
+ <Info size={12} style={{ color: 'var(--color-warm-gray)' }} />
+ <p className="text-xs font-semibold" style={{ color: 'var(--color-brown)' }}>
+ {photo.file.name}
+ </p>
+ </div>
+ <div className="flex flex-wrap gap-x-6 gap-y-2 px-4 py-3" style={{ background: 'rgba(255,255,255,0.5)' }}>
+ {m.dateTaken && (
+ <div className="flex items-center gap-1.5">
+ <Calendar size={12} style={{ color: 'var(--color-peach-dark)' }} />
+ <span className="text-xs" style={{ color: 'var(--color-brown)' }}>
+ {m.dateTaken}
+ </span>
+ </div>
+ )}
+ {m.cameraMake && (
+ <div className="flex items-center gap-1.5">
+ <Camera size={12} style={{ color: 'var(--color-sage-dark)' }} />
+ <span className="text-xs" style={{ color: 'var(--color-brown)' }}>
+ {m.cameraMake} {m.cameraModel || ''}
+ </span>
+ </div>
+ )}
+ {m.latitude && m.longitude && (
+ <div className="flex items-center gap-1.5">
+ <MapPin size={12} style={{ color: 'var(--color-warm-gray)' }} />
+ <span className="text-xs" style={{ color: 'var(--color-brown)' }}>
+ {m.latitude.toFixed(4)}, {m.longitude.toFixed(4)}
+ </span>
+ </div>
+ )}
+ {m.width && m.height && (
+ <div className="flex items-center gap-1.5">
+ <ImageIcon size={12} style={{ color: 'var(--color-warm-gray)' }} />
+ <span className="text-xs" style={{ color: 'var(--color-brown)' }}>
+ {m.width} x {m.height}
+ </span>
+ </div>
+ )}
+ </div>
+ </div>
+ )
+}
+
+export default function PhotoUpload({
+ photos,
+ onPhotosChange,
+ onNext,
+}: PhotoUploadProps) {
+ const [isDragOver, setIsDragOver] = useState(false)
+ const [errors, setErrors] = useState<string[]>([])
+ const [selectedId, setSelectedId] = useState<string | null>(null)
+ const fileInputRef = useRef<HTMLInputElement>(null)
+ const dirInputRef = useRef<HTMLInputElement>(null)
+
+ const processFiles = useCallback(
+ async (files: File[], directoryName?: string) => {
+ const newErrors: string[] = []
+ const validFiles: File[] = []
+
+ for (const file of files) {
+ if (
+ !ACCEPTED_TYPES.includes(file.type) &&
+ !file.name.match(/\.(jpe?g|png|webp|heic|gif|tiff?)$/i)
+ ) {
+ newErrors.push(`"${file.name}" is not a supported image type.`)
+ continue
+ }
+ if (file.size > MAX_FILE_SIZE_BYTES) {
+ newErrors.push(
+ `"${file.name}" exceeds the ${MAX_FILE_SIZE_MB}MB limit.`
+ )
+ continue
+ }
+ validFiles.push(file)
+ }
+
+ if (newErrors.length > 0) {
+ setErrors(newErrors)
+ setTimeout(() => setErrors([]), 5000)
+ }
+
+ if (validFiles.length === 0) return
+
+ // Build photo objects + extract EXIF metadata in parallel
+ const newPhotos: PhotoFile[] = await Promise.all(
+ validFiles.map(async (file) => {
+ const metadata = await extractMetadata(file)
+ // Extract directory from webkitRelativePath
+ const relPath = (file as File & { webkitRelativePath?: string })
+ .webkitRelativePath
+ const dir =
+ directoryName ||
+ (relPath ? relPath.split('/').slice(0, -1).join('/') : undefined)
+
+ return {
+ id: generateId(),
+ file,
+ preview: URL.createObjectURL(file),
+ progress: 0,
+ metadata,
+ directory: dir || undefined,
+ }
+ })
+ )
+
+ // Merge with existing
+ onPhotosChange((prev) => [...prev, ...newPhotos])
+
+ // Simulate upload progress
+ await Promise.all(
+ newPhotos.map((photo) =>
+ simulateUpload(photo.id, (id, progress) => {
+ onPhotosChange((prev) =>
+ prev.map((p) => (p.id === id ? { ...p, progress } : p))
+ )
+ })
+ )
+ )
+
+ // Auto-select first photo
+ if (newPhotos.length > 0) {
+ setSelectedId(newPhotos[0].id)
+ }
+ },
+ [onPhotosChange]
+ )
+
+ const handleDrop = useCallback(
+ async (e: React.DragEvent<HTMLDivElement>) => {
+ e.preventDefault()
+ setIsDragOver(false)
+
+ // Check for directory drops via DataTransferItemList
+ const items = e.dataTransfer.items
+ const allFiles: File[] = []
+
+ if (items) {
+ const entries: FileSystemEntry[] = []
+ for (let i = 0; i < items.length; i++) {
+ const entry = items[i].webkitGetAsEntry?.()
+ if (entry) entries.push(entry)
+ }
+
+ // Recursively read directories
+ for (const entry of entries) {
+ if (entry.isFile) {
+ const file = await new Promise<File>((resolve) =>
+ (entry as FileSystemFileEntry).file(resolve)
+ )
+ allFiles.push(file)
+ } else if (entry.isDirectory) {
+ const dirReader = (
+ entry as FileSystemDirectoryEntry
+ ).createReader()
+ const dirFiles = await new Promise<FileSystemEntry[]>(
+ (resolve) => dirReader.readEntries(resolve)
+ )
+ for (const dirEntry of dirFiles) {
+ if (dirEntry.isFile) {
+ const file = await new Promise<File>((resolve) =>
+ (dirEntry as FileSystemFileEntry).file(resolve)
+ )
+ allFiles.push(file)
+ }
+ }
+ }
+ }
+ }
+
+ if (allFiles.length > 0) {
+ processFiles(allFiles)
+ } else {
+ // Fallback to standard files
+ processFiles(Array.from(e.dataTransfer.files))
+ }
+ },
+ [processFiles]
+ )
+
+ const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
+ e.preventDefault()
+ setIsDragOver(true)
+ }
+
+ const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
+ setIsDragOver(false)
+ }
+ }
+
+ const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const files = Array.from(e.target.files ?? [])
+ processFiles(files)
+ if (fileInputRef.current) fileInputRef.current.value = ''
+ }
+
+ const handleDirInput = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const files = Array.from(e.target.files ?? [])
+ // Extract directory name from first file's path
+ const firstPath = (files[0] as File & { webkitRelativePath?: string })
+ ?.webkitRelativePath
+ const dirName = firstPath ? firstPath.split('/')[0] : undefined
+ processFiles(files, dirName)
+ if (dirInputRef.current) dirInputRef.current.value = ''
+ }
+
+ const removePhoto = (id: string) => {
+ const photo = photos.find((p) => p.id === id)
+ if (photo) URL.revokeObjectURL(photo.preview)
+ onPhotosChange((prev) => prev.filter((p) => p.id !== id))
+ if (selectedId === id) setSelectedId(null)
+ }
+
+ const selectedPhoto = photos.find((p) => p.id === selectedId)
+ const canProceed =
+ photos.length >= 1 && photos.every((p) => p.progress === 100)
+
+ // Count unique directories
+ const directories = new Set(photos.map((p) => p.directory).filter(Boolean))
+
+ return (
+ <div className="flex flex-col gap-6">
+ {/* Header */}
+ <div>
+ <h2
+ className="text-2xl md:text-3xl font-bold tracking-tight"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Upload Your Photos
+ </h2>
+ <p className="mt-2 text-base" style={{ color: 'var(--color-warm-gray)' }}>
+ Add childhood photos or entire folders. We'll extract date, location, and
+ camera info automatically.
+ </p>
+ </div>
+
+ {/* Drop zone */}
+ <div
+ role="button"
+ tabIndex={0}
+ aria-label="Click or drag photos to upload"
+ onDrop={handleDrop}
+ onDragOver={handleDragOver}
+ onDragLeave={handleDragLeave}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click()
+ }}
+ onClick={() => fileInputRef.current?.click()}
+ className={clsx(
+ 'relative flex flex-col items-center justify-center gap-4 px-8 py-12 rounded-3xl',
+ 'border-2 border-dashed cursor-pointer',
+ 'transition-all duration-200',
+ 'outline-none focus-visible:ring-2',
+ isDragOver && 'drag-over scale-[1.01]'
+ )}
+ style={{
+ borderColor: isDragOver
+ ? 'var(--color-peach-dark)'
+ : 'var(--color-cream-dark)',
+ background: isDragOver
+ ? 'rgba(255, 181, 167, 0.08)'
+ : 'rgba(255, 255, 255, 0.6)',
+ }}
+ >
+ <input
+ ref={fileInputRef}
+ type="file"
+ multiple
+ accept="image/*,.heic"
+ onChange={handleFileInput}
+ aria-hidden="true"
+ />
+ <input
+ ref={dirInputRef}
+ type="file"
+ // @ts-expect-error webkitdirectory is non-standard but widely supported
+ webkitdirectory=""
+ onChange={handleDirInput}
+ aria-hidden="true"
+ style={{
+ position: 'absolute',
+ width: '1px',
+ height: '1px',
+ overflow: 'hidden',
+ clip: 'rect(0,0,0,0)',
+ }}
+ />
+
+ <motion.div
+ animate={{ scale: isDragOver ? 1.15 : 1 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 25 }}
+ className="w-16 h-16 rounded-2xl flex items-center justify-center"
+ style={{ background: 'var(--color-peach)' }}
+ >
+ <Upload size={28} style={{ color: 'var(--color-brown)' }} />
+ </motion.div>
+
+ <div className="text-center">
+ <p
+ className="text-lg font-semibold tracking-wide"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ {isDragOver
+ ? 'Drop them here!'
+ : 'Drag & drop photos or folders'}
+ </p>
+ <p
+ className="mt-1 text-sm"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ or{' '}
+ <span
+ className="underline font-medium"
+ style={{ color: 'var(--color-peach-dark)' }}
+ >
+ browse to choose files
+ </span>
+ </p>
+ <p
+ className="mt-2 text-xs"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ JPG, PNG, WEBP, HEIC up to {MAX_FILE_SIZE_MB}MB each
+ </p>
+ </div>
+ </div>
+
+ {/* Upload folder button */}
+ <div className="flex gap-3">
+ <button
+ onClick={(e) => {
+ e.stopPropagation()
+ dirInputRef.current?.click()
+ }}
+ className="flex items-center gap-2 px-4 py-2.5 rounded-2xl text-sm font-medium transition-all hover:scale-105"
+ style={{
+ background: 'var(--color-cream-dark)',
+ color: 'var(--color-warm-gray)',
+ }}
+ >
+ <FolderOpen size={14} />
+ Upload entire folder
+ </button>
+ {directories.size > 0 && (
+ <span
+ className="flex items-center text-xs"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ {directories.size} folder{directories.size !== 1 ? 's' : ''} loaded
+ </span>
+ )}
+ </div>
+
+ {/* Errors */}
+ <AnimatePresence>
+ {errors.length > 0 && (
+ <motion.div
+ initial={{ opacity: 0, y: -8 }}
+ animate={{ opacity: 1, y: 0 }}
+ exit={{ opacity: 0, y: -8 }}
+ className="flex flex-col gap-2 p-4 rounded-2xl"
+ style={{
+ background: 'rgba(255, 181, 167, 0.2)',
+ border: '1px solid var(--color-peach)',
+ }}
+ role="alert"
+ >
+ {errors.map((err, i) => (
+ <div key={i} className="flex items-start gap-2">
+ <AlertCircle
+ size={16}
+ className="mt-0.5 shrink-0"
+ style={{ color: 'var(--color-peach-dark)' }}
+ />
+ <p className="text-sm" style={{ color: 'var(--color-brown)' }}>
+ {err}
+ </p>
+ </div>
+ ))}
+ </motion.div>
+ )}
+ </AnimatePresence>
+
+ {/* Photo grid + metadata panel */}
+ {photos.length > 0 && (
+ <div className="flex flex-col gap-4">
+ <div className="flex items-center justify-between">
+ <p
+ className="text-sm font-semibold"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ {photos.length} photo{photos.length !== 1 ? 's' : ''} selected
+ {photos.filter((p) => p.metadata?.dateTaken).length > 0 && (
+ <span
+ className="ml-2 font-normal"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ ({photos.filter((p) => p.metadata?.dateTaken).length} with
+ date metadata)
+ </span>
+ )}
+ </p>
+ <button
+ onClick={(e) => {
+ e.stopPropagation()
+ fileInputRef.current?.click()
+ }}
+ className="flex items-center gap-1.5 text-sm font-medium px-4 py-2 rounded-2xl transition-all hover:scale-105"
+ style={{
+ background: 'var(--color-cream-dark)',
+ color: 'var(--color-warm-gray)',
+ }}
+ aria-label="Add more photos"
+ >
+ <Plus size={14} />
+ Add more
+ </button>
+ </div>
+
+ <div
+ className="grid gap-3"
+ style={{
+ gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))',
+ }}
+ >
+ <AnimatePresence>
+ {photos.map((photo) => (
+ <PhotoCard
+ key={photo.id}
+ photo={photo}
+ onRemove={() => removePhoto(photo.id)}
+ onSelect={() => setSelectedId(photo.id)}
+ isSelected={selectedId === photo.id}
+ />
+ ))}
+ </AnimatePresence>
+ </div>
+
+ {/* Metadata panel for selected photo */}
+ <AnimatePresence>
+ {selectedPhoto && selectedPhoto.progress === 100 && (
+ <motion.div
+ initial={{ opacity: 0, height: 0 }}
+ animate={{ opacity: 1, height: 'auto' }}
+ exit={{ opacity: 0, height: 0 }}
+ transition={{ duration: 0.2 }}
+ className="overflow-hidden"
+ >
+ <MetadataPanel photo={selectedPhoto} />
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </div>
+ )}
+
+ {/* Proceed */}
+ <div className="flex justify-end pt-2">
+ <motion.button
+ whileHover={canProceed ? { scale: 1.03 } : {}}
+ whileTap={canProceed ? { scale: 0.97 } : {}}
+ onClick={onNext}
+ disabled={!canProceed}
+ className={clsx(
+ 'px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide',
+ 'transition-all duration-200',
+ !canProceed && 'opacity-40 cursor-not-allowed'
+ )}
+ style={{
+ background: canProceed
+ ? 'var(--color-peach)'
+ : 'var(--color-cream-dark)',
+ color: 'var(--color-brown)',
+ }}
+ aria-disabled={!canProceed}
+ >
+ {photos.length === 0
+ ? 'Upload at least 1 photo'
+ : photos.some((p) => p.progress < 100)
+ ? 'Uploading…'
+ : 'Continue to Copyright Info'}
+ </motion.button>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/licensor/ReviewSubmit.tsx b/src/components/licensor/ReviewSubmit.tsx
new file mode 100644
index 0000000..7894385
--- /dev/null
+++ b/src/components/licensor/ReviewSubmit.tsx
@@ -0,0 +1,359 @@
+'use client'
+
+import { useState } from 'react'
+import { motion, AnimatePresence } from 'framer-motion'
+import {
+ ChevronLeft,
+ Camera,
+ Copyright,
+ ShieldCheck,
+ CheckCircle2,
+ Loader2,
+ AlertCircle,
+ ExternalLink,
+} from 'lucide-react'
+import clsx from 'clsx'
+import type { PhotoFile } from './PhotoUpload'
+import type { CopyrightData } from './CopyrightInfo'
+import type { LikenessData } from './LikenessRelease'
+
+interface ReviewSubmitProps {
+ photos: PhotoFile[]
+ copyrightInfo: CopyrightData
+ likenessRelease: LikenessData
+ onBack: () => void
+}
+
+type SubmitState = 'idle' | 'submitting' | 'success' | 'error'
+
+function SectionCard({
+ icon,
+ title,
+ children,
+}: {
+ icon: React.ReactNode
+ title: string
+ children: React.ReactNode
+}) {
+ return (
+ <div
+ className="rounded-3xl overflow-hidden"
+ style={{ border: '1px solid var(--color-cream-dark)', background: 'rgba(255,255,255,0.6)' }}
+ >
+ <div
+ className="flex items-center gap-3 px-6 py-4 border-b"
+ style={{ borderColor: 'var(--color-cream-dark)', background: 'rgba(255,255,255,0.4)' }}
+ >
+ <div
+ className="w-8 h-8 rounded-xl flex items-center justify-center"
+ style={{ background: 'var(--color-peach)' }}
+ >
+ {icon}
+ </div>
+ <h3 className="text-sm font-bold tracking-wide" style={{ color: 'var(--color-brown)' }}>
+ {title}
+ </h3>
+ </div>
+ <div className="px-6 py-5">{children}</div>
+ </div>
+ )
+}
+
+function ReviewRow({ label, value }: { label: string; value: string | React.ReactNode }) {
+ return (
+ <div className="flex items-start justify-between gap-4 py-2.5 border-b last:border-b-0" style={{ borderColor: 'var(--color-cream-dark)' }}>
+ <span className="text-sm shrink-0" style={{ color: 'var(--color-warm-gray)' }}>
+ {label}
+ </span>
+ <span className="text-sm font-medium text-right" style={{ color: 'var(--color-brown)' }}>
+ {value}
+ </span>
+ </div>
+ )
+}
+
+const RELATIONSHIP_LABELS: Record<string, string> = {
+ parent: 'My parent',
+ family: 'Other family member',
+ professional: 'Professional photographer',
+ unknown: "Don't know",
+}
+
+const PERMISSION_LABELS: Record<string, string> = {
+ yes: 'Yes — has permission',
+ no: 'No explicit permission',
+ deceased: 'Photographer deceased',
+}
+
+export default function ReviewSubmit({
+ photos,
+ copyrightInfo,
+ likenessRelease,
+ onBack,
+}: ReviewSubmitProps) {
+ const [submitState, setSubmitState] = useState<SubmitState>('idle')
+ const [errorMsg, setErrorMsg] = useState('')
+
+ const usageTypes = [
+ likenessRelease.editorial && 'Editorial',
+ likenessRelease.commercial && 'Commercial',
+ likenessRelease.aiTraining && 'AI Training',
+ ]
+ .filter(Boolean)
+ .join(', ')
+
+ const handleSubmit = async () => {
+ setSubmitState('submitting')
+ setErrorMsg('')
+
+ // Simulated async submission
+ await new Promise((r) => setTimeout(r, 2200))
+
+ // In production: POST to /api/licensor/submit with FormData
+ const success = Math.random() > 0.05 // 95% success rate for demo
+ if (success) {
+ setSubmitState('success')
+ } else {
+ setSubmitState('error')
+ setErrorMsg('Something went wrong. Please try again.')
+ }
+ }
+
+ if (submitState === 'success') {
+ return (
+ <motion.div
+ initial={{ opacity: 0, scale: 0.96 }}
+ animate={{ opacity: 1, scale: 1 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 28 }}
+ className="flex flex-col items-center justify-center gap-8 py-16 text-center"
+ >
+ <motion.div
+ initial={{ scale: 0 }}
+ animate={{ scale: 1 }}
+ transition={{ type: 'spring', stiffness: 300, damping: 22, delay: 0.1 }}
+ className="w-24 h-24 rounded-full flex items-center justify-center"
+ style={{ background: 'var(--color-sage)' }}
+ >
+ <CheckCircle2 size={48} style={{ color: 'white' }} />
+ </motion.div>
+
+ <div className="flex flex-col gap-3 max-w-md">
+ <h2 className="text-3xl font-black tracking-tighter" style={{ color: 'var(--color-brown)' }}>
+ You're in the queue!
+ </h2>
+ <p className="text-base leading-relaxed" style={{ color: 'var(--color-warm-gray)' }}>
+ Your {photos.length} photo{photos.length !== 1 ? 's' : ''}{' '}
+ {photos.length !== 1 ? 'have' : 'has'} been submitted for rights review. We'll
+ notify you by email when they're cleared for licensing.
+ </p>
+ </div>
+
+ <div
+ className="w-full max-w-sm rounded-3xl p-6 flex flex-col gap-3"
+ style={{ background: 'var(--color-cream-dark)' }}
+ >
+ <p className="text-xs font-bold tracking-widest uppercase" style={{ color: 'var(--color-warm-gray)' }}>
+ What happens next
+ </p>
+ {[
+ { step: '1', text: 'Rights review (24–48 hours)' },
+ { step: '2', text: 'Copyright clearance notification' },
+ { step: '3', text: 'Photos go live in marketplace' },
+ { step: '4', text: 'Royalties deposited when licensed' },
+ ].map(({ step, text }) => (
+ <div key={step} className="flex items-center gap-3">
+ <div
+ className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 text-xs font-bold"
+ style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
+ >
+ {step}
+ </div>
+ <p className="text-sm" style={{ color: 'var(--color-brown)' }}>
+ {text}
+ </p>
+ </div>
+ ))}
+ </div>
+
+ <a
+ href="/dashboard"
+ className="inline-flex items-center gap-2 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide transition-all hover:scale-105"
+ style={{ background: 'var(--color-peach)', color: 'var(--color-brown)' }}
+ >
+ Go to My Dashboard
+ <ExternalLink size={14} />
+ </a>
+ </motion.div>
+ )
+ }
+
+ return (
+ <div className="flex flex-col gap-6">
+ {/* Header */}
+ <div>
+ <h2
+ className="text-2xl md:text-3xl font-bold tracking-tight"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ Review & Submit
+ </h2>
+ <p className="mt-2 text-base" style={{ color: 'var(--color-warm-gray)' }}>
+ Please review everything below before submitting for rights review.
+ </p>
+ </div>
+
+ {/* Photos section */}
+ <SectionCard icon={<Camera size={16} style={{ color: 'var(--color-brown)' }} />} title={`${photos.length} Photo${photos.length !== 1 ? 's' : ''}`}>
+ <div
+ className="flex gap-2 flex-wrap"
+ >
+ {photos.map((photo, i) => (
+ <div
+ key={photo.id}
+ className="relative rounded-xl overflow-hidden"
+ style={{ width: 72, height: 72, flexShrink: 0 }}
+ >
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ <img
+ src={photo.preview}
+ alt={`Photo ${i + 1}`}
+ className="w-full h-full object-cover"
+ />
+ </div>
+ ))}
+ </div>
+ </SectionCard>
+
+ {/* Copyright section */}
+ <SectionCard
+ icon={<Copyright size={16} style={{ color: 'var(--color-brown)' }} />}
+ title="Copyright Information"
+ >
+ <ReviewRow
+ label="Photographer"
+ value={RELATIONSHIP_LABELS[copyrightInfo.relationship] ?? copyrightInfo.relationship}
+ />
+ {copyrightInfo.photographer && (
+ <ReviewRow label="Name" value={copyrightInfo.photographer} />
+ )}
+ {copyrightInfo.year && (
+ <ReviewRow label="Year taken" value={copyrightInfo.year} />
+ )}
+ <ReviewRow
+ label="Permission"
+ value={PERMISSION_LABELS[copyrightInfo.hasPermission] ?? copyrightInfo.hasPermission}
+ />
+ {copyrightInfo.hasPermission === 'deceased' && (
+ <ReviewRow
+ label="Estate rights"
+ value={copyrightInfo.estateRights ? 'Has evidence' : 'No evidence'}
+ />
+ )}
+ </SectionCard>
+
+ {/* Likeness section */}
+ <SectionCard
+ icon={<ShieldCheck size={16} style={{ color: 'var(--color-brown)' }} />}
+ title="Likeness Release"
+ >
+ <ReviewRow label="Subject confirmed" value={likenessRelease.isSubject ? 'Yes' : 'No'} />
+ <ReviewRow label="Age confirmed (18+)" value={likenessRelease.isAdult ? 'Yes' : 'No'} />
+ <ReviewRow label="Licensed for" value={usageTypes || 'None selected'} />
+ <ReviewRow
+ label="Signed by"
+ value={
+ <span className="signature-field text-base">
+ {likenessRelease.signature}
+ </span>
+ }
+ />
+ <ReviewRow label="Date" value={likenessRelease.date} />
+ </SectionCard>
+
+ {/* Legal notice */}
+ <div
+ className="flex items-start gap-3 p-4 rounded-2xl text-xs leading-relaxed"
+ style={{ background: 'rgba(61, 64, 91, 0.05)', border: '1px solid var(--color-cream-dark)' }}
+ role="note"
+ >
+ <AlertCircle size={14} className="shrink-0 mt-0.5" style={{ color: 'var(--color-warm-gray)' }} />
+ <p style={{ color: 'var(--color-warm-gray)' }}>
+ By submitting, you certify that all information above is accurate and that you have the
+ legal authority to license these photos. False submissions may result in account termination
+ and legal liability.
+ </p>
+ </div>
+
+ {/* Error */}
+ <AnimatePresence>
+ {submitState === 'error' && (
+ <motion.div
+ initial={{ opacity: 0, y: -8 }}
+ animate={{ opacity: 1, y: 0 }}
+ exit={{ opacity: 0, y: -8 }}
+ className="flex items-center gap-3 p-4 rounded-2xl"
+ style={{ background: 'rgba(255, 181, 167, 0.2)', border: '1px solid var(--color-peach)' }}
+ role="alert"
+ >
+ <AlertCircle size={16} style={{ color: 'var(--color-peach-dark)' }} />
+ <p className="text-sm font-medium" style={{ color: 'var(--color-brown)' }}>
+ {errorMsg}
+ </p>
+ </motion.div>
+ )}
+ </AnimatePresence>
+
+ {/* Navigation */}
+ <div className="flex items-center justify-between pt-2">
+ <button
+ onClick={onBack}
+ disabled={submitState === 'submitting'}
+ className={clsx(
+ 'flex items-center gap-2 px-6 py-3 rounded-3xl text-sm font-semibold tracking-wide',
+ 'transition-all hover:scale-105 disabled:opacity-40 disabled:cursor-not-allowed'
+ )}
+ style={{
+ background: 'var(--color-cream-dark)',
+ color: 'var(--color-warm-gray)',
+ }}
+ >
+ <ChevronLeft size={16} />
+ Back
+ </button>
+
+ <motion.button
+ whileHover={submitState === 'idle' ? { scale: 1.03 } : {}}
+ whileTap={submitState === 'idle' ? { scale: 0.97 } : {}}
+ onClick={handleSubmit}
+ disabled={submitState === 'submitting'}
+ className={clsx(
+ 'flex items-center gap-2.5 px-8 py-3.5 rounded-3xl text-sm font-semibold tracking-wide',
+ 'transition-all duration-200',
+ submitState === 'submitting' && 'cursor-not-allowed'
+ )}
+ style={{
+ background:
+ submitState === 'error'
+ ? 'var(--color-peach-dark)'
+ : 'var(--color-peach)',
+ color: 'var(--color-brown)',
+ }}
+ >
+ {submitState === 'submitting' ? (
+ <>
+ <Loader2 size={16} className="animate-spin" />
+ Submitting…
+ </>
+ ) : submitState === 'error' ? (
+ 'Try Again'
+ ) : (
+ <>
+ <CheckCircle2 size={16} />
+ Submit for Review
+ </>
+ )}
+ </motion.button>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/licensor/Stepper.tsx b/src/components/licensor/Stepper.tsx
new file mode 100644
index 0000000..e8f3471
--- /dev/null
+++ b/src/components/licensor/Stepper.tsx
@@ -0,0 +1,139 @@
+'use client'
+
+import { motion } from 'framer-motion'
+import { Check } from 'lucide-react'
+import clsx from 'clsx'
+
+const STEPS = [
+ { label: 'Upload Photos', shortLabel: 'Upload' },
+ { label: 'Copyright Info', shortLabel: 'Copyright' },
+ { label: 'Likeness Release', shortLabel: 'Release' },
+ { label: 'Review & Submit', shortLabel: 'Review' },
+]
+
+interface StepperProps {
+ currentStep: number
+}
+
+export default function Stepper({ currentStep }: StepperProps) {
+ return (
+ <div className="w-full px-4 py-6 md:px-8 md:py-8">
+ <div className="max-w-2xl mx-auto">
+ <div className="flex items-center justify-between relative">
+ {/* Connecting lines (behind circles) */}
+ <div
+ className="absolute top-5 left-0 right-0 h-0.5 mx-12 md:mx-16"
+ style={{ background: 'var(--color-cream-dark)' }}
+ />
+ {/* Animated filled progress line */}
+ <div
+ className="absolute top-5 left-0 h-0.5 mx-12 md:mx-16 transition-all duration-700 ease-in-out"
+ style={{
+ background: 'var(--color-sage)',
+ right: 0,
+ width: `calc(${(currentStep / (STEPS.length - 1)) * 100}% - 6rem)`,
+ }}
+ />
+
+ {STEPS.map((step, index) => {
+ const isCompleted = index < currentStep
+ const isActive = index === currentStep
+ const isUpcoming = index > currentStep
+
+ return (
+ <div
+ key={step.label}
+ className="relative flex flex-col items-center gap-2 z-10"
+ >
+ {/* Circle */}
+ <motion.div
+ initial={false}
+ animate={{
+ scale: isActive ? 1.12 : 1,
+ }}
+ transition={{ type: 'spring', stiffness: 300, damping: 25 }}
+ className={clsx(
+ 'w-10 h-10 rounded-full flex items-center justify-center',
+ 'text-sm font-bold transition-colors duration-300',
+ 'shadow-sm',
+ {
+ 'text-white': isCompleted || isActive,
+ }
+ )}
+ style={{
+ background: isCompleted
+ ? 'var(--color-sage-dark)'
+ : isActive
+ ? 'var(--color-peach)'
+ : 'var(--color-cream)',
+ border: isUpcoming
+ ? '2px solid var(--color-cream-dark)'
+ : 'none',
+ color: isUpcoming
+ ? 'var(--color-warm-gray)'
+ : 'var(--color-brown)',
+ }}
+ aria-current={isActive ? 'step' : undefined}
+ aria-label={`Step ${index + 1}: ${step.label}${isCompleted ? ' (completed)' : isActive ? ' (current)' : ''}`}
+ >
+ {isCompleted ? (
+ <Check size={16} strokeWidth={3} />
+ ) : (
+ <span>{index + 1}</span>
+ )}
+ </motion.div>
+
+ {/* Label */}
+ <span
+ className={clsx(
+ 'text-xs font-medium tracking-wide text-center',
+ 'hidden sm:block',
+ {
+ 'font-semibold': isActive,
+ }
+ )}
+ style={{
+ color: isActive
+ ? 'var(--color-brown)'
+ : isCompleted
+ ? 'var(--color-sage-dark)'
+ : 'var(--color-warm-gray)',
+ }}
+ >
+ {step.label}
+ </span>
+ {/* Short label for mobile */}
+ <span
+ className={clsx(
+ 'text-xs font-medium tracking-wide text-center',
+ 'sm:hidden',
+ {
+ 'font-semibold': isActive,
+ }
+ )}
+ style={{
+ color: isActive
+ ? 'var(--color-brown)'
+ : isCompleted
+ ? 'var(--color-sage-dark)'
+ : 'var(--color-warm-gray)',
+ }}
+ >
+ {step.shortLabel}
+ </span>
+ </div>
+ )
+ })}
+ </div>
+
+ {/* Step counter (mobile) */}
+ <p
+ className="mt-4 text-center text-xs sm:hidden"
+ style={{ color: 'var(--color-warm-gray)' }}
+ >
+ Step {currentStep + 1} of {STEPS.length}: {STEPS[currentStep].label}
+ </p>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/shared/Navbar.tsx b/src/components/shared/Navbar.tsx
new file mode 100644
index 0000000..2481607
--- /dev/null
+++ b/src/components/shared/Navbar.tsx
@@ -0,0 +1,86 @@
+'use client'
+
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+import { motion } from 'framer-motion'
+import { Camera, Search, LayoutDashboard, LogIn } from 'lucide-react'
+import clsx from 'clsx'
+
+const NAV_LINKS = [
+ { href: '/browse', label: 'Browse', icon: Search },
+ { href: '/licensor', label: 'Upload', icon: Camera },
+ { href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
+]
+
+export default function Navbar() {
+ const pathname = usePathname()
+
+ return (
+ <nav
+ className="sticky top-0 z-50 flex items-center justify-between px-6 py-4 md:px-12 border-b"
+ style={{
+ background: 'rgba(255, 241, 230, 0.92)',
+ borderColor: 'var(--color-cream-dark)',
+ backdropFilter: 'blur(12px)',
+ WebkitBackdropFilter: 'blur(12px)',
+ }}
+ >
+ <Link
+ href="/"
+ className="text-xl font-black tracking-tighter transition-opacity hover:opacity-70"
+ style={{ color: 'var(--color-brown)' }}
+ >
+ i was cute
+ </Link>
+
+ <div className="flex items-center gap-1">
+ {NAV_LINKS.map(({ href, label, icon: Icon }) => {
+ const active = pathname.startsWith(href)
+ return (
+ <Link
+ key={href}
+ href={href}
+ className={clsx(
+ 'relative flex items-center gap-1.5 px-4 py-2 rounded-2xl text-sm font-medium',
+ 'transition-all duration-200 hover:scale-105'
+ )}
+ style={{
+ color: active ? 'var(--color-brown)' : 'var(--color-warm-gray)',
+ }}
+ >
+ {active && (
+ <motion.div
+ layoutId="nav-active"
+ className="absolute inset-0 rounded-2xl"
+ style={{ background: 'var(--color-cream-dark)' }}
+ transition={{ type: 'spring', stiffness: 380, damping: 30 }}
+ />
+ )}
+ <span className="relative z-10 flex items-center gap-1.5">
+ <Icon size={14} />
+ <span className="hidden sm:inline">{label}</span>
+ </span>
+ </Link>
+ )
+ })}
+
+ <div
+ className="w-px h-6 mx-2"
+ style={{ background: 'var(--color-cream-dark)' }}
+ />
+
+ <Link
+ href="/login"
+ className="flex items-center gap-1.5 px-4 py-2 rounded-2xl text-sm font-semibold transition-all hover:scale-105"
+ style={{
+ background: 'var(--color-peach)',
+ color: 'var(--color-brown)',
+ }}
+ >
+ <LogIn size={14} />
+ <span className="hidden sm:inline">Sign In</span>
+ </Link>
+ </div>
+ </nav>
+ )
+}
diff --git a/src/components/shared/VersionSwitcher.tsx b/src/components/shared/VersionSwitcher.tsx
new file mode 100644
index 0000000..19872e9
--- /dev/null
+++ b/src/components/shared/VersionSwitcher.tsx
@@ -0,0 +1,53 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+import { Palette, Check } from 'lucide-react'
+
+const VERSIONS = [
+ { path: '/', label: 'Original' },
+ { path: '/v1', label: 'V1 Silk' },
+ { path: '/v2', label: 'V2 Glass' },
+ { path: '/v3', label: 'V3 Polaroid' },
+]
+
+export default function VersionSwitcher() {
+ const pathname = usePathname()
+
+ const isLanding = pathname === '/' || pathname.startsWith('/v')
+ if (!isLanding) return null
+
+ return (
+ <div
+ className="fixed bottom-0 left-0 right-0 flex items-center justify-center gap-2 px-4 py-3"
+ style={{
+ zIndex: 99999,
+ background: 'rgba(61, 64, 91, 0.92)',
+ backdropFilter: 'blur(12px)',
+ pointerEvents: 'auto',
+ }}
+ >
+ <Palette size={14} style={{ color: 'var(--color-peach)' }} />
+ <span className="text-xs font-medium mr-1" style={{ color: 'rgba(255,255,255,0.6)' }}>
+ Design:
+ </span>
+ {VERSIONS.map((v) => {
+ const active = v.path === pathname
+ return (
+ <a
+ key={v.path}
+ href={v.path}
+ className="flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-semibold transition-all hover:scale-105 no-underline"
+ style={{
+ background: active ? 'var(--color-peach)' : 'rgba(255,255,255,0.12)',
+ color: active ? 'var(--color-brown)' : 'rgba(255,255,255,0.7)',
+ pointerEvents: 'auto',
+ }}
+ >
+ {active && <Check size={10} />}
+ {v.label}
+ </a>
+ )
+ })}
+ </div>
+ )
+}
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
new file mode 100644
index 0000000..56ec324
--- /dev/null
+++ b/src/lib/auth.ts
@@ -0,0 +1,23 @@
+import { cookies } from 'next/headers'
+import { query } from './db'
+
+export async function getSession() {
+ const cookieStore = await cookies()
+ const token = cookieStore.get('iwascute-session')?.value
+ if (!token) return null
+
+ const result = await query(
+ `SELECT s.*, u.id as user_id, u.email, u.display_name, u.role
+ FROM sessions s JOIN users u ON s.user_id = u.id
+ WHERE s.token = $1 AND s.expires_at > NOW()`,
+ [token]
+ )
+
+ return result.rows[0] || null
+}
+
+export async function requireAdmin() {
+ const session = await getSession()
+ if (!session || session.role !== 'admin') return null
+ return session
+}
diff --git a/src/lib/db.ts b/src/lib/db.ts
new file mode 100644
index 0000000..50eeb33
--- /dev/null
+++ b/src/lib/db.ts
@@ -0,0 +1,11 @@
+import { Pool } from 'pg'
+
+const pool = new Pool({
+ connectionString: 'postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/iwascute',
+ max: 10,
+})
+
+export async function query(text: string, params?: unknown[]) {
+ const result = await pool.query(text, params)
+ return result
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cf9c65d
--- /dev/null
+++ b/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"]
+}
(oldest)
·
back to IWasCute
·
snapshot: 2 file(s) changed, +1 new, ~1 modified 12b7f89 →