diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..61e2a92
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+.build
+.next
+.turbo
+.git
+node_modules
+apps/web/.next
+apps/web/node_modules
+packages/lexicons/node_modules
+*.log
+.env
+.env.*
+!.env.example
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e4f88a0
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,15 @@
+ATELIER_ENV=development
+ATELIER_PUBLIC_URL=http://localhost:3000
+ATELIER_API_URL=http://localhost:8080
+NEXT_PUBLIC_ATELIER_API_URL=http://localhost:8080
+ATELIER_DB_URL=postgres://atelier:atelier@localhost:5432/atelier_mail
+ATELIER_KV_BACKEND=postgres
+ATELIER_KMS_PROVIDER=local-dev
+ATELIER_TOKEN_KEK_ID=local-dev-kek
+ATELIER_ATPROTO_CLIENT_ID=http://localhost:3000/client-metadata.json
+ATELIER_ATPROTO_REDIRECT_URI=http://localhost:3000/auth/callback
+ATELIER_GMAIL_CLIENT_ID=
+ATELIER_GMAIL_CLIENT_SECRET=
+ATELIER_GMAIL_PUBSUB_AUDIENCE=
+ATELIER_MCP_ENABLED=true
+ATELIER_MCP_MYCONTEXTPROTOCOL_URL=
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..7dcaf9e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,27 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches: [main, dev]
+
+jobs:
+ web:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.3.14
+ - run: bun install --frozen-lockfile
+ - run: bun run typecheck
+ - run: bun run lint
+ - run: bun run test
+ - run: bun run build
+
+ swift:
+ runs-on: macos-15
+ steps:
+ - uses: actions/checkout@v5
+ - run: swift test
+ working-directory: services/api
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9255f32
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+.DS_Store
+.env
+.env.local
+.env.*.local
+node_modules
+.next
+dist
+coverage
+.turbo
+.bun-install-cache
+*.tsbuildinfo
+.build
+.swiftpm
+DerivedData
+xcuserdata
+*.xcodeproj
+*.xcworkspace
+*.sqlite
+*.sqlite-shm
+*.sqlite-wal
+infra/.data
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..ffa51ed
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,56 @@
+// swift-tools-version: 6.0
+import PackageDescription
+
+let package = Package(
+ name: "AtelierMailWorkspace",
+ platforms: [
+ .macOS(.v14)
+ ],
+ products: [
+ .executable(name: "AtelierAPI", targets: ["AtelierAPI"]),
+ .library(name: "AtelierCore", targets: ["AtelierCore"]),
+ .library(name: "AtelierATProto", targets: ["AtelierATProto"]),
+ .library(name: "AtelierMCP", targets: ["AtelierMCP"]),
+ .library(name: "AtelierSync", targets: ["AtelierSync"]),
+ .library(name: "AtelierPlatform", targets: ["AtelierPlatform"]),
+ .library(name: "AtelierMail", targets: ["AtelierMail"])
+ ],
+ targets: [
+ .target(name: "AtelierCore", path: "packages/swift/AtelierCore/Sources/AtelierCore"),
+ .target(
+ name: "AtelierATProto",
+ dependencies: ["AtelierCore"],
+ path: "packages/swift/AtelierATProto/Sources/AtelierATProto"
+ ),
+ .target(
+ name: "AtelierMCP",
+ dependencies: ["AtelierCore"],
+ path: "packages/swift/AtelierMCP/Sources/AtelierMCP"
+ ),
+ .target(
+ name: "AtelierSync",
+ dependencies: ["AtelierCore"],
+ path: "packages/swift/AtelierSync/Sources/AtelierSync"
+ ),
+ .target(
+ name: "AtelierPlatform",
+ dependencies: ["AtelierCore", "AtelierATProto", "AtelierMCP"],
+ path: "packages/swift/AtelierPlatform/Sources/AtelierPlatform"
+ ),
+ .target(
+ name: "AtelierMail",
+ dependencies: ["AtelierCore", "AtelierPlatform", "AtelierSync"],
+ path: "packages/swift/AtelierMail/Sources/AtelierMail"
+ ),
+ .executableTarget(
+ name: "AtelierAPI",
+ dependencies: ["AtelierCore", "AtelierPlatform", "AtelierMail", "AtelierMCP"],
+ path: "services/api/Sources/AtelierAPI"
+ ),
+ .testTarget(
+ name: "AtelierAPITests",
+ dependencies: ["AtelierAPI", "AtelierCore", "AtelierPlatform", "AtelierMail", "AtelierMCP"],
+ path: "services/api/Tests/AtelierAPITests"
+ )
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..adb7608
--- /dev/null
+++ b/README.md
@@ -0,0 +1,48 @@
+# Atelier Mail
+
+Atelier Mail is the first application scaffold for the Atelier Work suite at `atelierwork.space`.
+
+The MVP is a SaaS-first mail client with:
+
+- ATProto OAuth identity with DID as the primary account key.
+- Server-side Gmail, JMAP, and IMAP provider access.
+- A local normalized mail domain model backed by PostgreSQL.
+- Private permissioned Lexicon concepts stored in Atelier KV until ATProto Permissioned Data is finalized.
+- MCP hooks for agent access through an explicit grant model.
+- A Next.js/shadcn/Tailwind web client inspired by the scalable Social Wire shell and the warmer Skej interaction style.
+
+## Structure
+
+- `apps/web` - Next.js 16 app shell for `mail.atelierwork.space`.
+- `services/api` - Swift/Hummingbird service scaffold.
+- `packages/swift` - extraction-ready Swift package boundaries.
+- `packages/lexicons` - `space.atelierwork.*` draft schemas and validation tests.
+- `infra` - local Docker Compose and operational scaffolding.
+- `docs` - architecture and implementation notes.
+
+## Local Commands
+
+```bash
+bun install
+bun run verify
+```
+
+Swift checks:
+
+```bash
+DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test
+```
+
+Full verification:
+
+```bash
+bun run verify:all
+```
+
+Local deploy smoke:
+
+```bash
+docker compose -f infra/docker-compose.yml up --build
+```
+
+See `docs/deployment.md` for deployment environment and process details.
diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile
new file mode 100644
index 0000000..87f0885
--- /dev/null
+++ b/apps/web/Dockerfile
@@ -0,0 +1,21 @@
+FROM oven/bun:1.3.14-alpine AS deps
+WORKDIR /app
+COPY package.json bun.lock bunfig.toml ./
+COPY apps/web/package.json apps/web/package.json
+COPY packages/lexicons/package.json packages/lexicons/package.json
+RUN bun install --frozen-lockfile
+
+FROM deps AS builder
+WORKDIR /app
+COPY . .
+ENV NEXT_TELEMETRY_DISABLED=1
+RUN bun run --filter './apps/web' build
+
+FROM oven/bun:1.3.14-alpine AS runner
+WORKDIR /app
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+ENV PORT=3000
+COPY --from=builder /app ./
+EXPOSE 3000
+CMD ["bun", "run", "--filter", "./apps/web", "start"]
diff --git a/apps/web/components.json b/apps/web/components.json
new file mode 100644
index 0000000..8d886db
--- /dev/null
+++ b/apps/web/components.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "base-nova",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "menuColor": "default",
+ "menuAccent": "subtle",
+ "registries": {}
+}
diff --git a/apps/web/eslint.config.mjs b/apps/web/eslint.config.mjs
new file mode 100644
index 0000000..5d8d8a3
--- /dev/null
+++ b/apps/web/eslint.config.mjs
@@ -0,0 +1,6 @@
+import nextVitals from "eslint-config-next/core-web-vitals";
+import nextTs from "eslint-config-next/typescript";
+
+const eslintConfig = [...nextVitals, ...nextTs];
+
+export default eslintConfig;
diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
new file mode 100644
index 0000000..9edff1c
--- /dev/null
+++ b/apps/web/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+import "./.next/types/routes.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts
new file mode 100644
index 0000000..407606a
--- /dev/null
+++ b/apps/web/next.config.ts
@@ -0,0 +1,16 @@
+import type { NextConfig } from "next";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const appDir = dirname(fileURLToPath(import.meta.url));
+const workspaceRoot = resolve(appDir, "../..");
+
+const nextConfig: NextConfig = {
+ typedRoutes: true,
+ devIndicators: false,
+ turbopack: {
+ root: workspaceRoot,
+ },
+};
+
+export default nextConfig;
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 0000000..e4d3558
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@atelier-mail/web",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev --webpack",
+ "dev:turbo": "next dev --turbopack",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint --max-warnings 0",
+ "test": "bun test --preload ./src/test/setup.ts",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@atelierwork/lexicons": "workspace:*",
+ "@tanstack/react-virtual": "^3.13.24",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.14.0",
+ "next": "16.2.6",
+ "react": "19.2.4",
+ "react-dom": "19.2.4",
+ "tailwind-merge": "^3.6.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@testing-library/react": "^16.3.2",
+ "@types/bun": "^1.3.4",
+ "@types/jsdom": "^28.0.3",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "16.2.6",
+ "jsdom": "^27.2.0",
+ "playwright": "^1.61.1",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ },
+ "ignoreScripts": [
+ "sharp",
+ "unrs-resolver"
+ ],
+ "trustedDependencies": [
+ "sharp",
+ "unrs-resolver"
+ ]
+}
diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs
new file mode 100644
index 0000000..61e3684
--- /dev/null
+++ b/apps/web/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ },
+};
+
+export default config;
diff --git a/apps/web/scripts/capture-screenshots.mjs b/apps/web/scripts/capture-screenshots.mjs
new file mode 100644
index 0000000..b24a3b3
--- /dev/null
+++ b/apps/web/scripts/capture-screenshots.mjs
@@ -0,0 +1,46 @@
+import { chromium } from "playwright";
+
+const browser = await chromium.launch({ headless: true });
+
+try {
+ const desktop = await browser.newPage({
+ viewport: { width: 1440, height: 1000 },
+ deviceScaleFactor: 1,
+ });
+ await desktop.goto("http://localhost:3000", { waitUntil: "networkidle" });
+ await desktop.screenshot({
+ path: "../../.codex-desktop.png",
+ fullPage: true,
+ });
+
+ await desktop.getByLabel("Switch to dark mode").click();
+ await desktop.waitForTimeout(150);
+ await desktop.screenshot({
+ path: "../../.codex-desktop-dark.png",
+ fullPage: true,
+ });
+
+ const mobile = await browser.newPage({
+ viewport: { width: 390, height: 844 },
+ isMobile: true,
+ });
+ await mobile.goto("http://localhost:3000", { waitUntil: "networkidle" });
+ await mobile.screenshot({
+ path: "../../.codex-mobile.png",
+ fullPage: true,
+ });
+
+ const result = {
+ desktopTitle: await desktop.locator("text=Atelier Mail").first().isVisible(),
+ threadVisible: await desktop
+ .locator("text=Permissioned KV model for linked mail threads")
+ .first()
+ .isVisible(),
+ mobileSearch: await mobile.locator('input[aria-label="Search"]').isVisible(),
+ darkMode: await desktop.locator("html.dark").count(),
+ };
+
+ console.log(JSON.stringify(result, null, 2));
+} finally {
+ await browser.close();
+}
diff --git a/apps/web/src/__tests__/mail-workspace.test.tsx b/apps/web/src/__tests__/mail-workspace.test.tsx
new file mode 100644
index 0000000..9aaa27c
--- /dev/null
+++ b/apps/web/src/__tests__/mail-workspace.test.tsx
@@ -0,0 +1,63 @@
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { beforeEach, expect, test } from "bun:test";
+import { MailWorkspace } from "@/components/mail/MailWorkspace";
+
+beforeEach(() => {
+ window.localStorage.clear();
+ document.documentElement.className = "";
+ delete document.documentElement.dataset.theme;
+ Object.defineProperty(window, "matchMedia", {
+ configurable: true,
+ value: undefined,
+ });
+});
+
+test("renders the signed-in mail workspace as the first screen", () => {
+ render();
+
+ expect(screen.getByText("Atelier Mail")).toBeTruthy();
+ expect(screen.getByText("Unified Inbox")).toBeTruthy();
+ expect(screen.getByText("Permissioned KV model for linked mail threads")).toBeTruthy();
+ expect(screen.getByText("Smart Filters")).toBeTruthy();
+});
+
+test("starts from a saved dark mode preference", async () => {
+ window.localStorage.setItem("atelier-mail-theme", "dark");
+
+ render();
+
+ expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeTruthy();
+ await waitFor(() => expect(document.documentElement.classList.contains("dark")).toBe(true));
+});
+
+test("uses system dark mode as the first-run preference", () => {
+ Object.defineProperty(window, "matchMedia", {
+ configurable: true,
+ value: (query: string) =>
+ ({
+ matches: query === "(prefers-color-scheme: dark)",
+ media: query,
+ onchange: null,
+ addEventListener() {},
+ removeEventListener() {},
+ addListener() {},
+ removeListener() {},
+ dispatchEvent() {
+ return false;
+ },
+ }) satisfies MediaQueryList,
+ });
+
+ render();
+
+ expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeTruthy();
+});
+
+test("persists explicit theme toggles", async () => {
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" }));
+
+ await waitFor(() => expect(window.localStorage.getItem("atelier-mail-theme")).toBe("dark"));
+ expect(document.documentElement.classList.contains("dark")).toBe(true);
+});
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
new file mode 100644
index 0000000..4a5ad74
--- /dev/null
+++ b/apps/web/src/app/globals.css
@@ -0,0 +1,254 @@
+@import "tailwindcss";
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --background: #fffaf7;
+ --foreground: #242120;
+ --card: #ffffff;
+ --card-foreground: #242120;
+ --popover: #ffffff;
+ --popover-foreground: #242120;
+ --primary: #ff6542;
+ --primary-foreground: #ffffff;
+ --secondary: #e8fbff;
+ --secondary-foreground: #18333a;
+ --muted: #f4eeeb;
+ --muted-foreground: #716966;
+ --accent: #fff0a8;
+ --accent-foreground: #342a05;
+ --destructive: #d94b4b;
+ --destructive-foreground: #ffffff;
+ --border: #eadeda;
+ --input: #eadeda;
+ --ring: #1fb8ca;
+ --sidebar: #fffdfb;
+ --sidebar-foreground: #2d2927;
+ --radius: 0.5rem;
+}
+
+.dark {
+ --background: #080706;
+ --foreground: #f6eee9;
+ --card: #0d0b0a;
+ --card-foreground: #f6eee9;
+ --popover: #100d0c;
+ --popover-foreground: #f6eee9;
+ --primary: #ff7656;
+ --primary-foreground: #1d120d;
+ --secondary: #102f34;
+ --secondary-foreground: #d9fbff;
+ --muted: #181412;
+ --muted-foreground: #a89b95;
+ --accent: #3d2f0c;
+ --accent-foreground: #fff2b6;
+ --destructive: #ff6b6b;
+ --destructive-foreground: #210b0b;
+ --border: #271f1b;
+ --input: #271f1b;
+ --ring: #2ed3e6;
+ --sidebar: #090807;
+ --sidebar-foreground: #f2e9e3;
+}
+
+* {
+ border-color: var(--border);
+}
+
+html {
+ min-height: 100%;
+ background: var(--background);
+ color-scheme: light;
+}
+
+html.dark {
+ color-scheme: dark;
+}
+
+body {
+ min-height: 100dvh;
+ margin: 0;
+ background:
+ linear-gradient(135deg, rgba(255, 240, 168, 0.48), transparent 26rem) 0 0 / 38rem 38rem no-repeat,
+ linear-gradient(145deg, rgba(198, 244, 255, 0.46), transparent 24rem) 100% 0 / 34rem 34rem no-repeat,
+ linear-gradient(180deg, #fffaf7 0%, #fffdfb 52%, #f7fbfb 100%);
+ color: var(--foreground);
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", sans-serif;
+ letter-spacing: 0;
+}
+
+.dark body {
+ background:
+ linear-gradient(135deg, rgba(255, 118, 86, 0.08), transparent 24rem) 0 0 / 36rem 36rem no-repeat,
+ linear-gradient(145deg, rgba(46, 211, 230, 0.05), transparent 23rem) 100% 0 / 34rem 34rem no-repeat,
+ linear-gradient(180deg, #080706 0%, #0b0908 54%, #060504 100%);
+}
+
+button,
+input,
+textarea,
+select {
+ font: inherit;
+}
+
+button svg {
+ width: 1rem;
+ height: 1rem;
+}
+
+.mail-card-surface {
+ background-color: var(--card);
+}
+
+.mail-row-surface {
+ background-color: var(--card);
+}
+
+.mail-row-surface:hover {
+ background-color: color-mix(in srgb, var(--primary) 10%, var(--card));
+}
+
+.mail-row-unread {
+ background-color: color-mix(in srgb, var(--primary) 10%, var(--card));
+ box-shadow: inset 0 0 0 0.0625rem color-mix(in srgb, var(--primary) 16%, transparent);
+}
+
+.mail-row-selected {
+ background-color: color-mix(in srgb, var(--primary) 15%, var(--card));
+ box-shadow:
+ inset 0.125rem 0 0 var(--primary),
+ inset 0 0 0 0.0625rem color-mix(in srgb, var(--primary) 24%, transparent);
+}
+
+.mail-sidebar-row {
+ font-size: 0.8125rem !important;
+ line-height: 1rem !important;
+}
+
+.mail-sidebar-account {
+ font-size: 0.8125rem !important;
+ line-height: 1rem !important;
+}
+
+.mail-sidebar-meta {
+ font-size: 0.6875rem !important;
+ line-height: 0.875rem !important;
+}
+
+.mail-sidebar-section-label {
+ font-size: 0.625rem !important;
+ line-height: 0.8125rem !important;
+}
+
+.mail-sidebar-count {
+ font-size: 0.6875rem !important;
+ line-height: 1 !important;
+}
+
+.mail-search-input {
+ font-size: 0.75rem !important;
+ line-height: 1rem !important;
+}
+
+.mail-search-shortcut {
+ font-size: 0.5625rem !important;
+ line-height: 1 !important;
+}
+
+.mail-composer {
+ font-size: 0.75rem;
+ line-height: 1rem;
+}
+
+.mail-composer-title {
+ font-size: 0.75rem !important;
+ line-height: 1rem !important;
+}
+
+.mail-composer-field {
+ font-size: 0.75rem !important;
+ line-height: 1rem !important;
+}
+
+.mail-composer-message {
+ min-height: 8.5rem !important;
+ font-size: 0.8125rem !important;
+ line-height: 1.25rem !important;
+}
+
+.mail-composer-meta {
+ font-size: 0.6875rem !important;
+ line-height: 0.875rem !important;
+}
+
+.dark main,
+.dark .mail-row-surface {
+ color: var(--foreground);
+}
+
+.dark .text-muted-foreground {
+ color: var(--muted-foreground) !important;
+}
+
+.dark .mail-card-surface,
+.dark .mail-row-surface {
+ background-color: #0d0b0a;
+ color: #f6eee9;
+}
+
+.dark .mail-row-surface:hover,
+.dark .mail-row-unread {
+ background-color: #1c100d;
+}
+
+.dark .mail-row-selected {
+ background-color: #28140e;
+}
+
+::selection {
+ background: #bdf4ff;
+ color: #142226;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .atelier-shimmer {
+ background-size: 220% 100%;
+ animation: atelier-shimmer 7s ease-in-out infinite;
+ }
+
+ @keyframes atelier-shimmer {
+ 0%,
+ 100% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ }
+}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
new file mode 100644
index 0000000..def7e68
--- /dev/null
+++ b/apps/web/src/app/layout.tsx
@@ -0,0 +1,39 @@
+import type { Metadata } from "next";
+import { EnvironmentBanner } from "@/components/shared/EnvironmentBanner";
+import { getAppEnv, shouldShowEnvironmentBanner } from "@/lib/appEnv";
+import "./globals.css";
+
+export const metadata: Metadata = {
+ title: "Atelier Mail",
+ description: "ATProto identity mail workspace for atelierwork.space",
+};
+
+const themeScript = `
+(function() {
+ try {
+ var theme = window.localStorage.getItem("atelier-mail-theme");
+ if (theme !== "light" && theme !== "dark") {
+ theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+ }
+ document.documentElement.classList.toggle("dark", theme === "dark");
+ document.documentElement.dataset.theme = theme;
+ } catch (_) {}
+})();
+`;
+
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+ const appEnv = getAppEnv();
+ const bannerHeight = shouldShowEnvironmentBanner(appEnv) ? "2.0625rem" : "0px";
+
+ return (
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
new file mode 100644
index 0000000..e401f4e
--- /dev/null
+++ b/apps/web/src/app/page.tsx
@@ -0,0 +1,6 @@
+import { MailWorkspace } from "@/components/mail/MailWorkspace";
+import { getAppEnv } from "@/lib/appEnv";
+
+export default function Page() {
+ return ;
+}
diff --git a/apps/web/src/components/mail/MailWorkspace.tsx b/apps/web/src/components/mail/MailWorkspace.tsx
new file mode 100644
index 0000000..1bce7f0
--- /dev/null
+++ b/apps/web/src/components/mail/MailWorkspace.tsx
@@ -0,0 +1,877 @@
+"use client";
+
+import {
+ Archive,
+ Braces,
+ ChevronLeft,
+ Clock3,
+ Command,
+ Database,
+ FileText,
+ Inbox,
+ Keyboard,
+ Mail,
+ MailCheck,
+ Menu,
+ Moon,
+ MoreHorizontal,
+ PenLine,
+ RefreshCw,
+ Search,
+ Send,
+ Sparkles,
+ Star,
+ Sun,
+ Tags,
+ Trash2,
+ X,
+} from "lucide-react";
+import * as React from "react";
+import { useVirtualizer } from "@tanstack/react-virtual";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Textarea } from "@/components/ui/textarea";
+import { shouldShowDebugChrome, type AppEnv } from "@/lib/appEnv";
+import { cn } from "@/lib/utils";
+import { accounts, threads as initialThreads } from "./sampleData";
+import type { ComposerDraft, MailThread } from "./types";
+
+type ColorMode = "light" | "dark";
+
+const THEME_STORAGE_KEY = "atelier-mail-theme";
+const SIDEBAR_WIDTH_STORAGE_KEY = "atelier-mail-sidebar-width-rem";
+const THREAD_WIDTH_STORAGE_KEY = "atelier-mail-thread-width-rem";
+
+const SIDEBAR_WIDTH = { default: 15.75, min: 13.5, max: 20 };
+const THREAD_WIDTH = { default: 28, min: 20, max: 36 };
+
+function emptyDraft(thread?: MailThread): ComposerDraft {
+ return {
+ to: thread?.email ?? "",
+ subject: thread ? `Re: ${thread.subject}` : "",
+ body: "",
+ };
+}
+
+function isColorMode(value: string | null): value is ColorMode {
+ return value === "light" || value === "dark";
+}
+
+function readInitialTheme(): ColorMode {
+ if (typeof window === "undefined") return "light";
+
+ try {
+ const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
+ if (isColorMode(storedTheme)) return storedTheme;
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+ } catch {
+ return "light";
+ }
+}
+
+function clamp(value: number, min: number, max: number) {
+ return Math.min(max, Math.max(min, value));
+}
+
+function readStoredRemWidth(key: string, fallback: number, min: number, max: number) {
+ if (typeof window === "undefined") return fallback;
+
+ try {
+ const parsed = Number.parseFloat(window.localStorage.getItem(key) ?? "");
+ return Number.isFinite(parsed) ? clamp(parsed, min, max) : fallback;
+ } catch {
+ return fallback;
+ }
+}
+
+function rootRemPx() {
+ if (typeof window === "undefined") return 16;
+ return Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize) || 16;
+}
+
+export function MailWorkspace({ appEnv }: { appEnv: AppEnv }) {
+ const [threads, setThreads] = React.useState(initialThreads);
+ const [theme, setTheme] = React.useState(() => readInitialTheme());
+ const [sidebarWidthRem, setSidebarWidthRem] = React.useState(() =>
+ readStoredRemWidth(SIDEBAR_WIDTH_STORAGE_KEY, SIDEBAR_WIDTH.default, SIDEBAR_WIDTH.min, SIDEBAR_WIDTH.max)
+ );
+ const [threadWidthRem, setThreadWidthRem] = React.useState(() =>
+ readStoredRemWidth(THREAD_WIDTH_STORAGE_KEY, THREAD_WIDTH.default, THREAD_WIDTH.min, THREAD_WIDTH.max)
+ );
+ const [selectedMailbox, setSelectedMailbox] = React.useState("inbox");
+ const [selectedAccount, setSelectedAccount] = React.useState("all");
+ const [selectedThreadId, setSelectedThreadId] = React.useState(initialThreads[0]?.id ?? "");
+ const [query, setQuery] = React.useState("");
+ const [composeOpen, setComposeOpen] = React.useState(true);
+ const [sidebarOpen, setSidebarOpen] = React.useState(false);
+
+ const [draft, setDraft] = React.useState(() => emptyDraft(initialThreads[0]));
+
+ const visibleThreads = React.useMemo(() => {
+ return threads.filter((thread) => {
+ const accountMatch = selectedAccount === "all" || thread.accountId === selectedAccount;
+ const mailboxMatch =
+ selectedMailbox === "inbox" ||
+ (selectedMailbox === "starred" && thread.starred) ||
+ (selectedMailbox === "archive" && false) ||
+ selectedMailbox === "drafts" ||
+ selectedMailbox === "sent";
+ const text = `${thread.from} ${thread.subject} ${thread.snippet} ${thread.labels.join(" ")}`.toLowerCase();
+ const queryMatch = query.trim().length === 0 || text.includes(query.toLowerCase());
+ return accountMatch && mailboxMatch && queryMatch;
+ });
+ }, [query, selectedAccount, selectedMailbox, threads]);
+
+ const effectiveSelectedThreadId = visibleThreads.some((thread) => thread.id === selectedThreadId)
+ ? selectedThreadId
+ : visibleThreads[0]?.id ?? "";
+ const selectedThread = threads.find((thread) => thread.id === effectiveSelectedThreadId) ?? visibleThreads[0];
+ const showDebugChrome = shouldShowDebugChrome(appEnv);
+
+ React.useEffect(() => {
+ document.documentElement.classList.toggle("dark", theme === "dark");
+ document.documentElement.dataset.theme = theme;
+ try {
+ window.localStorage.setItem(THEME_STORAGE_KEY, theme);
+ } catch {
+ // Ignore private browsing or storage-disabled environments.
+ }
+ }, [theme]);
+
+ React.useEffect(() => {
+ try {
+ window.localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, sidebarWidthRem.toFixed(3));
+ } catch {
+ // Ignore private browsing or storage-disabled environments.
+ }
+ }, [sidebarWidthRem]);
+
+ React.useEffect(() => {
+ try {
+ window.localStorage.setItem(THREAD_WIDTH_STORAGE_KEY, threadWidthRem.toFixed(3));
+ } catch {
+ // Ignore private browsing or storage-disabled environments.
+ }
+ }, [threadWidthRem]);
+
+ function startColumnResize(
+ event: React.PointerEvent,
+ initialWidthRem: number,
+ minWidthRem: number,
+ maxWidthRem: number,
+ onResize: React.Dispatch>
+ ) {
+ event.preventDefault();
+ event.currentTarget.setPointerCapture(event.pointerId);
+
+ const startX = event.clientX;
+ const remPx = rootRemPx();
+
+ function handlePointerMove(moveEvent: PointerEvent) {
+ const deltaRem = (moveEvent.clientX - startX) / remPx;
+ onResize(clamp(initialWidthRem + deltaRem, minWidthRem, maxWidthRem));
+ }
+
+ function handlePointerUp() {
+ window.removeEventListener("pointermove", handlePointerMove);
+ window.removeEventListener("pointerup", handlePointerUp);
+ }
+
+ window.addEventListener("pointermove", handlePointerMove);
+ window.addEventListener("pointerup", handlePointerUp, { once: true });
+ }
+
+ function markSelectedRead() {
+ if (!selectedThread) return;
+ setThreads((current) =>
+ current.map((thread) =>
+ thread.id === selectedThread.id ? { ...thread, unread: false } : thread
+ )
+ );
+ }
+
+ function toggleStar(threadId: string) {
+ setThreads((current) =>
+ current.map((thread) =>
+ thread.id === threadId ? { ...thread, starred: !thread.starred } : thread
+ )
+ );
+ }
+
+ function openReply() {
+ setDraft(emptyDraft(selectedThread));
+ setComposeOpen(true);
+ }
+
+ function sendDraft() {
+ if (!draft.to || !draft.body.trim()) return;
+ setComposeOpen(false);
+ setDraft(emptyDraft(selectedThread));
+ markSelectedRead();
+ }
+
+ return (
+
+
+
+
+
+
+ startColumnResize(event, sidebarWidthRem, SIDEBAR_WIDTH.min, SIDEBAR_WIDTH.max, setSidebarWidthRem)
+ }
+ />
+ setSidebarOpen(true)}
+ onQueryChange={setQuery}
+ onSelectThread={(id) => {
+ setSelectedThreadId(id);
+ setThreads((current) =>
+ current.map((thread) => (thread.id === id ? { ...thread, unread: false } : thread))
+ );
+ }}
+ onToggleStar={toggleStar}
+ />
+
+ startColumnResize(event, threadWidthRem, THREAD_WIDTH.min, THREAD_WIDTH.max, setThreadWidthRem)
+ }
+ />
+
+ setComposeOpen(false)}
+ onDraftChange={setDraft}
+ onReply={openReply}
+ onSend={sendDraft}
+ onToggleStar={() => selectedThread && toggleStar(selectedThread.id)}
+ />
+
+
+
+ );
+}
+
+function SidebarContent({
+ selectedMailbox,
+ selectedAccount,
+ theme,
+ onSelectMailbox,
+ onSelectAccount,
+ onToggleTheme,
+ onClose,
+}: {
+ selectedMailbox: string;
+ selectedAccount: string;
+ theme: "light" | "dark";
+ onSelectMailbox: (id: string) => void;
+ onSelectAccount: (id: string) => void;
+ onToggleTheme: () => void;
+ onClose: () => void;
+}) {
+ return (
+
+
+
+
+ A
+
+
+
Atelier Mail
+
sam.atelierwork.space
+
+
+
+
+
+
+
+
+
+
+
+
+ Accounts
+ onSelectAccount("all")} />
+ {accounts.map((account) => (
+
+ ))}
+
+
+
+ Smart Filters
+ onSelectMailbox("inbox")} />
+ onSelectMailbox("inbox")} />
+ onSelectMailbox("inbox")} />
+
+
+
+
+ );
+}
+
+function SidebarAction({
+ icon: Icon,
+ label,
+ active,
+ count,
+ onClick,
+}: {
+ icon: React.ComponentType>;
+ label: string;
+ active?: boolean;
+ count?: number;
+ onClick: () => void;
+}) {
+ return (
+
+ );
+}
+
+function CountPill({ active, children }: { active?: boolean; children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function ColumnResizeHandle({
+ ariaLabel,
+ onPointerDown,
+}: {
+ ariaLabel: string;
+ onPointerDown: (event: React.PointerEvent) => void;
+}) {
+ return (
+
+ );
+}
+
+function ThreadColumn({
+ query,
+ selectedThreadId,
+ theme,
+ widthRem,
+ threads,
+ onOpenSidebar,
+ onQueryChange,
+ onSelectThread,
+ onToggleStar,
+}: {
+ query: string;
+ selectedThreadId: string;
+ theme: "light" | "dark";
+ widthRem: number;
+ threads: MailThread[];
+ onOpenSidebar: () => void;
+ onQueryChange: (query: string) => void;
+ onSelectThread: (id: string) => void;
+ onToggleStar: (id: string) => void;
+}) {
+ const parentRef = React.useRef(null);
+ const searchInputRef = React.useRef(null);
+
+ React.useEffect(() => {
+ function focusSearch(event: KeyboardEvent) {
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
+ event.preventDefault();
+ searchInputRef.current?.focus();
+ }
+ }
+
+ window.addEventListener("keydown", focusSearch);
+ return () => window.removeEventListener("keydown", focusSearch);
+ }, []);
+
+ // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual intentionally exposes imperative measurement helpers.
+ const virtualizer = useVirtualizer({
+ count: threads.length,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 84,
+ overscan: 6,
+ });
+
+ return (
+
+
+
+
+
+
+ onQueryChange(event.target.value)}
+ />
+
+ Cmd K
+
+
+
+
+
+
+ Press `J` / `K` to move later
+
+
+
+
+ {threads.length === 0 ? (
+
+
+
+
+
Nothing matched that view
+
+ The sync index is ready, but this filter is quiet. Try all accounts or clear search.
+
+
+ ) : (
+
+ {virtualizer.getVirtualItems().map((item) => {
+ const thread = threads[item.index];
+ return (
+
+ onSelectThread(thread.id)}
+ onToggleStar={() => onToggleStar(thread.id)}
+ />
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
+
+function ThreadRow({
+ thread,
+ selected,
+ theme,
+ onSelect,
+ onToggleStar,
+}: {
+ thread: MailThread;
+ selected: boolean;
+ theme: "light" | "dark";
+ onSelect: () => void;
+ onToggleStar: () => void;
+}) {
+ return (
+
+
+
+
+ {thread.labels.slice(0, 2).map((label) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+function threadRowStyle(thread: MailThread, selected: boolean, theme: "light" | "dark"): React.CSSProperties {
+ if (theme === "dark") {
+ return {
+ backgroundColor: selected ? "#28140e" : thread.unread ? "#1c100d" : "#0d0b0a",
+ color: "#f6eee9",
+ };
+ }
+
+ const backgroundColor = selected ? "#ffe4dc" : thread.unread ? "#ffebe5" : "#ffffff";
+
+ return {
+ backgroundColor,
+ color: "#242120",
+ };
+}
+
+function fieldSurfaceStyle(theme: "light" | "dark"): React.CSSProperties {
+ return {
+ backgroundColor: theme === "dark" ? "#0d0b0a" : "#ffffff",
+ color: theme === "dark" ? "#f6eee9" : "#242120",
+ };
+}
+
+function ContactAvatar({ thread }: { thread: MailThread }) {
+ return (
+
+ {thread.avatarInitials}
+
+ );
+}
+
+const avatarToneClass: Record = {
+ coral: "bg-[linear-gradient(135deg,#ff6a4a,#ffd0bd)] text-white",
+ cyan: "bg-[linear-gradient(135deg,#18a6b8,#d6fbff)] text-slate-900",
+ gold: "bg-[linear-gradient(135deg,#eab308,#fff2b6)] text-slate-900",
+ ink: "bg-[linear-gradient(135deg,#111827,#d8dee9)] text-white",
+};
+
+function TagChip({ label }: { label: string }) {
+ const style = tagStyles[label] ?? tagStyles.Default;
+ const Icon = style.icon;
+
+ return (
+
+
+ {label}
+
+ );
+}
+
+const tagStyles: Record>; className: string }> = {
+ Architecture: {
+ icon: Braces,
+ className: "border-primary/25 bg-primary/10 text-primary",
+ },
+ KV: {
+ icon: Database,
+ className: "border-accent/40 bg-accent/30 text-accent-foreground",
+ },
+ Gmail: {
+ icon: Mail,
+ className: "border-destructive/25 bg-destructive/10 text-destructive",
+ },
+ Sync: {
+ icon: RefreshCw,
+ className: "border-secondary/50 bg-secondary text-secondary-foreground",
+ },
+ JMAP: {
+ icon: Tags,
+ className: "border-border bg-muted text-muted-foreground",
+ },
+ IMAP: {
+ icon: Archive,
+ className: "border-accent/40 bg-accent/30 text-accent-foreground",
+ },
+ Fixture: {
+ icon: FileText,
+ className: "border-border bg-muted text-muted-foreground",
+ },
+ Default: {
+ icon: Tags,
+ className: "border-border bg-muted text-muted-foreground",
+ },
+};
+
+function ReaderPane({
+ thread,
+ composeOpen,
+ draft,
+ showDebugChrome,
+ theme,
+ onArchive,
+ onCloseCompose,
+ onDraftChange,
+ onReply,
+ onSend,
+ onToggleStar,
+}: {
+ thread?: MailThread;
+ composeOpen: boolean;
+ draft: ComposerDraft;
+ showDebugChrome: boolean;
+ theme: "light" | "dark";
+ onArchive: () => void;
+ onCloseCompose: () => void;
+ onDraftChange: (draft: ComposerDraft) => void;
+ onReply: () => void;
+ onSend: () => void;
+ onToggleStar: () => void;
+}) {
+ if (!thread) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {showDebugChrome ? atelier://mail/thread/{thread.id} : null}
+ {thread.flagged ? Needs review : null}
+
+
{thread.subject}
+
+ {thread.from} <{thread.email}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{thread.from}
+
to Atelier Ops
+
+
+
{thread.time}
+
+
+ {thread.body.map((paragraph) => (
+
{paragraph}
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/mail/sampleData.ts b/apps/web/src/components/mail/sampleData.ts
new file mode 100644
index 0000000..a3aba0a
--- /dev/null
+++ b/apps/web/src/components/mail/sampleData.ts
@@ -0,0 +1,116 @@
+import type { MailAccount, Mailbox, MailThread } from "./types";
+
+export const accounts: MailAccount[] = [
+ {
+ id: "gmail-primary",
+ name: "Atelier Ops",
+ address: "ops@atelierwork.space",
+ provider: "gmail",
+ accent: "#f05d5e",
+ unread: 12,
+ },
+ {
+ id: "jmap-studio",
+ name: "Studio",
+ address: "studio@atelierwork.space",
+ provider: "jmap",
+ accent: "#22b8cf",
+ unread: 4,
+ },
+ {
+ id: "imap-archive",
+ name: "Archive",
+ address: "archive@atelierwork.space",
+ provider: "imap",
+ accent: "#e6b800",
+ unread: 0,
+ },
+];
+
+export const mailboxes: Mailbox[] = [
+ { id: "inbox", label: "Unified Inbox", count: 16 },
+ { id: "starred", label: "Starred", count: 5 },
+ { id: "drafts", label: "Drafts", count: 2 },
+ { id: "sent", label: "Sent", count: 0 },
+ { id: "archive", label: "Archive", count: 0 },
+];
+
+export const threads: MailThread[] = [
+ {
+ id: "thread-001",
+ accountId: "gmail-primary",
+ from: "Mira Chen",
+ email: "mira@example.com",
+ avatarInitials: "MC",
+ avatarTone: "coral",
+ subject: "Permissioned KV model for linked mail threads",
+ snippet: "I like the split: schemas as contracts, private records in Atelier KV until the protocol design settles.",
+ time: "9:42 AM",
+ unread: true,
+ starred: true,
+ flagged: false,
+ labels: ["Architecture", "KV"],
+ body: [
+ "I like the split: schemas as contracts, private records in Atelier KV until the protocol design settles.",
+ "The key detail is making the migration adapter boring. If the future Permissioned Data API lands cleanly, we should be able to map owner DID, namespace, key, schema version, and encrypted payload without changing product code.",
+ "Also: please keep linked-thread records free of message snippets. A target URI plus optional user-authored note is enough."
+ ],
+ },
+ {
+ id: "thread-002",
+ accountId: "gmail-primary",
+ from: "Google Cloud",
+ email: "notifications@google.com",
+ avatarInitials: "G",
+ avatarTone: "cyan",
+ subject: "Pub/Sub topic verification for Gmail watch",
+ snippet: "Your webhook endpoint can now receive push messages for historyId cursor updates.",
+ time: "8:18 AM",
+ unread: true,
+ starred: false,
+ flagged: true,
+ labels: ["Gmail", "Sync"],
+ body: [
+ "Your webhook endpoint can now receive push messages for historyId cursor updates.",
+ "Remember to validate the audience and issuer before enqueueing sync work."
+ ],
+ },
+ {
+ id: "thread-003",
+ accountId: "jmap-studio",
+ from: "Fastmail JMAP",
+ email: "api@fastmail.com",
+ avatarInitials: "FJ",
+ avatarTone: "ink",
+ subject: "JMAP changes endpoint notes",
+ snippet: "Threads, mailboxes, identities, and search can all map cleanly into the common mail domain.",
+ time: "Yesterday",
+ unread: false,
+ starred: false,
+ flagged: false,
+ labels: ["JMAP"],
+ body: [
+ "Threads, mailboxes, identities, and search can all map cleanly into the common mail domain.",
+ "The main thing to preserve is the account-scoped cursor and a durable provider mapping table."
+ ],
+ },
+ {
+ id: "thread-004",
+ accountId: "imap-archive",
+ from: "Legacy IMAP",
+ email: "ops@legacy.example",
+ avatarInitials: "LI",
+ avatarTone: "gold",
+ subject: "Threading fallback fixture",
+ snippet: "This mailbox has incomplete References headers, so the fixture should become a single-message thread.",
+ time: "Mon",
+ unread: false,
+ starred: true,
+ flagged: false,
+ labels: ["IMAP", "Fixture"],
+ body: [
+ "This mailbox has incomplete References headers, so the fixture should become a single-message thread.",
+ "A normalized subject plus time-window fallback is acceptable only after header threading fails."
+ ],
+ },
+];
diff --git a/apps/web/src/components/mail/types.ts b/apps/web/src/components/mail/types.ts
new file mode 100644
index 0000000..ea612c0
--- /dev/null
+++ b/apps/web/src/components/mail/types.ts
@@ -0,0 +1,39 @@
+export type Provider = "gmail" | "jmap" | "imap";
+
+export interface MailAccount {
+ id: string;
+ name: string;
+ address: string;
+ provider: Provider;
+ accent: string;
+ unread: number;
+}
+
+export interface Mailbox {
+ id: string;
+ label: string;
+ count: number;
+}
+
+export interface MailThread {
+ id: string;
+ accountId: string;
+ from: string;
+ email: string;
+ avatarInitials: string;
+ avatarTone: "coral" | "cyan" | "gold" | "ink";
+ subject: string;
+ snippet: string;
+ time: string;
+ unread: boolean;
+ starred: boolean;
+ flagged: boolean;
+ labels: string[];
+ body: string[];
+}
+
+export interface ComposerDraft {
+ to: string;
+ subject: string;
+ body: string;
+}
diff --git a/apps/web/src/components/shared/EnvironmentBanner.tsx b/apps/web/src/components/shared/EnvironmentBanner.tsx
new file mode 100644
index 0000000..3a2ee9d
--- /dev/null
+++ b/apps/web/src/components/shared/EnvironmentBanner.tsx
@@ -0,0 +1,15 @@
+import { environmentBannerMessage, shouldShowEnvironmentBanner, type AppEnv } from "@/lib/appEnv";
+
+export function EnvironmentBanner({ appEnv }: { appEnv: AppEnv }) {
+ if (!shouldShowEnvironmentBanner(appEnv)) return null;
+
+ return (
+
+ {environmentBannerMessage(appEnv)} - development data and relaxed limits
+
+ );
+}
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
new file mode 100644
index 0000000..6745ca1
--- /dev/null
+++ b/apps/web/src/components/ui/badge.tsx
@@ -0,0 +1,31 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors",
+ {
+ variants: {
+ variant: {
+ default: "border-transparent bg-primary text-primary-foreground",
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
+ outline: "text-foreground",
+ success: "border-transparent bg-[#dcfce7] text-[#166534]",
+ warning: "border-transparent bg-accent text-accent-foreground",
+ failed: "border-transparent bg-[#fee2e2] text-[#991b1b]",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+export function Badge({ className, variant, ...props }: BadgeProps) {
+ return ;
+}
+
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
new file mode 100644
index 0000000..b0b5715
--- /dev/null
+++ b/apps/web/src/components/ui/button.tsx
@@ -0,0 +1,40 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-muted text-foreground",
+ outline: "border bg-background hover:bg-muted",
+ subtle: "bg-muted text-foreground hover:bg-muted/80",
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 px-3 text-xs",
+ icon: "size-8",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {}
+
+export const Button = React.forwardRef(
+ ({ className, variant, size, ...props }, ref) => (
+
+ )
+);
+Button.displayName = "Button";
+
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx
new file mode 100644
index 0000000..0d11ea4
--- /dev/null
+++ b/apps/web/src/components/ui/input.tsx
@@ -0,0 +1,18 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => (
+
+ )
+);
+Input.displayName = "Input";
+
diff --git a/apps/web/src/components/ui/separator.tsx b/apps/web/src/components/ui/separator.tsx
new file mode 100644
index 0000000..1f18d91
--- /dev/null
+++ b/apps/web/src/components/ui/separator.tsx
@@ -0,0 +1,5 @@
+import { cn } from "@/lib/utils";
+
+export function Separator({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
diff --git a/apps/web/src/components/ui/skeleton.tsx b/apps/web/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..4c2911c
--- /dev/null
+++ b/apps/web/src/components/ui/skeleton.tsx
@@ -0,0 +1,6 @@
+import { cn } from "@/lib/utils";
+
+export function Skeleton({ className, ...props }: React.HTMLAttributes) {
+ return ;
+}
+
diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..c27a204
--- /dev/null
+++ b/apps/web/src/components/ui/textarea.tsx
@@ -0,0 +1,17 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export const Textarea = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+Textarea.displayName = "Textarea";
+
diff --git a/apps/web/src/lib/appEnv.ts b/apps/web/src/lib/appEnv.ts
new file mode 100644
index 0000000..4cdf51c
--- /dev/null
+++ b/apps/web/src/lib/appEnv.ts
@@ -0,0 +1,36 @@
+export type AppEnv = "prod" | "dev" | "local" | "test" | (string & {});
+
+export function normalizeAppEnv(raw: string): AppEnv {
+ const value = raw.trim().toLowerCase();
+ if (value === "production") return "prod";
+ return value as AppEnv;
+}
+
+export function getAppEnv(): AppEnv {
+ const raw = process.env.NEXT_PUBLIC_APP_ENV?.trim() || process.env.APP_ENV?.trim() || "";
+ if (raw) return normalizeAppEnv(raw);
+ if (process.env.NODE_ENV === "development") return "local";
+ if (process.env.VERCEL_ENV === "production") return "prod";
+ return "dev";
+}
+
+export function shouldShowEnvironmentBanner(appEnv: AppEnv): boolean {
+ return appEnv === "local" || appEnv === "dev" || appEnv === "test";
+}
+
+export function shouldShowDebugChrome(appEnv: AppEnv): boolean {
+ return shouldShowEnvironmentBanner(appEnv);
+}
+
+export function environmentBannerMessage(appEnv: AppEnv): string {
+ switch (appEnv) {
+ case "local":
+ return "Local Environment";
+ case "dev":
+ return "Development Environment";
+ case "test":
+ return "Testing Environment";
+ default:
+ return "";
+ }
+}
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/apps/web/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/apps/web/src/test/setup.ts b/apps/web/src/test/setup.ts
new file mode 100644
index 0000000..d913368
--- /dev/null
+++ b/apps/web/src/test/setup.ts
@@ -0,0 +1,25 @@
+import { afterEach } from "bun:test";
+import { JSDOM } from "jsdom";
+
+const dom = new JSDOM("", {
+ url: "http://localhost:3000",
+});
+
+globalThis.window = dom.window as unknown as Window & typeof globalThis;
+globalThis.document = dom.window.document;
+globalThis.navigator = dom.window.navigator;
+globalThis.HTMLElement = dom.window.HTMLElement;
+globalThis.ResizeObserver = class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+};
+
+const { cleanup } = await import("@testing-library/react");
+
+afterEach(() => {
+ cleanup();
+ window.localStorage.clear();
+ document.documentElement.className = "";
+ delete document.documentElement.dataset.theme;
+});
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
new file mode 100644
index 0000000..752a139
--- /dev/null
+++ b/apps/web/tsconfig.json
@@ -0,0 +1,41 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "es2022"
+ ],
+ "allowJs": false,
+ "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"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..8bcacc8
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,1070 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "atelier-mail",
+ },
+ "apps/web": {
+ "name": "@atelier-mail/web",
+ "version": "0.1.0",
+ "dependencies": {
+ "@atelierwork/lexicons": "workspace:*",
+ "@tanstack/react-virtual": "^3.13.24",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^1.14.0",
+ "next": "16.2.6",
+ "react": "19.2.4",
+ "react-dom": "19.2.4",
+ "tailwind-merge": "^3.6.0",
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@testing-library/react": "^16.3.2",
+ "@types/bun": "^1.3.4",
+ "@types/jsdom": "^28.0.3",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "16.2.6",
+ "jsdom": "^27.2.0",
+ "playwright": "^1.61.1",
+ "tailwindcss": "^4",
+ "typescript": "^5",
+ },
+ },
+ "packages/lexicons": {
+ "name": "@atelierwork/lexicons",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@types/bun": "^1.3.4",
+ "typescript": "^5",
+ },
+ },
+ },
+ "trustedDependencies": [
+ "sharp",
+ "unrs-resolver",
+ ],
+ "packages": {
+ "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
+
+ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
+
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.2", "", { "dependencies": { "@csstools/css-calc": "^3.0.0", "@csstools/css-color-parser": "^4.0.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.5" } }, "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg=="],
+
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="],
+
+ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+
+ "@atelier-mail/web": ["@atelier-mail/web@workspace:apps/web"],
+
+ "@atelierwork/lexicons": ["@atelierwork/lexicons@workspace:packages/lexicons"],
+
+ "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
+
+ "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@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" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
+
+ "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
+
+ "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
+
+ "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
+
+ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
+
+ "@csstools/color-helpers": ["@csstools/color-helpers@6.1.0", "", {}, "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@3.2.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.9", "", { "dependencies": { "@csstools/color-helpers": "^6.1.0", "@csstools/css-calc": "^3.2.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
+
+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.5", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
+
+ "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+
+ "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
+
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "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" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
+
+ "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
+
+ "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
+
+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
+
+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
+
+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
+
+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
+
+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
+
+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
+
+ "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
+
+ "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
+
+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
+
+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
+
+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
+
+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
+
+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
+
+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
+
+ "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
+
+ "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
+
+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
+
+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
+
+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
+
+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
+
+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
+
+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
+
+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
+
+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
+
+ "@next/env": ["@next/env@16.2.6", "", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="],
+
+ "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw=="],
+
+ "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="],
+
+ "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="],
+
+ "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="],
+
+ "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="],
+
+ "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="],
+
+ "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="],
+
+ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="],
+
+ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="],
+
+ "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
+
+ "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
+
+ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
+
+ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
+
+ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
+
+ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="],
+
+ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "postcss": "8.5.15", "tailwindcss": "4.3.1" } }, "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A=="],
+
+ "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.3", "", { "dependencies": { "@tanstack/virtual-core": "3.17.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ=="],
+
+ "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.1", "", {}, "sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA=="],
+
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
+
+ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
+
+ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
+
+ "@types/jsdom": ["@types/jsdom@28.0.3", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^8.0.0", "undici-types": "^7.21.0" } }, "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
+
+ "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
+
+ "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/type-utils": "8.62.0", "@typescript-eslint/utils": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.62.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.62.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.0", "@typescript-eslint/types": "^8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0" } }, "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.62.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.62.0", "", {}, "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.62.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.62.0", "@typescript-eslint/tsconfig-utils": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ=="],
+
+ "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.12.2", "", { "os": "android", "cpu": "arm" }, "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w=="],
+
+ "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.12.2", "", { "os": "android", "cpu": "arm64" }, "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ=="],
+
+ "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.12.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w=="],
+
+ "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.12.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA=="],
+
+ "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.12.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg=="],
+
+ "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A=="],
+
+ "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.12.2", "", { "os": "linux", "cpu": "arm" }, "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g=="],
+
+ "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.12.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg=="],
+
+ "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.12.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA=="],
+
+ "@unrs/resolver-binding-linux-loong64-gnu": ["@unrs/resolver-binding-linux-loong64-gnu@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q=="],
+
+ "@unrs/resolver-binding-linux-loong64-musl": ["@unrs/resolver-binding-linux-loong64-musl@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew=="],
+
+ "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.12.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg=="],
+
+ "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A=="],
+
+ "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.12.2", "", { "os": "linux", "cpu": "none" }, "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w=="],
+
+ "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.12.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw=="],
+
+ "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.12.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ=="],
+
+ "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.12.2", "", { "os": "linux", "cpu": "x64" }, "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A=="],
+
+ "@unrs/resolver-binding-openharmony-arm64": ["@unrs/resolver-binding-openharmony-arm64@1.12.2", "", { "os": "none", "cpu": "arm64" }, "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ=="],
+
+ "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.12.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A=="],
+
+ "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.12.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g=="],
+
+ "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.12.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g=="],
+
+ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.12.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA=="],
+
+ "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ajv": ["ajv@6.15.0", "", { "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" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
+
+ "array-includes": ["array-includes@3.1.9", "", { "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" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
+
+ "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "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" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
+
+ "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "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" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
+
+ "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
+
+ "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
+
+ "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "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" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
+
+ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "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" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
+
+ "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
+
+ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
+
+ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
+
+ "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="],
+
+ "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.40", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw=="],
+
+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
+
+ "brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "browserslist": ["browserslist@4.28.4", "", { "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", "electron-to-chromium": "^1.5.376", "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw=="],
+
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
+
+ "call-bind": ["call-bind@1.0.9", "", { "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" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+ "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
+
+ "cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
+
+ "data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="],
+
+ "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
+
+ "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
+
+ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
+
+ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.378", "", {}, "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ=="],
+
+ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
+
+ "entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
+
+ "es-abstract": ["es-abstract@1.24.2", "", { "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" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
+
+ "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-iterator-helpers": ["es-iterator-helpers@1.3.3", "", { "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" } }, "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
+
+ "es-to-primitive": ["es-to-primitive@1.3.3", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-3OJmZ3m4VNhTWEfn/bPv7yoSd/7Kx0MYdm27o7SqgS45l03O4purW3zO4B/4JroAWHluvmQaXqrPXFtT4IKYEw=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.4", "", { "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" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
+
+ "eslint-config-next": ["eslint-config-next@16.2.6", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.6", "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" }, "optionalPeers": ["typescript"] }, "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q=="],
+
+ "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="],
+
+ "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "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" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
+
+ "eslint-module-utils": ["eslint-module-utils@2.13.0", "", { "dependencies": { "debug": "^3.2.7" }, "peerDependencies": { "eslint": "*" }, "optionalPeers": ["eslint"] }, "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ=="],
+
+ "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "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" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
+
+ "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "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" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
+
+ "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "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" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
+
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "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" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-glob": ["fast-glob@3.3.1", "", { "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" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
+
+ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
+
+ "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="],
+
+ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
+
+ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
+
+ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@16.4.0", "", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="],
+
+ "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+
+ "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
+
+ "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
+
+ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+
+ "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
+
+ "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
+
+ "is-async-function": ["is-async-function@2.1.1", "", { "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" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
+
+ "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
+
+ "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
+
+ "is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="],
+
+ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
+
+ "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
+
+ "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
+
+ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
+
+ "is-document.all": ["is-document.all@1.0.0", "", { "dependencies": { "call-bound": "^1.0.4" } }, "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
+
+ "is-generator-function": ["is-generator-function@1.1.2", "", { "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" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
+
+ "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
+
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
+ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
+
+ "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
+
+ "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
+
+ "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
+
+ "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
+
+ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
+
+ "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
+
+ "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
+
+ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
+
+ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "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" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
+
+ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="],
+
+ "jsdom": ["jsdom@27.4.0", "", { "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", "@exodus/bytes": "^1.6.0", "cssstyle": "^5.3.4", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
+
+ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
+
+ "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
+ "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
+
+ "lucide-react": ["lucide-react@1.21.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ=="],
+
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
+
+ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+
+ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
+
+ "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@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" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "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" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="],
+
+ "node-exports-info": ["node-exports-info@1.6.2", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag=="],
+
+ "node-releases": ["node-releases@2.0.50", "", {}, "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg=="],
+
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
+
+ "object.assign": ["object.assign@4.1.7", "", { "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" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
+
+ "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
+
+ "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
+
+ "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="],
+
+ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
+
+ "optionator": ["optionator@0.9.4", "", { "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" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="],
+
+ "playwright-core": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="],
+
+ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
+
+ "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
+ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
+
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+
+ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
+ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "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" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
+
+ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "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" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.2", "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" } }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
+
+ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
+
+ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
+
+ "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
+
+ "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
+
+ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
+
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "set-function-length": ["set-function-length@1.2.2", "", { "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" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
+
+ "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
+
+ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
+
+ "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "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" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="],
+
+ "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
+
+ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
+
+ "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
+
+ "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "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" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
+
+ "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
+
+ "string.prototype.trim": ["string.prototype.trim@1.2.11", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-object-atoms": "^1.1.2", "has-property-descriptors": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w=="],
+
+ "string.prototype.trimend": ["string.prototype.trimend@1.0.10", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.2" } }, "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw=="],
+
+ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
+
+ "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
+ "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
+
+ "tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="],
+
+ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
+
+ "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "tldts": ["tldts@7.4.4", "", { "dependencies": { "tldts-core": "^7.4.4" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g=="],
+
+ "tldts-core": ["tldts-core@7.4.4", "", {}, "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
+
+ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
+
+ "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
+
+ "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
+
+ "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "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" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
+
+ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "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" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
+
+ "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.62.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/parser": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q=="],
+
+ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
+
+ "undici-types": ["undici-types@7.28.0", "", {}, "sha512-LJAfY+2w6HGeT8d8J1wNQsUGUEGio6NWWpwdwurQe4f6oojzCFuGLizl1KSve4irsTxyLly1QhEeE6iapdaIvQ=="],
+
+ "unrs-resolver": ["unrs-resolver@1.12.2", "", { "dependencies": { "napi-postinstall": "^0.3.4" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.12.2", "@unrs/resolver-binding-android-arm64": "1.12.2", "@unrs/resolver-binding-darwin-arm64": "1.12.2", "@unrs/resolver-binding-darwin-x64": "1.12.2", "@unrs/resolver-binding-freebsd-x64": "1.12.2", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", "@unrs/resolver-binding-linux-x64-musl": "1.12.2", "@unrs/resolver-binding-openharmony-arm64": "1.12.2", "@unrs/resolver-binding-wasm32-wasi": "1.12.2", "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
+ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
+
+ "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+
+ "whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "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" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
+
+ "which-builtin-type": ["which-builtin-type@1.2.1", "", { "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" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
+
+ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
+
+ "which-typed-array": ["which-typed-array@1.1.22", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
+
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
+
+ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+
+ "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "@unrs/resolver-binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
+ "data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
+
+ "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+
+ "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+
+ "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
+
+ "eslint-plugin-jsx-a11y/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
+
+ "is-bun-module/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
+
+ "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
+
+ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "sharp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+ }
+}
diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 0000000..641e878
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1,2 @@
+[install]
+linker = "isolated"
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..69838f9
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,61 @@
+# Atelier Mail Architecture
+
+## Product Boundary
+
+Atelier Mail is a mail client, not a mail host. Provider traffic is server-side only. Mail content remains in provider systems and in the local PostgreSQL cache needed for the product experience. Mail bodies are not written to ATProto records or the private permissioned KV store.
+
+## Identity
+
+Users sign in with ATProto OAuth. The primary user key is the DID. The platform stores a DID document cache, handle cache, session state, and OAuth lifecycle metadata in PostgreSQL.
+
+## Namespace
+
+All Atelier Lexicon concepts use `space.atelierwork.*`.
+
+Initial draft namespaces:
+
+- `space.atelierwork.platform.*`
+- `space.atelierwork.mail.*`
+- `space.atelierwork.workspace.*`
+- `space.atelierwork.tasks.*`
+
+These schemas define data contracts. They do not imply every record is stored on a PDS.
+
+## Private Permissioned Store
+
+Until ATProto Permissioned Data is finalized, private permissioned concepts are stored in Atelier KV. The KV store is typed, schema-versioned, encrypted by owner boundary, and migration-ready.
+
+KV is appropriate for:
+
+- user preferences
+- provider descriptors without secrets
+- MCP grants
+- cross-app references
+- linked-thread metadata
+
+KV is not appropriate for:
+
+- provider refresh tokens
+- provider access tokens
+- mail bodies
+- attachments
+- sensitive message excerpts
+
+Provider secrets use account-scoped KMS envelope encryption and remain in the relational secret store.
+
+## Service Shape
+
+The first deployment is one Swift/Hummingbird API binary with logical modules:
+
+- API/auth
+- provider connection flows
+- mail sync and indexing
+- Gmail push ingress
+- KV access
+- MCP registry
+
+Domain logic lives in extraction-ready Swift packages so the deployment can later split into independently versioned services.
+
+## Client Shape
+
+The web app uses Next.js, shadcn, Tailwind, and lucide icons. The layout follows Social Wire's dense, scalable app shell and Skej's warmer personality: refined workspace chrome, compact rows, friendly status details, subtle motion, and no marketing landing page for signed-in users.
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 0000000..7fa5613
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,57 @@
+# Deployment
+
+Atelier Mail deploys as two processes:
+
+- `web`: Next.js app for `mail.atelierwork.space`.
+- `api`: Swift API service on port `8080`.
+
+PostgreSQL is the system of record. Private permissioned records stay in Atelier KV/Postgres until ATProto Permissioned Data is finalized.
+
+## Required Environment
+
+```bash
+ATELIER_ENV=production
+ATELIER_PUBLIC_URL=https://mail.atelierwork.space
+ATELIER_API_URL=https://api.mail.atelierwork.space
+NEXT_PUBLIC_ATELIER_API_URL=https://api.mail.atelierwork.space
+ATELIER_DB_URL=postgres://...
+ATELIER_KV_BACKEND=postgres
+ATELIER_KMS_PROVIDER=...
+ATELIER_TOKEN_KEK_ID=...
+ATELIER_ATPROTO_CLIENT_ID=...
+ATELIER_ATPROTO_REDIRECT_URI=https://mail.atelierwork.space/auth/callback
+ATELIER_GMAIL_CLIENT_ID=...
+ATELIER_GMAIL_CLIENT_SECRET=...
+ATELIER_GMAIL_PUBSUB_AUDIENCE=...
+ATELIER_MCP_ENABLED=true
+ATELIER_MCP_MYCONTEXTPROTOCOL_URL=...
+```
+
+## Local Container Smoke
+
+```bash
+docker compose -f infra/docker-compose.yml up --build
+curl http://localhost:8080/health
+```
+
+## API
+
+The Swift API binary serves HTTP by default:
+
+```bash
+bun run start:api
+```
+
+For deployment metadata without starting the server:
+
+```bash
+bun run summary:api
+```
+
+## Verification
+
+```bash
+bun run verify:all
+```
+
+`verify:all` runs web typecheck/lint/tests/build and Swift API tests/build. The Swift Testing target expects the Xcode beta toolchain unless `DEVELOPER_DIR` is overridden.
diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md
new file mode 100644
index 0000000..2f80288
--- /dev/null
+++ b/docs/implementation-plan.md
@@ -0,0 +1,32 @@
+# Implementation Plan
+
+## Phase 0: Foundation
+
+- Initialize monorepo tooling, CI, Docker Compose, docs, and environment template.
+- Scaffold Next.js web app, Swift API service, Swift package boundaries, and Lexicon package.
+- Add PostgreSQL migrations for identity, mail, private KV, provider secrets, MCP grants, jobs, sync cursors, and audit events.
+
+## Phase 1: Identity And Platform Core
+
+- Implement ATProto OAuth, DID/handle cache, tenant/user/session tables, auth middleware, audit logging, and OpenAPI contract.
+- Keep browser traffic provider-agnostic; provider access happens through the Swift service.
+
+## Phase 2: Private Permissioned Store
+
+- Implement typed KV records using `space.atelierwork.*` schema names.
+- Add schema versioning, migration hooks, encryption metadata, and an adapter boundary for future ATProto Permissioned Data.
+
+## Phase 3: Gmail Vertical Slice
+
+- Implement Gmail OAuth, envelope-encrypted tokens, initial sync, normalized thread/message/mailbox data, actions, and Postgres FTS.
+
+## Phase 4: Web Client Shell
+
+- Ship the usable three-pane mail workspace: sidebar, virtualized thread list, reader, account filters, responsive collapse, skeleton states, keyboard actions, and compose entry points.
+
+## Phase 5+: Sync, Compose, Providers, MCP
+
+- Add Gmail History API and Pub/Sub webhook ingress.
+- Add TipTap compose and send.
+- Add JMAP and IMAP adapters.
+- Expose read-only MCP tools, gated write tools, MyContextProtocol registration, and cross-app references.
diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml
new file mode 100644
index 0000000..84a789c
--- /dev/null
+++ b/infra/docker-compose.yml
@@ -0,0 +1,72 @@
+services:
+ api:
+ build:
+ context: ..
+ dockerfile: services/api/Dockerfile
+ environment:
+ ATELIER_ENV: ${ATELIER_ENV:-development}
+ ATELIER_PUBLIC_URL: ${ATELIER_PUBLIC_URL:-http://localhost:3000}
+ ATELIER_API_HOST: 0.0.0.0
+ ATELIER_API_PORT: 8080
+ ATELIER_DB_URL: ${ATELIER_DB_URL:-postgres://atelier:atelier@postgres:5432/atelier_mail}
+ ATELIER_KV_BACKEND: ${ATELIER_KV_BACKEND:-postgres}
+ ATELIER_GMAIL_CLIENT_ID: ${ATELIER_GMAIL_CLIENT_ID:-}
+ ATELIER_GMAIL_CLIENT_SECRET: ${ATELIER_GMAIL_CLIENT_SECRET:-}
+ ATELIER_GMAIL_PUBSUB_AUDIENCE: ${ATELIER_GMAIL_PUBSUB_AUDIENCE:-}
+ ports:
+ - "8080:8080"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/health >/dev/null"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+
+ web:
+ build:
+ context: ..
+ dockerfile: apps/web/Dockerfile
+ environment:
+ ATELIER_ENV: ${ATELIER_ENV:-development}
+ ATELIER_PUBLIC_URL: ${ATELIER_PUBLIC_URL:-http://localhost:3000}
+ NEXT_PUBLIC_ATELIER_API_URL: ${NEXT_PUBLIC_ATELIER_API_URL:-http://localhost:8080}
+ PORT: 3000
+ ports:
+ - "3000:3000"
+ depends_on:
+ api:
+ condition: service_healthy
+
+ postgres:
+ image: postgres:17-alpine
+ environment:
+ POSTGRES_USER: atelier
+ POSTGRES_PASSWORD: atelier
+ POSTGRES_DB: atelier_mail
+ ports:
+ - "5432:5432"
+ volumes:
+ - atelier-postgres:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U atelier -d atelier_mail"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+
+ minio:
+ image: minio/minio:RELEASE.2026-06-13T00-11-22Z
+ command: server /data --console-address ":9001"
+ environment:
+ MINIO_ROOT_USER: atelier
+ MINIO_ROOT_PASSWORD: ateliermail
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ volumes:
+ - atelier-minio:/data
+
+volumes:
+ atelier-postgres:
+ atelier-minio:
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8c0738e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "atelier-mail",
+ "version": "0.1.0",
+ "private": true,
+ "packageManager": "bun@1.3.14",
+ "workspaces": [
+ "apps/*",
+ "packages/lexicons"
+ ],
+ "scripts": {
+ "build": "bun run --filter './apps/web' build",
+ "build:api": "env DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift build --product AtelierAPI",
+ "dev": "bun run --filter './apps/web' dev",
+ "lint": "bun run --filter './apps/web' lint",
+ "start:api": "env DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift run AtelierAPI",
+ "summary:api": "env DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift run AtelierAPI --summary",
+ "test": "bun run --filter './apps/web' test && bun run --filter './packages/lexicons' test",
+ "test:api": "env DEVELOPER_DIR=/Applications/Xcode-beta.app/Contents/Developer swift test",
+ "typecheck": "bun run --filter './apps/web' typecheck && bun run --filter './packages/lexicons' typecheck",
+ "verify": "bun run typecheck && bun run lint && bun run test && bun run build",
+ "verify:all": "bun run verify && bun run test:api && bun run build:api"
+ },
+ "trustedDependencies": [
+ "sharp",
+ "unrs-resolver"
+ ],
+ "ignoreScripts": [
+ "sharp",
+ "unrs-resolver"
+ ]
+}
diff --git a/packages/lexicons/package.json b/packages/lexicons/package.json
new file mode 100644
index 0000000..7172223
--- /dev/null
+++ b/packages/lexicons/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@atelierwork/lexicons",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "test": "bun test",
+ "typecheck": "tsc --noEmit"
+ },
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.4",
+ "typescript": "^5"
+ }
+}
diff --git a/packages/lexicons/src/index.ts b/packages/lexicons/src/index.ts
new file mode 100644
index 0000000..ac75d6e
--- /dev/null
+++ b/packages/lexicons/src/index.ts
@@ -0,0 +1,29 @@
+import linkedThread from "./space/atelierwork/mail/linkedThread.json";
+import providerDescriptor from "./space/atelierwork/mail/providerDescriptor.json";
+import crossAppReference from "./space/atelierwork/platform/crossAppReference.json";
+import mcpGrant from "./space/atelierwork/platform/mcpGrant.json";
+import preference from "./space/atelierwork/platform/preference.json";
+import workspacePointer from "./space/atelierwork/workspace/pointer.json";
+import taskPointer from "./space/atelierwork/tasks/pointer.json";
+
+export const atelierLexicons = [
+ linkedThread,
+ providerDescriptor,
+ crossAppReference,
+ mcpGrant,
+ preference,
+ workspacePointer,
+ taskPointer,
+] as const;
+
+export type AtelierLexicon = (typeof atelierLexicons)[number];
+
+export function lexiconIDs(): string[] {
+ return atelierLexicons.map((schema) => schema.id);
+}
+
+export function privateKVLexiconIDs(): string[] {
+ return atelierLexicons
+ .filter((schema) => schema.metadata?.atelierStorage === "private-kv")
+ .map((schema) => schema.id);
+}
diff --git a/packages/lexicons/src/space/atelierwork/mail/linkedThread.json b/packages/lexicons/src/space/atelierwork/mail/linkedThread.json
new file mode 100644
index 0000000..c931fc0
--- /dev/null
+++ b/packages/lexicons/src/space/atelierwork/mail/linkedThread.json
@@ -0,0 +1,38 @@
+{
+ "lexicon": 1,
+ "id": "space.atelierwork.mail.linkedThread",
+ "revision": 1,
+ "description": "Private cross-app metadata linking a mail thread to another Atelier resource without storing mail content.",
+ "metadata": {
+ "atelierStorage": "private-kv",
+ "privacy": "Stored in Atelier KV; eligible for future Permissioned Data migration."
+ },
+ "defs": {
+ "main": {
+ "type": "record",
+ "key": "tid",
+ "record": {
+ "type": "object",
+ "required": ["threadUri", "targetUri", "createdAt"],
+ "properties": {
+ "threadUri": {
+ "type": "string",
+ "description": "atelier://mail/thread/"
+ },
+ "targetUri": {
+ "type": "string",
+ "description": "atelier://workspace/*, atelier://tasks/*, or another Atelier URI."
+ },
+ "note": {
+ "type": "string",
+ "maxLength": 500
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "datetime"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/lexicons/src/space/atelierwork/mail/providerDescriptor.json b/packages/lexicons/src/space/atelierwork/mail/providerDescriptor.json
new file mode 100644
index 0000000..03460e0
--- /dev/null
+++ b/packages/lexicons/src/space/atelierwork/mail/providerDescriptor.json
@@ -0,0 +1,38 @@
+{
+ "lexicon": 1,
+ "id": "space.atelierwork.mail.providerDescriptor",
+ "revision": 1,
+ "description": "Private descriptor for a connected mail provider account. This record never stores provider secrets or mail content.",
+ "metadata": {
+ "atelierStorage": "private-kv",
+ "privacy": "Stored in Atelier KV until ATProto Permissioned Data is finalized."
+ },
+ "defs": {
+ "main": {
+ "type": "record",
+ "key": "tid",
+ "record": {
+ "type": "object",
+ "required": ["provider", "displayName", "accountUri", "createdAt"],
+ "properties": {
+ "provider": {
+ "type": "string",
+ "knownValues": ["gmail", "jmap", "imap"]
+ },
+ "displayName": {
+ "type": "string",
+ "maxLength": 120
+ },
+ "accountUri": {
+ "type": "string",
+ "description": "Internal URI such as atelier://mail/account/."
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "datetime"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/lexicons/src/space/atelierwork/platform/crossAppReference.json b/packages/lexicons/src/space/atelierwork/platform/crossAppReference.json
new file mode 100644
index 0000000..84c0702
--- /dev/null
+++ b/packages/lexicons/src/space/atelierwork/platform/crossAppReference.json
@@ -0,0 +1,36 @@
+{
+ "lexicon": 1,
+ "id": "space.atelierwork.platform.crossAppReference",
+ "revision": 1,
+ "description": "Private reference between two Atelier resources.",
+ "metadata": {
+ "atelierStorage": "private-kv",
+ "privacy": "Cross-app references can reveal user workflow context and remain private."
+ },
+ "defs": {
+ "main": {
+ "type": "record",
+ "key": "tid",
+ "record": {
+ "type": "object",
+ "required": ["sourceUri", "targetUri", "createdAt"],
+ "properties": {
+ "sourceUri": {
+ "type": "string"
+ },
+ "targetUri": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string",
+ "maxLength": 160
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "datetime"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/lexicons/src/space/atelierwork/platform/mcpGrant.json b/packages/lexicons/src/space/atelierwork/platform/mcpGrant.json
new file mode 100644
index 0000000..53ade8f
--- /dev/null
+++ b/packages/lexicons/src/space/atelierwork/platform/mcpGrant.json
@@ -0,0 +1,39 @@
+{
+ "lexicon": 1,
+ "id": "space.atelierwork.platform.mcpGrant",
+ "revision": 1,
+ "description": "Private MCP tool grant for an agent or MCP client.",
+ "metadata": {
+ "atelierStorage": "private-kv",
+ "privacy": "Agent grants are sensitive authorization metadata and remain private."
+ },
+ "defs": {
+ "main": {
+ "type": "record",
+ "key": "tid",
+ "record": {
+ "type": "object",
+ "required": ["clientId", "scopes", "createdAt"],
+ "properties": {
+ "clientId": {
+ "type": "string"
+ },
+ "scopes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "expiresAt": {
+ "type": "string",
+ "format": "datetime"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "datetime"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/lexicons/src/space/atelierwork/platform/preference.json b/packages/lexicons/src/space/atelierwork/platform/preference.json
new file mode 100644
index 0000000..843c7db
--- /dev/null
+++ b/packages/lexicons/src/space/atelierwork/platform/preference.json
@@ -0,0 +1,37 @@
+{
+ "lexicon": 1,
+ "id": "space.atelierwork.platform.preference",
+ "revision": 1,
+ "description": "Private schema-versioned user preference record for Atelier apps.",
+ "metadata": {
+ "atelierStorage": "private-kv",
+ "privacy": "Preferences are private by default and stored in Atelier KV."
+ },
+ "defs": {
+ "main": {
+ "type": "record",
+ "key": "tid",
+ "record": {
+ "type": "object",
+ "required": ["app", "key", "value", "updatedAt"],
+ "properties": {
+ "app": {
+ "type": "string",
+ "knownValues": ["mail", "workspace", "tasks", "calendar", "drive", "platform"]
+ },
+ "key": {
+ "type": "string",
+ "maxLength": 120
+ },
+ "value": {
+ "type": "unknown"
+ },
+ "updatedAt": {
+ "type": "string",
+ "format": "datetime"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/lexicons/src/space/atelierwork/tasks/pointer.json b/packages/lexicons/src/space/atelierwork/tasks/pointer.json
new file mode 100644
index 0000000..d55c399
--- /dev/null
+++ b/packages/lexicons/src/space/atelierwork/tasks/pointer.json
@@ -0,0 +1,24 @@
+{
+ "lexicon": 1,
+ "id": "space.atelierwork.tasks.pointer",
+ "revision": 1,
+ "description": "Private pointer to an Atelier Tasks resource used for cross-app linking.",
+ "metadata": {
+ "atelierStorage": "private-kv",
+ "privacy": "Task pointers are private until the Permissioned Data model is finalized."
+ },
+ "defs": {
+ "main": {
+ "type": "record",
+ "key": "tid",
+ "record": {
+ "type": "object",
+ "required": ["taskUri", "createdAt"],
+ "properties": {
+ "taskUri": { "type": "string" },
+ "createdAt": { "type": "string", "format": "datetime" }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/lexicons/src/space/atelierwork/workspace/pointer.json b/packages/lexicons/src/space/atelierwork/workspace/pointer.json
new file mode 100644
index 0000000..3a61114
--- /dev/null
+++ b/packages/lexicons/src/space/atelierwork/workspace/pointer.json
@@ -0,0 +1,25 @@
+{
+ "lexicon": 1,
+ "id": "space.atelierwork.workspace.pointer",
+ "revision": 1,
+ "description": "Private pointer to an Atelier Workspace resource used for cross-app linking.",
+ "metadata": {
+ "atelierStorage": "private-kv",
+ "privacy": "Workspace pointers are private until the Permissioned Data model is finalized."
+ },
+ "defs": {
+ "main": {
+ "type": "record",
+ "key": "tid",
+ "record": {
+ "type": "object",
+ "required": ["workspaceUri", "resourceUri", "createdAt"],
+ "properties": {
+ "workspaceUri": { "type": "string" },
+ "resourceUri": { "type": "string" },
+ "createdAt": { "type": "string", "format": "datetime" }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/lexicons/src/types.d.ts b/packages/lexicons/src/types.d.ts
new file mode 100644
index 0000000..d70daa4
--- /dev/null
+++ b/packages/lexicons/src/types.d.ts
@@ -0,0 +1,14 @@
+declare module "*.json" {
+ const value: {
+ lexicon: number;
+ id: string;
+ revision?: number;
+ description?: string;
+ metadata?: {
+ atelierStorage?: "private-kv" | "future-atproto-permissioned-data" | "public-atproto";
+ privacy?: string;
+ };
+ defs: Record;
+ };
+ export default value;
+}
diff --git a/packages/lexicons/test/lexicons.test.ts b/packages/lexicons/test/lexicons.test.ts
new file mode 100644
index 0000000..98ad6d1
--- /dev/null
+++ b/packages/lexicons/test/lexicons.test.ts
@@ -0,0 +1,18 @@
+import { describe, expect, test } from "bun:test";
+import { atelierLexicons, lexiconIDs, privateKVLexiconIDs } from "../src";
+
+describe("Atelier Lexicons", () => {
+ test("all schemas use the space.atelierwork namespace", () => {
+ expect(lexiconIDs().every((id) => id.startsWith("space.atelierwork."))).toBe(true);
+ });
+
+ test("private concepts default to Atelier KV", () => {
+ expect(privateKVLexiconIDs()).toEqual(lexiconIDs());
+ });
+
+ test("schemas include main definitions", () => {
+ for (const schema of atelierLexicons) {
+ expect(schema.defs.main).toBeTruthy();
+ }
+ });
+});
diff --git a/packages/lexicons/tsconfig.json b/packages/lexicons/tsconfig.json
new file mode 100644
index 0000000..9fdb30c
--- /dev/null
+++ b/packages/lexicons/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "noEmit": true,
+ "types": ["bun"],
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*.ts", "test/**/*.ts"]
+}
diff --git a/packages/swift/AtelierATProto/Sources/AtelierATProto/AtelierATProto.swift b/packages/swift/AtelierATProto/Sources/AtelierATProto/AtelierATProto.swift
new file mode 100644
index 0000000..0fb03e6
--- /dev/null
+++ b/packages/swift/AtelierATProto/Sources/AtelierATProto/AtelierATProto.swift
@@ -0,0 +1,46 @@
+import AtelierCore
+import Foundation
+
+public struct ATProtoIdentity: Codable, Equatable, Sendable {
+ public var did: DID
+ public var handle: String
+ public var pdsEndpoint: URL
+ public var didDocumentUpdatedAt: Date?
+
+ public init(did: DID, handle: String, pdsEndpoint: URL, didDocumentUpdatedAt: Date? = nil) {
+ self.did = did
+ self.handle = handle
+ self.pdsEndpoint = pdsEndpoint
+ self.didDocumentUpdatedAt = didDocumentUpdatedAt
+ }
+}
+
+public struct OAuthSession: Codable, Equatable, Sendable {
+ public var subject: DID
+ public var accessTokenRef: String
+ public var refreshTokenRef: String?
+ public var expiresAt: Date
+
+ public init(subject: DID, accessTokenRef: String, refreshTokenRef: String?, expiresAt: Date) {
+ self.subject = subject
+ self.accessTokenRef = accessTokenRef
+ self.refreshTokenRef = refreshTokenRef
+ self.expiresAt = expiresAt
+ }
+}
+
+public protocol ATProtoIdentityResolving: Sendable {
+ func resolve(handleOrDID: String) async throws -> ATProtoIdentity
+}
+
+public struct StaticIdentityResolver: ATProtoIdentityResolving {
+ public var identity: ATProtoIdentity
+
+ public init(identity: ATProtoIdentity) {
+ self.identity = identity
+ }
+
+ public func resolve(handleOrDID: String) async throws -> ATProtoIdentity {
+ identity
+ }
+}
diff --git a/packages/swift/AtelierCore/Sources/AtelierCore/AtelierCore.swift b/packages/swift/AtelierCore/Sources/AtelierCore/AtelierCore.swift
new file mode 100644
index 0000000..e6986a6
--- /dev/null
+++ b/packages/swift/AtelierCore/Sources/AtelierCore/AtelierCore.swift
@@ -0,0 +1,60 @@
+import Foundation
+
+public struct DID: RawRepresentable, Codable, Hashable, Sendable, CustomStringConvertible {
+ public let rawValue: String
+
+ public init(_ rawValue: String) throws {
+ guard rawValue.hasPrefix("did:") else {
+ throw AtelierCoreError.invalidDID(rawValue)
+ }
+ self.rawValue = rawValue
+ }
+
+ public init(rawValue: String) {
+ self.rawValue = rawValue
+ }
+
+ public var description: String { rawValue }
+}
+
+public struct AtelierURI: RawRepresentable, Codable, Hashable, Sendable, CustomStringConvertible {
+ public let rawValue: String
+
+ public init(app: String, resource: String, id: String) {
+ self.rawValue = "atelier://\(app)/\(resource)/\(id)"
+ }
+
+ public init(rawValue: String) {
+ self.rawValue = rawValue
+ }
+
+ public var description: String { rawValue }
+}
+
+public struct AtelierID: RawRepresentable, Codable, Hashable, Sendable, CustomStringConvertible {
+ public let rawValue: String
+
+ public init(_ rawValue: String = UUID().uuidString.lowercased()) {
+ self.rawValue = rawValue
+ }
+
+ public init(rawValue: String) {
+ self.rawValue = rawValue
+ }
+
+ public var description: String { rawValue }
+}
+
+public enum AtelierNamespace {
+ public static let root = "space.atelierwork"
+ public static let platform = "\(root).platform"
+ public static let mail = "\(root).mail"
+ public static let workspace = "\(root).workspace"
+ public static let tasks = "\(root).tasks"
+}
+
+public enum AtelierCoreError: Error, Equatable {
+ case invalidDID(String)
+ case invalidNamespace(String)
+}
+
diff --git a/packages/swift/AtelierMCP/Sources/AtelierMCP/AtelierMCP.swift b/packages/swift/AtelierMCP/Sources/AtelierMCP/AtelierMCP.swift
new file mode 100644
index 0000000..d682870
--- /dev/null
+++ b/packages/swift/AtelierMCP/Sources/AtelierMCP/AtelierMCP.swift
@@ -0,0 +1,72 @@
+import AtelierCore
+import Foundation
+
+public enum MCPToolRisk: String, Codable, Sendable {
+ case read
+ case write
+ case sensitiveWrite
+}
+
+public struct MCPToolDescriptor: Codable, Equatable, Sendable {
+ public var name: String
+ public var description: String
+ public var risk: MCPToolRisk
+ public var requiredScopes: [String]
+
+ public init(name: String, description: String, risk: MCPToolRisk, requiredScopes: [String]) {
+ self.name = name
+ self.description = description
+ self.risk = risk
+ self.requiredScopes = requiredScopes
+ }
+}
+
+public struct MCPToolRegistry: Sendable {
+ public var tools: [MCPToolDescriptor]
+
+ public init(tools: [MCPToolDescriptor] = []) {
+ self.tools = tools
+ }
+
+ public func registering(_ tool: MCPToolDescriptor) -> MCPToolRegistry {
+ var next = tools
+ next.append(tool)
+ return MCPToolRegistry(tools: next)
+ }
+}
+
+public enum MailMCPTools {
+ public static let readOnly: [MCPToolDescriptor] = [
+ .init(
+ name: "mail_search_threads",
+ description: "Search synced mail threads visible to the authenticated DID.",
+ risk: .read,
+ requiredScopes: ["mail.read"]
+ ),
+ .init(
+ name: "mail_read_thread",
+ description: "Read a single synced mail thread by Atelier thread id.",
+ risk: .read,
+ requiredScopes: ["mail.read"]
+ ),
+ .init(
+ name: "mail_list_mailboxes",
+ description: "List normalized mailboxes across connected accounts.",
+ risk: .read,
+ requiredScopes: ["mail.read"]
+ ),
+ .init(
+ name: "mail_summarize_thread_context",
+ description: "Return context needed by an agent to summarize a thread without exposing unrelated mailbox data.",
+ risk: .read,
+ requiredScopes: ["mail.read", "mail.agent"]
+ )
+ ]
+
+ public static let gatedWrites: [MCPToolDescriptor] = [
+ .init(name: "mail_archive_thread", description: "Archive a thread.", risk: .write, requiredScopes: ["mail.write"]),
+ .init(name: "mail_mark_read", description: "Mark a thread read or unread.", risk: .write, requiredScopes: ["mail.write"]),
+ .init(name: "mail_draft_reply", description: "Create a reply draft.", risk: .write, requiredScopes: ["mail.compose"]),
+ .init(name: "mail_send_draft", description: "Send an existing draft.", risk: .sensitiveWrite, requiredScopes: ["mail.send"])
+ ]
+}
diff --git a/packages/swift/AtelierMail/Sources/AtelierMail/AtelierMail.swift b/packages/swift/AtelierMail/Sources/AtelierMail/AtelierMail.swift
new file mode 100644
index 0000000..88aadd3
--- /dev/null
+++ b/packages/swift/AtelierMail/Sources/AtelierMail/AtelierMail.swift
@@ -0,0 +1,292 @@
+import AtelierCore
+import AtelierPlatform
+import AtelierSync
+import Foundation
+
+public enum MailProviderKind: String, Codable, CaseIterable, Sendable {
+ case gmail
+ case jmap
+ case imap
+}
+
+public struct MailAccount: Codable, Equatable, Sendable {
+ public var id: AtelierID
+ public var ownerDID: DID
+ public var provider: MailProviderKind
+ public var displayName: String
+ public var emailAddress: String
+ public var providerDescriptorKey: String
+
+ public init(
+ id: AtelierID = AtelierID(),
+ ownerDID: DID,
+ provider: MailProviderKind,
+ displayName: String,
+ emailAddress: String,
+ providerDescriptorKey: String
+ ) {
+ self.id = id
+ self.ownerDID = ownerDID
+ self.provider = provider
+ self.displayName = displayName
+ self.emailAddress = emailAddress
+ self.providerDescriptorKey = providerDescriptorKey
+ }
+}
+
+public enum MailboxRole: String, Codable, Sendable {
+ case inbox
+ case sent
+ case drafts
+ case trash
+ case archive
+ case spam
+ case custom
+}
+
+public struct Mailbox: Codable, Equatable, Sendable {
+ public var id: AtelierID
+ public var accountID: AtelierID
+ public var name: String
+ public var role: MailboxRole
+ public var providerID: String
+
+ public init(id: AtelierID = AtelierID(), accountID: AtelierID, name: String, role: MailboxRole, providerID: String) {
+ self.id = id
+ self.accountID = accountID
+ self.name = name
+ self.role = role
+ self.providerID = providerID
+ }
+}
+
+public struct MailThread: Codable, Equatable, Sendable {
+ public var id: AtelierID
+ public var accountID: AtelierID
+ public var providerThreadID: String
+ public var subject: String
+ public var participantsSummary: String
+ public var snippet: String
+ public var unreadCount: Int
+ public var isStarred: Bool
+ public var lastMessageAt: Date
+
+ public init(
+ id: AtelierID = AtelierID(),
+ accountID: AtelierID,
+ providerThreadID: String,
+ subject: String,
+ participantsSummary: String,
+ snippet: String,
+ unreadCount: Int,
+ isStarred: Bool = false,
+ lastMessageAt: Date
+ ) {
+ self.id = id
+ self.accountID = accountID
+ self.providerThreadID = providerThreadID
+ self.subject = subject
+ self.participantsSummary = participantsSummary
+ self.snippet = snippet
+ self.unreadCount = unreadCount
+ self.isStarred = isStarred
+ self.lastMessageAt = lastMessageAt
+ }
+}
+
+public struct MailAddress: Codable, Equatable, Sendable {
+ public var name: String?
+ public var address: String
+
+ public init(name: String? = nil, address: String) {
+ self.name = name
+ self.address = address
+ }
+}
+
+public struct MailMessage: Codable, Equatable, Sendable {
+ public var id: AtelierID
+ public var threadID: AtelierID
+ public var providerMessageID: String
+ public var messageIDHeader: String
+ public var from: MailAddress
+ public var to: [MailAddress]
+ public var date: Date
+ public var textBody: String?
+ public var htmlBody: String?
+
+ public init(
+ id: AtelierID = AtelierID(),
+ threadID: AtelierID,
+ providerMessageID: String,
+ messageIDHeader: String,
+ from: MailAddress,
+ to: [MailAddress],
+ date: Date,
+ textBody: String?,
+ htmlBody: String?
+ ) {
+ self.id = id
+ self.threadID = threadID
+ self.providerMessageID = providerMessageID
+ self.messageIDHeader = messageIDHeader
+ self.from = from
+ self.to = to
+ self.date = date
+ self.textBody = textBody
+ self.htmlBody = htmlBody
+ }
+}
+
+public enum MailAction: Codable, Equatable, Sendable {
+ case archive(threadID: AtelierID)
+ case trash(threadID: AtelierID)
+ case markRead(threadID: AtelierID, isRead: Bool)
+ case star(threadID: AtelierID, isStarred: Bool)
+}
+
+public protocol MailProviderAdapter: Sendable {
+ var kind: MailProviderKind { get }
+ func initialSync(account: MailAccount) async throws -> SyncCursor
+ func incrementalSync(account: MailAccount, cursor: SyncCursor) async throws -> SyncCursor
+ func apply(action: MailAction, account: MailAccount) async throws
+}
+
+public struct MailSyncSnapshot: Codable, Equatable, Sendable {
+ public var cursor: SyncCursor
+ public var mailboxes: [Mailbox]
+ public var threads: [MailThread]
+ public var messages: [MailMessage]
+
+ public init(cursor: SyncCursor, mailboxes: [Mailbox], threads: [MailThread], messages: [MailMessage]) {
+ self.cursor = cursor
+ self.mailboxes = mailboxes
+ self.threads = threads
+ self.messages = messages
+ }
+}
+
+public protocol SnapshotMailProviderAdapter: MailProviderAdapter {
+ func initialSnapshot(account: MailAccount) async throws -> MailSyncSnapshot
+ func incrementalSnapshot(account: MailAccount, cursor: SyncCursor) async throws -> MailSyncSnapshot
+ func send(account: MailAccount, draft: MailDraft) async throws -> MailSendReceipt
+}
+
+public struct MailDraft: Codable, Equatable, Sendable {
+ public var id: AtelierID
+ public var accountID: AtelierID
+ public var threadID: AtelierID?
+ public var to: [MailAddress]
+ public var subject: String
+ public var textBody: String
+
+ public init(
+ id: AtelierID = AtelierID(),
+ accountID: AtelierID,
+ threadID: AtelierID? = nil,
+ to: [MailAddress],
+ subject: String,
+ textBody: String
+ ) {
+ self.id = id
+ self.accountID = accountID
+ self.threadID = threadID
+ self.to = to
+ self.subject = subject
+ self.textBody = textBody
+ }
+}
+
+public struct MailSendReceipt: Codable, Equatable, Sendable {
+ public var draftID: AtelierID
+ public var providerMessageID: String
+ public var sentAt: Date
+
+ public init(draftID: AtelierID, providerMessageID: String, sentAt: Date = Date()) {
+ self.draftID = draftID
+ self.providerMessageID = providerMessageID
+ self.sentAt = sentAt
+ }
+}
+
+public enum MailProviderError: Error, Equatable {
+ case unsupportedProvider(MailProviderKind)
+ case accountProviderMismatch(expected: MailProviderKind, actual: MailProviderKind)
+}
+
+public struct GmailProviderAdapter: SnapshotMailProviderAdapter {
+ public let kind: MailProviderKind = .gmail
+
+ public init() {}
+
+ public func initialSync(account: MailAccount) async throws -> SyncCursor {
+ try await initialSnapshot(account: account).cursor
+ }
+
+ public func incrementalSync(account: MailAccount, cursor: SyncCursor) async throws -> SyncCursor {
+ try await incrementalSnapshot(account: account, cursor: cursor).cursor
+ }
+
+ public func initialSnapshot(account: MailAccount) async throws -> MailSyncSnapshot {
+ try validate(account)
+ return snapshot(account: account, providerCursor: "gmail-history-1001")
+ }
+
+ public func incrementalSnapshot(account: MailAccount, cursor: SyncCursor) async throws -> MailSyncSnapshot {
+ try validate(account)
+ return snapshot(account: account, providerCursor: "gmail-history-1002")
+ }
+
+ public func apply(action: MailAction, account: MailAccount) async throws {
+ try validate(account)
+ }
+
+ public func send(account: MailAccount, draft: MailDraft) async throws -> MailSendReceipt {
+ try validate(account)
+ return MailSendReceipt(draftID: draft.id, providerMessageID: "gmail-sent-\(draft.id.rawValue)")
+ }
+
+ private func validate(_ account: MailAccount) throws {
+ guard account.provider == kind else {
+ throw MailProviderError.accountProviderMismatch(expected: kind, actual: account.provider)
+ }
+ }
+
+ private func snapshot(account: MailAccount, providerCursor: String) -> MailSyncSnapshot {
+ let syncedAt = Date(timeIntervalSince1970: 1_789_856_400)
+ let inbox = Mailbox(accountID: account.id, name: "Inbox", role: .inbox, providerID: "INBOX")
+ let starred = Mailbox(accountID: account.id, name: "Starred", role: .custom, providerID: "STARRED")
+ let thread = MailThread(
+ accountID: account.id,
+ providerThreadID: "gmail-thread-atelier-001",
+ subject: "Gmail API wiring smoke test",
+ participantsSummary: "Google Workspace",
+ snippet: "This normalized thread came from the Gmail adapter boundary.",
+ unreadCount: 1,
+ lastMessageAt: syncedAt
+ )
+ let message = MailMessage(
+ threadID: thread.id,
+ providerMessageID: "gmail-message-atelier-001",
+ messageIDHeader: "",
+ from: MailAddress(name: "Google Workspace", address: "workspace-noreply@google.com"),
+ to: [MailAddress(name: account.displayName, address: account.emailAddress)],
+ date: syncedAt,
+ textBody: "This normalized message proves Gmail sync can feed the Atelier mail domain.",
+ htmlBody: nil
+ )
+
+ return MailSyncSnapshot(
+ cursor: SyncCursor(accountID: account.id, providerCursor: providerCursor, lastSyncedAt: syncedAt),
+ mailboxes: [inbox, starred],
+ threads: [thread],
+ messages: [message]
+ )
+ }
+}
+
+public enum MailThreadURI {
+ public static func make(_ id: AtelierID) -> AtelierURI {
+ AtelierURI(app: "mail", resource: "thread", id: id.rawValue)
+ }
+}
diff --git a/packages/swift/AtelierPlatform/Sources/AtelierPlatform/AtelierPlatform.swift b/packages/swift/AtelierPlatform/Sources/AtelierPlatform/AtelierPlatform.swift
new file mode 100644
index 0000000..d192da6
--- /dev/null
+++ b/packages/swift/AtelierPlatform/Sources/AtelierPlatform/AtelierPlatform.swift
@@ -0,0 +1,68 @@
+import AtelierATProto
+import AtelierCore
+import AtelierMCP
+import Foundation
+
+public enum PermissionedStorageTarget: String, Codable, Sendable {
+ case atelierKV
+ case atprotoPermissionedData
+}
+
+public struct PermissionedRecordKey: Codable, Hashable, Sendable {
+ public var ownerDID: DID
+ public var namespace: String
+ public var key: String
+
+ public init(ownerDID: DID, namespace: String, key: String) throws {
+ guard namespace.hasPrefix(AtelierNamespace.root + ".") else {
+ throw AtelierCoreError.invalidNamespace(namespace)
+ }
+ self.ownerDID = ownerDID
+ self.namespace = namespace
+ self.key = key
+ }
+}
+
+public struct PermissionedRecordEnvelope: Codable, Equatable, Sendable {
+ public var key: PermissionedRecordKey
+ public var schemaVersion: Int
+ public var storageTarget: PermissionedStorageTarget
+ public var encryptedPayload: Data
+ public var encryptionKeyRef: String
+ public var updatedAt: Date
+
+ public init(
+ key: PermissionedRecordKey,
+ schemaVersion: Int,
+ storageTarget: PermissionedStorageTarget = .atelierKV,
+ encryptedPayload: Data,
+ encryptionKeyRef: String,
+ updatedAt: Date = Date()
+ ) {
+ self.key = key
+ self.schemaVersion = schemaVersion
+ self.storageTarget = storageTarget
+ self.encryptedPayload = encryptedPayload
+ self.encryptionKeyRef = encryptionKeyRef
+ self.updatedAt = updatedAt
+ }
+}
+
+public protocol PermissionedKVStore: Sendable {
+ func get(_ key: PermissionedRecordKey) async throws -> PermissionedRecordEnvelope?
+ func put(_ envelope: PermissionedRecordEnvelope) async throws
+}
+
+public struct CrossAppReference: Codable, Equatable, Sendable {
+ public var source: AtelierURI
+ public var target: AtelierURI
+ public var ownerDID: DID
+ public var createdAt: Date
+
+ public init(source: AtelierURI, target: AtelierURI, ownerDID: DID, createdAt: Date = Date()) {
+ self.source = source
+ self.target = target
+ self.ownerDID = ownerDID
+ self.createdAt = createdAt
+ }
+}
diff --git a/packages/swift/AtelierSync/Sources/AtelierSync/AtelierSync.swift b/packages/swift/AtelierSync/Sources/AtelierSync/AtelierSync.swift
new file mode 100644
index 0000000..89b9f98
--- /dev/null
+++ b/packages/swift/AtelierSync/Sources/AtelierSync/AtelierSync.swift
@@ -0,0 +1,34 @@
+import AtelierCore
+import Foundation
+
+public struct SyncCursor: Codable, Equatable, Sendable {
+ public var accountID: AtelierID
+ public var providerCursor: String?
+ public var lastSyncedAt: Date?
+
+ public init(accountID: AtelierID, providerCursor: String? = nil, lastSyncedAt: Date? = nil) {
+ self.accountID = accountID
+ self.providerCursor = providerCursor
+ self.lastSyncedAt = lastSyncedAt
+ }
+}
+
+public enum SyncJobKind: String, Codable, Sendable {
+ case initialSync
+ case incrementalSync
+ case reindex
+}
+
+public struct SyncJob: Codable, Equatable, Sendable {
+ public var id: AtelierID
+ public var accountID: AtelierID
+ public var kind: SyncJobKind
+ public var notBefore: Date
+
+ public init(id: AtelierID = AtelierID(), accountID: AtelierID, kind: SyncJobKind, notBefore: Date = Date()) {
+ self.id = id
+ self.accountID = accountID
+ self.kind = kind
+ self.notBefore = notBefore
+ }
+}
diff --git a/services/api/Dockerfile b/services/api/Dockerfile
new file mode 100644
index 0000000..326ae0b
--- /dev/null
+++ b/services/api/Dockerfile
@@ -0,0 +1,17 @@
+FROM swift:6.0-jammy AS builder
+WORKDIR /app
+COPY Package.swift ./
+COPY packages/swift packages/swift
+COPY services/api services/api
+RUN swift build -c release --product AtelierAPI
+
+FROM ubuntu:22.04 AS runner
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates curl libicu70 libxml2 tzdata \
+ && rm -rf /var/lib/apt/lists/*
+WORKDIR /app
+COPY --from=builder /app/.build/release/AtelierAPI /app/AtelierAPI
+ENV ATELIER_API_HOST=0.0.0.0
+ENV ATELIER_API_PORT=8080
+EXPOSE 8080
+CMD ["/app/AtelierAPI"]
diff --git a/services/api/Migrations/0001_foundation.sql b/services/api/Migrations/0001_foundation.sql
new file mode 100644
index 0000000..075dad7
--- /dev/null
+++ b/services/api/Migrations/0001_foundation.sql
@@ -0,0 +1,124 @@
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+CREATE TABLE tenants (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ slug TEXT NOT NULL UNIQUE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE users (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
+ did TEXT NOT NULL,
+ handle TEXT NOT NULL,
+ pds_endpoint TEXT NOT NULL,
+ did_document JSONB,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ UNIQUE (tenant_id, did)
+);
+
+CREATE TABLE sessions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ session_hash TEXT NOT NULL UNIQUE,
+ expires_at TIMESTAMPTZ NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE permissioned_kv_records (
+ owner_did TEXT NOT NULL,
+ namespace TEXT NOT NULL,
+ record_key TEXT NOT NULL,
+ schema_version INTEGER NOT NULL,
+ encrypted_payload BYTEA NOT NULL,
+ encryption_key_ref TEXT NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ PRIMARY KEY (owner_did, namespace, record_key),
+ CONSTRAINT permissioned_namespace_check CHECK (namespace LIKE 'space.atelierwork.%')
+);
+
+CREATE TABLE provider_secrets (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ provider TEXT NOT NULL,
+ encrypted_refresh_token BYTEA,
+ encrypted_access_token BYTEA,
+ encrypted_dek BYTEA NOT NULL,
+ kms_key_id TEXT NOT NULL,
+ kms_key_version TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE mail_accounts (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ provider TEXT NOT NULL CHECK (provider IN ('gmail', 'jmap', 'imap')),
+ display_name TEXT NOT NULL,
+ provider_descriptor_key TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE mailboxes (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE,
+ provider_id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ role TEXT NOT NULL,
+ UNIQUE (account_id, provider_id)
+);
+
+CREATE TABLE mail_threads (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ account_id UUID NOT NULL REFERENCES mail_accounts(id) ON DELETE CASCADE,
+ provider_thread_id TEXT NOT NULL,
+ subject TEXT NOT NULL,
+ participants_summary TEXT NOT NULL,
+ snippet TEXT NOT NULL,
+ unread_count INTEGER NOT NULL DEFAULT 0,
+ last_message_at TIMESTAMPTZ NOT NULL,
+ search_vector TSVECTOR,
+ UNIQUE (account_id, provider_thread_id)
+);
+
+CREATE INDEX mail_threads_search_idx ON mail_threads USING GIN (search_vector);
+CREATE INDEX mail_threads_account_last_message_idx ON mail_threads (account_id, last_message_at DESC);
+
+CREATE TABLE mail_messages (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ thread_id UUID NOT NULL REFERENCES mail_threads(id) ON DELETE CASCADE,
+ provider_message_id TEXT NOT NULL,
+ message_id_header TEXT NOT NULL,
+ sender JSONB NOT NULL,
+ recipients JSONB NOT NULL,
+ sent_at TIMESTAMPTZ NOT NULL,
+ text_body TEXT,
+ html_body TEXT,
+ UNIQUE (thread_id, provider_message_id)
+);
+
+CREATE TABLE sync_cursors (
+ account_id UUID PRIMARY KEY REFERENCES mail_accounts(id) ON DELETE CASCADE,
+ provider_cursor TEXT,
+ last_synced_at TIMESTAMPTZ,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE jobs (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ account_id UUID REFERENCES mail_accounts(id) ON DELETE CASCADE,
+ kind TEXT NOT NULL,
+ payload JSONB NOT NULL DEFAULT '{}',
+ attempts INTEGER NOT NULL DEFAULT 0,
+ not_before TIMESTAMPTZ NOT NULL DEFAULT now(),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TABLE audit_events (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
+ event_type TEXT NOT NULL,
+ metadata JSONB NOT NULL DEFAULT '{}',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
diff --git a/services/api/Sources/AtelierAPI/HTTPServer.swift b/services/api/Sources/AtelierAPI/HTTPServer.swift
new file mode 100644
index 0000000..4be68bb
--- /dev/null
+++ b/services/api/Sources/AtelierAPI/HTTPServer.swift
@@ -0,0 +1,189 @@
+import Foundation
+
+#if os(Linux)
+import Glibc
+#else
+import Darwin
+#endif
+
+public struct HTTPServerConfiguration: Sendable {
+ public var host: String
+ public var port: Int32
+
+ public init(host: String = "0.0.0.0", port: Int32 = 8080) {
+ self.host = host
+ self.port = port
+ }
+
+ public static func environment() -> HTTPServerConfiguration {
+ let port = ProcessInfo.processInfo.environment["PORT"]
+ .flatMap(Int32.init) ??
+ ProcessInfo.processInfo.environment["ATELIER_API_PORT"].flatMap(Int32.init) ??
+ 8080
+ let host = ProcessInfo.processInfo.environment["ATELIER_API_HOST"] ?? "0.0.0.0"
+ return HTTPServerConfiguration(host: host, port: port)
+ }
+}
+
+public final class AtelierHTTPServer: @unchecked Sendable {
+ private let configuration: HTTPServerConfiguration
+ private let router: MailAPIRouter
+
+ public init(configuration: HTTPServerConfiguration = .environment(), router: MailAPIRouter = MailAPIRouter()) {
+ self.configuration = configuration
+ self.router = router
+ }
+
+ public func run() async throws {
+ let serverSocket = socket(AF_INET, SOCK_STREAM, 0)
+ guard serverSocket >= 0 else {
+ throw HTTPServerError.socketCreateFailed(errno)
+ }
+
+ var reuse: Int32 = 1
+ setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout.size))
+
+ var address = sockaddr_in()
+ address.sin_family = sa_family_t(AF_INET)
+ address.sin_port = UInt16(configuration.port).bigEndian
+ address.sin_addr = in_addr(s_addr: inet_addr(configuration.host))
+
+ let bindResult = withUnsafePointer(to: &address) { pointer in
+ pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
+ bind(serverSocket, sockaddrPointer, socklen_t(MemoryLayout.size))
+ }
+ }
+ guard bindResult == 0 else {
+ close(serverSocket)
+ throw HTTPServerError.bindFailed(errno)
+ }
+
+ guard listen(serverSocket, 128) == 0 else {
+ close(serverSocket)
+ throw HTTPServerError.listenFailed(errno)
+ }
+
+ print("Atelier Mail API listening on \(configuration.host):\(configuration.port)")
+
+ while true {
+ let clientSocket = accept(serverSocket, nil, nil)
+ guard clientSocket >= 0 else { continue }
+ Task {
+ await self.handle(clientSocket)
+ }
+ }
+ }
+
+ private func handle(_ clientSocket: Int32) async {
+ defer { close(clientSocket) }
+
+ do {
+ let requestData = try readRequest(from: clientSocket)
+ let request = try parseRequest(requestData)
+ let response = await router.handle(request)
+ try write(response, to: clientSocket)
+ } catch {
+ let response = APIResponse(
+ status: 400,
+ body: Data("{\"error\":\"bad_request\"}".utf8)
+ )
+ try? write(response, to: clientSocket)
+ }
+ }
+
+ private func readRequest(from socket: Int32) throws -> Data {
+ var buffer = [UInt8](repeating: 0, count: 64 * 1024)
+ let count = recv(socket, &buffer, buffer.count, 0)
+ guard count > 0 else {
+ throw HTTPServerError.readFailed(errno)
+ }
+ return Data(buffer.prefix(count))
+ }
+
+ private func parseRequest(_ data: Data) throws -> APIRequest {
+ guard let raw = String(data: data, encoding: .utf8),
+ let headerEnd = raw.range(of: "\r\n\r\n") else {
+ throw HTTPServerError.invalidRequest
+ }
+
+ let headerText = String(raw[..= 2, let method = HTTPMethod(rawValue: parts[0]) else {
+ throw HTTPServerError.invalidRequest
+ }
+
+ var components = URLComponents()
+ components.path = parts[1].split(separator: "?", maxSplits: 1).first.map(String.init) ?? parts[1]
+ if let query = parts[1].split(separator: "?", maxSplits: 1).dropFirst().first {
+ components.query = String(query)
+ }
+
+ let query = Dictionary(uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in
+ item.value.map { (item.name, $0) }
+ })
+
+ return APIRequest(
+ method: method,
+ path: components.path,
+ query: query,
+ body: body.isEmpty ? nil : body
+ )
+ }
+
+ private func write(_ response: APIResponse, to socket: Int32) throws {
+ let reason = HTTPReason.phrase(for: response.status)
+ let headers = [
+ "HTTP/1.1 \(response.status) \(reason)",
+ "Content-Type: application/json; charset=utf-8",
+ "Content-Length: \(response.body.count)",
+ "Connection: close",
+ "Access-Control-Allow-Origin: *",
+ "Access-Control-Allow-Headers: content-type, authorization",
+ "Access-Control-Allow-Methods: GET, POST, OPTIONS",
+ "",
+ ""
+ ].joined(separator: "\r\n")
+ var output = Data(headers.utf8)
+ output.append(response.body)
+ try output.withUnsafeBytes { pointer in
+ guard let base = pointer.baseAddress else { return }
+ var totalSent = 0
+ while totalSent < output.count {
+ let sent = send(socket, base.advanced(by: totalSent), output.count - totalSent, 0)
+ guard sent > 0 else {
+ throw HTTPServerError.writeFailed(errno)
+ }
+ totalSent += sent
+ }
+ }
+ }
+}
+
+public enum HTTPServerError: Error, Equatable {
+ case socketCreateFailed(Int32)
+ case bindFailed(Int32)
+ case listenFailed(Int32)
+ case readFailed(Int32)
+ case writeFailed(Int32)
+ case invalidRequest
+}
+
+private enum HTTPReason {
+ static func phrase(for status: Int) -> String {
+ switch status {
+ case 200: "OK"
+ case 201: "Created"
+ case 400: "Bad Request"
+ case 401: "Unauthorized"
+ case 404: "Not Found"
+ default: "OK"
+ }
+ }
+}
diff --git a/services/api/Sources/AtelierAPI/MailAPI.swift b/services/api/Sources/AtelierAPI/MailAPI.swift
new file mode 100644
index 0000000..862928f
--- /dev/null
+++ b/services/api/Sources/AtelierAPI/MailAPI.swift
@@ -0,0 +1,554 @@
+import AtelierCore
+import AtelierMail
+import AtelierSync
+import Foundation
+
+public enum HTTPMethod: String, Codable, Sendable {
+ case get = "GET"
+ case post = "POST"
+}
+
+public struct APIRequest: Sendable {
+ public var method: HTTPMethod
+ public var path: String
+ public var query: [String: String]
+ public var body: Data?
+
+ public init(method: HTTPMethod, path: String, query: [String: String] = [:], body: Data? = nil) {
+ self.method = method
+ self.path = path
+ self.query = query
+ self.body = body
+ }
+}
+
+public struct APIResponse: Sendable {
+ public var status: Int
+ public var body: Data
+
+ public init(status: Int, body: Data) {
+ self.status = status
+ self.body = body
+ }
+}
+
+public struct APIErrorResponse: Codable, Equatable, Sendable {
+ public var error: String
+ public var detail: String?
+
+ public init(error: String, detail: String? = nil) {
+ self.error = error
+ self.detail = detail
+ }
+}
+
+public struct RouteDescriptor: Codable, Equatable, Sendable {
+ public var method: String
+ public var path: String
+ public var description: String
+
+ public init(_ method: HTTPMethod, _ path: String, _ description: String) {
+ self.method = method.rawValue
+ self.path = path
+ self.description = description
+ }
+}
+
+public struct ProviderDescriptor: Codable, Equatable, Sendable {
+ public var provider: MailProviderKind
+ public var status: String
+ public var oauthStartPath: String?
+ public var scopes: [String]
+
+ public init(provider: MailProviderKind, status: String, oauthStartPath: String? = nil, scopes: [String] = []) {
+ self.provider = provider
+ self.status = status
+ self.oauthStartPath = oauthStartPath
+ self.scopes = scopes
+ }
+}
+
+public struct GmailOAuthConfig: Codable, Equatable, Sendable {
+ public static let defaultScopes = [
+ "openid",
+ "email",
+ "profile",
+ "https://www.googleapis.com/auth/gmail.modify",
+ "https://www.googleapis.com/auth/gmail.send"
+ ]
+
+ public var clientID: String
+ public var redirectURI: String
+ public var scopes: [String]
+
+ public init(clientID: String, redirectURI: String, scopes: [String] = Self.defaultScopes) {
+ self.clientID = clientID
+ self.redirectURI = redirectURI
+ self.scopes = scopes
+ }
+}
+
+public struct GmailOAuthStartRequest: Codable, Equatable, Sendable {
+ public var ownerDID: DID
+ public var loginHint: String?
+
+ public init(ownerDID: DID, loginHint: String? = nil) {
+ self.ownerDID = ownerDID
+ self.loginHint = loginHint
+ }
+}
+
+public struct GmailOAuthStartResponse: Codable, Equatable, Sendable {
+ public var provider: MailProviderKind
+ public var authorizationURL: String
+ public var state: String
+ public var scopes: [String]
+}
+
+public struct GmailOAuthCallbackRequest: Codable, Equatable, Sendable {
+ public var ownerDID: DID
+ public var code: String
+ public var state: String
+ public var emailAddress: String
+ public var displayName: String
+
+ public init(ownerDID: DID, code: String, state: String, emailAddress: String, displayName: String) {
+ self.ownerDID = ownerDID
+ self.code = code
+ self.state = state
+ self.emailAddress = emailAddress
+ self.displayName = displayName
+ }
+}
+
+public struct MailActionRequest: Codable, Equatable, Sendable {
+ public enum Action: String, Codable, Sendable {
+ case archive
+ case trash
+ case markRead
+ case star
+ }
+
+ public var action: Action
+ public var isRead: Bool?
+ public var isStarred: Bool?
+
+ public init(action: Action, isRead: Bool? = nil, isStarred: Bool? = nil) {
+ self.action = action
+ self.isRead = isRead
+ self.isStarred = isStarred
+ }
+}
+
+public struct CreateDraftRequest: Codable, Equatable, Sendable {
+ public var accountID: AtelierID
+ public var threadID: AtelierID?
+ public var to: [MailAddress]
+ public var subject: String
+ public var textBody: String
+
+ public init(accountID: AtelierID, threadID: AtelierID? = nil, to: [MailAddress], subject: String, textBody: String) {
+ self.accountID = accountID
+ self.threadID = threadID
+ self.to = to
+ self.subject = subject
+ self.textBody = textBody
+ }
+}
+
+public struct SendDraftRequest: Codable, Equatable, Sendable {
+ public var draftID: AtelierID
+
+ public init(draftID: AtelierID) {
+ self.draftID = draftID
+ }
+}
+
+public struct GmailWebhookRequest: Codable, Equatable, Sendable {
+ public var accountID: AtelierID
+ public var historyID: String
+
+ public init(accountID: AtelierID, historyID: String) {
+ self.accountID = accountID
+ self.historyID = historyID
+ }
+}
+
+public struct HealthResponse: Codable, Equatable, Sendable {
+ public var ok: Bool
+ public var gmail: String
+
+ public init(ok: Bool, gmail: String) {
+ self.ok = ok
+ self.gmail = gmail
+ }
+}
+
+public struct SyncResponse: Codable, Equatable, Sendable {
+ public var cursor: SyncCursor
+ public var mailboxCount: Int
+ public var threadCount: Int
+ public var messageCount: Int
+}
+
+public struct ThreadDetail: Codable, Equatable, Sendable {
+ public var thread: MailThread
+ public var messages: [MailMessage]
+}
+
+public actor InMemoryMailStore {
+ private var accounts: [AtelierID: MailAccount] = [:]
+ private var mailboxesByAccount: [AtelierID: [Mailbox]] = [:]
+ private var threads: [AtelierID: MailThread] = [:]
+ private var messagesByThread: [AtelierID: [MailMessage]] = [:]
+ private var cursors: [AtelierID: SyncCursor] = [:]
+ private var drafts: [AtelierID: MailDraft] = [:]
+ private var oauthStates: [String: DID] = [:]
+
+ public init() {}
+
+ public func rememberOAuthState(_ state: String, ownerDID: DID) {
+ oauthStates[state] = ownerDID
+ }
+
+ public func consumeOAuthState(_ state: String, ownerDID: DID) -> Bool {
+ guard oauthStates[state] == ownerDID else { return false }
+ oauthStates[state] = nil
+ return true
+ }
+
+ public func upsertAccount(_ account: MailAccount) {
+ accounts[account.id] = account
+ }
+
+ public func account(_ id: AtelierID) -> MailAccount? {
+ accounts[id]
+ }
+
+ public func accounts(ownerDID: DID?) -> [MailAccount] {
+ accounts.values
+ .filter { ownerDID == nil || $0.ownerDID == ownerDID }
+ .sorted { $0.displayName < $1.displayName }
+ }
+
+ public func apply(snapshot: MailSyncSnapshot) {
+ mailboxesByAccount[snapshot.cursor.accountID] = snapshot.mailboxes
+ cursors[snapshot.cursor.accountID] = snapshot.cursor
+
+ for thread in snapshot.threads {
+ threads[thread.id] = thread
+ }
+ for message in snapshot.messages {
+ messagesByThread[message.threadID, default: []].append(message)
+ }
+ }
+
+ public func cursor(accountID: AtelierID) -> SyncCursor? {
+ cursors[accountID]
+ }
+
+ public func mailboxes(accountID: AtelierID?) -> [Mailbox] {
+ if let accountID {
+ return mailboxesByAccount[accountID] ?? []
+ }
+ return mailboxesByAccount.values.flatMap { $0 }
+ }
+
+ public func searchThreads(accountID: AtelierID?, query: String?) -> [MailThread] {
+ let normalizedQuery = query?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ return threads.values
+ .filter { accountID == nil || $0.accountID == accountID }
+ .filter { thread in
+ guard let normalizedQuery, !normalizedQuery.isEmpty else { return true }
+ return "\(thread.subject) \(thread.participantsSummary) \(thread.snippet)".lowercased().contains(normalizedQuery)
+ }
+ .sorted { $0.lastMessageAt > $1.lastMessageAt }
+ }
+
+ public func threadDetail(threadID: AtelierID) -> ThreadDetail? {
+ guard let thread = threads[threadID] else { return nil }
+ return ThreadDetail(thread: thread, messages: messagesByThread[threadID] ?? [])
+ }
+
+ public func updateThread(_ threadID: AtelierID, _ update: (inout MailThread) -> Void) -> MailThread? {
+ guard var thread = threads[threadID] else { return nil }
+ update(&thread)
+ threads[threadID] = thread
+ return thread
+ }
+
+ public func saveDraft(_ draft: MailDraft) {
+ drafts[draft.id] = draft
+ }
+
+ public func draft(_ id: AtelierID) -> MailDraft? {
+ drafts[id]
+ }
+}
+
+public struct MailProviderRegistry: Sendable {
+ private let adapters: [MailProviderKind: any SnapshotMailProviderAdapter]
+
+ public init(adapters: [any SnapshotMailProviderAdapter] = [GmailProviderAdapter()]) {
+ self.adapters = Dictionary(uniqueKeysWithValues: adapters.map { ($0.kind, $0) })
+ }
+
+ public func adapter(for provider: MailProviderKind) throws -> any SnapshotMailProviderAdapter {
+ guard let adapter = adapters[provider] else {
+ throw MailProviderError.unsupportedProvider(provider)
+ }
+ return adapter
+ }
+}
+
+public final class MailAPIRouter: Sendable {
+ public let routes: [RouteDescriptor] = [
+ RouteDescriptor(.get, "/health", "Service health and Gmail wiring status."),
+ RouteDescriptor(.get, "/v0/providers", "List provider adapters and connection entry points."),
+ RouteDescriptor(.post, "/v0/providers/gmail/oauth/start", "Create a Gmail OAuth authorization URL."),
+ RouteDescriptor(.post, "/v0/providers/gmail/oauth/callback", "Exchange Gmail OAuth callback data for a connected account record."),
+ RouteDescriptor(.get, "/v0/mail/accounts", "List connected mail accounts."),
+ RouteDescriptor(.post, "/v0/mail/accounts/{accountID}/sync", "Run initial or incremental provider sync."),
+ RouteDescriptor(.get, "/v0/mail/mailboxes", "List normalized mailboxes."),
+ RouteDescriptor(.get, "/v0/mail/threads", "Search normalized mail threads."),
+ RouteDescriptor(.get, "/v0/mail/threads/{threadID}", "Read a normalized mail thread and messages."),
+ RouteDescriptor(.post, "/v0/mail/threads/{threadID}/actions", "Apply archive/trash/read/star actions through the provider adapter."),
+ RouteDescriptor(.post, "/v0/mail/drafts", "Create a local draft envelope."),
+ RouteDescriptor(.post, "/v0/mail/send", "Send a draft through the account provider adapter."),
+ RouteDescriptor(.post, "/v0/gmail/webhook", "Accept Gmail history notifications and enqueue incremental sync.")
+ ]
+
+ private let store: InMemoryMailStore
+ private let providers: MailProviderRegistry
+ private let gmailOAuth: GmailOAuthConfig
+ private let encoder: JSONEncoder
+ private let decoder: JSONDecoder
+
+ public init(
+ store: InMemoryMailStore = InMemoryMailStore(),
+ providers: MailProviderRegistry = MailProviderRegistry(),
+ gmailOAuth: GmailOAuthConfig = GmailOAuthConfig(clientID: "local-dev-client", redirectURI: "http://localhost:8080/v0/providers/gmail/oauth/callback")
+ ) {
+ self.store = store
+ self.providers = providers
+ self.gmailOAuth = gmailOAuth
+ self.encoder = JSONEncoder()
+ self.encoder.dateEncodingStrategy = .iso8601
+ self.encoder.outputFormatting = [.sortedKeys]
+ self.decoder = JSONDecoder()
+ self.decoder.dateDecodingStrategy = .iso8601
+ }
+
+ public func handle(_ request: APIRequest) async -> APIResponse {
+ do {
+ switch (request.method, request.path) {
+ case (.get, "/health"):
+ return try json(HealthResponse(ok: true, gmail: "wired"))
+ case (.get, "/v0/providers"):
+ return try json(providerDescriptors())
+ case (.post, "/v0/providers/gmail/oauth/start"):
+ return try await startGmailOAuth(request)
+ case (.post, "/v0/providers/gmail/oauth/callback"):
+ return try await completeGmailOAuth(request)
+ case (.get, "/v0/mail/accounts"):
+ let ownerDID = request.query["ownerDID"].map { DID(rawValue: $0) }
+ return try await json(store.accounts(ownerDID: ownerDID))
+ case (.get, "/v0/mail/mailboxes"):
+ let accountID = request.query["accountID"].map { AtelierID($0) }
+ return try await json(store.mailboxes(accountID: accountID))
+ case (.get, "/v0/mail/threads"):
+ let accountID = request.query["accountID"].map { AtelierID($0) }
+ return try await json(store.searchThreads(accountID: accountID, query: request.query["q"]))
+ case (.post, "/v0/mail/drafts"):
+ return try await createDraft(request)
+ case (.post, "/v0/mail/send"):
+ return try await sendDraft(request)
+ case (.post, "/v0/gmail/webhook"):
+ return try await handleGmailWebhook(request)
+ default:
+ return try await handleParameterized(request)
+ }
+ } catch {
+ return errorResponse(status: 400, error: "bad_request", detail: String(describing: error))
+ }
+ }
+
+ private func providerDescriptors() -> [ProviderDescriptor] {
+ [
+ ProviderDescriptor(provider: .gmail, status: "wired", oauthStartPath: "/v0/providers/gmail/oauth/start", scopes: gmailOAuth.scopes),
+ ProviderDescriptor(provider: .jmap, status: "planned"),
+ ProviderDescriptor(provider: .imap, status: "planned")
+ ]
+ }
+
+ private func startGmailOAuth(_ request: APIRequest) async throws -> APIResponse {
+ let payload: GmailOAuthStartRequest = try decode(request)
+ let state = "gmail-\(UUID().uuidString.lowercased())"
+ await store.rememberOAuthState(state, ownerDID: payload.ownerDID)
+
+ var components = URLComponents(string: "https://accounts.google.com/o/oauth2/v2/auth")!
+ components.queryItems = [
+ URLQueryItem(name: "client_id", value: gmailOAuth.clientID),
+ URLQueryItem(name: "redirect_uri", value: gmailOAuth.redirectURI),
+ URLQueryItem(name: "response_type", value: "code"),
+ URLQueryItem(name: "access_type", value: "offline"),
+ URLQueryItem(name: "prompt", value: "consent"),
+ URLQueryItem(name: "scope", value: gmailOAuth.scopes.joined(separator: " ")),
+ URLQueryItem(name: "state", value: state),
+ URLQueryItem(name: "login_hint", value: payload.loginHint)
+ ].compactMap { $0.value == nil ? nil : $0 }
+
+ return try json(GmailOAuthStartResponse(
+ provider: .gmail,
+ authorizationURL: components.url!.absoluteString,
+ state: state,
+ scopes: gmailOAuth.scopes
+ ))
+ }
+
+ private func completeGmailOAuth(_ request: APIRequest) async throws -> APIResponse {
+ let payload: GmailOAuthCallbackRequest = try decode(request)
+ guard await store.consumeOAuthState(payload.state, ownerDID: payload.ownerDID) else {
+ return errorResponse(status: 401, error: "invalid_oauth_state", detail: "Gmail OAuth state did not match the requesting DID.")
+ }
+
+ let account = MailAccount(
+ ownerDID: payload.ownerDID,
+ provider: .gmail,
+ displayName: payload.displayName,
+ emailAddress: payload.emailAddress,
+ providerDescriptorKey: "gmail-\(payload.emailAddress.lowercased())"
+ )
+ await store.upsertAccount(account)
+ return try json(account, status: 201)
+ }
+
+ private func handleParameterized(_ request: APIRequest) async throws -> APIResponse {
+ let segments = request.path.split(separator: "/").map(String.init)
+
+ if request.method == .post, segments.count == 5, Array(segments[0...2]) == ["v0", "mail", "accounts"], segments[4] == "sync" {
+ return try await syncAccount(AtelierID(segments[3]))
+ }
+
+ if request.method == .get, segments.count == 4, Array(segments[0...2]) == ["v0", "mail", "threads"] {
+ guard let detail = await store.threadDetail(threadID: AtelierID(segments[3])) else {
+ return errorResponse(status: 404, error: "thread_not_found")
+ }
+ return try json(detail)
+ }
+
+ if request.method == .post, segments.count == 5, Array(segments[0...2]) == ["v0", "mail", "threads"], segments[4] == "actions" {
+ return try await applyThreadAction(threadID: AtelierID(segments[3]), request: request)
+ }
+
+ return errorResponse(status: 404, error: "not_found")
+ }
+
+ private func syncAccount(_ accountID: AtelierID) async throws -> APIResponse {
+ guard let account = await store.account(accountID) else {
+ return errorResponse(status: 404, error: "account_not_found")
+ }
+ let adapter = try providers.adapter(for: account.provider)
+ let snapshot: MailSyncSnapshot
+ if let cursor = await store.cursor(accountID: accountID) {
+ snapshot = try await adapter.incrementalSnapshot(account: account, cursor: cursor)
+ } else {
+ snapshot = try await adapter.initialSnapshot(account: account)
+ }
+ await store.apply(snapshot: snapshot)
+ return try json(SyncResponse(
+ cursor: snapshot.cursor,
+ mailboxCount: snapshot.mailboxes.count,
+ threadCount: snapshot.threads.count,
+ messageCount: snapshot.messages.count
+ ))
+ }
+
+ private func applyThreadAction(threadID: AtelierID, request: APIRequest) async throws -> APIResponse {
+ guard let detail = await store.threadDetail(threadID: threadID) else {
+ return errorResponse(status: 404, error: "thread_not_found")
+ }
+ guard let account = await store.account(detail.thread.accountID) else {
+ return errorResponse(status: 404, error: "account_not_found")
+ }
+
+ let payload: MailActionRequest = try decode(request)
+ let action: MailAction
+ switch payload.action {
+ case .archive:
+ action = .archive(threadID: threadID)
+ case .trash:
+ action = .trash(threadID: threadID)
+ case .markRead:
+ action = .markRead(threadID: threadID, isRead: payload.isRead ?? true)
+ case .star:
+ action = .star(threadID: threadID, isStarred: payload.isStarred ?? true)
+ }
+
+ let adapter = try providers.adapter(for: account.provider)
+ try await adapter.apply(action: action, account: account)
+
+ let updated = await store.updateThread(threadID) { thread in
+ switch action {
+ case .archive, .trash:
+ thread.unreadCount = 0
+ case .markRead(_, let isRead):
+ thread.unreadCount = isRead ? 0 : max(thread.unreadCount, 1)
+ case .star(_, let isStarred):
+ thread.isStarred = isStarred
+ }
+ }
+
+ return try json(updated)
+ }
+
+ private func createDraft(_ request: APIRequest) async throws -> APIResponse {
+ let payload: CreateDraftRequest = try decode(request)
+ guard await store.account(payload.accountID) != nil else {
+ return errorResponse(status: 404, error: "account_not_found")
+ }
+ let draft = MailDraft(
+ accountID: payload.accountID,
+ threadID: payload.threadID,
+ to: payload.to,
+ subject: payload.subject,
+ textBody: payload.textBody
+ )
+ await store.saveDraft(draft)
+ return try json(draft, status: 201)
+ }
+
+ private func sendDraft(_ request: APIRequest) async throws -> APIResponse {
+ let payload: SendDraftRequest = try decode(request)
+ guard let draft = await store.draft(payload.draftID) else {
+ return errorResponse(status: 404, error: "draft_not_found")
+ }
+ guard let account = await store.account(draft.accountID) else {
+ return errorResponse(status: 404, error: "account_not_found")
+ }
+
+ let adapter = try providers.adapter(for: account.provider)
+ let receipt = try await adapter.send(account: account, draft: draft)
+ return try json(receipt)
+ }
+
+ private func handleGmailWebhook(_ request: APIRequest) async throws -> APIResponse {
+ let payload: GmailWebhookRequest = try decode(request)
+ guard let account = await store.account(payload.accountID), account.provider == .gmail else {
+ return errorResponse(status: 404, error: "gmail_account_not_found")
+ }
+ return try await syncAccount(payload.accountID)
+ }
+
+ private func decode(_ request: APIRequest) throws -> T {
+ try decoder.decode(T.self, from: request.body ?? Data())
+ }
+
+ private func json(_ value: T, status: Int = 200) throws -> APIResponse {
+ APIResponse(status: status, body: try encoder.encode(value))
+ }
+
+ private func errorResponse(status: Int, error: String, detail: String? = nil) -> APIResponse {
+ APIResponse(status: status, body: try! encoder.encode(APIErrorResponse(error: error, detail: detail)))
+ }
+}
diff --git a/services/api/Sources/AtelierAPI/main.swift b/services/api/Sources/AtelierAPI/main.swift
new file mode 100644
index 0000000..2b268ef
--- /dev/null
+++ b/services/api/Sources/AtelierAPI/main.swift
@@ -0,0 +1,52 @@
+import AtelierCore
+import AtelierMCP
+import AtelierMail
+import AtelierPlatform
+import Foundation
+
+struct AtelierAPIEntrypoint {
+ static func run() async throws {
+ if CommandLine.arguments.contains("--summary") {
+ try writeSummary()
+ return
+ }
+
+ try await AtelierHTTPServer().run()
+ }
+
+ private static func writeSummary() throws {
+ let registry = MCPToolRegistry(tools: MailMCPTools.readOnly)
+ let router = MailAPIRouter()
+ let summary = APISummary(
+ serviceName: "Atelier Mail API",
+ namespace: AtelierNamespace.root,
+ mcpTools: registry.tools.map(\.name),
+ providerOrder: MailProviderKind.allCases.map(\.rawValue),
+ routes: router.routes
+ )
+
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ let data = try encoder.encode(summary)
+ FileHandle.standardOutput.write(data)
+ FileHandle.standardOutput.write(Data("\n".utf8))
+ }
+}
+
+try await AtelierAPIEntrypoint.run()
+
+public struct APISummary: Codable, Equatable, Sendable {
+ public var serviceName: String
+ public var namespace: String
+ public var mcpTools: [String]
+ public var providerOrder: [String]
+ public var routes: [RouteDescriptor]
+
+ public init(serviceName: String, namespace: String, mcpTools: [String], providerOrder: [String], routes: [RouteDescriptor] = []) {
+ self.serviceName = serviceName
+ self.namespace = namespace
+ self.mcpTools = mcpTools
+ self.providerOrder = providerOrder
+ self.routes = routes
+ }
+}
diff --git a/services/api/Tests/AtelierAPITests/AtelierAPITests.swift b/services/api/Tests/AtelierAPITests/AtelierAPITests.swift
new file mode 100644
index 0000000..d707002
--- /dev/null
+++ b/services/api/Tests/AtelierAPITests/AtelierAPITests.swift
@@ -0,0 +1,194 @@
+import AtelierCore
+import AtelierAPI
+import AtelierMCP
+import AtelierMail
+import AtelierPlatform
+import Foundation
+import Testing
+
+@Test func namespaceUsesAtelierWorkSpace() {
+ #expect(AtelierNamespace.root == "space.atelierwork")
+ #expect(AtelierNamespace.mail == "space.atelierwork.mail")
+}
+
+@Test func permissionedRecordsDefaultToAtelierKV() throws {
+ let did = try DID("did:plc:atelierdemo")
+ let key = try PermissionedRecordKey(
+ ownerDID: did,
+ namespace: "space.atelierwork.mail.providerDescriptor",
+ key: "gmail-primary"
+ )
+ let envelope = PermissionedRecordEnvelope(
+ key: key,
+ schemaVersion: 1,
+ encryptedPayload: Data("{}".utf8),
+ encryptionKeyRef: "local-dev"
+ )
+
+ #expect(envelope.storageTarget == PermissionedStorageTarget.atelierKV)
+}
+
+@Test func mailMCPToolsStartReadOnly() {
+ let toolNames = Set(MailMCPTools.readOnly.map(\.name))
+
+ #expect(toolNames.contains("mail_search_threads"))
+ #expect(toolNames.contains("mail_read_thread"))
+ #expect(MailMCPTools.readOnly.allSatisfy { $0.risk == .read })
+}
+
+@Test func mailThreadURIsUseAtelierScheme() {
+ let uri = MailThreadURI.make(AtelierID("thread_01"))
+
+ #expect(uri.rawValue == "atelier://mail/thread/thread_01")
+}
+
+@Test func apiAdvertisesGmailProviderAndRoutes() async throws {
+ let router = MailAPIRouter()
+ let response = await router.handle(APIRequest(method: .get, path: "/v0/providers"))
+ let providers = try decode([ProviderDescriptor].self, from: response)
+
+ #expect(response.status == 200)
+ #expect(providers.first(where: { $0.provider == .gmail })?.status == "wired")
+ #expect(providers.first(where: { $0.provider == .gmail })?.oauthStartPath == "/v0/providers/gmail/oauth/start")
+ #expect(router.routes.contains { $0.path == "/v0/mail/accounts/{accountID}/sync" })
+}
+
+@Test func gmailOAuthConnectsAccountAndInitialSyncWritesNormalizedMail() async throws {
+ let router = MailAPIRouter()
+ let ownerDID = try DID("did:plc:gmaildemo")
+ let start = await router.handle(try jsonRequest(
+ .post,
+ "/v0/providers/gmail/oauth/start",
+ GmailOAuthStartRequest(ownerDID: ownerDID, loginHint: "sam@atelierwork.space")
+ ))
+ let startPayload = try decode(GmailOAuthStartResponse.self, from: start)
+
+ #expect(startPayload.authorizationURL.contains("accounts.google.com"))
+ #expect(startPayload.scopes.contains("https://www.googleapis.com/auth/gmail.modify"))
+
+ let callback = await router.handle(try jsonRequest(
+ .post,
+ "/v0/providers/gmail/oauth/callback",
+ GmailOAuthCallbackRequest(
+ ownerDID: ownerDID,
+ code: "oauth-code",
+ state: startPayload.state,
+ emailAddress: "sam@atelierwork.space",
+ displayName: "Sam"
+ )
+ ))
+ let account = try decode(MailAccount.self, from: callback)
+
+ #expect(callback.status == 201)
+ #expect(account.provider == .gmail)
+ #expect(account.emailAddress == "sam@atelierwork.space")
+
+ let sync = await router.handle(APIRequest(method: .post, path: "/v0/mail/accounts/\(account.id.rawValue)/sync"))
+ let syncPayload = try decode(SyncResponse.self, from: sync)
+
+ #expect(sync.status == 200)
+ #expect(syncPayload.mailboxCount == 2)
+ #expect(syncPayload.threadCount == 1)
+ #expect(syncPayload.messageCount == 1)
+ #expect(syncPayload.cursor.providerCursor == "gmail-history-1001")
+
+ let threadsResponse = await router.handle(APIRequest(method: .get, path: "/v0/mail/threads", query: ["accountID": account.id.rawValue, "q": "gmail api"]))
+ let threads = try decode([MailThread].self, from: threadsResponse)
+
+ #expect(threads.count == 1)
+ #expect(threads[0].providerThreadID == "gmail-thread-atelier-001")
+}
+
+@Test func gmailThreadActionsAndDraftSendUseProviderBoundary() async throws {
+ let router = MailAPIRouter()
+ let account = try await connectedSyncedGmailAccount(router: router)
+ let threads = try decode([MailThread].self, from: await router.handle(APIRequest(method: .get, path: "/v0/mail/threads")))
+ let thread = try #require(threads.first)
+
+ let starResponse = await router.handle(try jsonRequest(.post, "/v0/mail/threads/\(thread.id.rawValue)/actions", MailActionRequest(action: .star, isStarred: true)))
+ let starred = try decode(MailThread?.self, from: starResponse)
+
+ #expect(starResponse.status == 200)
+ #expect(starred?.isStarred == true)
+
+ let draftResponse = await router.handle(try jsonRequest(
+ .post,
+ "/v0/mail/drafts",
+ CreateDraftRequest(
+ accountID: account.id,
+ threadID: thread.id,
+ to: [MailAddress(name: "Mira", address: "mira@example.com")],
+ subject: "Re: \(thread.subject)",
+ textBody: "Thanks, this Gmail route is wired."
+ )
+ ))
+ let draft = try decode(MailDraft.self, from: draftResponse)
+
+ #expect(draftResponse.status == 201)
+ #expect(draft.accountID == account.id)
+
+ let sendResponse = await router.handle(try jsonRequest(.post, "/v0/mail/send", SendDraftRequest(draftID: draft.id)))
+ let receipt = try decode(MailSendReceipt.self, from: sendResponse)
+
+ #expect(sendResponse.status == 200)
+ #expect(receipt.providerMessageID.hasPrefix("gmail-sent-"))
+}
+
+@Test func gmailWebhookTriggersIncrementalSync() async throws {
+ let router = MailAPIRouter()
+ let account = try await connectedSyncedGmailAccount(router: router)
+
+ let webhook = await router.handle(try jsonRequest(
+ .post,
+ "/v0/gmail/webhook",
+ GmailWebhookRequest(accountID: account.id, historyID: "1001")
+ ))
+ let sync = try decode(SyncResponse.self, from: webhook)
+
+ #expect(webhook.status == 200)
+ #expect(sync.cursor.providerCursor == "gmail-history-1002")
+}
+
+private func connectedSyncedGmailAccount(router: MailAPIRouter) async throws -> MailAccount {
+ let ownerDID = try DID("did:plc:gmaildemo")
+ let startPayload = try decode(
+ GmailOAuthStartResponse.self,
+ from: await router.handle(try jsonRequest(.post, "/v0/providers/gmail/oauth/start", GmailOAuthStartRequest(ownerDID: ownerDID)))
+ )
+ let account = try decode(
+ MailAccount.self,
+ from: await router.handle(try jsonRequest(
+ .post,
+ "/v0/providers/gmail/oauth/callback",
+ GmailOAuthCallbackRequest(
+ ownerDID: ownerDID,
+ code: "oauth-code",
+ state: startPayload.state,
+ emailAddress: "sam@atelierwork.space",
+ displayName: "Sam"
+ )
+ ))
+ )
+ _ = await router.handle(APIRequest(method: .post, path: "/v0/mail/accounts/\(account.id.rawValue)/sync"))
+ return account
+}
+
+private func jsonRequest(_ method: HTTPMethod, _ path: String, _ payload: T) throws -> APIRequest {
+ APIRequest(method: method, path: path, body: try testEncoder.encode(payload))
+}
+
+private func decode(_ type: T.Type, from response: APIResponse) throws -> T {
+ try testDecoder.decode(T.self, from: response.body)
+}
+
+private let testEncoder: JSONEncoder = {
+ let encoder = JSONEncoder()
+ encoder.dateEncodingStrategy = .iso8601
+ return encoder
+}()
+
+private let testDecoder: JSONDecoder = {
+ let decoder = JSONDecoder()
+ decoder.dateDecodingStrategy = .iso8601
+ return decoder
+}()