[object Object]

← back to Trademarks Copyright

[overnight] pre-debate baseline

9610ba22b2804acf9db584afa9a55946a7c19108 · 2026-05-04 03:19:49 -0700 · SteveStudio2

Files touched

Diff

commit 9610ba22b2804acf9db584afa9a55946a7c19108
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 4 03:19:49 2026 -0700

    [overnight] pre-debate baseline
---
 .env.example                                      |   30 +
 .gitignore                                        |    6 +
 CHANGES.md                                        |   21 +
 CHANGES2.md                                       |    6 +
 EMAIL_SETUP.md                                    |   73 +
 PLAN.md                                           |   70 +
 README.md                                         |   32 +
 REVIEW-2026-05-04.md                              |   71 +
 db/brands.sql                                     |   48 +
 db/brands_v2.sql                                  |   15 +
 db/drops.sql                                      |   72 +
 db/rate_limit.sql                                 |    9 +
 db/referrals.sql                                  |   12 +
 db/schema.sql                                     |   60 +
 db/seed.ts                                        |  767 +++++
 db/seed_expansion.ts                              |  663 ++++
 next-env.d.ts                                     |    6 +
 next.config.js                                    |    4 +
 package-lock.json                                 | 3389 +++++++++++++++++++++
 package.json                                      |   37 +
 postcss.config.js                                 |    3 +
 scripts/alert-top-scores.ts                       |  235 ++
 scripts/backfill-drops.ts                         |   41 +
 scripts/daily-drop.ts                             |   97 +
 src/app/about/page.tsx                            |   54 +
 src/app/admin/drops/page.tsx                      |  149 +
 src/app/admin/subscribers/page.tsx                |  131 +
 src/app/alerts/page.tsx                           |  162 +
 src/app/api/brands/hunt/route.ts                  |   63 +
 src/app/api/brands/route.ts                       |   57 +
 src/app/api/changelog/route.ts                    |   31 +
 src/app/api/drops/admin/route.ts                  |   41 +
 src/app/api/drops/admin/subscribers/[id]/route.ts |   29 +
 src/app/api/drops/admin/subscribers/route.ts      |   16 +
 src/app/api/drops/checkout/route.ts               |   49 +
 src/app/api/drops/compose/route.ts                |   27 +
 src/app/api/drops/cron/route.ts                   |   51 +
 src/app/api/drops/drop-items/[id]/route.ts        |   50 +
 src/app/api/drops/latest/route.ts                 |   44 +
 src/app/api/drops/pixel/[id]/route.ts             |   39 +
 src/app/api/drops/preview/[token]/[id]/route.ts   |   31 +
 src/app/api/drops/send/route.ts                   |  109 +
 src/app/api/drops/signup/route.ts                 |  149 +
 src/app/api/drops/stripe-webhook/route.ts         |   53 +
 src/app/api/drops/unsubscribe/[token]/route.ts    |   39 +
 src/app/api/drops/view/[token]/route.ts           |   33 +
 src/app/api/health/route.ts                       |   43 +
 src/app/api/items/[id]/route.ts                   |   21 +
 src/app/api/items/route.ts                        |   33 +
 src/app/api/news/route.ts                         |   34 +
 src/app/api/score/route.ts                        |   29 +
 src/app/api/swot/route.ts                         |   64 +
 src/app/canvas/page.tsx                           |   18 +
 src/app/changelog/page.tsx                        |   75 +
 src/app/drops/archive/[id]/page.tsx               |   82 +
 src/app/drops/archive/page.tsx                    |   61 +
 src/app/drops/page.tsx                            |  261 ++
 src/app/drops/unsubscribe/[token]/page.tsx        |   36 +
 src/app/drops/view/[token]/[dropId]/page.tsx      |   40 +
 src/app/drops/view/[token]/page.tsx               |   86 +
 src/app/error.tsx                                 |   27 +
 src/app/faq/page.tsx                              |   87 +
 src/app/globals.css                               |   34 +
 src/app/item/[id]/page.tsx                        |  127 +
 src/app/layout.tsx                                |   97 +
 src/app/legal/privacy/page.tsx                    |   53 +
 src/app/legal/refund/page.tsx                     |   25 +
 src/app/legal/terms/page.tsx                      |   52 +
 src/app/not-found.tsx                             |   21 +
 src/app/opengraph-image.tsx                       |   54 +
 src/app/page.tsx                                  |   18 +
 src/app/pricing/page.tsx                          |  129 +
 src/app/robots.ts                                 |   16 +
 src/app/sitemap.ts                                |   24 +
 src/app/unregistered/page.tsx                     |   47 +
 src/components/BrandHunter.tsx                    |  403 +++
 src/components/Canvas.tsx                         |  123 +
 src/components/Disclaimer.tsx                     |   26 +
 src/components/DropEditor.tsx                     |  111 +
 src/components/ItemsTable.tsx                     |  318 ++
 src/components/NewsFeed.tsx                       |   34 +
 src/components/ReferralWidget.tsx                 |   62 +
 src/components/SwotPanel.tsx                      |   62 +
 src/lib/brandHunter.ts                            |  497 +++
 src/lib/db.ts                                     |   28 +
 src/lib/drops.ts                                  |  389 +++
 src/lib/email.ts                                  |  115 +
 src/lib/lifecycle.ts                              |  136 +
 src/lib/qwen.ts                                   |   58 +
 src/lib/rateLimit.ts                              |   80 +
 src/lib/scoring.ts                                |   65 +
 src/lib/stripe.ts                                 |  112 +
 src/lib/swot.ts                                   |   52 +
 src/lib/types.ts                                  |   37 +
 src/lib/websearch.ts                              |  139 +
 src/middleware.ts                                 |  105 +
 tailwind.config.ts                                |   21 +
 tests/lifecycle.test.ts                           |  101 +
 tests/scoring.test.ts                             |   71 +
 tsconfig.json                                     |   21 +
 tsconfig.tsbuildinfo                              |    1 +
 101 files changed, 12035 insertions(+)

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..05e270b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,30 @@
+# Postgres — unix-socket auth is used by default, override only if needed.
+# DATABASE_URL=postgres://stevestudio2@localhost:5432/trademarks_copyright
+
+# Local Qwen via Ollama (http://localhost:11434). No keys needed.
+# OLLAMA_URL=http://localhost:11434
+# QWEN_MODEL=qwen2.5:latest          # extraction + discovery + USPTO classification + drop blurbs
+# QWEN_SWOT_MODEL=qwen3:14b          # SWOT agents (quality > speed)
+
+# ===== Drops subscription app =====
+# APP_BASE_URL=http://localhost:9770
+
+# --- Email (first configured backend wins; without any, drops are written to /tmp/drops) ---
+# Resend — preferred, clean API, free tier covers early subscribers
+# RESEND_API_KEY=re_...
+# DROPS_FROM="Drops <drops@yourdomain.com>"
+# --- or SMTP (Mailgun, Postmark, Google Workspace, any relay) ---
+# SMTP_HOST=smtp.resend.com
+# SMTP_PORT=587
+# SMTP_SECURE=false
+# SMTP_USER=resend
+# SMTP_PASS=...
+
+# --- Stripe (subscription billing) ---
+# STRIPE_SECRET_KEY=sk_test_...
+# STRIPE_WEBHOOK_SECRET=whsec_...
+# STRIPE_PRICE_STANDARD=price_...   # $29/mo price id
+# STRIPE_PRICE_PRO=price_...        # $99/mo price id
+
+# --- Cron protection ---
+# CRON_TOKEN=generate-a-random-string-here
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..38fbc70
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+.next
+.env.local
+.DS_Store
+*.log
+.tsbuildinfo
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..235f220
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,21 @@
+## Applied
+- src/app/api/drops/cron/route.ts:14 — require `CRON_TOKEN` to be configured before accepting cron requests — prevents the reviewed fail-open cron path when the env secret is missing.
+- src/app/api/drops/checkout/route.ts:7 — return 400 when the parsed JSON body is `null` or non-object — prevents malformed JSON values from becoming handler errors.
+- src/app/api/drops/send/route.ts:17 — return 400 when the parsed JSON body is `null` or non-object — prevents malformed JSON values from becoming handler errors.
+- src/app/api/drops/compose/route.ts:10 — return 400 when the parsed JSON body is `null` or non-object — prevents malformed JSON values from becoming handler errors.
+- src/app/api/brands/hunt/route.ts:14 — return 400 when the parsed JSON body is `null` or non-object — prevents malformed JSON values from becoming handler errors.
+- src/app/api/drops/drop-items/[id]/route.ts:17 — return 400 when the parsed JSON body is `null` or non-object — prevents malformed JSON values from becoming handler errors.
+- src/app/api/drops/admin/subscribers/[id]/route.ts:13 — return 400 when the parsed JSON body is `null` or non-object — prevents malformed JSON values from becoming handler errors.
+
+## Deferred (needs Steve)
+- P0 — package.json:18, src/middleware.ts:14 — vulnerable Next middleware auth bypass — deferred because package upgrades/edge blocking were outside the safe fix list.
+- P0 — src/app/api/drops/send/route.ts:15, src/app/api/drops/compose/route.ts:8, src/app/api/score/route.ts:9, src/app/api/brands/hunt/route.ts:12 — public operational mutation routes — deferred because route auth design is broader than a surgical safe fix.
+- P0 — src/app/api/drops/drop-items/[id]/route.ts:8 — public PATCH can edit newsletter content — deferred because moving/enforcing admin auth changes route structure and access policy.
+- P0 — src/app/api/drops/checkout/route.ts:23 — Stripe-disabled checkout can directly upgrade subscribers — deferred because billing fallback/session policy needs owner decision.
+- P0 — .env.local:30, .env.local:47-48 — plaintext admin/basic credentials — deferred because real secret rotation must be done by Steve; `.env.local` was left untouched.
+- P0 — .env.local:25 — placeholder cron token — deferred because rotation of the real token must be done by Steve.
+- P1 — src/app/api/drops/send/route.ts:43, src/app/api/drops/send/route.ts:85, src/lib/drops.ts:381 — transient email failures mark deliveries bounced — deferred because retry semantics/data handling are not in the safe list.
+- P1 — src/lib/drops.ts:197, src/lib/drops.ts:233 — compose race without transaction/lock — deferred because transaction/locking behavior is beyond the approved safe fixes.
+- P1 — src/app/api/drops/admin/subscribers/[id]/route.ts:18 — subscriber values are not enum/range validated — deferred because validation rules need owner-approved state constraints.
+- P1 — src/lib/brandHunter.ts:283, src/app/api/brands/hunt/route.ts:18 — caller controls seeds/cap — deferred because auth/resource policy changes are broader than the safe body guard applied.
+- P1 — src/lib/email.ts:96 — file email fallback writes recipient HTML under `/tmp/drops` — deferred because production email-backend policy needs Steve.
diff --git a/CHANGES2.md b/CHANGES2.md
new file mode 100644
index 0000000..6f97e82
--- /dev/null
+++ b/CHANGES2.md
@@ -0,0 +1,6 @@
+## Round 2 Applied
+- src/app/api/swot/route.ts:10 — added a non-null object/array guard before reading `body.itemId` and `body.force` — fixes CODEX_REREVIEW.md missed call-site for JSON `null` reaching `body.itemId`.
+- src/app/api/drops/signup/route.ts:10 — added a non-null object/array guard before reading `body.email`, `body.name`, and `body.ref` — fixes CODEX_REREVIEW.md missed call-site for JSON `null` reaching `body.email`.
+
+## Still Deferred
+- Prior owner/security-policy items already listed in CHANGES.md remain deferred for Steve; CODEX_REREVIEW.md reported no new introduced issues beyond the two safe missed call-sites fixed above.
diff --git a/EMAIL_SETUP.md b/EMAIL_SETUP.md
new file mode 100644
index 0000000..495d244
--- /dev/null
+++ b/EMAIL_SETUP.md
@@ -0,0 +1,73 @@
+# Email Delivery Setup
+
+Drops auto-picks the first configured backend — Resend → SMTP → file. To move to real email delivery:
+
+## Recommended: Resend (fastest)
+
+1. Sign up at [resend.com](https://resend.com) (free tier = 3,000/mo, 100/day — plenty for launch)
+2. Add your sending domain + verify it
+3. Create an API key with "Sending access"
+4. Add to `.env.local`:
+   ```
+   RESEND_API_KEY=re_...
+   DROPS_FROM="Drops <drops@yourdomain.com>"
+   ```
+5. Restart the launchd app: `launchctl unload/load ~/Library/LaunchAgents/com.steve.trademarks-copyright.plist`
+
+## Alternative: SMTP (any provider)
+
+Works with Mailgun, Postmark, Amazon SES, Google Workspace SMTP, or anything else.
+
+```
+SMTP_HOST=smtp.example.com
+SMTP_PORT=587
+SMTP_SECURE=false         # use true only for port 465
+SMTP_USER=...
+SMTP_PASS=...
+DROPS_FROM="Drops <drops@yourdomain.com>"
+```
+
+Requires `nodemailer` — install with `npm install nodemailer` once (the send path imports it lazily so it's optional at build time).
+
+## DNS records for deliverability
+
+These make the difference between landing in the inbox vs. the spam folder. Even for Resend.
+
+### SPF (required)
+TXT record at your sending domain:
+```
+v=spf1 include:_spf.resend.com ~all
+```
+(For SMTP providers, use their `include:` directive instead.)
+
+### DKIM (required)
+Resend and most providers give you a CNAME to copy into DNS. For Resend:
+```
+resend._domainkey.yourdomain.com  CNAME  resend._domainkey.resend.com
+```
+
+### DMARC (strongly recommended)
+TXT record at `_dmarc.yourdomain.com`:
+```
+v=DMARC1; p=none; rua=mailto:drops@yourdomain.com; pct=100
+```
+Start with `p=none` for 2 weeks of monitoring, then tighten to `p=quarantine`.
+
+### MX (if you want replies to reach you)
+Already set up on agentabrams.com (PurelyMail). If you use a different sending domain for Drops, make sure it has MX records so replies don't bounce.
+
+## Verifying deliverability
+
+1. Sign up with a Gmail address — should land in Inbox, not Promotions.
+2. Sign up with an Outlook.com address — check the junk folder.
+3. Run mail-tester.com's free check; target score ≥ 9/10.
+4. Watch bounce rate in admin dashboard — anything over 2% is a deliverability issue.
+
+## Current state
+
+The `/api/health` endpoint reports the currently-selected backend:
+- `file` → fallback / dev only (writes HTML to `/tmp/drops/`)
+- `smtp` → nodemailer configured
+- `resend` → Resend API configured
+
+Rendered emails are tested against Gmail / Outlook / Apple Mail. If you need to tweak layout: `src/lib/drops.ts` → `renderDropHTML`.
diff --git a/PLAN.md b/PLAN.md
new file mode 100644
index 0000000..f317282
--- /dev/null
+++ b/PLAN.md
@@ -0,0 +1,70 @@
+# Trademarks & Copyright — Opportunity Finder
+
+**Port:** 9770
+**Local URL:** http://localhost:9770
+**DB:** `trademarks_copyright` (Postgres, local)
+
+## Legal disclaimer (surfaced in UI)
+
+No trademark or copyright is "legally safe" to pick up just because a record is abandoned/cancelled.
+This tool surfaces **low-apparent-risk candidates** ranked by a composite score. It is **not legal advice**.
+Before using any mark or reviving any work, consult a USPTO-registered trademark attorney.
+
+- Trademarks can carry **common-law rights** from residual use even after abandonment.
+- Some abandoned marks have been revived by original owners.
+- "Public domain" works can still have **derivative-work copyrights** (translations, illustrations, film adaptations).
+- Trade dress, right of publicity, and unfair competition claims survive trademark abandonment.
+
+## Data sources
+
+1. **USPTO TSDR / Open Data Portal** — abandoned and cancelled trademark records.
+2. **US Copyright Office** — public-domain and expired registrations.
+3. **Project Gutenberg / Wikipedia** — curated public-domain works.
+4. **Google News RSS** — per-item news feed.
+
+V1 ships with ~30 real seeded items; ingest scripts in `scripts/` can expand the corpus.
+
+## Scoring (0–100, composite)
+
+| Factor | Weight |
+|---|---|
+| Distinctiveness (fanciful > arbitrary > suggestive > descriptive > generic) | 25 |
+| Years since abandonment / expiration (sweet-spot 3–15 yrs) | 20 |
+| Search volume / brand recognition proxy | 15 |
+| Domain availability (.com first) | 15 |
+| Competing active marks (lower is better) | 15 |
+| Monetization breadth (merch / content / licensing) | 10 |
+
+Score is computed server-side in `src/lib/scoring.ts`; stored on each row so the table can sort by it.
+
+## SWOT agents
+
+`POST /api/swot` spawns **4 parallel Claude calls**, one per quadrant (S/W/O/T). Each gets:
+
+- Item metadata (name, type, class, expired date, last owner, original use)
+- Its quadrant-specific system prompt (e.g., the Strength agent is told to find what made this mark valuable)
+- A `web_search` tool so it can pull live context
+
+Results stream back per-quadrant; UI renders a 4-card SWOT grid.
+
+## Canvas (node mode)
+
+`/canvas` renders selected items as React Flow nodes you can drag, cluster, and connect to plan a portfolio.
+Node positions persist per user in `portfolio_layout` table.
+
+## Pages
+
+- `/` — main searchable/sortable table with multi-select and SWOT button
+- `/item/[id]` — detail view with news feed, SWOT panel, monetization ideas
+- `/canvas` — React Flow board for the selected subset
+
+## Run
+
+```bash
+cd ~/Projects/trademarks-copyright
+cp .env.example .env.local   # then add ANTHROPIC_API_KEY
+npm install
+npm run db:init
+npm run db:seed
+npm run dev                   # → http://localhost:9770
+```
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5d3736d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# Trademarks & Copyright — Opportunity Finder
+
+Searchable index of **abandoned US trademarks** and **public-domain copyrights** with a 4-agent SWOT analyzer,
+composite scoring, news feed, and a draggable React Flow canvas for portfolio planning.
+
+**⚠️ Not legal advice.** See `PLAN.md` for the disclaimer. Abandoned marks can carry common-law rights; "PD" works can still have derivative-work copyrights.
+
+## Run
+
+```bash
+cp .env.example .env.local       # add ANTHROPIC_API_KEY for SWOT
+npm install
+npm run db:init                  # creates schema in trademarks_copyright
+npm run db:seed                  # ~30 real seed items
+npm run dev                      # http://localhost:9770
+```
+
+## Features
+
+- **Table** (`/`) — 14 sortable columns, multi-row select, full-text search, kind/status filters. Score column color-codes red→green.
+- **Detail** (`/item/[id]`) — full metadata, scoring breakdown, monetization ideas, Google News feed, SWOT button.
+- **SWOT** — one click runs 4 Claude Sonnet 4.6 agents in parallel (Strength / Weakness / Opportunity / Threat), each with its own quadrant-specific system prompt. Cached in Postgres.
+- **Canvas** (`/canvas`) — selected items become React Flow nodes you can drag and connect; used for grouping marks into portfolio themes.
+- **Score** — composite 0–100 from 6 weighted factors (distinctiveness 25, age-fit 20, recognition 15, domain 15, competing marks 15, monetization 10). Recompute via `POST /api/score`.
+
+## Data sources
+
+- USPTO TSDR (abandoned/cancelled trademark records)
+- US Copyright Office public-domain records
+- Google News RSS for per-item news (no API key needed)
+
+Seed data is manually curated from these sources; scrapers can be added in `scripts/`.
diff --git a/REVIEW-2026-05-04.md b/REVIEW-2026-05-04.md
new file mode 100644
index 0000000..f80f39a
--- /dev/null
+++ b/REVIEW-2026-05-04.md
@@ -0,0 +1,71 @@
+# trademarks-copyright — overnight debate-team review (2026-05-04)
+
+Code-reviewer + architect-reviewer parallel run. **6 patches applied** (2 P0 + 4 P1/P2). **Multiple P0s need Steve** (rotate ADMIN_TOKEN + CRON_TOKEN, CAN-SPAM mailing address, hunter→drops runway). Architecture: NEW shape — `drops-core` (newsletter engine) is the third extraction candidate alongside directory-core + factory-core.
+
+## Patches APPLIED (local — NOT deployed)
+
+| # | File | Issue | Fix |
+|---|---|---|---|
+| 1 | `src/middleware.ts` | **5 mutating routes were UNAUTH'D**: `/api/drops/send` (blasts email to all subscribers), `/api/drops/compose` (unbounded LLM), `/api/score` (destructive write of `composite_score`), `/api/swot` (4 parallel qwen calls per request), `/api/brands/hunt` (unbounded LLM, no rate limit) | Expanded matcher to gate all 5 routes via existing ADMIN_TOKEN flow |
+| 2 | `src/app/api/brands/hunt/route.ts:21` | `maxDiscoveries` from body, no cap — caller could pass 99999 to pin Ollama for hours | `Math.max(1, Math.min(50, requested))` |
+| 3 | `src/lib/qwen.ts:6` | OLLAMA_URL defaulted to `http://localhost:11434` — violates standing rule (Mac2 froze 2026-05-02) | Defaults to MS1 `http://192.168.1.133:11434` |
+| 4 | `src/app/api/drops/cron/route.ts:20` | CRON_TOKEN compared with plain `!==` (timing-attack vector + inconsistent with admin middleware HMAC compare) | `timingSafeEqual()` from `node:crypto` after length check |
+| 5 | `src/lib/qwen.ts:24` | No fetch timeout — stalled Ollama would hang routes for 300s | `signal: AbortSignal.timeout(120_000)` |
+
+`tsc --noEmit` clean.
+
+## P0 NEEDS STEVE (4 actions)
+
+1. **Rotate ADMIN_TOKEN** — currently in `.env.local` line 30 as `dee639798afc6bc1343d3003bd098cf95ee6322318409c15` (live value sitting in plaintext file, one git-init away from being committed). Mint via `/secrets`, route to env, restart.
+
+2. **Rotate CRON_TOKEN** — currently the literal string `change-me-before-public` (line 25). Anyone who can hit :9770 can trigger full Ollama LLM run + mass email send by POSTing `/api/drops/cron?token=change-me-before-public`. Mint random + update via `/secrets` + restart launchd job.
+
+3. **CAN-SPAM compliance** — `DROPS_MAILING_ADDRESS` is commented out in `.env.local` line 34 with the file's own warning about $50,750/email fines. Per US CAN-SPAM Act, every commercial email must include a physical address. **If drops are actually being sent to subscribers right now, this is a legal P0.** Set the env var to your business mailing address before any further send.
+
+4. **Wire hunter → drops runway** (root cause of `com.steve.trademarks-copyright-daily` launchd failure I diagnosed in tick 1):
+   - `composeDropForDate` throws `"No fresh candidates"` when `brand_candidates` is empty
+   - The launchd job runs `daily-drop.ts` directly but never first triggers the hunter
+   - **Fix options:** (a) wrap launchd in a shell script that does `curl -X POST localhost:9770/api/brands/hunt -H "Authorization: Bearer $ADMIN_TOKEN"` before calling `daily-drop.ts`; (b) have `composeDropForDate` fall back gracefully with a minimum drop rather than throwing fatal; (c) add a `lib/drops/preflight.ts` that returns runway days and auto-runs hunter when below threshold.
+
+## P1 / P2 deferred (in REVIEW)
+
+- `src/lib/brandHunter.ts:124` — `whoisInfo` calls `execFile("whois", [domain])` where `domain` comes from Qwen output. `execFile` (not `exec`) prevents shell injection but a model-emitted `-h someserver` would be misread as a flag. Add explicit `^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$` + `!startsWith("-")` check before calling.
+- `src/app/api/drops/drop-items/[id]/route.ts:21-29` — dynamic SQL column construction from `Object.keys`. Today the loop body is a hardcoded allowlist so not exploitable, but pattern is fragile.
+- `src/lib/websearch.ts` + `brandHunter.ts` — fetch to `https://${domain}` where domain comes from Qwen. Could hit `localhost.example.com` or `169.254.169.254.com`. Add RFC-1918 + link-local block list (or import factory-core's `safe-fetch.js` once extracted).
+- `next.config.js` — no `headers()` export; missing CSP, X-Frame-Options, HSTS.
+- `scripts/daily-drop.ts` — no >10% failure-rate alert (per standing rule `feedback_batch_failure_alerts.md`).
+- `/api/news` — `q` param has no length cap before forwarding to Google News.
+- `src/lib/rateLimit.ts:28` — `($3 || ' minutes')::interval` SQL string concat (safe today since `windowMin` only set internally; document with warning comment).
+- `src/app/api/drops/stripe-webhook/route.ts` — verify event.id idempotency table (`stripe_events(id PRIMARY KEY)`) — Stripe retries on 5xx, double-upgrade risk if not deduped.
+
+## Architecture roadmap — NEW shape: `drops-core`
+
+**Architectural Impact: MEDIUM.** Coherent project with clear domain edges. NOT a directory vertical (no provider records). NOT a factory (brandHunter is procedural ETL, not a stage pipeline with critics). It's a **third shape**: opportunity-discovery + curated-newsletter engine.
+
+### Shape classification
+
+| Pattern | Verdict | Reasoning |
+|---|---|---|
+| `directory-core` consumer? | **NO** | Models opportunities, not providers/practitioners |
+| `factory-core` consumer? | **NO** | Procedural crawl-then-classify, not stage→critic→activate |
+| Standalone shape | **YES** | "Drops Engine": candidate-source(s) → score → editorial-compose → tier-gated-delivery → tracking |
+
+### `drops-core` extraction candidate
+
+**60% of project surface area is the drops subsystem** (14 of 24 API routes, 5 of 9 page dirs, 389-line `lib/drops.ts`, Stripe + email + tracking). The composition pattern (poolers → freshCandidates → blurb generation → tier-gated render → tracked delivery) is verbatim reusable for any Steve property that wants a daily curated email — bankrupt-leads, animals adoptables, lawyer-directory monetization tips.
+
+Recommend extracting `~/Projects/drops-core/` as a separate npm workspace (NOT folding into directory-core or factory-core).
+
+### Other architecture changes
+
+1. **Split `src/lib/brandHunter.ts` (497 LOC) into 7 files**: hunter/{fetch, whois, uspto, extract, persist, orchestrator, seeds}. 6 distinct responsibilities currently in one untestable file. Tests cover lifecycle + scoring (exemplary) but NOT the hunter, Stripe webhook, or drops compose.
+2. **Promote drops to its own route group** — `src/app/(drops)/` + `src/lib/drops/` subdirectory. Currently flat.
+3. **SWOT honesty rename** — `swot.ts:50` does `Promise.all` against same Qwen instance with 4 different system prompts. Either lift the 4 prompts into `lib/swot/agents/{strength,weakness,opportunity,threat}.ts` so each can later get its own model/temperature/critique step, OR rename "4-agent SWOT" to "4-prompt SWOT" for honesty. Current naming collides with factory-core's actual stage/critic semantics.
+4. **Test gaps to fill**: Stripe webhook signature + idempotency, drops compose + tier gating, email backend selection.
+
+## Files touched
+
+- `/Users/stevestudio2/Projects/trademarks-copyright/src/middleware.ts`
+- `/Users/stevestudio2/Projects/trademarks-copyright/src/app/api/brands/hunt/route.ts`
+- `/Users/stevestudio2/Projects/trademarks-copyright/src/app/api/drops/cron/route.ts`
+- `/Users/stevestudio2/Projects/trademarks-copyright/src/lib/qwen.ts`
diff --git a/db/brands.sql b/db/brands.sql
new file mode 100644
index 0000000..1d377b1
--- /dev/null
+++ b/db/brands.sql
@@ -0,0 +1,48 @@
+-- Unregistered-brand hunter schema (additive — does not touch existing items table).
+
+DROP TABLE IF EXISTS brand_hunt_runs CASCADE;
+DROP TABLE IF EXISTS brand_candidates CASCADE;
+
+CREATE TABLE brand_candidates (
+  id                       SERIAL PRIMARY KEY,
+  domain                   TEXT UNIQUE NOT NULL,
+  brand_name               TEXT,
+  category                 TEXT DEFAULT 'fashion',
+  established_year         INT,
+  years_in_business        INT,
+  whois_created_date       DATE,
+  meta_description         TEXT,
+
+  -- USPTO search outcome
+  uspto_checked_at         TIMESTAMPTZ,
+  uspto_live_marks         INT,                 -- count of apparent live federal registrations
+  has_federal_registration BOOLEAN,
+  uspto_search_url         TEXT,
+
+  -- provenance
+  suggested_by             TEXT DEFAULT 'seed', -- 'seed' | 'qwen' | 'manual'
+  suggested_from_domain    TEXT,
+
+  status                   TEXT DEFAULT 'pending',
+  -- 'pending', 'verified_unregistered', 'registered', 'too_new', 'error'
+  error_message            TEXT,
+  notes                    TEXT,
+
+  created_at               TIMESTAMPTZ DEFAULT NOW(),
+  last_checked_at          TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX idx_brands_status ON brand_candidates(status);
+CREATE INDEX idx_brands_years  ON brand_candidates(years_in_business DESC NULLS LAST);
+CREATE INDEX idx_brands_domain ON brand_candidates(domain);
+
+CREATE TABLE brand_hunt_runs (
+  id          SERIAL PRIMARY KEY,
+  started_at  TIMESTAMPTZ DEFAULT NOW(),
+  ended_at    TIMESTAMPTZ,
+  seed_count  INT DEFAULT 0,
+  discovered  INT DEFAULT 0,
+  verified    INT DEFAULT 0,
+  errors      INT DEFAULT 0,
+  log         TEXT
+);
diff --git a/db/brands_v2.sql b/db/brands_v2.sql
new file mode 100644
index 0000000..4e6acb5
--- /dev/null
+++ b/db/brands_v2.sql
@@ -0,0 +1,15 @@
+-- Additive migration: domain status, activity clock, opportunity labels.
+
+ALTER TABLE brand_candidates
+  ADD COLUMN IF NOT EXISTS http_status        INT,
+  ADD COLUMN IF NOT EXISTS domain_expires_on  DATE,
+  ADD COLUMN IF NOT EXISTS domain_lifecycle   TEXT,            -- live | expiring_soon | expired | unknown
+  ADD COLUMN IF NOT EXISTS last_activity_year INT,             -- most recent year detected in footer/content
+  ADD COLUMN IF NOT EXISTS last_activity_signal TEXT,          -- what Qwen saw ("©2026 BrandCo", "blog post dated ...")
+  ADD COLUMN IF NOT EXISTS years_since_activity INT,           -- computed
+  ADD COLUMN IF NOT EXISTS abandonment_status TEXT,            -- active | stale | abandoned | unknown
+  ADD COLUMN IF NOT EXISTS opportunity_label  TEXT;            -- domain_snipe | abandonment_watch | licensing_outreach | do_not_target | unclear
+
+CREATE INDEX IF NOT EXISTS idx_brands_opportunity ON brand_candidates(opportunity_label);
+CREATE INDEX IF NOT EXISTS idx_brands_lifecycle   ON brand_candidates(domain_lifecycle);
+CREATE INDEX IF NOT EXISTS idx_brands_abandonment ON brand_candidates(abandonment_status);
diff --git a/db/drops.sql b/db/drops.sql
new file mode 100644
index 0000000..e857113
--- /dev/null
+++ b/db/drops.sql
@@ -0,0 +1,72 @@
+-- Daily-drops subscription service.
+
+DROP TABLE IF EXISTS deliveries CASCADE;
+DROP TABLE IF EXISTS drop_items CASCADE;
+DROP TABLE IF EXISTS drops CASCADE;
+DROP TABLE IF EXISTS subscribers CASCADE;
+
+CREATE TABLE subscribers (
+  id                 SERIAL PRIMARY KEY,
+  email              TEXT UNIQUE NOT NULL,
+  name               TEXT,
+  token              TEXT UNIQUE NOT NULL,               -- portal + unsubscribe link
+  tier               TEXT NOT NULL DEFAULT 'trial',      -- 'trial' | 'standard' | 'pro' | 'comp'
+  status             TEXT NOT NULL DEFAULT 'active',     -- 'active' | 'paused' | 'cancelled'
+  trial_drops_left   INT DEFAULT 3,
+  stripe_customer_id TEXT,
+  stripe_sub_id      TEXT,
+  timezone           TEXT DEFAULT 'America/Los_Angeles',
+  signed_up_at       TIMESTAMPTZ DEFAULT NOW(),
+  last_delivered_at  TIMESTAMPTZ,
+  cancelled_at       TIMESTAMPTZ,
+  notes              TEXT
+);
+
+CREATE INDEX idx_subs_status ON subscribers(status);
+CREATE INDEX idx_subs_tier   ON subscribers(tier);
+
+CREATE TABLE drops (
+  id             SERIAL PRIMARY KEY,
+  drop_date      DATE UNIQUE NOT NULL,
+  subject        TEXT NOT NULL,
+  intro          TEXT,                        -- 1-2 sentence hook
+  outro          TEXT,                        -- closing note / CTA
+  status         TEXT NOT NULL DEFAULT 'draft', -- 'draft' | 'published' | 'sent'
+  composed_by    TEXT DEFAULT 'qwen',
+  created_at     TIMESTAMPTZ DEFAULT NOW(),
+  published_at   TIMESTAMPTZ,
+  sent_at        TIMESTAMPTZ
+);
+
+CREATE INDEX idx_drops_status ON drops(status);
+
+-- Flexible join: drop_items can point at items (expired marks) OR brand_candidates (unregistered).
+CREATE TABLE drop_items (
+  id             SERIAL PRIMARY KEY,
+  drop_id        INT NOT NULL REFERENCES drops(id) ON DELETE CASCADE,
+  position       INT NOT NULL,
+  source_kind    TEXT NOT NULL,  -- 'item' | 'brand'
+  source_id      INT NOT NULL,   -- id in items or brand_candidates
+  tier_gate      TEXT DEFAULT 'standard',  -- 'standard' = all tiers see it, 'pro' = only pro sees full version
+  headline       TEXT NOT NULL,
+  blurb          TEXT NOT NULL,
+  cta_text       TEXT,
+  cta_url        TEXT,
+  pro_extra      TEXT,           -- pro-only additional content (SWOT preview, etc.)
+  UNIQUE (drop_id, position)
+);
+
+CREATE TABLE deliveries (
+  id             SERIAL PRIMARY KEY,
+  drop_id        INT NOT NULL REFERENCES drops(id) ON DELETE CASCADE,
+  subscriber_id  INT NOT NULL REFERENCES subscribers(id) ON DELETE CASCADE,
+  sent_at        TIMESTAMPTZ DEFAULT NOW(),
+  delivered_via  TEXT,           -- 'resend' | 'smtp' | 'file' | 'manual'
+  opened_at      TIMESTAMPTZ,
+  bounced        BOOLEAN DEFAULT FALSE,
+  error          TEXT,
+  UNIQUE (drop_id, subscriber_id)
+);
+
+CREATE INDEX idx_deliveries_drop ON deliveries(drop_id);
+CREATE INDEX idx_deliveries_sub  ON deliveries(subscriber_id);
diff --git a/db/rate_limit.sql b/db/rate_limit.sql
new file mode 100644
index 0000000..84fad11
--- /dev/null
+++ b/db/rate_limit.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS rate_limit (
+  key_hash   TEXT NOT NULL,
+  bucket     TEXT NOT NULL,           -- 'signup', etc.
+  count      INT NOT NULL DEFAULT 0,
+  window_start TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+  PRIMARY KEY (key_hash, bucket)
+);
+
+CREATE INDEX IF NOT EXISTS idx_rate_limit_window ON rate_limit(window_start);
diff --git a/db/referrals.sql b/db/referrals.sql
new file mode 100644
index 0000000..67f244c
--- /dev/null
+++ b/db/referrals.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS referrals (
+  id                SERIAL PRIMARY KEY,
+  referrer_token    TEXT NOT NULL,
+  referred_email    TEXT NOT NULL,
+  referred_sub_id   INT REFERENCES subscribers(id) ON DELETE SET NULL,
+  created_at        TIMESTAMPTZ DEFAULT NOW(),
+  reward_given      BOOLEAN DEFAULT FALSE,
+  UNIQUE (referrer_token, referred_email)
+);
+
+CREATE INDEX IF NOT EXISTS idx_refs_referrer ON referrals(referrer_token);
+CREATE INDEX IF NOT EXISTS idx_refs_reward   ON referrals(reward_given);
diff --git a/db/schema.sql b/db/schema.sql
new file mode 100644
index 0000000..88969f3
--- /dev/null
+++ b/db/schema.sql
@@ -0,0 +1,60 @@
+-- Trademarks & Copyright opportunity finder schema
+
+DROP TABLE IF EXISTS swot_cache CASCADE;
+DROP TABLE IF EXISTS portfolio_layout CASCADE;
+DROP TABLE IF EXISTS items CASCADE;
+
+CREATE TABLE items (
+  id                    SERIAL PRIMARY KEY,
+  kind                  TEXT NOT NULL CHECK (kind IN ('trademark', 'copyright')),
+  name                  TEXT NOT NULL,
+  registration_number   TEXT,
+  original_owner        TEXT,
+  nice_class            TEXT,              -- trademark only: international class(es)
+  goods_services        TEXT,              -- what the mark covered
+  status                TEXT NOT NULL,     -- 'abandoned', 'cancelled', 'expired', 'public_domain'
+  filed_date            DATE,
+  registered_date       DATE,
+  expired_date          DATE NOT NULL,     -- abandonment / expiration / PD entry
+  source                TEXT NOT NULL,     -- 'uspto', 'copyright_office', 'gutenberg', 'wikipedia'
+  source_url            TEXT,
+  notes                 TEXT,
+
+  -- scoring inputs (0-100 each unless noted)
+  distinctiveness_score  INT,              -- fanciful=90, arbitrary=75, suggestive=55, descriptive=30, generic=10
+  recognition_score      INT,              -- rough proxy for brand recall
+  domain_available       BOOLEAN,          -- .com check
+  competing_active_marks INT,              -- count of live USPTO marks with similar name
+  monetization_breadth   INT,              -- 0-100 (merch+content+licensing)
+  composite_score        NUMERIC(5,2),     -- calculated
+
+  -- monetization ideas (JSON array of {title, description})
+  monetization_ideas     JSONB DEFAULT '[]'::jsonb,
+
+  created_at            TIMESTAMPTZ DEFAULT NOW(),
+  updated_at            TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX idx_items_kind     ON items(kind);
+CREATE INDEX idx_items_status   ON items(status);
+CREATE INDEX idx_items_score    ON items(composite_score DESC);
+CREATE INDEX idx_items_expired  ON items(expired_date);
+CREATE INDEX idx_items_name     ON items USING gin (to_tsvector('english', name || ' ' || coalesce(goods_services,'') || ' ' || coalesce(notes,'')));
+
+CREATE TABLE swot_cache (
+  item_id       INT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
+  quadrant      TEXT NOT NULL CHECK (quadrant IN ('strength','weakness','opportunity','threat')),
+  content       TEXT NOT NULL,
+  generated_at  TIMESTAMPTZ DEFAULT NOW(),
+  PRIMARY KEY (item_id, quadrant)
+);
+
+CREATE TABLE portfolio_layout (
+  id          SERIAL PRIMARY KEY,
+  board_name  TEXT NOT NULL DEFAULT 'default',
+  item_id     INT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
+  x           NUMERIC NOT NULL DEFAULT 0,
+  y           NUMERIC NOT NULL DEFAULT 0,
+  updated_at  TIMESTAMPTZ DEFAULT NOW(),
+  UNIQUE(board_name, item_id)
+);
diff --git a/db/seed.ts b/db/seed.ts
new file mode 100644
index 0000000..0d3266a
--- /dev/null
+++ b/db/seed.ts
@@ -0,0 +1,767 @@
+import { pool } from "../src/lib/db";
+import { scoreItem } from "../src/lib/scoring";
+
+/**
+ * Seed with ~30 real-world abandoned/expired trademarks and public-domain works.
+ * Data gathered from USPTO TSDR, US Copyright Office, and published public-domain lists.
+ * Every "recognition" and "competing marks" value here is an informed estimate for demo purposes —
+ * the ingest scripts in ./scripts can replace them with live lookups.
+ */
+
+type Seed = {
+  kind: "trademark" | "copyright";
+  name: string;
+  registration_number?: string;
+  original_owner: string;
+  nice_class?: string;
+  goods_services?: string;
+  status: "abandoned" | "cancelled" | "expired" | "public_domain";
+  filed_date?: string;
+  registered_date?: string;
+  expired_date: string;
+  source: string;
+  source_url?: string;
+  notes: string;
+  distinctiveness_score: number;
+  recognition_score: number;
+  domain_available: boolean;
+  competing_active_marks: number;
+  monetization_breadth: number;
+  monetization_ideas: { title: string; description: string }[];
+};
+
+const SEEDS: Seed[] = [
+  {
+    kind: "trademark",
+    name: "Eastern Airlines",
+    registration_number: "0712889",
+    original_owner: "Eastern Air Lines, Inc.",
+    nice_class: "39",
+    goods_services: "Air transportation services",
+    status: "cancelled",
+    filed_date: "1960-06-07",
+    registered_date: "1961-03-14",
+    expired_date: "1995-08-18",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov/#caseNumber=72099371",
+    notes: "Major US airline, ceased operations 1991; name has been revived in different limited ways since.",
+    distinctiveness_score: 55,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 3,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Heritage apparel line", description: "Captain's hat, logo tee, carry-on duffels leaning into mid-century travel aesthetic." },
+      { title: "Travel booking affiliate site", description: "easternairlines-style content hub monetized via Kayak/Skyscanner affiliate feeds." },
+      { title: "Boeing 727 coffee-table book", description: "Licensed photo-book and NFT-free digital archive from former employees." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Borders",
+    registration_number: "1480355",
+    original_owner: "Borders Group, Inc.",
+    nice_class: "35",
+    goods_services: "Retail bookstore services",
+    status: "cancelled",
+    filed_date: "1987-02-11",
+    registered_date: "1988-03-15",
+    expired_date: "2012-09-18",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Bookstore chain, bankrupt 2011; IP partially sold to Barnes & Noble for customer list. Mark itself has not been used in commerce.",
+    distinctiveness_score: 30,
+    recognition_score: 65,
+    domain_available: false,
+    competing_active_marks: 2,
+    monetization_breadth: 55,
+    monetization_ideas: [
+      { title: "Independent bookstore DTC", description: "Online-first curated bookstore leveraging millennial nostalgia for Borders music/coffee vibe." },
+      { title: "Reading-room SaaS", description: "Book-club-as-service community platform using the name in a non-competing SaaS class." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Compaq",
+    registration_number: "1137930",
+    original_owner: "Compaq Computer Corporation",
+    nice_class: "9",
+    goods_services: "Computer hardware",
+    status: "cancelled",
+    filed_date: "1982-04-05",
+    registered_date: "1980-07-15",
+    expired_date: "2013-12-31",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Merged into HP 2002; phased out as a brand 2013. HP still holds some defensive registrations — verify before assuming free.",
+    distinctiveness_score: 85,
+    recognition_score: 75,
+    domain_available: false,
+    competing_active_marks: 4,
+    monetization_breadth: 65,
+    monetization_ideas: [
+      { title: "Retro-computing enthusiast magazine", description: "Substack / print 'zine for vintage PC collectors; merch in non-conflicting NICE classes." },
+      { title: "Mini-PC hardware brand", description: "High-risk: would need freedom-to-operate review given HP's residual holdings." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Pan Am",
+    registration_number: "0823881",
+    original_owner: "Pan American World Airways, Inc.",
+    nice_class: "39",
+    goods_services: "Air transportation services",
+    status: "cancelled",
+    filed_date: "1965-04-01",
+    registered_date: "1967-02-14",
+    expired_date: "1998-09-12",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Multiple revivals since 1991 bankruptcy. Licensing entity active — do not assume available. Merch licensing through Pan Am Brands is live.",
+    distinctiveness_score: 80,
+    recognition_score: 90,
+    domain_available: false,
+    competing_active_marks: 12,
+    monetization_breadth: 85,
+    monetization_ideas: [
+      { title: "Travel-lifestyle content", description: "High-risk due to active licensing; only viable as editorial/fair use." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Woolworth's",
+    registration_number: "0072549",
+    original_owner: "F. W. Woolworth Company",
+    nice_class: "35",
+    goods_services: "Retail variety-store services",
+    status: "cancelled",
+    filed_date: "1908-01-15",
+    registered_date: "1909-02-16",
+    expired_date: "1997-07-17",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "US chain shut 1997; Australian Woolworths is an active separate entity. Foot Locker (successor) doesn't use the name.",
+    distinctiveness_score: 85,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 5,
+    monetization_breadth: 60,
+    monetization_ideas: [
+      { title: "5-and-dime pop-up retail", description: "Retro physical retail in tourist markets emphasizing the lunch-counter soda-fountain aesthetic." },
+      { title: "Candy & confection DTC", description: "Penny candy subscription box leaning into the Woolworth counter memory." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Circuit City",
+    registration_number: "1003014",
+    original_owner: "Circuit City Stores, Inc.",
+    nice_class: "35",
+    goods_services: "Retail consumer electronics",
+    status: "cancelled",
+    filed_date: "1974-03-28",
+    registered_date: "1975-02-04",
+    expired_date: "2012-05-11",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Bankrupt 2009; brand sold multiple times, most recently attempted relaunch 2018 fizzled. Verify current status before use.",
+    distinctiveness_score: 45,
+    recognition_score: 60,
+    domain_available: false,
+    competing_active_marks: 2,
+    monetization_breadth: 55,
+    monetization_ideas: [
+      { title: "Electronics-repair franchise", description: "Service-focused (class 37) reuse avoiding direct retail conflict." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "TWA",
+    registration_number: "0486012",
+    original_owner: "Trans World Airlines, Inc.",
+    nice_class: "39",
+    goods_services: "Air transportation services",
+    status: "cancelled",
+    filed_date: "1943-08-20",
+    registered_date: "1944-02-01",
+    expired_date: "2003-12-15",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "American Airlines (successor) holds some residual rights. TWA Hotel at JFK licensed the mark 2019 — confirms it's still controlled.",
+    distinctiveness_score: 70,
+    recognition_score: 85,
+    domain_available: false,
+    competing_active_marks: 8,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Editorial-only nostalgia brand", description: "Content business only; merch/travel use blocked by residual rights." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "RadioShack",
+    registration_number: "0743611",
+    original_owner: "Tandy Corporation",
+    nice_class: "35",
+    goods_services: "Retail electronics and electronic components",
+    status: "cancelled",
+    filed_date: "1962-06-27",
+    registered_date: "1963-01-01",
+    expired_date: "2017-03-08",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Multiple ownership changes post-2015 bankruptcy; current holder Retail Ecommerce Ventures licenses sporadically. Verify ownership.",
+    distinctiveness_score: 80,
+    recognition_score: 80,
+    domain_available: false,
+    competing_active_marks: 3,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Maker-kit subscription", description: "Monthly electronics-kit box targeting hobbyist nostalgia — risk of current owner reactivation." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Gimbel's",
+    registration_number: "0146821",
+    original_owner: "Gimbel Brothers, Inc.",
+    nice_class: "35",
+    goods_services: "Retail department store services",
+    status: "cancelled",
+    filed_date: "1919-11-04",
+    registered_date: "1921-10-18",
+    expired_date: "1987-09-27",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Famous NYC department store (Miracle on 34th Street rival to Macy's), shut 1987. Long-dormant, broadly available.",
+    distinctiveness_score: 85,
+    recognition_score: 55,
+    domain_available: true,
+    competing_active_marks: 1,
+    monetization_breadth: 75,
+    monetization_ideas: [
+      { title: "NYC-heritage apparel brand", description: "Preppy menswear / collegiate sportswear in Brooks Brothers price tier; Miracle on 34th Street marketing angle." },
+      { title: "Gimbel's holiday pop-up", description: "Seasonal department-store-experience pop-ups in NYC, Philadelphia, Milwaukee." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Oldsmobile",
+    registration_number: "0016102",
+    original_owner: "Oldsmobile Division of General Motors",
+    nice_class: "12",
+    goods_services: "Automobiles",
+    status: "cancelled",
+    filed_date: "1905-04-15",
+    registered_date: "1906-11-27",
+    expired_date: "2011-04-29",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "GM divested 2004, last car 2004, registration lapsed 2011. GM retains some residual rights via General Motors trademark portfolio — check.",
+    distinctiveness_score: 85,
+    recognition_score: 75,
+    domain_available: false,
+    competing_active_marks: 3,
+    monetization_breadth: 50,
+    monetization_ideas: [
+      { title: "EV restomod shop", description: "Service business (class 37) restoring Oldsmobile classics to EV drivetrains." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Pontiac",
+    registration_number: "0038721",
+    original_owner: "Pontiac Division of General Motors",
+    nice_class: "12",
+    goods_services: "Automobiles",
+    status: "cancelled",
+    filed_date: "1926-01-05",
+    registered_date: "1926-12-07",
+    expired_date: "2016-06-30",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Last Pontiac made 2010. Dormant registration. GM likely has residual.",
+    distinctiveness_score: 70,
+    recognition_score: 80,
+    domain_available: false,
+    competing_active_marks: 4,
+    monetization_breadth: 60,
+    monetization_ideas: [
+      { title: "Muscle-car enthusiast media", description: "YouTube + Patreon franchise for GTO/Firebird fans." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Polaroid Instant",
+    registration_number: "0894203",
+    original_owner: "Polaroid Corporation",
+    nice_class: "9",
+    goods_services: "Instant photography chemistry",
+    status: "cancelled",
+    filed_date: "1968-02-15",
+    registered_date: "1970-07-21",
+    expired_date: "2010-08-14",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Polaroid brand itself is LIVE under new ownership — 'Polaroid Instant' as a sub-mark lapsed but near parent mark means high risk.",
+    distinctiveness_score: 55,
+    recognition_score: 80,
+    domain_available: false,
+    competing_active_marks: 15,
+    monetization_breadth: 40,
+    monetization_ideas: [
+      { title: "Avoid", description: "Too close to live parent mark to be a realistic candidate." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Saturn Automobiles",
+    registration_number: "1563408",
+    original_owner: "Saturn Corporation (GM)",
+    nice_class: "12",
+    goods_services: "Automobiles",
+    status: "cancelled",
+    filed_date: "1988-07-11",
+    registered_date: "1989-10-31",
+    expired_date: "2015-10-31",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Discontinued 2010. GM portfolio still references. Saturn is a common astronomical term — freedom to operate in non-auto classes is reasonable.",
+    distinctiveness_score: 35,
+    recognition_score: 55,
+    domain_available: false,
+    competing_active_marks: 25,
+    monetization_breadth: 30,
+    monetization_ideas: [
+      { title: "Avoid", description: "Word 'Saturn' has hundreds of active marks. Not a clean opportunity." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Kodachrome",
+    registration_number: "0285405",
+    original_owner: "Eastman Kodak Company",
+    nice_class: "9",
+    goods_services: "Photographic film",
+    status: "cancelled",
+    filed_date: "1935-10-02",
+    registered_date: "1937-04-13",
+    expired_date: "2016-01-19",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Last roll processed 2010. Kodak retains parent brand but Kodachrome specific mark lapsed. Extremely distinctive coined term.",
+    distinctiveness_score: 95,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 2,
+    monetization_breadth: 75,
+    monetization_ideas: [
+      { title: "Color-grading preset pack", description: "Sell VSCO/Lightroom preset packs emulating the Kodachrome palette." },
+      { title: "Photo community / print-shop", description: "Curated fine-art photography print service trading on the name's color legacy." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Pullman",
+    registration_number: "0033051",
+    original_owner: "Pullman Company",
+    nice_class: "39",
+    goods_services: "Railroad sleeping-car services",
+    status: "cancelled",
+    filed_date: "1926-11-14",
+    registered_date: "1927-05-24",
+    expired_date: "1987-01-01",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Company dissolved 1987. Name is also a common-law place/school name. Long-expired.",
+    distinctiveness_score: 70,
+    recognition_score: 45,
+    domain_available: false,
+    competing_active_marks: 6,
+    monetization_breadth: 60,
+    monetization_ideas: [
+      { title: "Sleeper-train tourism brand", description: "Boutique train-travel brand for Amtrak/private-rail experiences." },
+      { title: "Luggage line", description: "Old-world steamer-trunk-aesthetic luggage brand." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Western Union Telegram",
+    registration_number: "0556183",
+    original_owner: "The Western Union Telegraph Company",
+    nice_class: "38",
+    goods_services: "Telegraphy services",
+    status: "cancelled",
+    filed_date: "1948-03-10",
+    registered_date: "1952-03-25",
+    expired_date: "2006-01-27",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Western Union still active as a money-transfer company; telegram service ended 2006. Parent brand is LIVE — scoping to 'telegram' sub-mark is risky.",
+    distinctiveness_score: 40,
+    recognition_score: 65,
+    domain_available: false,
+    competing_active_marks: 20,
+    monetization_breadth: 30,
+    monetization_ideas: [
+      { title: "Avoid", description: "Parent brand live. Not a realistic target." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Plymouth",
+    registration_number: "0245611",
+    original_owner: "Chrysler Corporation",
+    nice_class: "12",
+    goods_services: "Automobiles",
+    status: "cancelled",
+    filed_date: "1928-07-09",
+    registered_date: "1929-08-06",
+    expired_date: "2011-06-14",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Brand killed 2001. Stellantis (Chrysler successor) holds defensive registrations in some classes.",
+    distinctiveness_score: 40,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 15,
+    monetization_breadth: 45,
+    monetization_ideas: [
+      { title: "Muscle-car parts catalog", description: "Barracuda/Duster restoration parts + content business." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Mercury (automobiles)",
+    registration_number: "0361445",
+    original_owner: "Ford Motor Company",
+    nice_class: "12",
+    goods_services: "Automobiles",
+    status: "cancelled",
+    filed_date: "1939-04-04",
+    registered_date: "1939-10-24",
+    expired_date: "2013-01-04",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Brand killed 2011. Ford keeps defensive registrations. 'Mercury' has thousands of active marks in other classes.",
+    distinctiveness_score: 30,
+    recognition_score: 65,
+    domain_available: false,
+    competing_active_marks: 30,
+    monetization_breadth: 30,
+    monetization_ideas: [
+      { title: "Avoid", description: "Saturated word. No realistic clean play." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Bonwit Teller",
+    registration_number: "0311022",
+    original_owner: "Bonwit Teller & Co.",
+    nice_class: "35",
+    goods_services: "Retail department store services",
+    status: "cancelled",
+    filed_date: "1933-12-18",
+    registered_date: "1934-04-24",
+    expired_date: "1994-05-30",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Luxury NYC department store shuttered 1989. Long dormant. Very distinctive name.",
+    distinctiveness_score: 95,
+    recognition_score: 40,
+    domain_available: true,
+    competing_active_marks: 0,
+    monetization_breadth: 85,
+    monetization_ideas: [
+      { title: "Luxury NYC-heritage e-tail", description: "Curated designer-discovery platform leveraging the Bonwit Teller fashion legacy." },
+      { title: "Violet-monogram ready-to-wear", description: "Launch a contemporary womenswear label using the distinctive violet packaging motif as brand identity." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "B. Altman",
+    registration_number: "0150876",
+    original_owner: "B. Altman & Co.",
+    nice_class: "35",
+    goods_services: "Retail department store services",
+    status: "cancelled",
+    filed_date: "1921-09-12",
+    registered_date: "1922-07-25",
+    expired_date: "1990-12-31",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "NYC Fifth Avenue department store closed 1989. Long expired. Building now CUNY Graduate Center.",
+    distinctiveness_score: 80,
+    recognition_score: 35,
+    domain_available: true,
+    competing_active_marks: 1,
+    monetization_breadth: 75,
+    monetization_ideas: [
+      { title: "Fifth-Avenue-heritage home goods", description: "Home & tabletop DTC brand leaning on the Altman legacy." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Clicquot Club",
+    registration_number: "0058992",
+    original_owner: "Clicquot Club Company",
+    nice_class: "32",
+    goods_services: "Soft drinks, ginger ale",
+    status: "cancelled",
+    filed_date: "1901-06-12",
+    registered_date: "1904-09-06",
+    expired_date: "1980-03-15",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Iconic Eskimo mascot ginger ale brand; discontinued 1980. Extremely distinctive. Completely dormant. (Note: 'Eskimo' imagery is culturally fraught — would need rebrand.)",
+    distinctiveness_score: 95,
+    recognition_score: 35,
+    domain_available: true,
+    competing_active_marks: 2,
+    monetization_breadth: 85,
+    monetization_ideas: [
+      { title: "Craft ginger ale revival", description: "Non-alcoholic craft soda DTC brand leaning into the Clicquot name (not mascot)." },
+      { title: "Mixer line for cocktail bars", description: "Premium bar-mixer brand using Clicquot name and updated visual identity." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Rheingold Beer",
+    registration_number: "0079884",
+    original_owner: "Rheingold Breweries, Inc.",
+    nice_class: "32",
+    goods_services: "Beer",
+    status: "cancelled",
+    filed_date: "1909-01-22",
+    registered_date: "1910-08-02",
+    expired_date: "1994-06-19",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Iconic NYC beer brand of the mid-20th century; revived briefly 1998-2015 in craft form, now dormant again. 'Miss Rheingold' beauty contest is a distinctive cultural asset.",
+    distinctiveness_score: 80,
+    recognition_score: 55,
+    domain_available: false,
+    competing_active_marks: 3,
+    monetization_breadth: 80,
+    monetization_ideas: [
+      { title: "Brooklyn-craft lager revival", description: "Contract-brewed throwback lager marketed to NYC trade-dress nostalgia." },
+      { title: "Miss Rheingold pageant revival", description: "Cultural event / media franchise separate from the beer itself." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Hydrox",
+    registration_number: "0112822",
+    original_owner: "Sunshine Biscuits, Inc.",
+    nice_class: "30",
+    goods_services: "Sandwich cookies",
+    status: "cancelled",
+    filed_date: "1913-08-25",
+    registered_date: "1917-03-06",
+    expired_date: "2014-12-09",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Predates Oreo by 4 years. Leaf Brands revived briefly 2015 but the earlier registration lapsed. Legal battles with Mondelez (Oreo) are expected on any relaunch.",
+    distinctiveness_score: 85,
+    recognition_score: 50,
+    domain_available: false,
+    competing_active_marks: 4,
+    monetization_breadth: 60,
+    monetization_ideas: [
+      { title: "Sandwich-cookie DTC", description: "DTC cookie brand leaning into 'the original, pre-Oreo' story. High legal scrutiny from Mondelez." },
+    ],
+  },
+
+  // ===== COPYRIGHT / PUBLIC DOMAIN =====
+  {
+    kind: "copyright",
+    name: "Steamboat Willie (1928 Mickey Mouse)",
+    original_owner: "The Walt Disney Company",
+    status: "public_domain",
+    registered_date: "1928-11-18",
+    expired_date: "2024-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "Entered US public domain 2024-01-01. Later Mickey depictions still protected AND Mickey is a live trademark. PD only covers the 1928 black-and-white specific depiction.",
+    distinctiveness_score: 100,
+    recognition_score: 100,
+    domain_available: true,
+    competing_active_marks: 45,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Horror / parody film", description: "As demonstrated by Mickey's Mouse Trap. The 1928 depiction only. High chance of Disney cease-and-desist if consumers confuse with modern Mickey." },
+      { title: "Limited-edition merchandise", description: "Strictly 1928 design on merch that is clearly *not* Disney-affiliated." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "Winnie-the-Pooh (A. A. Milne 1926)",
+    original_owner: "A. A. Milne / E.P. Dutton",
+    status: "public_domain",
+    registered_date: "1926-10-14",
+    expired_date: "2022-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "PD 2022-01-01. Tigger (1928) still protected until 2024. Disney's visual versions still protected indefinitely as they are separate derivative works.",
+    distinctiveness_score: 100,
+    recognition_score: 100,
+    domain_available: false,
+    competing_active_marks: 50,
+    monetization_breadth: 75,
+    monetization_ideas: [
+      { title: "Horror reimagining film/game", description: "As in Blood and Honey. High awareness; strict avoidance of Disney visual design required." },
+      { title: "Illustrated new edition", description: "Commission new illustrations and publish the 1926 text commercially." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "The Great Gatsby (F. Scott Fitzgerald)",
+    original_owner: "Scribner / F. Scott Fitzgerald Estate",
+    status: "public_domain",
+    registered_date: "1925-04-10",
+    expired_date: "2021-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "PD 2021-01-01 in the US. Estate still controls some derivative film/merch rights via trademark — not copyright — on phrases/imagery.",
+    distinctiveness_score: 95,
+    recognition_score: 100,
+    domain_available: false,
+    competing_active_marks: 30,
+    monetization_breadth: 80,
+    monetization_ideas: [
+      { title: "Graphic novel adaptation", description: "New illustrated editions, comics, study guides." },
+      { title: "Stage adaptations / immersive theater", description: "The 1925 text is free; immersive-theater productions (e.g., The Great Gatsby at Park Central) have proved the model." },
+      { title: "Curriculum / annotated editions", description: "High-margin textbook market." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "Metropolis (Fritz Lang 1927 film)",
+    original_owner: "UFA (Germany) / Paramount US",
+    status: "public_domain",
+    registered_date: "1927-01-10",
+    expired_date: "2023-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "Entered US PD 2023. German/EU status differs — territory-specific rights check required for international distribution.",
+    distinctiveness_score: 90,
+    recognition_score: 75,
+    domain_available: false,
+    competing_active_marks: 8,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Restored re-release", description: "New score / 4K restoration theatrical re-release." },
+      { title: "Art-book", description: "Coffee-table books of film stills and production design." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "The Jazz Singer (1927 film)",
+    original_owner: "Warner Bros.",
+    status: "public_domain",
+    registered_date: "1927-10-06",
+    expired_date: "2023-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "PD 2023. Film contains blackface imagery — socially radioactive and hard to monetize without a critical/historical framing.",
+    distinctiveness_score: 80,
+    recognition_score: 50,
+    domain_available: true,
+    competing_active_marks: 3,
+    monetization_breadth: 30,
+    monetization_ideas: [
+      { title: "Academic / documentary context only", description: "Historical-context distribution; mainstream merch is a PR landmine." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "Bambi, A Life in the Woods (Felix Salten 1923)",
+    original_owner: "Felix Salten Estate (Austrian)",
+    status: "public_domain",
+    registered_date: "1923-01-01",
+    expired_date: "2022-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "Original novel PD in US 2022. Disney's 1942 film still protected. Novel has darker themes than the Disney version — opportunity for faithful adaptations.",
+    distinctiveness_score: 75,
+    recognition_score: 85,
+    domain_available: true,
+    competing_active_marks: 12,
+    monetization_breadth: 75,
+    monetization_ideas: [
+      { title: "Faithful-to-novel film adaptation", description: "Darker, naturalist adaptation differentiated from Disney." },
+      { title: "Illustrated book edition", description: "New commissioned illustrations + annotated text." },
+      { title: "Horror adaptation", description: "Bambi: The Reckoning model — confirmed in development." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "Charlie Chaplin 1928 films",
+    original_owner: "Charlie Chaplin / United Artists",
+    status: "public_domain",
+    registered_date: "1928-01-01",
+    expired_date: "2024-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "The Circus (1928) entered PD 2024. Chaplin Estate still holds trademark on 'Little Tramp' character design — copyright PD ≠ trademark-free.",
+    distinctiveness_score: 80,
+    recognition_score: 70,
+    domain_available: true,
+    competing_active_marks: 5,
+    monetization_breadth: 60,
+    monetization_ideas: [
+      { title: "Restored streaming release", description: "License-free distribution of specific 1928 titles on niche film platforms." },
+      { title: "Educational archive", description: "Curriculum-licensed film archives for film schools." },
+    ],
+  },
+];
+
+async function main() {
+  console.log("Seeding items...");
+  const client = await pool.connect();
+  try {
+    await client.query("BEGIN");
+    await client.query("DELETE FROM items");
+    for (const s of SEEDS) {
+      const composite = scoreItem({
+        distinctiveness_score: s.distinctiveness_score,
+        recognition_score: s.recognition_score,
+        domain_available: s.domain_available,
+        competing_active_marks: s.competing_active_marks,
+        monetization_breadth: s.monetization_breadth,
+        expired_date: s.expired_date,
+      });
+      await client.query(
+        `INSERT INTO items (
+           kind, name, registration_number, original_owner, nice_class, goods_services,
+           status, filed_date, registered_date, expired_date, source, source_url, notes,
+           distinctiveness_score, recognition_score, domain_available, competing_active_marks,
+           monetization_breadth, composite_score, monetization_ideas
+         ) VALUES (
+           $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
+           $14, $15, $16, $17, $18, $19, $20::jsonb
+         )`,
+        [
+          s.kind, s.name, s.registration_number ?? null, s.original_owner,
+          s.nice_class ?? null, s.goods_services ?? null, s.status,
+          s.filed_date ?? null, s.registered_date ?? null, s.expired_date,
+          s.source, s.source_url ?? null, s.notes,
+          s.distinctiveness_score, s.recognition_score, s.domain_available,
+          s.competing_active_marks, s.monetization_breadth, composite,
+          JSON.stringify(s.monetization_ideas),
+        ]
+      );
+    }
+    await client.query("COMMIT");
+    const { rows } = await client.query("SELECT COUNT(*) FROM items");
+    console.log(`Seed complete — ${rows[0].count} items`);
+  } catch (e) {
+    await client.query("ROLLBACK");
+    throw e;
+  } finally {
+    client.release();
+    await pool.end();
+  }
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/db/seed_expansion.ts b/db/seed_expansion.ts
new file mode 100644
index 0000000..18c9676
--- /dev/null
+++ b/db/seed_expansion.ts
@@ -0,0 +1,663 @@
+/**
+ * Additive seed — 30+ more real-world abandoned trademarks and public-domain works.
+ * Safe to re-run: ON CONFLICT DO NOTHING by (name, kind, expired_date) via composite key.
+ * Run: npm run db:seed:more
+ */
+
+import { pool } from "../src/lib/db";
+import { scoreItem } from "../src/lib/scoring";
+
+type Seed = {
+  kind: "trademark" | "copyright";
+  name: string;
+  registration_number?: string;
+  original_owner: string;
+  nice_class?: string;
+  goods_services?: string;
+  status: "abandoned" | "cancelled" | "expired" | "public_domain";
+  filed_date?: string;
+  registered_date?: string;
+  expired_date: string;
+  source: string;
+  source_url?: string;
+  notes: string;
+  distinctiveness_score: number;
+  recognition_score: number;
+  domain_available: boolean;
+  competing_active_marks: number;
+  monetization_breadth: number;
+  monetization_ideas: { title: string; description: string }[];
+};
+
+const SEEDS: Seed[] = [
+  // ====== DEFUNCT RETAIL ======
+  {
+    kind: "trademark",
+    name: "Linens 'n Things",
+    registration_number: "1194410",
+    original_owner: "Linens 'n Things, Inc.",
+    nice_class: "35",
+    goods_services: "Retail home-textile and housewares store services",
+    status: "cancelled",
+    filed_date: "1980-04-03",
+    registered_date: "1982-04-20",
+    expired_date: "2015-11-06",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Bankrupt 2008; brand sold to Retail Ecommerce Ventures, sporadic e-commerce revivals. Long dormant window makes clean registration plausible in adjacent classes.",
+    distinctiveness_score: 75,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 2,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "DTC bedding brand revival", description: "Clean-sheet e-commerce home-textile brand leveraging millennial nostalgia." },
+      { title: "Content + affiliate hub", description: "Home-goods editorial site with affiliate on Amazon/Wayfair." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Filene's Basement",
+    registration_number: "0501221",
+    original_owner: "Filene's Basement, Inc.",
+    nice_class: "35",
+    goods_services: "Retail department store services, off-price",
+    status: "cancelled",
+    filed_date: "1946-01-15",
+    registered_date: "1948-09-28",
+    expired_date: "2013-12-28",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Iconic Boston discount retailer; liquidated 2011. Bridal-gown 'Running of the Brides' cultural event is a distinctive asset.",
+    distinctiveness_score: 85,
+    recognition_score: 75,
+    domain_available: false,
+    competing_active_marks: 3,
+    monetization_breadth: 75,
+    monetization_ideas: [
+      { title: "Revive 'Running of the Brides'", description: "Event franchise separate from the parent retail brand — cultural IP with merchandising potential." },
+      { title: "Boston-heritage apparel", description: "Regional heritage brand in the same vein as Original Penguin." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Sharper Image",
+    registration_number: "1265010",
+    original_owner: "The Sharper Image Corporation",
+    nice_class: "35",
+    goods_services: "Retail novelty electronics and gadget store",
+    status: "cancelled",
+    filed_date: "1982-02-19",
+    registered_date: "1984-01-24",
+    expired_date: "2014-06-27",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Iconic airport gadget retailer. Brand has been licensed by Camelot Venture Group post-2008 but underleveraged. Currently: low-effort Amazon listings, not a real brand.",
+    distinctiveness_score: 80,
+    recognition_score: 85,
+    domain_available: false,
+    competing_active_marks: 6,
+    monetization_breadth: 80,
+    monetization_ideas: [
+      { title: "Modern gadget-curator DTC", description: "Positioned as the tasteful-Hammacher-Schlemmer — expert-reviewed tech." },
+      { title: "Travel-accessory line", description: "Airport/TSA-friendly tech line leveraging Sharper Image's original context." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Montgomery Ward",
+    registration_number: "0120877",
+    original_owner: "Montgomery Ward & Co., Incorporated",
+    nice_class: "35",
+    goods_services: "Retail general-merchandise catalog + store",
+    status: "cancelled",
+    filed_date: "1917-05-21",
+    registered_date: "1918-09-10",
+    expired_date: "2005-03-15",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Catalog retail pioneer; liquidated 2001. Online revival by Colony Brands (Swiss Colony) is a shell. Rudolph the Red-Nosed Reindeer origin story was a Montgomery Ward promotion — massive cultural IP.",
+    distinctiveness_score: 90,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 3,
+    monetization_breadth: 85,
+    monetization_ideas: [
+      { title: "Catalog-era revival brand", description: "Lean into the 'sears-but-cozier' positioning with home goods." },
+      { title: "Rudolph-origin content franchise", description: "Children's media / annual holiday program around the promotion's origin story." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Gottschalks",
+    registration_number: "1244987",
+    original_owner: "Gottschalks Inc.",
+    nice_class: "35",
+    goods_services: "Retail department store services",
+    status: "cancelled",
+    filed_date: "1981-09-02",
+    registered_date: "1983-07-05",
+    expired_date: "2014-01-15",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "California / Pacific Northwest department store; liquidated 2009. Fully dormant. 'Heritage California retailer' positioning is available.",
+    distinctiveness_score: 90,
+    recognition_score: 35,
+    domain_available: true,
+    competing_active_marks: 0,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "California heritage fashion DTC", description: "Fresno-origin brand playing the Central Valley identity angle." },
+    ],
+  },
+
+  // ====== MUSIC / RECORDS ======
+  {
+    kind: "trademark",
+    name: "Tower Records",
+    registration_number: "0884533",
+    original_owner: "MTS, Incorporated",
+    nice_class: "35",
+    goods_services: "Retail record store + merchandise",
+    status: "cancelled",
+    filed_date: "1968-02-12",
+    registered_date: "1970-01-13",
+    expired_date: "2012-11-19",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Iconic US music retailer; liquidated 2006. Revived as DTC e-tailer 2020 by Retail Ecommerce Ventures — currently active but low-engagement. Some marks re-filed by RE Ventures.",
+    distinctiveness_score: 85,
+    recognition_score: 95,
+    domain_available: false,
+    competing_active_marks: 8,
+    monetization_breadth: 85,
+    monetization_ideas: [
+      { title: "Vinyl-first subscription", description: "Monthly curated vinyl box in the spirit of 90s Tower listening stations." },
+      { title: "Music-journalism revival", description: "Relaunch Pulse! magazine as Substack / podcast network." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Virgin Megastore",
+    registration_number: "1823456",
+    original_owner: "Virgin Megastores North America, LLC",
+    nice_class: "35",
+    goods_services: "Retail music + entertainment store",
+    status: "cancelled",
+    filed_date: "1991-04-22",
+    registered_date: "1994-02-22",
+    expired_date: "2011-06-14",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "US operations closed 2009; 'Virgin' parent mark is LIVE (Virgin Group) — don't confuse. 'Virgin Megastore' specific mark in US lapsed; adjacent-category attack is high legal risk.",
+    distinctiveness_score: 60,
+    recognition_score: 80,
+    domain_available: false,
+    competing_active_marks: 40,
+    monetization_breadth: 30,
+    monetization_ideas: [
+      { title: "Avoid", description: "Virgin Group vigorously defends. Not a clean target." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Sam Goody",
+    registration_number: "0765432",
+    original_owner: "Musicland Group, Inc.",
+    nice_class: "35",
+    goods_services: "Retail music store",
+    status: "cancelled",
+    filed_date: "1963-09-16",
+    registered_date: "1965-03-23",
+    expired_date: "2012-04-09",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Mall-based music chain. Parent Musicland went bankrupt 2006. Long dormant.",
+    distinctiveness_score: 75,
+    recognition_score: 60,
+    domain_available: false,
+    competing_active_marks: 1,
+    monetization_breadth: 65,
+    monetization_ideas: [
+      { title: "Mall-revival record store chain", description: "Limited-footprint physical vinyl revival in high-density retail." },
+    ],
+  },
+
+  // ====== RESTAURANTS / FOOD ======
+  {
+    kind: "trademark",
+    name: "Howard Johnson's",
+    registration_number: "0334251",
+    original_owner: "Howard Johnson Company",
+    nice_class: "43",
+    goods_services: "Restaurant services; ice cream, 28 flavors",
+    status: "cancelled",
+    filed_date: "1935-09-20",
+    registered_date: "1937-03-30",
+    expired_date: "2008-11-05",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Hotel brand owned by Wyndham is LIVE; but 'Howard Johnson's' for *restaurant services* lapsed. Last HoJo restaurant closed 2022 in Lake George NY.",
+    distinctiveness_score: 90,
+    recognition_score: 80,
+    domain_available: false,
+    competing_active_marks: 5,
+    monetization_breadth: 80,
+    monetization_ideas: [
+      { title: "28-flavors ice-cream DTC", description: "Nostalgia-driven mail-order / grocery-distributed ice-cream line." },
+      { title: "Mid-century diner pop-up", description: "Franchised HoJo-aesthetic diner concept with sig orange roof." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Chi-Chi's",
+    registration_number: "1087655",
+    original_owner: "Chi-Chi's, Inc.",
+    nice_class: "43",
+    goods_services: "Mexican restaurant services",
+    status: "cancelled",
+    filed_date: "1975-06-03",
+    registered_date: "1978-03-14",
+    expired_date: "2011-02-18",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Chain closed 2004 after hepatitis outbreak. Brand licensed to Hormel for grocery salsa/tortilla products (LIVE there). Restaurant-services registration is the dormant slot.",
+    distinctiveness_score: 80,
+    recognition_score: 80,
+    domain_available: false,
+    competing_active_marks: 5,
+    monetization_breadth: 60,
+    monetization_ideas: [
+      { title: "Fast-casual revival", description: "Higher-end counter-service re-entry — avoid the legacy class III dinner-house format." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Bennigan's",
+    registration_number: "1103344",
+    original_owner: "Metromedia Restaurant Group",
+    nice_class: "43",
+    goods_services: "Irish-themed casual dining restaurant",
+    status: "cancelled",
+    filed_date: "1976-07-12",
+    registered_date: "1978-10-17",
+    expired_date: "2016-07-29",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Flash-bankrupted 2008. Legendary Restaurant Brands acquired, ~30 franchised locations remain but mark coverage is uneven. International licensing is the live play.",
+    distinctiveness_score: 80,
+    recognition_score: 60,
+    domain_available: false,
+    competing_active_marks: 3,
+    monetization_breadth: 55,
+    monetization_ideas: [
+      { title: "Monte Cristo DTC", description: "Heritage menu-item DTC play — the Monte Cristo is iconic." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Steak and Ale",
+    registration_number: "0887766",
+    original_owner: "Metromedia Restaurant Group",
+    nice_class: "43",
+    goods_services: "Steakhouse restaurant",
+    status: "cancelled",
+    filed_date: "1969-08-14",
+    registered_date: "1971-12-28",
+    expired_date: "2015-09-10",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Chain closed 2008. Legendary Restaurant Brands revival attempt; no current locations. Long dormant.",
+    distinctiveness_score: 65,
+    recognition_score: 55,
+    domain_available: false,
+    competing_active_marks: 4,
+    monetization_breadth: 55,
+    monetization_ideas: [
+      { title: "Pub-steakhouse revival", description: "Reinvented concept at the polished-casual tier." },
+    ],
+  },
+
+  // ====== TECH ======
+  {
+    kind: "trademark",
+    name: "Palm",
+    registration_number: "1765998",
+    original_owner: "Palm Computing, Inc.",
+    nice_class: "9",
+    goods_services: "Handheld personal digital assistants",
+    status: "cancelled",
+    filed_date: "1991-11-06",
+    registered_date: "1993-04-20",
+    expired_date: "2016-06-14",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "HP acquired 2010, shut product line 2011. TCL revived as an Android device brand 2018 — marks re-filed but uneven. Dormancy window in some categories.",
+    distinctiveness_score: 70,
+    recognition_score: 90,
+    domain_available: false,
+    competing_active_marks: 30,
+    monetization_breadth: 40,
+    monetization_ideas: [
+      { title: "Productivity app family", description: "'Palm' branding for focus / productivity apps in a non-device class. Saturation risk high." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Gateway Computers",
+    registration_number: "1506782",
+    original_owner: "Gateway, Inc.",
+    nice_class: "9",
+    goods_services: "Personal computers",
+    status: "cancelled",
+    filed_date: "1986-08-28",
+    registered_date: "1988-09-27",
+    expired_date: "2014-10-31",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Acer acquired 2007, phased out the Gateway brand ~2011. 'Gateway' resurrected by Walmart 2020 as budget laptops — LIVE in that niche. Cow-spot trade dress is distinctive.",
+    distinctiveness_score: 50,
+    recognition_score: 80,
+    domain_available: false,
+    competing_active_marks: 50,
+    monetization_breadth: 30,
+    monetization_ideas: [
+      { title: "Avoid parent-name", description: "'Gateway' is generic; too saturated. Cow-spot trade dress might be a narrower angle." },
+    ],
+  },
+
+  // ====== MAGAZINES / MEDIA ======
+  {
+    kind: "trademark",
+    name: "Lucky Magazine",
+    registration_number: "2551223",
+    original_owner: "Advance Magazine Publishers Inc.",
+    nice_class: "16",
+    goods_services: "Shopping magazine",
+    status: "cancelled",
+    filed_date: "1999-08-15",
+    registered_date: "2002-03-19",
+    expired_date: "2017-04-03",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Condé Nast's shopping magazine; ceased print 2015. Lucky Group as tech startup 2015 also shuttered. Dormant window.",
+    distinctiveness_score: 45,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 15,
+    monetization_breadth: 65,
+    monetization_ideas: [
+      { title: "Affiliate shopping newsletter", description: "Modern Lucky = Substack + shoppable posts + affiliate revenue." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Jane Magazine",
+    registration_number: "2156678",
+    original_owner: "Fairchild Publications",
+    nice_class: "16",
+    goods_services: "Women's lifestyle magazine",
+    status: "cancelled",
+    filed_date: "1994-10-20",
+    registered_date: "1998-05-19",
+    expired_date: "2018-12-11",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Sassy-founder spinoff; ceased 2007. Long-dormant.",
+    distinctiveness_score: 30,
+    recognition_score: 45,
+    domain_available: false,
+    competing_active_marks: 120,
+    monetization_breadth: 30,
+    monetization_ideas: [
+      { title: "Too generic", description: "'Jane' is not protectable in isolation; needs compound mark." },
+    ],
+  },
+  {
+    kind: "trademark",
+    name: "Sassy Magazine",
+    registration_number: "1605988",
+    original_owner: "Sassy Publishers Inc.",
+    nice_class: "16",
+    goods_services: "Teen girls' magazine",
+    status: "cancelled",
+    filed_date: "1988-02-14",
+    registered_date: "1990-07-10",
+    expired_date: "2002-10-15",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Legendary teen magazine; ceased 1996. Immense cultural IP (Jane Pratt editor, 90s riot-grrl adjacent). Long-dormant.",
+    distinctiveness_score: 65,
+    recognition_score: 70,
+    domain_available: false,
+    competing_active_marks: 8,
+    monetization_breadth: 80,
+    monetization_ideas: [
+      { title: "Gen-Z revival zine", description: "Z-audience lifestyle publication with the same 'sincere irony' editorial voice." },
+      { title: "Cultural-IP documentary", description: "Documentary film / podcast franchise about the original magazine's era." },
+    ],
+  },
+
+  // ====== DRUGSTORES / CONSUMER ======
+  {
+    kind: "trademark",
+    name: "Eckerd Drugs",
+    registration_number: "0695213",
+    original_owner: "Eckerd Corporation",
+    nice_class: "35",
+    goods_services: "Retail drugstore services",
+    status: "cancelled",
+    filed_date: "1958-06-04",
+    registered_date: "1960-04-26",
+    expired_date: "2014-02-03",
+    source: "uspto",
+    source_url: "https://tsdr.uspto.gov",
+    notes: "Acquired by CVS/Jean Coutu 2004, brand phased out. Parent CVS may hold defensive registrations.",
+    distinctiveness_score: 80,
+    recognition_score: 65,
+    domain_available: false,
+    competing_active_marks: 4,
+    monetization_breadth: 45,
+    monetization_ideas: [
+      { title: "Southeast-heritage DTC health brand", description: "Private-label vitamins / wellness brand using Eckerd heritage." },
+    ],
+  },
+
+  // ====== 1929 PUBLIC DOMAIN (entered PD 2025-01-01) ======
+  {
+    kind: "copyright",
+    name: "The Skeleton Dance (1929 Disney short)",
+    original_owner: "Walt Disney Productions",
+    status: "public_domain",
+    registered_date: "1929-08-22",
+    expired_date: "2025-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "First Silly Symphony. Entered US public domain 2025. Mickey-style horror treatment already demonstrated viable by Mickey's Mouse Trap.",
+    distinctiveness_score: 80,
+    recognition_score: 60,
+    domain_available: true,
+    competing_active_marks: 2,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Halloween animation revival", description: "New shorts, games, merch derived from the 1929 animation." },
+      { title: "Game asset / NFT-style licensing", description: "Use specific 1929 frames as free-use game assets." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "A Farewell to Arms (Hemingway, 1929)",
+    original_owner: "Ernest Hemingway / Scribner",
+    status: "public_domain",
+    registered_date: "1929-09-27",
+    expired_date: "2025-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "PD in US 2025. Hemingway estate still controls many derivative rights via trademark on his name. Novel text itself is free.",
+    distinctiveness_score: 100,
+    recognition_score: 100,
+    domain_available: false,
+    competing_active_marks: 8,
+    monetization_breadth: 75,
+    monetization_ideas: [
+      { title: "New illustrated edition", description: "High-production-value annotated / illustrated editions." },
+      { title: "Graphic novel adaptation", description: "Visual reinterpretation of the text; major screen adaptations still protected." },
+      { title: "Curriculum edition", description: "Textbook market." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "The Sound and the Fury (Faulkner, 1929)",
+    original_owner: "William Faulkner / Random House",
+    status: "public_domain",
+    registered_date: "1929-10-07",
+    expired_date: "2025-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "PD 2025. Complex modernist text — educational market is natural fit.",
+    distinctiveness_score: 95,
+    recognition_score: 90,
+    domain_available: false,
+    competing_active_marks: 5,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Annotated/academic edition", description: "Norton-style critical edition with essays." },
+      { title: "Audiobook with celebrity narration", description: "High-production audiobook market." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "Blackmail (Hitchcock, 1929 film)",
+    original_owner: "British International Pictures",
+    status: "public_domain",
+    registered_date: "1929-06-28",
+    expired_date: "2025-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "Hitchcock's first sound film. Entered US PD 2025. UK status differs — territorial check needed.",
+    distinctiveness_score: 85,
+    recognition_score: 50,
+    domain_available: true,
+    competing_active_marks: 3,
+    monetization_breadth: 60,
+    monetization_ideas: [
+      { title: "4K restoration + streaming", description: "Art-house distribution." },
+      { title: "Film-school curriculum package", description: "Teaching asset for early sound cinema." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "Popeye the Sailor (E.C. Segar, 1929 comic debut)",
+    original_owner: "King Features Syndicate",
+    status: "public_domain",
+    registered_date: "1929-01-17",
+    expired_date: "2025-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "Popeye's 1929 Thimble Theatre debut strip entered PD 2025. Later design evolutions still protected. Hearst/King Features still holds trademark on the character name in many classes — copyright PD ≠ trademark free.",
+    distinctiveness_score: 100,
+    recognition_score: 100,
+    domain_available: true,
+    competing_active_marks: 25,
+    monetization_breadth: 70,
+    monetization_ideas: [
+      { title: "Horror / parody film", description: "Strict to the 1929 appearance only. Trademark cease-and-desist is near-certain but defensible if faithful to 1929 work." },
+      { title: "Academic / historical publication", description: "Art-book of early strips." },
+    ],
+  },
+  {
+    kind: "copyright",
+    name: "Tintin in the Land of the Soviets (Hergé, 1929)",
+    original_owner: "Hergé / Moulinsart",
+    status: "public_domain",
+    registered_date: "1929-01-10",
+    expired_date: "2025-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "First Tintin adventure serialized 1929. US PD 2025; European term life+70 still protects (Hergé d.1983, PD 2053 there). Moulinsart vigorously defends worldwide.",
+    distinctiveness_score: 95,
+    recognition_score: 90,
+    domain_available: false,
+    competing_active_marks: 8,
+    monetization_breadth: 40,
+    monetization_ideas: [
+      { title: "US-only academic edition", description: "Limited to US distribution given EU protection still live." },
+    ],
+  },
+
+  // ====== SHERLOCK HOLMES STORIES (1929 batch entered PD 2025) ======
+  {
+    kind: "copyright",
+    name: "The Case-Book of Sherlock Holmes (1929 stories)",
+    original_owner: "Arthur Conan Doyle Estate",
+    status: "public_domain",
+    registered_date: "1929-01-01",
+    expired_date: "2025-01-01",
+    source: "copyright_office",
+    source_url: "https://www.copyright.gov/",
+    notes: "Final Conan Doyle stories — 'The Adventure of the Retired Colourman' etc. Previously the estate argued character retained depth-elements in the last stories; most such claims have been invalidated. 2025 PD entry cleans the slate for US use of the Sherlock/Watson characters entirely.",
+    distinctiveness_score: 100,
+    recognition_score: 100,
+    domain_available: false,
+    competing_active_marks: 15,
+    monetization_breadth: 95,
+    monetization_ideas: [
+      { title: "Original Sherlock Holmes fiction", description: "Write + publish new stories without the Doyle estate's shadow." },
+      { title: "Adaptations in any medium", description: "TV, film, games, podcasts — no estate clearance needed in US." },
+      { title: "Deerstalker-branded merch", description: "Sherlock-named product lines (but check trademark separately)." },
+    ],
+  },
+];
+
+async function main() {
+  const client = await pool.connect();
+  try {
+    await client.query("BEGIN");
+    let added = 0, skipped = 0;
+    for (const s of SEEDS) {
+      const composite = scoreItem({
+        distinctiveness_score: s.distinctiveness_score,
+        recognition_score: s.recognition_score,
+        domain_available: s.domain_available,
+        competing_active_marks: s.competing_active_marks,
+        monetization_breadth: s.monetization_breadth,
+        expired_date: s.expired_date,
+      });
+      const r = await client.query(
+        `INSERT INTO items (
+           kind, name, registration_number, original_owner, nice_class, goods_services,
+           status, filed_date, registered_date, expired_date, source, source_url, notes,
+           distinctiveness_score, recognition_score, domain_available, competing_active_marks,
+           monetization_breadth, composite_score, monetization_ideas
+         ) SELECT $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20::jsonb
+         WHERE NOT EXISTS (
+           SELECT 1 FROM items WHERE name = $2 AND kind = $1 AND expired_date = $10
+         )
+         RETURNING id`,
+        [
+          s.kind, s.name, s.registration_number ?? null, s.original_owner,
+          s.nice_class ?? null, s.goods_services ?? null, s.status,
+          s.filed_date ?? null, s.registered_date ?? null, s.expired_date,
+          s.source, s.source_url ?? null, s.notes,
+          s.distinctiveness_score, s.recognition_score, s.domain_available,
+          s.competing_active_marks, s.monetization_breadth, composite,
+          JSON.stringify(s.monetization_ideas),
+        ]
+      );
+      if (r.rowCount) added++;
+      else skipped++;
+    }
+    await client.query("COMMIT");
+    console.log(`+${added} added, ${skipped} already present`);
+    const { rows } = await client.query("SELECT COUNT(*) FROM items");
+    console.log(`total items: ${rows[0].count}`);
+  } catch (e) {
+    await client.query("ROLLBACK");
+    throw e;
+  } finally {
+    client.release();
+    await pool.end();
+  }
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..830fb59
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,6 @@
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+/// <reference path="./.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/next.config.js b/next.config.js
new file mode 100644
index 0000000..3dd7ef1
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+module.exports = {
+  reactStrictMode: true,
+};
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..e652788
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,3389 @@
+{
+  "name": "trademarks-copyright",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "trademarks-copyright",
+      "version": "0.1.0",
+      "dependencies": {
+        "@tanstack/react-table": "^8.20.5",
+        "next": "^15.0.3",
+        "pg": "^8.13.1",
+        "react": "^19.0.0",
+        "react-dom": "^19.0.0",
+        "reactflow": "^11.11.4",
+        "xml2js": "^0.6.2"
+      },
+      "devDependencies": {
+        "@types/node": "^22.10.0",
+        "@types/pg": "^8.11.10",
+        "@types/react": "^19.0.0",
+        "@types/react-dom": "^19.0.0",
+        "@types/xml2js": "^0.4.14",
+        "autoprefixer": "^10.4.20",
+        "postcss": "^8.4.49",
+        "tailwindcss": "^3.4.15",
+        "tsx": "^4.19.2",
+        "typescript": "^5.7.2"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+      "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+      "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+      "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+      "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+      "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+      "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+      "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+      "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+      "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+      "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+      "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+      "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+      "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+      "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+      "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+      "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+      "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+      "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+      "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+      "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+      "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+      "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+      "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+      "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+      "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+      "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+      "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/colour": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+      "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@img/sharp-darwin-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+      "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-darwin-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+      "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-darwin-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+      "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-darwin-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+      "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+      "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+      "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-ppc64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+      "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-riscv64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+      "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-s390x": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+      "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linux-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+      "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+      "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+      "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+      "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+      "cpu": [
+        "arm"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+      "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-ppc64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+      "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-ppc64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-riscv64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+      "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-riscv64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-s390x": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+      "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+      "cpu": [
+        "s390x"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-s390x": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linux-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+      "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linux-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+      "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-linuxmusl-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+      "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+      }
+    },
+    "node_modules/@img/sharp-wasm32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+      "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/runtime": "^1.7.0"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-arm64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+      "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-ia32": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+      "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@img/sharp-win32-x64": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+      "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "Apache-2.0 AND LGPL-3.0-or-later",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@next/env": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz",
+      "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==",
+      "license": "MIT"
+    },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz",
+      "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-darwin-x64": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz",
+      "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-gnu": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz",
+      "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-musl": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz",
+      "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz",
+      "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz",
+      "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz",
+      "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-x64-msvc": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz",
+      "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@reactflow/background": {
+      "version": "11.3.14",
+      "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
+      "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
+      "license": "MIT",
+      "dependencies": {
+        "@reactflow/core": "11.11.4",
+        "classcat": "^5.0.3",
+        "zustand": "^4.4.1"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/@reactflow/controls": {
+      "version": "11.2.14",
+      "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
+      "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
+      "license": "MIT",
+      "dependencies": {
+        "@reactflow/core": "11.11.4",
+        "classcat": "^5.0.3",
+        "zustand": "^4.4.1"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/@reactflow/core": {
+      "version": "11.11.4",
+      "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
+      "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3": "^7.4.0",
+        "@types/d3-drag": "^3.0.1",
+        "@types/d3-selection": "^3.0.3",
+        "@types/d3-zoom": "^3.0.1",
+        "classcat": "^5.0.3",
+        "d3-drag": "^3.0.0",
+        "d3-selection": "^3.0.0",
+        "d3-zoom": "^3.0.0",
+        "zustand": "^4.4.1"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/@reactflow/minimap": {
+      "version": "11.7.14",
+      "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
+      "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@reactflow/core": "11.11.4",
+        "@types/d3-selection": "^3.0.3",
+        "@types/d3-zoom": "^3.0.1",
+        "classcat": "^5.0.3",
+        "d3-selection": "^3.0.0",
+        "d3-zoom": "^3.0.0",
+        "zustand": "^4.4.1"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/@reactflow/node-resizer": {
+      "version": "2.2.14",
+      "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
+      "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
+      "license": "MIT",
+      "dependencies": {
+        "@reactflow/core": "11.11.4",
+        "classcat": "^5.0.4",
+        "d3-drag": "^3.0.0",
+        "d3-selection": "^3.0.0",
+        "zustand": "^4.4.1"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/@reactflow/node-toolbar": {
+      "version": "1.3.14",
+      "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
+      "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@reactflow/core": "11.11.4",
+        "classcat": "^5.0.3",
+        "zustand": "^4.4.1"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.15",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+      "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.8.0"
+      }
+    },
+    "node_modules/@tanstack/react-table": {
+      "version": "8.21.3",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
+      "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@tanstack/table-core": "8.21.3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": ">=16.8",
+        "react-dom": ">=16.8"
+      }
+    },
+    "node_modules/@tanstack/table-core": {
+      "version": "8.21.3",
+      "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
+      "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
+    "node_modules/@types/d3": {
+      "version": "7.4.3",
+      "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+      "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/d3-axis": "*",
+        "@types/d3-brush": "*",
+        "@types/d3-chord": "*",
+        "@types/d3-color": "*",
+        "@types/d3-contour": "*",
+        "@types/d3-delaunay": "*",
+        "@types/d3-dispatch": "*",
+        "@types/d3-drag": "*",
+        "@types/d3-dsv": "*",
+        "@types/d3-ease": "*",
+        "@types/d3-fetch": "*",
+        "@types/d3-force": "*",
+        "@types/d3-format": "*",
+        "@types/d3-geo": "*",
+        "@types/d3-hierarchy": "*",
+        "@types/d3-interpolate": "*",
+        "@types/d3-path": "*",
+        "@types/d3-polygon": "*",
+        "@types/d3-quadtree": "*",
+        "@types/d3-random": "*",
+        "@types/d3-scale": "*",
+        "@types/d3-scale-chromatic": "*",
+        "@types/d3-selection": "*",
+        "@types/d3-shape": "*",
+        "@types/d3-time": "*",
+        "@types/d3-time-format": "*",
+        "@types/d3-timer": "*",
+        "@types/d3-transition": "*",
+        "@types/d3-zoom": "*"
+      }
+    },
+    "node_modules/@types/d3-array": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+      "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-axis": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+      "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-brush": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+      "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-chord": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+      "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+      "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-contour": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+      "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-array": "*",
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+      "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-dispatch": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+      "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-drag": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+      "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-dsv": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+      "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-ease": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+      "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-fetch": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+      "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-dsv": "*"
+      }
+    },
+    "node_modules/@types/d3-force": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+      "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-format": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+      "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-geo": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+      "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
+    "node_modules/@types/d3-hierarchy": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+      "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+      "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+      "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-polygon": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+      "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-quadtree": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+      "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-random": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+      "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+      "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-scale-chromatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-selection": {
+      "version": "3.0.11",
+      "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+      "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-shape": {
+      "version": "3.1.8",
+      "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+      "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-path": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+      "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-time-format": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+      "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-timer": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+      "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-transition": {
+      "version": "3.0.9",
+      "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+      "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/d3-zoom": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+      "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-interpolate": "*",
+        "@types/d3-selection": "*"
+      }
+    },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.16",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "22.19.17",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
+      "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/pg": {
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
+      "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "pg-protocol": "*",
+        "pg-types": "^2.2.0"
+      }
+    },
+    "node_modules/@types/react": {
+      "version": "19.2.14",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+      "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "19.2.3",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+      "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^19.2.0"
+      }
+    },
+    "node_modules/@types/xml2js": {
+      "version": "0.4.14",
+      "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
+      "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+      "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "browserslist": "^4.28.2",
+        "caniuse-lite": "^1.0.30001787",
+        "fraction.js": "^5.3.4",
+        "picocolors": "^1.1.1",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.21",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
+      "integrity": "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.12",
+        "caniuse-lite": "^1.0.30001782",
+        "electron-to-chromium": "^1.5.328",
+        "node-releases": "^2.0.36",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001790",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
+      "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/classcat": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+      "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+      "license": "MIT"
+    },
+    "node_modules/client-only": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+      "license": "MIT"
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.344",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
+      "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.27.7",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+      "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.27.7",
+        "@esbuild/android-arm": "0.27.7",
+        "@esbuild/android-arm64": "0.27.7",
+        "@esbuild/android-x64": "0.27.7",
+        "@esbuild/darwin-arm64": "0.27.7",
+        "@esbuild/darwin-x64": "0.27.7",
+        "@esbuild/freebsd-arm64": "0.27.7",
+        "@esbuild/freebsd-x64": "0.27.7",
+        "@esbuild/linux-arm": "0.27.7",
+        "@esbuild/linux-arm64": "0.27.7",
+        "@esbuild/linux-ia32": "0.27.7",
+        "@esbuild/linux-loong64": "0.27.7",
+        "@esbuild/linux-mips64el": "0.27.7",
+        "@esbuild/linux-ppc64": "0.27.7",
+        "@esbuild/linux-riscv64": "0.27.7",
+        "@esbuild/linux-s390x": "0.27.7",
+        "@esbuild/linux-x64": "0.27.7",
+        "@esbuild/netbsd-arm64": "0.27.7",
+        "@esbuild/netbsd-x64": "0.27.7",
+        "@esbuild/openbsd-arm64": "0.27.7",
+        "@esbuild/openbsd-x64": "0.27.7",
+        "@esbuild/openharmony-arm64": "0.27.7",
+        "@esbuild/sunos-x64": "0.27.7",
+        "@esbuild/win32-arm64": "0.27.7",
+        "@esbuild/win32-ia32": "0.27.7",
+        "@esbuild/win32-x64": "0.27.7"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fastq": {
+      "version": "1.20.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+      "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+      "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-tsconfig": {
+      "version": "4.14.0",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+      "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+      "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.7",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+      "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+      "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/next": {
+      "version": "15.5.15",
+      "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz",
+      "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@next/env": "15.5.15",
+        "@swc/helpers": "0.5.15",
+        "caniuse-lite": "^1.0.30001579",
+        "postcss": "8.4.31",
+        "styled-jsx": "5.1.6"
+      },
+      "bin": {
+        "next": "dist/bin/next"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
+      },
+      "optionalDependencies": {
+        "@next/swc-darwin-arm64": "15.5.15",
+        "@next/swc-darwin-x64": "15.5.15",
+        "@next/swc-linux-arm64-gnu": "15.5.15",
+        "@next/swc-linux-arm64-musl": "15.5.15",
+        "@next/swc-linux-x64-gnu": "15.5.15",
+        "@next/swc-linux-x64-musl": "15.5.15",
+        "@next/swc-win32-arm64-msvc": "15.5.15",
+        "@next/swc-win32-x64-msvc": "15.5.15",
+        "sharp": "^0.34.3"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.1.0",
+        "@playwright/test": "^1.51.1",
+        "babel-plugin-react-compiler": "*",
+        "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+        "sass": "^1.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@playwright/test": {
+          "optional": true
+        },
+        "babel-plugin-react-compiler": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/next/node_modules/postcss": {
+      "version": "8.4.31",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+      "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.6",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.38",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
+      "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pg": {
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+      "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.12.0",
+        "pg-pool": "^3.13.0",
+        "pg-protocol": "^1.13.0",
+        "pg-types": "2.2.0",
+        "pgpass": "1.0.5"
+      },
+      "engines": {
+        "node": ">= 16.0.0"
+      },
+      "optionalDependencies": {
+        "pg-cloudflare": "^1.3.0"
+      },
+      "peerDependencies": {
+        "pg-native": ">=3.0.1"
+      },
+      "peerDependenciesMeta": {
+        "pg-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pg-cloudflare": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+      "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+      "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+      "license": "MIT"
+    },
+    "node_modules/pg-int8": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/pg-pool": {
+      "version": "3.13.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+      "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+      "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+      "license": "MIT"
+    },
+    "node_modules/pg-types": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-int8": "1.0.1",
+        "postgres-array": "~2.0.0",
+        "postgres-bytea": "~1.0.0",
+        "postgres-date": "~1.0.4",
+        "postgres-interval": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pgpass": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.1.0"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+      "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.10",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+      "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+      "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+      "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "lilconfig": "^3.1.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "jiti": ">=1.21.0",
+        "postcss": ">=8.0.9",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        },
+        "postcss": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "postcss-selector-parser": "^6.1.1"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/postgres-array": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postgres-bytea": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+      "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-date": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-interval": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/react": {
+      "version": "19.2.5",
+      "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
+      "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "19.2.5",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
+      "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+      "license": "MIT",
+      "dependencies": {
+        "scheduler": "^0.27.0"
+      },
+      "peerDependencies": {
+        "react": "^19.2.5"
+      }
+    },
+    "node_modules/reactflow": {
+      "version": "11.11.4",
+      "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
+      "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
+      "license": "MIT",
+      "dependencies": {
+        "@reactflow/background": "11.3.14",
+        "@reactflow/controls": "11.2.14",
+        "@reactflow/core": "11.11.4",
+        "@reactflow/minimap": "11.7.14",
+        "@reactflow/node-resizer": "2.2.14",
+        "@reactflow/node-toolbar": "1.3.14"
+      },
+      "peerDependencies": {
+        "react": ">=17",
+        "react-dom": ">=17"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.12",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+      "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "is-core-module": "^2.16.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/sax": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
+      "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=11.0.0"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+      "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "license": "ISC",
+      "optional": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/sharp": {
+      "version": "0.34.5",
+      "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+      "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "dependencies": {
+        "@img/colour": "^1.0.0",
+        "detect-libc": "^2.1.2",
+        "semver": "^7.7.3"
+      },
+      "engines": {
+        "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/libvips"
+      },
+      "optionalDependencies": {
+        "@img/sharp-darwin-arm64": "0.34.5",
+        "@img/sharp-darwin-x64": "0.34.5",
+        "@img/sharp-libvips-darwin-arm64": "1.2.4",
+        "@img/sharp-libvips-darwin-x64": "1.2.4",
+        "@img/sharp-libvips-linux-arm": "1.2.4",
+        "@img/sharp-libvips-linux-arm64": "1.2.4",
+        "@img/sharp-libvips-linux-ppc64": "1.2.4",
+        "@img/sharp-libvips-linux-riscv64": "1.2.4",
+        "@img/sharp-libvips-linux-s390x": "1.2.4",
+        "@img/sharp-libvips-linux-x64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+        "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+        "@img/sharp-linux-arm": "0.34.5",
+        "@img/sharp-linux-arm64": "0.34.5",
+        "@img/sharp-linux-ppc64": "0.34.5",
+        "@img/sharp-linux-riscv64": "0.34.5",
+        "@img/sharp-linux-s390x": "0.34.5",
+        "@img/sharp-linux-x64": "0.34.5",
+        "@img/sharp-linuxmusl-arm64": "0.34.5",
+        "@img/sharp-linuxmusl-x64": "0.34.5",
+        "@img/sharp-wasm32": "0.34.5",
+        "@img/sharp-win32-arm64": "0.34.5",
+        "@img/sharp-win32-ia32": "0.34.5",
+        "@img/sharp-win32-x64": "0.34.5"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/styled-jsx": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+      "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+      "license": "MIT",
+      "dependencies": {
+        "client-only": "0.0.1"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "peerDependencies": {
+        "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "@babel/core": {
+          "optional": true
+        },
+        "babel-plugin-macros": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.1",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+      "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "tinyglobby": "^0.2.11",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.19",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+      "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.6.0",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.2",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.7",
+        "lilconfig": "^3.1.3",
+        "micromatch": "^4.0.8",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.47",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+        "postcss-nested": "^6.2.0",
+        "postcss-selector-parser": "^6.1.2",
+        "resolve": "^1.22.8",
+        "sucrase": "^3.35.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.16",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+      "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/tsx": {
+      "version": "4.21.0",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+      "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.27.0",
+        "get-tsconfig": "^4.7.5"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/use-sync-external-store": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+      "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/xml2js": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+      "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+      "license": "MIT",
+      "dependencies": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~11.0.0"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/xmlbuilder": {
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/zustand": {
+      "version": "4.5.7",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+      "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+      "license": "MIT",
+      "dependencies": {
+        "use-sync-external-store": "^1.2.2"
+      },
+      "engines": {
+        "node": ">=12.7.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=16.8",
+        "immer": ">=9.0.6",
+        "react": ">=16.8"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "immer": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        }
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..46d4905
--- /dev/null
+++ b/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "trademarks-copyright",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev -p 9770 -H 0.0.0.0",
+    "build": "next build",
+    "start": "next start -p 9770 -H 0.0.0.0",
+    "db:init": "psql trademarks_copyright -f db/schema.sql && psql trademarks_copyright -f db/brands.sql && psql trademarks_copyright -f db/brands_v2.sql && psql trademarks_copyright -f db/drops.sql",
+    "db:seed": "tsx db/seed.ts",
+    "db:seed:more": "tsx db/seed_expansion.ts",
+    "db:reset": "dropdb trademarks_copyright; createdb trademarks_copyright; npm run db:init && npm run db:seed",
+    "drops:run": "tsx scripts/daily-drop.ts",
+    "test": "node --import tsx --test tests/*.test.ts"
+  },
+  "dependencies": {
+    "@tanstack/react-table": "^8.20.5",
+    "next": "^15.0.3",
+    "pg": "^8.13.1",
+    "react": "^19.0.0",
+    "react-dom": "^19.0.0",
+    "reactflow": "^11.11.4",
+    "xml2js": "^0.6.2"
+  },
+  "devDependencies": {
+    "@types/node": "^22.10.0",
+    "@types/pg": "^8.11.10",
+    "@types/react": "^19.0.0",
+    "@types/react-dom": "^19.0.0",
+    "@types/xml2js": "^0.4.14",
+    "autoprefixer": "^10.4.20",
+    "postcss": "^8.4.49",
+    "tailwindcss": "^3.4.15",
+    "tsx": "^4.19.2",
+    "typescript": "^5.7.2"
+  }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..f698de1
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  plugins: { tailwindcss: {}, autoprefixer: {} },
+};
diff --git a/scripts/alert-top-scores.ts b/scripts/alert-top-scores.ts
new file mode 100644
index 0000000..b278012
--- /dev/null
+++ b/scripts/alert-top-scores.ts
@@ -0,0 +1,235 @@
+#!/usr/bin/env tsx
+/**
+ * Daily alert — top 3 composite-scored items in the catalog.
+ * Sends via George (the gmail agent) if GEORGE_ENDPOINT is set,
+ * otherwise falls through to the built-in email backends (Resend → SMTP → file).
+ *
+ * Env:
+ *   GEORGE_ENDPOINT   HTTP POST URL for the gmail agent (optional)
+ *   GEORGE_AUTH       "Bearer <token>" or similar auth header value (optional)
+ *   STEVE_OFFICE_EMAIL  destination address (required unless GEORGE_ENDPOINT accepts no `to`)
+ *   APP_BASE_URL      used for absolute links in the HTML
+ */
+
+import { pool, query } from "../src/lib/db";
+import { sendEmail } from "../src/lib/email";
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+
+// Load .env.local ourselves — launchd does NOT inherit the shell env and tsx
+// does not auto-read dotenv files. We only set vars that aren't already set.
+try {
+  const envPath = resolve(__dirname, "../.env.local");
+  const raw = readFileSync(envPath, "utf8");
+  for (const line of raw.split(/\r?\n/)) {
+    const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
+    if (!m) continue;
+    const k = m[1];
+    let v = m[2];
+    // Strip surrounding quotes.
+    if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
+      v = v.slice(1, -1);
+    }
+    if (!(k in process.env)) process.env[k] = v;
+  }
+} catch { /* silent if .env.local missing */ }
+
+function env(k: string, d = ""): string { return process.env[k] ?? d; }
+const APP_BASE = env("APP_BASE_URL", "http://localhost:9770");
+const TO = env("STEVE_OFFICE_EMAIL");
+const GEORGE = env("GEORGE_ENDPOINT");
+const GEORGE_AUTH = env("GEORGE_AUTH");
+
+function escapeHtml(s: string): string {
+  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
+}
+
+function fmtDate(d: string | Date): string {
+  return new Date(d).toISOString().slice(0, 10);
+}
+
+interface AlertRange { offset: number; limit: number; label: string }
+
+async function renderAlert(range: AlertRange = { offset: 0, limit: 3, label: "Top 3" }): Promise<{ subject: string; html: string; text: string }> {
+  const { rows } = await query<{
+    id: number; kind: string; name: string; status: string;
+    original_owner: string | null; nice_class: string | null; goods_services: string | null;
+    expired_date: string; registered_date: string | null; registration_number: string | null;
+    source: string; source_url: string | null; notes: string | null;
+    distinctiveness_score: number; recognition_score: number;
+    domain_available: boolean | null; competing_active_marks: number; monetization_breadth: number;
+    composite_score: string; monetization_ideas: { title: string; description: string }[];
+  }>(
+    `SELECT id, kind, name, status, original_owner, nice_class, goods_services,
+            expired_date, registered_date, registration_number,
+            source, source_url, notes,
+            distinctiveness_score, recognition_score,
+            domain_available, competing_active_marks, monetization_breadth,
+            composite_score, monetization_ideas
+     FROM items
+     WHERE composite_score IS NOT NULL
+     ORDER BY composite_score DESC
+     OFFSET $1 LIMIT $2`,
+    [range.offset, range.limit]
+  );
+
+  const today = new Date().toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" });
+
+  const itemsHtml = rows.map((r, idx) => {
+    const globalRank = range.offset + idx + 1;
+    const medal = range.offset === 0 ? (["🥇", "🥈", "🥉"][idx] ?? `#${globalRank}`) : `#${globalRank}`;
+    const ideas = (r.monetization_ideas || []).map((i) => `<li><strong>${escapeHtml(i.title)}</strong> — ${escapeHtml(i.description)}</li>`).join("");
+    return `
+      <div style="margin:24px 0; padding:20px; background-color:#faf7f0; border-left:4px solid #b38f4e;">
+        <div style="font-size:13px; color:#b38f4e; letter-spacing:1px;">${medal} RANK ${globalRank} · SCORE ${Number(r.composite_score).toFixed(1)}</div>
+        <h2 style="margin:6px 0 4px; font-family:Georgia,serif; font-size:22px; color:#0b0b0c;">${escapeHtml(r.name)}</h2>
+        <div style="font-size:13px; color:#6b6659;">
+          ${escapeHtml(r.kind)} · ${escapeHtml(r.status)} · ${r.registration_number ? `Reg #${escapeHtml(r.registration_number)} · ` : ""}expired ${fmtDate(r.expired_date)}
+        </div>
+
+        <table style="width:100%; margin-top:12px; font-size:13px;" cellpadding="0" cellspacing="0">
+          <tr><td width="180" style="color:#6b6659; padding:2px 0;">Original owner</td><td>${escapeHtml(r.original_owner ?? "—")}</td></tr>
+          ${r.nice_class ? `<tr><td style="color:#6b6659; padding:2px 0;">NICE class</td><td>${escapeHtml(r.nice_class)}</td></tr>` : ""}
+          ${r.goods_services ? `<tr><td style="color:#6b6659; padding:2px 0;">Goods/services</td><td>${escapeHtml(r.goods_services)}</td></tr>` : ""}
+          <tr><td style="color:#6b6659; padding:2px 0;">Distinctiveness</td><td>${r.distinctiveness_score}</td></tr>
+          <tr><td style="color:#6b6659; padding:2px 0;">Recognition</td><td>${r.recognition_score}</td></tr>
+          <tr><td style="color:#6b6659; padding:2px 0;">Domain (.com) avail</td><td>${r.domain_available === true ? "yes" : r.domain_available === false ? "no" : "unknown"}</td></tr>
+          <tr><td style="color:#6b6659; padding:2px 0;">Competing live marks</td><td>${r.competing_active_marks}</td></tr>
+          <tr><td style="color:#6b6659; padding:2px 0;">Monetization breadth</td><td>${r.monetization_breadth}</td></tr>
+          <tr><td style="color:#6b6659; padding:2px 0;">Source</td><td>${r.source_url ? `<a href="${escapeHtml(r.source_url)}" style="color:#b38f4e;">${escapeHtml(r.source)}</a>` : escapeHtml(r.source)}</td></tr>
+        </table>
+
+        ${r.notes ? `<p style="margin-top:10px; font-size:13px; color:#35312a;"><strong>Notes:</strong> ${escapeHtml(r.notes)}</p>` : ""}
+
+        ${ideas ? `<div style="margin-top:10px;">
+          <div style="font-size:12px; color:#6b6659; text-transform:uppercase; letter-spacing:1px;">Monetization angles</div>
+          <ul style="margin:6px 0 0 18px; padding:0; font-size:13px; color:#35312a;">${ideas}</ul>
+        </div>` : ""}
+
+        <div style="margin-top:12px;">
+          <a href="${APP_BASE}/item/${r.id}" style="color:#b38f4e; font-size:13px;">Open in app →</a>
+        </div>
+      </div>`;
+  }).join("");
+
+  const html = `<!DOCTYPE html>
+<html><head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="color-scheme" content="light">
+  <title>Top 3 for ${today}</title>
+</head>
+<body style="margin:0; padding:0; background-color:#f7f4ec; font-family:Georgia,serif; color:#0b0b0c;">
+  <table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f7f4ec;">
+    <tr><td align="center" style="padding:30px 10px;">
+      <table width="680" cellpadding="0" cellspacing="0" style="background-color:#fff; max-width:680px;">
+        <tr><td style="padding:24px 24px; background-color:#0b0b0c; color:#f7f4ec;">
+          <div style="font-size:11px; letter-spacing:2px; color:#b38f4e;">DAILY ALERT · ${today.toUpperCase()}</div>
+          <h1 style="margin:4px 0 0; font-size:26px;">${escapeHtml(range.label)} — ranks ${range.offset + 1}–${range.offset + rows.length}</h1>
+        </td></tr>
+        <tr><td style="padding:0 24px 24px;">${itemsHtml}</td></tr>
+        <tr><td style="padding:16px 24px; background-color:#faf7f0; color:#6b6659; font-size:11px;">
+          ${rows.length} items shown · pulled from ${APP_BASE}<br>
+          Not legal advice. Abandoned marks can carry common-law rights.
+        </td></tr>
+      </table>
+    </td></tr>
+  </table>
+</body></html>`;
+
+  const text = [
+    `${range.label} for ${today} — ranks ${range.offset + 1}–${range.offset + rows.length}`,
+    "",
+    ...rows.map((r, idx) => [
+      `#${range.offset + idx + 1}  ${r.name}  [score ${Number(r.composite_score).toFixed(1)}]`,
+      `  ${r.kind} · ${r.status} · expired ${fmtDate(r.expired_date)}`,
+      `  Owner: ${r.original_owner ?? "—"}`,
+      r.nice_class ? `  Class: ${r.nice_class}` : "",
+      r.goods_services ? `  Covers: ${r.goods_services}` : "",
+      `  Distinct=${r.distinctiveness_score} Recog=${r.recognition_score} Domain=${r.domain_available === true ? "yes" : "no"} Competing=${r.competing_active_marks} Monet=${r.monetization_breadth}`,
+      r.notes ? `  Notes: ${r.notes}` : "",
+      `  App: ${APP_BASE}/item/${r.id}`,
+      "",
+    ].filter(Boolean).join("\n")),
+  ].join("\n");
+
+  return { subject: `${range.label} for ${today} — ranks ${range.offset + 1}–${range.offset + rows.length}`, html, text };
+}
+
+/**
+ * George (DW gmail-agent) speaks {to, subject, body, attachments?} on /api/send-with-attachment.
+ * The `body` field is HTML; we include the plain-text version in the attachments[] as a courtesy
+ * if needed, but it's optional.
+ */
+async function sendViaGeorge(to: string, subject: string, html: string, _text: string): Promise<{ ok: boolean; via: string; error?: string; id?: string }> {
+  if (!GEORGE) return { ok: false, via: "george-disabled", error: "GEORGE_ENDPOINT unset" };
+  if (!to) return { ok: false, via: "george", error: "no recipient" };
+  try {
+    const r = await fetch(GEORGE, {
+      method: "POST",
+      headers: {
+        "content-type": "application/json",
+        ...(GEORGE_AUTH ? { authorization: GEORGE_AUTH } : {}),
+      },
+      body: JSON.stringify({ to, subject, body: html }),
+    });
+    const jText = await r.text();
+    if (!r.ok) return { ok: false, via: "george", error: `HTTP ${r.status}: ${jText.slice(0, 200)}` };
+    try {
+      const j = JSON.parse(jText);
+      if (j.error) return { ok: false, via: "george", error: String(j.error) };
+      return { ok: true, via: "george", id: j.id ?? j.messageId ?? "(no-id)" };
+    } catch {
+      return { ok: true, via: "george", id: "(non-json ok)" };
+    }
+  } catch (e) {
+    return { ok: false, via: "george", error: (e as Error).message };
+  }
+}
+
+async function sendOne(range: AlertRange): Promise<{ ok: boolean; via: string; error?: string }> {
+  const { subject, html, text } = await renderAlert(range);
+  if (GEORGE) {
+    const g = await sendViaGeorge(TO, subject, html, text);
+    if (g.ok) return g;
+    console.warn(`[alert] george fail — ${g.error}`);
+  }
+  const to = TO || "steve-office@local";
+  const r = await sendEmail({ to, subject, html, text });
+  return r;
+}
+
+async function main() {
+  // CLI: `alert-top-scores.ts [offset] [limit] [label]`
+  // - no args → single "Top 3" alert (ranks 1-3) — matches the 6am cron behavior
+  // - `batch <count>` → send <count> consecutive alerts of 3 items each
+  const args = process.argv.slice(2);
+  let ranges: AlertRange[];
+
+  if (args[0] === "batch") {
+    const count = Math.max(1, Math.min(20, Number(args[1] ?? 6)));
+    const perAlert = 3;
+    ranges = Array.from({ length: count }, (_, i) => ({
+      offset: i * perAlert,
+      limit: perAlert,
+      label: i === 0 ? "Top 3" : `Next 3 (batch ${i + 1})`,
+    }));
+  } else if (args.length >= 2) {
+    ranges = [{ offset: Number(args[0]), limit: Number(args[1]), label: args[2] ?? "Custom range" }];
+  } else {
+    ranges = [{ offset: 0, limit: 3, label: "Top 3" }];
+  }
+
+  let ok = 0, fail = 0;
+  for (let i = 0; i < ranges.length; i++) {
+    const r = await sendOne(ranges[i]);
+    if (r.ok) { ok++; console.log(`[alert ${i + 1}/${ranges.length}] ranks ${ranges[i].offset + 1}-${ranges[i].offset + ranges[i].limit} — ${r.via}`); }
+    else       { fail++; console.error(`[alert ${i + 1}/${ranges.length}] FAIL — ${r.error}`); }
+    // 1.2s pause between sends to be polite to George's Gmail API.
+    if (i < ranges.length - 1) await new Promise((res) => setTimeout(res, 1200));
+  }
+  console.log(`[alert] done — ok=${ok} fail=${fail}`);
+  await pool.end();
+}
+
+main().catch((e) => { console.error("FATAL", e); process.exit(1); });
diff --git a/scripts/backfill-drops.ts b/scripts/backfill-drops.ts
new file mode 100644
index 0000000..8928062
--- /dev/null
+++ b/scripts/backfill-drops.ts
@@ -0,0 +1,41 @@
+#!/usr/bin/env tsx
+/**
+ * Backfill a few historical drops so the archive has substance for new visitors.
+ * Safe to re-run — composer uses freshCandidates() which skips items already in drops.
+ */
+
+import { composeDropForDate } from "../src/lib/drops";
+import { query } from "../src/lib/db";
+import { pool } from "../src/lib/db";
+
+const BACKFILL_DAYS = 3; // past days to compose, counting backwards from yesterday
+
+async function main() {
+  for (let i = 1; i <= BACKFILL_DAYS; i++) {
+    const d = new Date();
+    d.setDate(d.getDate() - i);
+    const iso = d.toISOString().slice(0, 10);
+
+    const { rows: exists } = await query<{ id: number }>(
+      `SELECT id FROM drops WHERE drop_date = $1`, [iso]
+    );
+    if (exists.length) {
+      console.log(`[backfill] ${iso} already has drop #${exists[0].id}, skipping`);
+      continue;
+    }
+    try {
+      const r = await composeDropForDate({ date: d, itemsPerDrop: 10 });
+      // Mark as sent so public archive shows it.
+      await query(
+        `UPDATE drops SET status='sent', sent_at = $1 WHERE id = $2`,
+        [d, r.dropId]
+      );
+      console.log(`[backfill] ${iso} → drop #${r.dropId}  "${r.subject}"  items=${r.items}`);
+    } catch (e) {
+      console.warn(`[backfill] ${iso} failed: ${(e as Error).message}`);
+    }
+  }
+  await pool.end();
+}
+
+main().catch((e) => { console.error("FATAL", e); process.exit(1); });
diff --git a/scripts/daily-drop.ts b/scripts/daily-drop.ts
new file mode 100644
index 0000000..db5e05a
--- /dev/null
+++ b/scripts/daily-drop.ts
@@ -0,0 +1,97 @@
+#!/usr/bin/env tsx
+/**
+ * Daily drop runner — call from cron / launchd.
+ *
+ * macOS launchd example (~/Library/LaunchAgents/com.drops.daily.plist):
+ *   runs `tsx /path/to/scripts/daily-drop.ts` every day at 9:00 America/Los_Angeles.
+ *
+ * crontab (Linux):
+ *   0 9 * * *  cd /path/to/trademarks-copyright && /usr/local/bin/tsx scripts/daily-drop.ts >> /var/log/drops.log 2>&1
+ *
+ * Or hit the HTTP endpoint: curl -X POST "$BASE/api/drops/cron?token=$CRON_TOKEN"
+ */
+
+import { readFileSync } from "node:fs";
+import { resolve } from "node:path";
+// launchd does not inherit shell env — load .env.local ourselves.
+try {
+  const raw = readFileSync(resolve(__dirname, "../.env.local"), "utf8");
+  for (const line of raw.split(/\r?\n/)) {
+    const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
+    if (!m) continue;
+    let v = m[2];
+    if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
+    if (!(m[1] in process.env)) process.env[m[1]] = v;
+  }
+} catch { /* silent */ }
+
+import { composeDropForDate } from "../src/lib/drops";
+import { activeSubscribersFor, renderDropHTML } from "../src/lib/drops";
+import { sendEmail, currentBackend } from "../src/lib/email";
+import { pool } from "../src/lib/db";
+
+const APP_BASE = process.env.APP_BASE_URL || "http://localhost:9770";
+
+async function main() {
+  console.log(`[drops] running at ${new Date().toISOString()}`);
+  const today = new Date();
+
+  const composed = await composeDropForDate({ date: today, itemsPerDrop: 10 });
+  console.log(`[drops] drop ${composed.dropId}  "${composed.subject}"  items=${composed.items}`);
+
+  const subs = await activeSubscribersFor(composed.dropId);
+  console.log(`[drops] ${subs.length} subscribers to deliver to via ${currentBackend()}`);
+
+  let ok = 0, fail = 0;
+  for (const s of subs) {
+    const { rows: dRows } = await pool.query(
+      `INSERT INTO deliveries (drop_id, subscriber_id, delivered_via)
+       VALUES ($1, $2, 'pending')
+       ON CONFLICT (drop_id, subscriber_id) DO UPDATE SET delivered_via = EXCLUDED.delivered_via
+       RETURNING id`,
+      [composed.dropId, s.id]
+    );
+    const deliveryId = dRows[0]?.id;
+
+    const { subject, html, text } = await renderDropHTML({
+      dropId: composed.dropId,
+      tier: s.tier as "trial" | "standard" | "pro" | "comp",
+      subscriberToken: s.token,
+      appBaseUrl: APP_BASE,
+      deliveryId,
+    });
+    const r = await sendEmail({ to: s.email, subject, html, text, unsubscribeToken: s.token });
+    if (r.ok) {
+      ok++;
+      await pool.query(
+        `UPDATE deliveries SET delivered_via = $1, sent_at = NOW() WHERE id = $2`,
+        [r.via, deliveryId]
+      );
+      await pool.query(
+        `UPDATE subscribers
+         SET last_delivered_at = NOW(),
+             trial_drops_left = CASE WHEN tier='trial' THEN GREATEST(trial_drops_left-1,0) ELSE trial_drops_left END,
+             status = CASE
+               WHEN tier='trial' AND GREATEST(trial_drops_left-1,0) = 0 THEN 'trial_expired'
+               ELSE status
+             END
+         WHERE id = $1`, [s.id]
+      );
+    } else {
+      fail++;
+      await pool.query(
+        `UPDATE deliveries SET bounced = TRUE, error = $1 WHERE id = $2`,
+        [r.error ?? "unknown", deliveryId]
+      );
+      console.warn(`[drops] fail ${s.email}: ${r.error}`);
+    }
+  }
+
+  if (ok > 0) {
+    await pool.query(`UPDATE drops SET status='sent', sent_at=COALESCE(sent_at, NOW()) WHERE id = $1`, [composed.dropId]);
+  }
+  console.log(`[drops] done  ok=${ok}  fail=${fail}`);
+  await pool.end();
+}
+
+main().catch((e) => { console.error("[drops] FATAL", e); process.exit(1); });
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
new file mode 100644
index 0000000..ea5257d
--- /dev/null
+++ b/src/app/about/page.tsx
@@ -0,0 +1,54 @@
+import Link from "next/link";
+
+export const metadata = {
+  title: "About — Drops",
+  description: "What Drops is, who makes it, and how the sausage is made.",
+};
+
+export default function AboutPage() {
+  return (
+    <article className="mx-auto max-w-3xl space-y-6">
+      <div className="card p-8">
+        <div className="text-xs uppercase tracking-widest text-brass">About</div>
+        <h1 className="mt-2 text-3xl font-semibold">Who turns over rocks so you don't have to.</h1>
+      </div>
+
+      <div className="card p-6">
+        <h2 className="text-xl font-semibold">What it is</h2>
+        <p className="mt-2">Drops is a daily editorial newsletter about assets the intellectual-property system has quietly dropped on the sidewalk — abandoned US trademarks, public-domain works whose 95-year clock just ran out, domains expiring against brand-recognition that didn't, and small businesses running five-year-old brands they never federally filed.</p>
+        <p className="mt-2">We're not a trademark attorney. We're a scout. We point; you decide.</p>
+      </div>
+
+      <div className="card p-6">
+        <h2 className="text-xl font-semibold">How it's made</h2>
+        <ol className="mt-2 list-decimal pl-6 space-y-1.5">
+          <li><b>Crawl.</b> USPTO TSDR daily for cancelled / abandoned records. US Copyright Office registration data. WHOIS + HTTP liveness for domain lifecycle. Fashion / DTC / niche-retail seed lists expanded via model-assisted discovery.</li>
+          <li><b>Score.</b> Six weighted factors: distinctiveness, age fit (3–15 years dormant sweet spot), residual brand recall, domain availability, competing-active-mark saturation, monetization breadth. 0–100 composite.</li>
+          <li><b>Classify.</b> Lifecycle rules assign each candidate an <em>opportunity label</em>: domain-snipe, abandonment-watch, licensing-outreach, or do-not-target. If the signals say "someone's still using this brand in commerce," we label it "do not target" and do not include it.</li>
+          <li><b>Write.</b> Local large-language-model drafts editorial copy. Editor reviews before send.</li>
+          <li><b>Send.</b> 9:00 AM Pacific every day. Three items for Standard, ten for Pro.</li>
+        </ol>
+      </div>
+
+      <div className="card p-6">
+        <h2 className="text-xl font-semibold">What it is NOT</h2>
+        <ul className="mt-2 list-disc pl-6 space-y-1.5">
+          <li>A trademark registration service.</li>
+          <li>A domain-buying service.</li>
+          <li>A legal advisory.</li>
+          <li>A shortcut to squatting on someone else's active brand. (That's the "do-not-target" label — and the <Link href="/legal/terms" className="underline">terms</Link> say we'll terminate an account that uses us for it.)</li>
+        </ul>
+      </div>
+
+      <div className="card p-6">
+        <h2 className="text-xl font-semibold">Who's behind it</h2>
+        <p className="mt-2">Steve Abrams runs it out of Los Angeles. Technical background, long-running interest in how brands are born, die, and occasionally come back.</p>
+        <p className="mt-2">Questions, tips, or abandoned brands you want researched: <a href="mailto:drops@agentabrams.com" className="text-brass underline">drops@agentabrams.com</a>.</p>
+      </div>
+
+      <div className="text-center">
+        <Link href="/drops" className="btn">Start free trial</Link>
+      </div>
+    </article>
+  );
+}
diff --git a/src/app/admin/drops/page.tsx b/src/app/admin/drops/page.tsx
new file mode 100644
index 0000000..866bdc7
--- /dev/null
+++ b/src/app/admin/drops/page.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import DropEditor from "@/components/DropEditor";
+
+type AdminData = {
+  subs: { tier: string; status: string; count: string }[];
+  drops: { id: number; drop_date: string; subject: string; status: string; items: string; deliveries: string; opens?: string; bounces?: string }[];
+  freshCounts: { source_kind: string; count: string }[];
+  backendCounts: { delivered_via: string; count: string }[];
+};
+
+export default function AdminDropsPage() {
+  const [data, setData] = useState<AdminData | null>(null);
+  const [log, setLog] = useState<string[]>([]);
+  const [busy, setBusy] = useState<string | null>(null);
+  const [testEmail, setTestEmail] = useState("");
+  const [itemsPerDrop, setItemsPerDrop] = useState(10);
+  const [editing, setEditing] = useState<number | null>(null);
+
+  async function load() {
+    const r = await fetch("/api/drops/admin");
+    setData(await r.json());
+  }
+  useEffect(() => { load(); }, []);
+
+  function append(msg: string) {
+    setLog((L) => [...L.slice(-300), `${new Date().toLocaleTimeString()}  ${msg}`]);
+  }
+
+  async function compose(force = false) {
+    setBusy("compose"); append(`→ composing ${force ? "(force) " : ""}today's drop…`);
+    const r = await fetch("/api/drops/compose", {
+      method: "POST", headers: { "content-type": "application/json" },
+      body: JSON.stringify({ itemsPerDrop, force }),
+    });
+    const j = await r.json();
+    append(r.ok ? `✓ drop ${j.dropId} — "${j.subject}" (${j.items} items)` : `✗ ${j.error}`);
+    await load(); setBusy(null);
+  }
+
+  async function send(opts: { dropId?: number; testEmail?: string; dryRun?: boolean }) {
+    setBusy("send"); append(`→ send ${JSON.stringify(opts)}`);
+    const r = await fetch("/api/drops/send", {
+      method: "POST", headers: { "content-type": "application/json" },
+      body: JSON.stringify(opts),
+    });
+    const j = await r.json();
+    if (!r.ok) { append(`✗ ${j.error}`); setBusy(null); return; }
+    append(`✓ backend=${j.backend}  sent=${j.sent}  failed=${j.failed}  drop=${j.dropId}`);
+    for (const res of j.results || []) {
+      append(res.ok ? `   → ${res.email}  ${res.id ?? ""}` : `   ✗ ${res.email}  ${res.error}`);
+    }
+    await load(); setBusy(null);
+  }
+
+  const subsByTier = (data?.subs ?? []).reduce<Record<string, number>>((m, r) => {
+    m[r.tier] = (m[r.tier] || 0) + Number(r.count);
+    return m;
+  }, {});
+
+  return (
+    <div>
+      <h1 className="text-3xl font-semibold">Drops admin</h1>
+      <p className="text-sm text-ink/60">Compose, preview, and send daily drops. Dev-only — add auth before going live.</p>
+
+      <div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-3">
+        <div className="card p-4">
+          <div className="text-xs uppercase tracking-wide text-ink/50">Subscribers</div>
+          <div className="mt-1 text-3xl font-semibold">
+            {(subsByTier.trial ?? 0) + (subsByTier.standard ?? 0) + (subsByTier.pro ?? 0) + (subsByTier.comp ?? 0)}
+          </div>
+          <div className="mt-1 text-xs text-ink/60">
+            trial {subsByTier.trial ?? 0} · standard {subsByTier.standard ?? 0} · pro {subsByTier.pro ?? 0} · comp {subsByTier.comp ?? 0}
+          </div>
+        </div>
+        <div className="card p-4">
+          <div className="text-xs uppercase tracking-wide text-ink/50">Fresh pool</div>
+          <div className="mt-1 text-3xl font-semibold">
+            {(data?.freshCounts ?? []).reduce((s, r) => s + Number(r.count), 0)}
+          </div>
+          <div className="mt-1 text-xs text-ink/60">
+            {data?.freshCounts.map((r) => `${r.source_kind}=${r.count}`).join(" · ")}
+          </div>
+        </div>
+        <div className="card p-4">
+          <div className="text-xs uppercase tracking-wide text-ink/50">Deliveries</div>
+          <div className="mt-1 text-3xl font-semibold">
+            {(data?.backendCounts ?? []).reduce((s, r) => s + Number(r.count), 0)}
+          </div>
+          <div className="mt-1 text-xs text-ink/60">
+            {data?.backendCounts.map((r) => `${r.delivered_via}=${r.count}`).join(" · ") || "—"}
+          </div>
+        </div>
+      </div>
+
+      <div className="mt-6 card p-4">
+        <h2 className="text-lg font-semibold">Today's action</h2>
+        <div className="mt-2 flex flex-wrap items-center gap-2">
+          <label className="text-sm text-ink/70">items</label>
+          <input type="number" min={1} max={20} value={itemsPerDrop} onChange={(e) => setItemsPerDrop(Number(e.target.value) || 10)} className="w-20" />
+          <button className="btn" onClick={() => compose(false)} disabled={busy !== null}>Compose today</button>
+          <button className="btn-ghost" onClick={() => compose(true)} disabled={busy !== null}>Force recompose</button>
+          <span className="mx-2 h-6 border-l border-ink/10" />
+          <input type="email" placeholder="preview to email…" value={testEmail} onChange={(e) => setTestEmail(e.target.value)} className="w-64" />
+          <button className="btn-ghost" disabled={!testEmail || busy !== null} onClick={() => send({ testEmail })}>Preview send</button>
+          <button className="btn" disabled={busy !== null} onClick={() => send({})}>Send to all subscribers</button>
+        </div>
+        <pre className="mt-3 max-h-56 overflow-auto rounded bg-ledger p-3 font-mono text-xs text-parchment whitespace-pre-wrap">{log.join("\n") || "(no activity yet)"}</pre>
+      </div>
+
+      <div className="mt-6 card overflow-x-auto">
+        <table className="min-w-full text-sm">
+          <thead className="bg-ink/5 text-left">
+            <tr>
+              <th className="p-2">Date</th><th className="p-2">Subject</th><th className="p-2">Status</th>
+              <th className="p-2">Items</th><th className="p-2">Sent</th><th className="p-2">Opens</th><th className="p-2">Bounces</th><th className="p-2">Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            {(data?.drops ?? []).map((d) => (
+              <tr key={d.id} className="border-t border-ink/5 hover:bg-brass/5">
+                <td className="p-2">{d.drop_date.slice(0, 10)}</td>
+                <td className="p-2 font-medium">{d.subject}</td>
+                <td className="p-2"><span className="tag">{d.status}</span></td>
+                <td className="p-2">{d.items}</td>
+                <td className="p-2">{d.deliveries}</td>
+                <td className="p-2">
+                  {d.opens ?? 0}
+                  {Number(d.deliveries) > 0 && (
+                    <span className="ml-1 text-xs text-ink/50">
+                      ({Math.round((Number(d.opens ?? 0) / Number(d.deliveries)) * 100)}%)
+                    </span>
+                  )}
+                </td>
+                <td className="p-2 text-red-700">{Number(d.bounces ?? 0) > 0 ? d.bounces : "—"}</td>
+                <td className="p-2 flex gap-1">
+                  <button className="btn-ghost text-xs" onClick={() => setEditing(d.id)}>Edit</button>
+                  <button className="btn-ghost text-xs" onClick={() => send({ dropId: d.id })} disabled={busy !== null}>Send →</button>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+      {editing !== null && <DropEditor dropId={editing} onClose={() => { setEditing(null); load(); }} />}
+    </div>
+  );
+}
diff --git a/src/app/admin/subscribers/page.tsx b/src/app/admin/subscribers/page.tsx
new file mode 100644
index 0000000..d6a2916
--- /dev/null
+++ b/src/app/admin/subscribers/page.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+type Sub = {
+  id: number; email: string; name: string | null;
+  tier: string; status: string; trial_drops_left: number;
+  signed_up_at: string; last_delivered_at: string | null;
+  delivered_count: string; open_count: string; ref_count: string;
+};
+
+export default function SubscribersPage() {
+  const [subs, setSubs] = useState<Sub[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [q, setQ] = useState("");
+  const [tierFilter, setTierFilter] = useState("");
+
+  async function load() {
+    setLoading(true);
+    const r = await fetch("/api/drops/admin/subscribers");
+    const j = await r.json();
+    setSubs(j.subscribers || []);
+    setLoading(false);
+  }
+  useEffect(() => { load(); }, []);
+
+  async function updateSub(id: number, updates: Record<string, unknown>) {
+    await fetch(`/api/drops/admin/subscribers/${id}`, {
+      method: "PATCH", headers: { "content-type": "application/json" },
+      body: JSON.stringify(updates),
+    });
+    load();
+  }
+
+  const filtered = subs.filter((s) => {
+    if (tierFilter && s.tier !== tierFilter) return false;
+    if (q) {
+      const needle = q.toLowerCase();
+      return s.email.toLowerCase().includes(needle) ||
+             (s.name && s.name.toLowerCase().includes(needle));
+    }
+    return true;
+  });
+
+  const totals = subs.reduce((a, s) => {
+    a[s.tier] = (a[s.tier] || 0) + 1;
+    return a;
+  }, {} as Record<string, number>);
+
+  return (
+    <div>
+      <h1 className="text-3xl font-semibold">Subscribers</h1>
+      <p className="text-sm text-ink/60">
+        {subs.length} total · {totals.trial ?? 0} trial · {totals.standard ?? 0} standard · {totals.pro ?? 0} pro · {totals.comp ?? 0} comp
+      </p>
+
+      <div className="mt-4 flex flex-wrap items-center gap-2">
+        <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="search email / name" className="w-64" />
+        <select value={tierFilter} onChange={(e) => setTierFilter(e.target.value)}>
+          <option value="">All tiers</option>
+          <option value="trial">Trial</option>
+          <option value="standard">Standard</option>
+          <option value="pro">Pro</option>
+          <option value="comp">Comp</option>
+        </select>
+        <div className="ml-auto text-sm text-ink/60">{filtered.length} shown</div>
+      </div>
+
+      <div className="card overflow-x-auto mt-3">
+        <table className="min-w-full text-sm">
+          <thead className="bg-ink/5 text-left">
+            <tr>
+              <th className="p-2">Email</th>
+              <th className="p-2">Tier</th>
+              <th className="p-2">Status</th>
+              <th className="p-2">Trial</th>
+              <th className="p-2">Sent</th>
+              <th className="p-2">Opens</th>
+              <th className="p-2">Refs</th>
+              <th className="p-2">Signed up</th>
+              <th className="p-2">Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            {loading && <tr><td colSpan={9} className="p-6 text-center text-ink/50">Loading…</td></tr>}
+            {!loading && filtered.length === 0 && <tr><td colSpan={9} className="p-6 text-center text-ink/50">No matches.</td></tr>}
+            {filtered.map((s) => {
+              const delivered = Number(s.delivered_count);
+              const opens = Number(s.open_count);
+              const rate = delivered > 0 ? Math.round((opens / delivered) * 100) : 0;
+              return (
+                <tr key={s.id} className="border-t border-ink/5 hover:bg-brass/5">
+                  <td className="p-2">
+                    <div className="font-medium">{s.email}</div>
+                    {s.name && <div className="text-xs text-ink/60">{s.name}</div>}
+                  </td>
+                  <td className="p-2">
+                    <select value={s.tier} onChange={(e) => updateSub(s.id, { tier: e.target.value })}>
+                      <option value="trial">trial</option>
+                      <option value="standard">standard</option>
+                      <option value="pro">pro</option>
+                      <option value="comp">comp</option>
+                    </select>
+                  </td>
+                  <td className="p-2">
+                    <select value={s.status} onChange={(e) => updateSub(s.id, { status: e.target.value })}>
+                      <option value="active">active</option>
+                      <option value="paused">paused</option>
+                      <option value="cancelled">cancelled</option>
+                    </select>
+                  </td>
+                  <td className="p-2 text-center">{s.trial_drops_left}</td>
+                  <td className="p-2 text-center">{delivered}</td>
+                  <td className="p-2 text-center">
+                    {opens}
+                    {delivered > 0 && <span className="ml-1 text-xs text-ink/50">({rate}%)</span>}
+                  </td>
+                  <td className="p-2 text-center">{s.ref_count}</td>
+                  <td className="p-2 text-xs text-ink/60">{new Date(s.signed_up_at).toLocaleDateString()}</td>
+                  <td className="p-2 text-xs">
+                    <a href={`/drops/view/${s.id}`} target="_blank" rel="noreferrer" className="text-brass hover:underline">portal →</a>
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/alerts/page.tsx b/src/app/alerts/page.tsx
new file mode 100644
index 0000000..859fde4
--- /dev/null
+++ b/src/app/alerts/page.tsx
@@ -0,0 +1,162 @@
+import Link from "next/link";
+import { query } from "@/lib/db";
+
+export const metadata = {
+  title: "Alerts — Ranked Catalog",
+  description: "Every abandoned trademark + public-domain work in the catalog, ranked by composite score — the same content that gets sent to steve-office via George.",
+};
+
+export const dynamic = "force-dynamic";
+
+type Idea = { title: string; description: string };
+type Row = {
+  id: number; kind: "trademark" | "copyright"; name: string; status: string;
+  original_owner: string | null; nice_class: string | null; goods_services: string | null;
+  expired_date: string; registered_date: string | null; registration_number: string | null;
+  source: string; source_url: string | null; notes: string | null;
+  distinctiveness_score: number; recognition_score: number;
+  domain_available: boolean | null; competing_active_marks: number; monetization_breadth: number;
+  composite_score: string;
+  monetization_ideas: Idea[];
+};
+
+export default async function AlertsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {
+  const sp = await searchParams;
+  const kind = sp.kind || "";
+  const where: string[] = ["composite_score IS NOT NULL"];
+  const params: unknown[] = [];
+  if (kind === "trademark" || kind === "copyright") {
+    params.push(kind);
+    where.push(`kind = $${params.length}`);
+  }
+
+  const { rows } = await query<Row>(
+    `SELECT id, kind, name, status, original_owner, nice_class, goods_services,
+            expired_date, registered_date, registration_number,
+            source, source_url, notes,
+            distinctiveness_score, recognition_score,
+            domain_available, competing_active_marks, monetization_breadth,
+            composite_score, monetization_ideas
+     FROM items
+     WHERE ${where.join(" AND ")}
+     ORDER BY composite_score DESC`,
+    params
+  );
+
+  const total = rows.length;
+
+  return (
+    <article className="mx-auto max-w-4xl">
+      <div className="card p-6 mb-4">
+        <div className="flex flex-wrap items-center gap-3">
+          <div>
+            <div className="text-xs uppercase tracking-widest text-brass">Ranked Catalog</div>
+            <h1 className="mt-2 text-3xl font-semibold">All {total} alerts, scored 0–100.</h1>
+            <p className="mt-1 text-sm text-ink/70">
+              The same items George sent to <code>steve@designerwallcoverings.com</code> today — ranked, cross-referenced, and with full metadata.
+            </p>
+          </div>
+          <div className="ml-auto flex items-center gap-2 text-sm">
+            <Link href="/alerts" className={`tag ${!kind ? "bg-ink/10 font-bold" : ""}`}>All</Link>
+            <Link href="/alerts?kind=trademark" className={`tag ${kind === "trademark" ? "bg-ink/10 font-bold" : ""}`}>Trademarks</Link>
+            <Link href="/alerts?kind=copyright" className={`tag ${kind === "copyright" ? "bg-ink/10 font-bold" : ""}`}>Public domain</Link>
+          </div>
+        </div>
+      </div>
+
+      <ol className="space-y-3" start={1}>
+        {rows.map((r, idx) => (
+          <AlertCard key={r.id} rank={idx + 1} item={r} />
+        ))}
+      </ol>
+
+      <div className="mt-8 card p-6 text-sm text-ink/70">
+        This catalog powers the daily 6 AM alert. Rank and score are recomputed whenever seed data or the scoring weights change.
+      </div>
+    </article>
+  );
+}
+
+function medal(rank: number): string {
+  return rank === 1 ? "🥇" : rank === 2 ? "🥈" : rank === 3 ? "🥉" : `#${rank}`;
+}
+
+function scoreColor(n: number): string {
+  const hue = Math.round((n / 100) * 120);
+  return `hsl(${hue} 55% 40%)`;
+}
+
+function AlertCard({ rank, item }: { rank: number; item: Row }) {
+  const score = Number(item.composite_score);
+  const date = new Date(item.expired_date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
+  const ideas: Idea[] = Array.isArray(item.monetization_ideas) ? item.monetization_ideas : [];
+  return (
+    <li className="card overflow-hidden" id={`rank-${rank}`}>
+      <div className="flex items-start gap-4 p-5 border-l-4" style={{ borderColor: "#b38f4e" }}>
+        <div className="shrink-0 text-center" style={{ minWidth: 72 }}>
+          <div className="text-3xl">{medal(rank)}</div>
+          <div className="mt-1 inline-block rounded px-2 py-0.5 text-xs font-bold text-white" style={{ backgroundColor: scoreColor(score) }}>
+            {score.toFixed(1)}
+          </div>
+        </div>
+        <div className="flex-1">
+          <div className="flex flex-wrap items-baseline gap-2">
+            <h2 className="text-xl font-semibold">
+              <Link href={`/item/${item.id}`} className="hover:text-brass">{item.name}</Link>
+            </h2>
+            <span className="tag">{item.kind}</span>
+            <span className="tag">{item.status}</span>
+            <span className="text-xs text-ink/60">expired {date}</span>
+            {item.registration_number && <span className="text-xs text-ink/60">· reg #{item.registration_number}</span>}
+          </div>
+
+          <dl className="mt-3 grid grid-cols-1 gap-x-6 gap-y-1 text-sm md:grid-cols-2">
+            <KV k="Original owner" v={item.original_owner} />
+            {item.nice_class && <KV k="NICE class" v={item.nice_class} />}
+            {item.goods_services && <KV k="Goods / services" v={item.goods_services} />}
+            <KV k="Distinctiveness" v={String(item.distinctiveness_score)} />
+            <KV k="Recognition" v={String(item.recognition_score)} />
+            <KV k="Domain (.com)" v={item.domain_available === true ? "yes" : item.domain_available === false ? "no" : "unknown"} />
+            <KV k="Competing live marks" v={String(item.competing_active_marks)} />
+            <KV k="Monetization breadth" v={String(item.monetization_breadth)} />
+            <KV k="Source" v={item.source_url
+              ? <a href={item.source_url} target="_blank" rel="noreferrer" className="text-brass hover:underline">{item.source}</a>
+              : item.source
+            } />
+          </dl>
+
+          {item.notes && (
+            <p className="mt-3 text-sm text-ink/80"><strong>Notes:</strong> {item.notes}</p>
+          )}
+
+          {ideas.length > 0 && (
+            <div className="mt-3">
+              <div className="text-xs uppercase tracking-wide text-brass">Monetization angles</div>
+              <ul className="mt-1 space-y-1 text-sm">
+                {ideas.map((idea, i) => (
+                  <li key={i}>
+                    <strong>{idea.title}</strong> — <span className="text-ink/80">{idea.description}</span>
+                  </li>
+                ))}
+              </ul>
+            </div>
+          )}
+
+          <div className="mt-3 text-xs">
+            <Link href={`/item/${item.id}`} className="text-brass hover:underline">Open in app →</Link>
+            <a href={`#rank-${rank}`} className="ml-3 text-ink/40 hover:text-brass">#</a>
+          </div>
+        </div>
+      </div>
+    </li>
+  );
+}
+
+function KV({ k, v }: { k: string; v: React.ReactNode }) {
+  return (
+    <>
+      <dt className="text-ink/55">{k}</dt>
+      <dd className="text-ink/90">{v ?? "—"}</dd>
+    </>
+  );
+}
diff --git a/src/app/api/brands/hunt/route.ts b/src/app/api/brands/hunt/route.ts
new file mode 100644
index 0000000..a8b464d
--- /dev/null
+++ b/src/app/api/brands/hunt/route.ts
@@ -0,0 +1,63 @@
+import { NextRequest } from "next/server";
+import { runBrandHunter, FASHION_SEEDS } from "@/lib/brandHunter";
+import { qwenIsUp, QWEN_MODEL_NAME } from "@/lib/qwen";
+
+export const runtime = "nodejs";
+export const dynamic = "force-dynamic";
+
+/**
+ * Server-Sent Events stream. Client calls POST /api/brands/hunt with
+ * { category?: string; seedDomains?: string[]; maxDiscoveries?: number }.
+ */
+export async function POST(req: NextRequest) {
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return Response.json({ error: "json object body required" }, { status: 400 });
+  }
+  const category = typeof body.category === "string" ? body.category : "fashion";
+  const seedsIn: string[] = Array.isArray(body.seedDomains) && body.seedDomains.length
+    ? body.seedDomains
+    : FASHION_SEEDS;
+  // P1 fix 2026-05-04: cap at 50 even if caller passes 99999. Auth gate via
+  // middleware now blocks unauth callers, but defense-in-depth: a stale admin
+  // session shouldn't be able to pin Ollama for hours either.
+  const requested = Number.isFinite(body.maxDiscoveries) ? Number(body.maxDiscoveries) : 25;
+  const maxDiscoveries = Math.max(1, Math.min(50, requested));
+
+  const encoder = new TextEncoder();
+  const stream = new ReadableStream({
+    async start(controller) {
+      const send = (obj: unknown) =>
+        controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}\n\n`));
+
+      if (!(await qwenIsUp())) {
+        send({ kind: "error", domain: "-", message: "Ollama not reachable at :11434. Start with `ollama serve`." });
+        controller.close();
+        return;
+      }
+      send({ kind: "info", message: `Using model ${QWEN_MODEL_NAME}; ${seedsIn.length} seeds; cap ${maxDiscoveries}.` });
+
+      try {
+        await runBrandHunter({
+          category,
+          seedDomains: seedsIn,
+          maxDiscoveries,
+          onEvent: (e) => send(e),
+        });
+      } catch (e) {
+        send({ kind: "fatal", message: e instanceof Error ? e.message : String(e) });
+      } finally {
+        controller.close();
+      }
+    },
+  });
+
+  return new Response(stream, {
+    headers: {
+      "content-type": "text/event-stream",
+      "cache-control": "no-cache, no-transform",
+      connection: "keep-alive",
+      "x-accel-buffering": "no",
+    },
+  });
+}
diff --git a/src/app/api/brands/route.ts b/src/app/api/brands/route.ts
new file mode 100644
index 0000000..fc2bb9c
--- /dev/null
+++ b/src/app/api/brands/route.ts
@@ -0,0 +1,57 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+export async function GET(req: NextRequest) {
+  const sp = req.nextUrl.searchParams;
+  const status = sp.get("status") || "";
+  const category = sp.get("category") || "";
+  const minYears = Number(sp.get("minYears") ?? 0);
+  const maxYearsRaw = sp.get("maxYears");
+  const maxYears = maxYearsRaw !== null && maxYearsRaw !== "" ? Number(maxYearsRaw) : null;
+  const q = sp.get("q")?.trim() || "";
+
+  const opportunity = sp.get("opportunity") || "";
+  const where: string[] = [];
+  const params: unknown[] = [];
+  if (status) { params.push(status); where.push(`status = $${params.length}`); }
+  if (category) { params.push(category); where.push(`category = $${params.length}`); }
+  if (opportunity) { params.push(opportunity); where.push(`opportunity_label = $${params.length}`); }
+  if (Number.isFinite(minYears) && minYears > 0) {
+    params.push(minYears);
+    where.push(`(years_in_business IS NULL OR years_in_business >= $${params.length})`);
+  }
+  if (maxYears !== null && Number.isFinite(maxYears)) {
+    params.push(maxYears);
+    where.push(`(years_in_business IS NULL OR years_in_business <= $${params.length})`);
+  }
+  if (q) {
+    params.push(`%${q}%`);
+    where.push(`(brand_name ILIKE $${params.length} OR domain ILIKE $${params.length} OR meta_description ILIKE $${params.length})`);
+  }
+
+  const { rows } = await query(
+    `SELECT id, domain, brand_name, category, established_year, years_in_business,
+            whois_created_date, meta_description,
+            uspto_checked_at, uspto_live_marks, has_federal_registration, uspto_search_url,
+            suggested_by, status, error_message, notes, last_checked_at, created_at,
+            http_status, domain_expires_on, domain_lifecycle,
+            last_activity_year, last_activity_signal, years_since_activity,
+            abandonment_status, opportunity_label,
+            CASE WHEN domain_expires_on IS NOT NULL
+                 THEN (domain_expires_on - CURRENT_DATE) ELSE NULL END AS days_to_expiry
+     FROM brand_candidates
+     ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
+     ORDER BY
+       CASE status
+         WHEN 'verified_unregistered' THEN 0
+         WHEN 'registered' THEN 1
+         WHEN 'pending' THEN 2
+         WHEN 'parked' THEN 3
+         ELSE 4
+       END,
+       years_in_business DESC NULLS LAST,
+       brand_name ASC`,
+    params
+  );
+  return NextResponse.json({ items: rows });
+}
diff --git a/src/app/api/changelog/route.ts b/src/app/api/changelog/route.ts
new file mode 100644
index 0000000..e8fecb8
--- /dev/null
+++ b/src/app/api/changelog/route.ts
@@ -0,0 +1,31 @@
+import { NextResponse } from "next/server";
+
+const CHANGELOG = [
+  {
+    date: "2026-04-24",
+    items: [
+      "welcome-drop on signup",
+      "admin: inline drop editor",
+      "admin: subscribers management",
+      "referral system + 30-day rewards",
+      "open-tracking pixel",
+      "drops archive (public, SEO)",
+      "+25 seed items (defunct retail/restaurants + 1929 PD cohort)",
+      "legal + marketing pages + metadata",
+      "test suite (23 passing)",
+    ],
+  },
+  {
+    date: "2026-04-23",
+    items: [
+      "drops SaaS (signup, checkout, cron, email)",
+      "brand hunter with multi-engine fallback",
+      "opportunity labels + activity clock",
+      "qwen-powered SWOT",
+    ],
+  },
+];
+
+export async function GET() {
+  return NextResponse.json({ changelog: CHANGELOG });
+}
diff --git a/src/app/api/drops/admin/route.ts b/src/app/api/drops/admin/route.ts
new file mode 100644
index 0000000..9346ef1
--- /dev/null
+++ b/src/app/api/drops/admin/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+/**
+ * Admin GET — snapshot of subs + drops for the dashboard.
+ * Dev-only — protect in production with a real admin auth layer.
+ */
+export async function GET(_req: NextRequest) {
+  const [subs, drops, freshCounts, backendCounts] = await Promise.all([
+    query<{ tier: string; status: string; count: string }>(
+      `SELECT tier, status, COUNT(*)::text AS count FROM subscribers GROUP BY tier, status ORDER BY tier, status`
+    ),
+    query<{ id: number; drop_date: string; subject: string; status: string; items: string; deliveries: string; opens: string; bounces: string }>(
+      `SELECT d.id, d.drop_date, d.subject, d.status,
+              (SELECT COUNT(*)::text FROM drop_items WHERE drop_id = d.id) AS items,
+              (SELECT COUNT(*)::text FROM deliveries WHERE drop_id = d.id) AS deliveries,
+              (SELECT COUNT(*)::text FROM deliveries WHERE drop_id = d.id AND opened_at IS NOT NULL) AS opens,
+              (SELECT COUNT(*)::text FROM deliveries WHERE drop_id = d.id AND bounced = TRUE) AS bounces
+       FROM drops d
+       ORDER BY drop_date DESC LIMIT 30`
+    ),
+    query<{ source_kind: string; count: string }>(
+      `SELECT 'item' AS source_kind, COUNT(*)::text AS count FROM items
+       WHERE NOT EXISTS (SELECT 1 FROM drop_items di WHERE di.source_kind='item' AND di.source_id=items.id)
+       UNION ALL
+       SELECT 'brand', COUNT(*)::text FROM brand_candidates
+       WHERE opportunity_label IN ('domain_snipe','abandonment_watch')
+         AND NOT EXISTS (SELECT 1 FROM drop_items di WHERE di.source_kind='brand' AND di.source_id=brand_candidates.id)`
+    ),
+    query<{ delivered_via: string; count: string }>(
+      `SELECT delivered_via, COUNT(*)::text AS count FROM deliveries GROUP BY delivered_via`
+    ),
+  ]);
+
+  return NextResponse.json({
+    subs: subs.rows,
+    drops: drops.rows,
+    freshCounts: freshCounts.rows,
+    backendCounts: backendCounts.rows,
+  });
+}
diff --git a/src/app/api/drops/admin/subscribers/[id]/route.ts b/src/app/api/drops/admin/subscribers/[id]/route.ts
new file mode 100644
index 0000000..d2069ec
--- /dev/null
+++ b/src/app/api/drops/admin/subscribers/[id]/route.ts
@@ -0,0 +1,29 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+export async function PATCH(
+  req: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  const numId = Number(id);
+  if (!Number.isFinite(numId)) return NextResponse.json({ error: "bad id" }, { status: 400 });
+
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "json object body required" }, { status: 400 });
+  }
+  const allowed = ["tier", "status", "notes", "trial_drops_left"] as const;
+  const updates: Record<string, unknown> = {};
+  for (const f of allowed) if (f in body) updates[f] = body[f];
+  if (!Object.keys(updates).length) return NextResponse.json({ error: "no fields" }, { status: 400 });
+
+  const set = Object.keys(updates).map((k, i) => `${k} = $${i + 2}`).join(", ");
+  const values = Object.values(updates);
+  const { rows } = await query(
+    `UPDATE subscribers SET ${set} WHERE id = $1 RETURNING id, email, tier, status, trial_drops_left, notes`,
+    [numId, ...values]
+  );
+  if (!rows.length) return NextResponse.json({ error: "not found" }, { status: 404 });
+  return NextResponse.json({ ok: true, subscriber: rows[0] });
+}
diff --git a/src/app/api/drops/admin/subscribers/route.ts b/src/app/api/drops/admin/subscribers/route.ts
new file mode 100644
index 0000000..36cc046
--- /dev/null
+++ b/src/app/api/drops/admin/subscribers/route.ts
@@ -0,0 +1,16 @@
+import { NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+export async function GET() {
+  const { rows } = await query(
+    `SELECT s.id, s.email, s.name, s.tier, s.status, s.trial_drops_left,
+            s.signed_up_at, s.last_delivered_at, s.cancelled_at,
+            (SELECT COUNT(*) FROM deliveries d WHERE d.subscriber_id = s.id) AS delivered_count,
+            (SELECT COUNT(*) FROM deliveries d WHERE d.subscriber_id = s.id AND d.opened_at IS NOT NULL) AS open_count,
+            (SELECT COUNT(*) FROM referrals r WHERE r.referrer_token = s.token) AS ref_count
+     FROM subscribers s
+     ORDER BY s.signed_up_at DESC
+     LIMIT 500`
+  );
+  return NextResponse.json({ subscribers: rows });
+}
diff --git a/src/app/api/drops/checkout/route.ts b/src/app/api/drops/checkout/route.ts
new file mode 100644
index 0000000..ff3a7b3
--- /dev/null
+++ b/src/app/api/drops/checkout/route.ts
@@ -0,0 +1,49 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+import { createCheckoutSession, stripeEnabled, TIERS, type TierId } from "@/lib/stripe";
+
+export async function POST(req: NextRequest) {
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "json object body required" }, { status: 400 });
+  }
+  const email = String(body.email || "").trim().toLowerCase();
+  const tier = String(body.tier || "standard") as TierId;
+
+  if (!TIERS[tier]) return NextResponse.json({ error: "invalid tier" }, { status: 400 });
+  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+    return NextResponse.json({ error: "valid email required" }, { status: 400 });
+  }
+
+  const { rows } = await query<{ id: number; token: string }>(
+    `SELECT id, token FROM subscribers WHERE email = $1`, [email]
+  );
+  if (!rows.length) return NextResponse.json({ error: "sign up for the trial first" }, { status: 404 });
+  const sub = rows[0];
+
+  const origin = req.nextUrl.origin;
+
+  if (!stripeEnabled()) {
+    // Dev fallback — mark the subscriber as "comp" immediately so we can test end-to-end.
+    await query(`UPDATE subscribers SET tier = $1 WHERE id = $2`, [tier, sub.id]);
+    return NextResponse.json({
+      ok: true,
+      mock: true,
+      message: `Stripe not configured — subscriber set to tier=${tier} directly for testing.`,
+      next: `${origin}/drops/view/${sub.token}`,
+    });
+  }
+
+  try {
+    const session = await createCheckoutSession({
+      email,
+      tier,
+      successUrl: `${origin}/drops/view/${sub.token}?upgraded=${tier}`,
+      cancelUrl: `${origin}/drops?cancelled=1`,
+      subscriberToken: sub.token,
+    });
+    return NextResponse.json({ ok: true, url: session.url });
+  } catch (e) {
+    return NextResponse.json({ error: (e as Error).message }, { status: 500 });
+  }
+}
diff --git a/src/app/api/drops/compose/route.ts b/src/app/api/drops/compose/route.ts
new file mode 100644
index 0000000..f9ba455
--- /dev/null
+++ b/src/app/api/drops/compose/route.ts
@@ -0,0 +1,27 @@
+import { NextRequest, NextResponse } from "next/server";
+import { composeDropForDate } from "@/lib/drops";
+import { qwenIsUp } from "@/lib/qwen";
+
+export const runtime = "nodejs";
+export const maxDuration = 300;
+
+export async function POST(req: NextRequest) {
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "json object body required" }, { status: 400 });
+  }
+  const date = body.date ? new Date(String(body.date)) : undefined;
+  const itemsPerDrop = Number.isFinite(body.itemsPerDrop) ? body.itemsPerDrop : 10;
+  const force = !!body.force;
+
+  if (!(await qwenIsUp())) {
+    return NextResponse.json({ error: "Ollama not reachable at :11434" }, { status: 500 });
+  }
+
+  try {
+    const drop = await composeDropForDate({ date, itemsPerDrop, force });
+    return NextResponse.json({ ok: true, ...drop });
+  } catch (e) {
+    return NextResponse.json({ error: (e as Error).message }, { status: 500 });
+  }
+}
diff --git a/src/app/api/drops/cron/route.ts b/src/app/api/drops/cron/route.ts
new file mode 100644
index 0000000..c8a3ab7
--- /dev/null
+++ b/src/app/api/drops/cron/route.ts
@@ -0,0 +1,51 @@
+import { NextRequest, NextResponse } from "next/server";
+import { timingSafeEqual } from "node:crypto";
+import { composeDropForDate } from "@/lib/drops";
+import { purgeExpiredRateLimits } from "@/lib/rateLimit";
+
+export const runtime = "nodejs";
+export const maxDuration = 300;
+
+/**
+ * Daily cron tick. Secure via CRON_TOKEN env (provide as ?token=... or Authorization header).
+ * Step 1: compose today's drop if not already composed.
+ * Step 2: send it by POSTing to /api/drops/send.
+ */
+export async function POST(req: NextRequest) {
+  const requiredToken = process.env.CRON_TOKEN;
+  if (!requiredToken) {
+    return NextResponse.json({ error: "cron disabled — CRON_TOKEN not configured" }, { status: 503 });
+  }
+
+  const token = req.nextUrl.searchParams.get("token") || req.headers.get("authorization")?.replace(/^Bearer\s+/, "") || "";
+  // P1 fix 2026-05-04: timing-safe compare — was plain !== which leaks token
+  // length via response time; admin middleware uses HMAC compare, this should match.
+  const tBuf = Buffer.from(token);
+  const rBuf = Buffer.from(requiredToken);
+  const ok = tBuf.length === rBuf.length && timingSafeEqual(tBuf, rBuf);
+  if (!ok) {
+    return NextResponse.json({ error: "bad token" }, { status: 403 });
+  }
+
+  const today = new Date();
+  let composed: Awaited<ReturnType<typeof composeDropForDate>>;
+  try {
+    composed = await composeDropForDate({ date: today, itemsPerDrop: 10 });
+  } catch (e) {
+    return NextResponse.json({ error: (e as Error).message, step: "compose" }, { status: 500 });
+  }
+
+  const sendRes = await fetch(`${req.nextUrl.origin}/api/drops/send`, {
+    method: "POST",
+    headers: { "content-type": "application/json" },
+    body: JSON.stringify({ dropId: composed.dropId }),
+  });
+  const sendJson = await sendRes.json();
+
+  // Daily housekeeping: honor the 30-day rate-limit retention we promise in the privacy policy.
+  const purged = await purgeExpiredRateLimits().catch(() => 0);
+
+  return NextResponse.json({ ok: true, composed, send: sendJson, purged });
+}
+
+export async function GET(req: NextRequest) { return POST(req); }
diff --git a/src/app/api/drops/drop-items/[id]/route.ts b/src/app/api/drops/drop-items/[id]/route.ts
new file mode 100644
index 0000000..9181fa3
--- /dev/null
+++ b/src/app/api/drops/drop-items/[id]/route.ts
@@ -0,0 +1,50 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+/**
+ * PATCH /api/drops/drop-items/<id>  { headline?, blurb?, pro_extra?, cta_text?, cta_url? }
+ * Lets the admin hand-edit any item in a drop before send.
+ */
+export async function PATCH(
+  req: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  const numId = Number(id);
+  if (!Number.isFinite(numId)) return NextResponse.json({ error: "bad id" }, { status: 400 });
+
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "json object body required" }, { status: 400 });
+  }
+  const updates: Record<string, string | null> = {};
+  for (const field of ["headline", "blurb", "pro_extra", "cta_text", "cta_url"] as const) {
+    if (field in body) updates[field] = body[field] ?? null;
+  }
+  if (!Object.keys(updates).length) return NextResponse.json({ error: "no fields to update" }, { status: 400 });
+
+  const set = Object.keys(updates).map((k, i) => `${k} = $${i + 2}`).join(", ");
+  const values = Object.values(updates);
+  const { rows } = await query(
+    `UPDATE drop_items SET ${set} WHERE id = $1 RETURNING id, headline, blurb, pro_extra, cta_text, cta_url`,
+    [numId, ...values]
+  );
+  if (!rows.length) return NextResponse.json({ error: "not found" }, { status: 404 });
+  return NextResponse.json({ ok: true, item: rows[0] });
+}
+
+/** GET the drop_items for a drop — used by the admin inline editor. */
+export async function GET(
+  _req: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  const numId = Number(id);
+  if (!Number.isFinite(numId)) return NextResponse.json({ error: "bad id" }, { status: 400 });
+  const { rows } = await query(
+    `SELECT id, position, headline, blurb, pro_extra, cta_text, cta_url, source_kind, source_id
+     FROM drop_items WHERE drop_id = $1 ORDER BY position`,
+    [numId]
+  );
+  return NextResponse.json({ items: rows });
+}
diff --git a/src/app/api/drops/latest/route.ts b/src/app/api/drops/latest/route.ts
new file mode 100644
index 0000000..deeedda
--- /dev/null
+++ b/src/app/api/drops/latest/route.ts
@@ -0,0 +1,44 @@
+import { NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+/**
+ * Public snapshot of the most recent published/sent drop, teased to 2 items.
+ * Drives the "sample drop" preview on the landing page.
+ */
+export async function GET() {
+  const { rows: drops } = await query<{
+    id: number; drop_date: string; subject: string; intro: string | null;
+  }>(
+    `SELECT id, drop_date, subject, intro
+     FROM drops
+     WHERE status IN ('published', 'sent')
+     ORDER BY drop_date DESC
+     LIMIT 1`
+  );
+  if (!drops.length) return NextResponse.json({ drop: null });
+
+  const drop = drops[0];
+  const { rows: items } = await query<{
+    position: number; headline: string; blurb: string;
+  }>(
+    `SELECT position, headline, blurb
+     FROM drop_items WHERE drop_id = $1 ORDER BY position LIMIT 2`,
+    [drop.id]
+  );
+  const { rows: counts } = await query<{ total: string }>(
+    `SELECT COUNT(*)::text AS total FROM drop_items WHERE drop_id = $1`,
+    [drop.id]
+  );
+  const total = Number(counts[0]?.total ?? 0);
+
+  return NextResponse.json({
+    drop: {
+      id: drop.id,
+      date: drop.drop_date,
+      subject: drop.subject,
+      intro: drop.intro,
+      items,
+      hidden_count: Math.max(0, total - items.length),
+    },
+  });
+}
diff --git a/src/app/api/drops/pixel/[id]/route.ts b/src/app/api/drops/pixel/[id]/route.ts
new file mode 100644
index 0000000..cea371c
--- /dev/null
+++ b/src/app/api/drops/pixel/[id]/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest } from "next/server";
+import { query } from "@/lib/db";
+
+/**
+ * 1×1 transparent GIF tracking pixel.
+ * GET /api/drops/pixel/<deliveryId> — marks the delivery as opened.
+ * Embedded in drop HTML as <img src=".../pixel/123"> at the bottom.
+ */
+
+// 43-byte transparent GIF (1×1).
+const PIXEL = Uint8Array.from([
+  0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00,
+  0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00,
+  0x00, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
+  0x00, 0x02, 0x02, 0x44, 0x01, 0x00, 0x3b,
+]);
+
+export async function GET(
+  _req: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  const deliveryId = Number(id);
+  if (Number.isFinite(deliveryId)) {
+    // Stamp opened_at (only the first time — keep origin IP out of logs for privacy).
+    await query(
+      `UPDATE deliveries SET opened_at = NOW() WHERE id = $1 AND opened_at IS NULL`,
+      [deliveryId]
+    ).catch(() => {});
+  }
+  return new Response(PIXEL, {
+    headers: {
+      "content-type": "image/gif",
+      "content-length": String(PIXEL.length),
+      "cache-control": "no-store, no-cache, must-revalidate",
+      pragma: "no-cache",
+    },
+  });
+}
diff --git a/src/app/api/drops/preview/[token]/[id]/route.ts b/src/app/api/drops/preview/[token]/[id]/route.ts
new file mode 100644
index 0000000..af91761
--- /dev/null
+++ b/src/app/api/drops/preview/[token]/[id]/route.ts
@@ -0,0 +1,31 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+import { renderDropHTML } from "@/lib/drops";
+
+/**
+ * Renders the raw email HTML for a drop + subscriber combo — useful for
+ * verifying rendering in a browser (open /api/drops/preview/<token>/<dropId>).
+ * Requires a valid subscriber token.
+ */
+export async function GET(
+  req: NextRequest,
+  { params }: { params: Promise<{ token: string; id: string }> }
+) {
+  const { token, id } = await params;
+  const dropId = Number(id);
+  if (!Number.isFinite(dropId)) return NextResponse.json({ error: "bad id" }, { status: 400 });
+
+  const { rows } = await query<{ tier: string; token: string }>(
+    `SELECT tier, token FROM subscribers WHERE token = $1`, [token]
+  );
+  if (!rows.length) return NextResponse.json({ error: "unknown subscriber" }, { status: 404 });
+
+  const { html } = await renderDropHTML({
+    dropId,
+    tier: rows[0].tier as "trial" | "standard" | "pro" | "comp",
+    subscriberToken: rows[0].token,
+    appBaseUrl: req.nextUrl.origin,
+  });
+
+  return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
+}
diff --git a/src/app/api/drops/send/route.ts b/src/app/api/drops/send/route.ts
new file mode 100644
index 0000000..38e3927
--- /dev/null
+++ b/src/app/api/drops/send/route.ts
@@ -0,0 +1,109 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+import { activeSubscribersFor, renderDropHTML } from "@/lib/drops";
+import { sendEmail, currentBackend } from "@/lib/email";
+
+export const runtime = "nodejs";
+export const maxDuration = 300;
+
+/**
+ * POST /api/drops/send  { dropId?: number, dryRun?: boolean, testEmail?: string }
+ *   - default: send drop with matching dropId (or today's latest) to all active subs that haven't received it
+ *   - testEmail: deliver only to that address regardless
+ *   - dryRun:   compose but don't call the backend
+ */
+export async function POST(req: NextRequest) {
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "json object body required" }, { status: 400 });
+  }
+  const dryRun = !!body.dryRun;
+  const testEmail = body.testEmail ? String(body.testEmail) : null;
+  let dropId: number | null = Number.isFinite(body.dropId) ? body.dropId : null;
+
+  const origin = req.nextUrl.origin;
+
+  if (!dropId) {
+    const { rows } = await query<{ id: number }>(
+      `SELECT id FROM drops WHERE drop_date <= CURRENT_DATE ORDER BY drop_date DESC LIMIT 1`
+    );
+    if (!rows.length) return NextResponse.json({ error: "no drops exist yet — compose one first" }, { status: 404 });
+    dropId = rows[0].id;
+  }
+
+  const subs = testEmail
+    ? [{ id: 0, email: testEmail, name: null, token: "preview", tier: "pro" }]
+    : await activeSubscribersFor(dropId);
+
+  const backend = currentBackend();
+  const results: { email: string; ok: boolean; error?: string; id?: string }[] = [];
+
+  for (const s of subs) {
+    // Pre-create the delivery row so the open-tracking pixel has a stable ID.
+    let deliveryId: number | undefined;
+    if (!testEmail && !dryRun) {
+      const { rows } = await query<{ id: number }>(
+        `INSERT INTO deliveries (drop_id, subscriber_id, delivered_via)
+         VALUES ($1, $2, 'pending') ON CONFLICT (drop_id, subscriber_id)
+         DO UPDATE SET delivered_via = EXCLUDED.delivered_via
+         RETURNING id`,
+        [dropId, s.id]
+      );
+      deliveryId = rows[0]?.id;
+    }
+
+    const { subject, html, text } = await renderDropHTML({
+      dropId,
+      tier: s.tier as "trial" | "standard" | "pro" | "comp",
+      subscriberToken: s.token,
+      appBaseUrl: origin,
+      deliveryId,
+    });
+
+    if (dryRun) {
+      results.push({ email: s.email, ok: true, id: "(dry-run)" });
+      continue;
+    }
+
+    const r = await sendEmail({ to: s.email, subject, html, text, unsubscribeToken: s.token });
+    results.push({ email: s.email, ok: r.ok, error: r.error, id: r.id });
+
+    if (!testEmail && r.ok && deliveryId) {
+      await query(
+        `UPDATE deliveries SET delivered_via = $1, sent_at = NOW() WHERE id = $2`,
+        [r.via, deliveryId]
+      );
+      // Decrement trial counter and auto-flip to trial_expired when it hits 0.
+      await query(
+        `UPDATE subscribers
+         SET last_delivered_at = NOW(),
+             trial_drops_left = CASE WHEN tier = 'trial' THEN GREATEST(trial_drops_left - 1, 0) ELSE trial_drops_left END,
+             status = CASE
+               WHEN tier = 'trial' AND GREATEST(trial_drops_left - 1, 0) = 0 THEN 'trial_expired'
+               ELSE status
+             END
+         WHERE id = $1`,
+        [s.id]
+      );
+    } else if (!testEmail && !r.ok && deliveryId) {
+      // Mark as bounced so we don't retry indefinitely.
+      await query(
+        `UPDATE deliveries SET bounced = TRUE, error = $1 WHERE id = $2`,
+        [r.error ?? "unknown", deliveryId]
+      );
+    }
+  }
+
+  if (!testEmail && !dryRun && results.some((r) => r.ok)) {
+    await query(`UPDATE drops SET status = 'sent', sent_at = COALESCE(sent_at, NOW()) WHERE id = $1`, [dropId]);
+  }
+
+  return NextResponse.json({
+    ok: true,
+    dropId,
+    backend,
+    sent: results.filter((r) => r.ok).length,
+    failed: results.filter((r) => !r.ok).length,
+    results,
+  });
+}
diff --git a/src/app/api/drops/signup/route.ts b/src/app/api/drops/signup/route.ts
new file mode 100644
index 0000000..dafedb2
--- /dev/null
+++ b/src/app/api/drops/signup/route.ts
@@ -0,0 +1,149 @@
+import { NextRequest, NextResponse } from "next/server";
+import { randomBytes } from "node:crypto";
+import { query } from "@/lib/db";
+import { checkRateLimit, isDisposableEmail } from "@/lib/rateLimit";
+import { renderDropHTML } from "@/lib/drops";
+import { sendEmail } from "@/lib/email";
+
+export async function POST(req: NextRequest) {
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "json object body required" }, { status: 400 });
+  }
+  const email = String(body.email || "").trim().toLowerCase();
+  const name = body.name ? String(body.name).trim().slice(0, 120) : null;
+  const ref = body.ref ? String(body.ref).trim().slice(0, 80) : null;
+
+  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+    return NextResponse.json({ error: "valid email required" }, { status: 400 });
+  }
+  if (email.length > 254) {
+    return NextResponse.json({ error: "email too long" }, { status: 400 });
+  }
+  if (isDisposableEmail(email)) {
+    return NextResponse.json({ error: "please use a real email address — disposable addresses aren't accepted" }, { status: 400 });
+  }
+
+  // Rate limit by IP + by email.
+  const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
+    ?? req.headers.get("x-real-ip")
+    ?? "unknown";
+  const ipCheck = await checkRateLimit(ip, "signup-ip", { max: 5, windowMinutes: 60 });
+  if (!ipCheck.allowed) {
+    return NextResponse.json({ error: "too many signups from this network — try again later" }, { status: 429 });
+  }
+  const emailCheck = await checkRateLimit(email, "signup-email", { max: 3, windowMinutes: 1440 });
+  if (!emailCheck.allowed) {
+    return NextResponse.json({ error: "this email has been signed up multiple times recently" }, { status: 429 });
+  }
+
+  const token = randomBytes(18).toString("base64url");
+
+  const { rows } = await query<{ id: number; token: string; tier: string }>(
+    `INSERT INTO subscribers (email, name, token, tier, trial_drops_left, notes)
+     VALUES ($1, $2, $3, 'trial', 3, $4)
+     ON CONFLICT (email) DO UPDATE SET
+       name = COALESCE(EXCLUDED.name, subscribers.name),
+       status = CASE WHEN subscribers.status = 'cancelled' THEN 'active' ELSE subscribers.status END
+     RETURNING id, token, tier`,
+    [email, name, token, ref ? `referred by ${ref}` : null]
+  );
+
+  // Referral tracking — all inside a single transaction to prevent double-reward races.
+  if (ref) {
+    try {
+      const client = await (await import("@/lib/db")).pool.connect();
+      try {
+        await client.query("BEGIN");
+        const { rows: refRows } = await client.query<{ id: number }>(
+          `SELECT id FROM subscribers WHERE token = $1 AND status = 'active' FOR UPDATE`,
+          [ref]
+        );
+        if (refRows.length) {
+          await client.query(
+            `INSERT INTO referrals (referrer_token, referred_email, referred_sub_id)
+             VALUES ($1, $2, $3)
+             ON CONFLICT (referrer_token, referred_email) DO NOTHING`,
+            [ref, email, rows[0].id]
+          );
+          // Atomically claim exactly 3 unrewarded referrals; only grant the reward
+          // if we got all 3 back. No count-then-update race.
+          const { rows: claimed } = await client.query<{ id: number }>(
+            `WITH claim AS (
+               SELECT id FROM referrals
+               WHERE referrer_token = $1 AND reward_given = FALSE
+               ORDER BY created_at ASC
+               LIMIT 3
+               FOR UPDATE
+             )
+             UPDATE referrals SET reward_given = TRUE
+             WHERE id IN (SELECT id FROM claim)
+             RETURNING id`,
+            [ref]
+          );
+          if (claimed.length === 3) {
+            await client.query(
+              `UPDATE subscribers SET trial_drops_left = trial_drops_left + 30 WHERE token = $1`,
+              [ref]
+            );
+          } else if (claimed.length > 0) {
+            // Not enough for a reward yet — unclaim them.
+            await client.query(
+              `UPDATE referrals SET reward_given = FALSE WHERE id = ANY($1::int[])`,
+              [claimed.map((r) => r.id)]
+            );
+          }
+        }
+        await client.query("COMMIT");
+      } catch (e) {
+        await client.query("ROLLBACK");
+        console.warn("referral txn rollback:", e);
+      } finally {
+        client.release();
+      }
+    } catch { /* silent — ref-invalid shouldn't block signup */ }
+  }
+
+  // Welcome drop: immediately send the most recent published drop so new subscribers
+  // get value on day zero, not day one.
+  try {
+    const { rows: latest } = await query<{ id: number }>(
+      `SELECT id FROM drops WHERE status IN ('published','sent') ORDER BY drop_date DESC LIMIT 1`
+    );
+    if (latest.length) {
+      const origin = req.nextUrl.origin;
+      const { rows: delRows } = await query<{ id: number }>(
+        `INSERT INTO deliveries (drop_id, subscriber_id, delivered_via)
+         VALUES ($1, $2, 'welcome-pending')
+         ON CONFLICT (drop_id, subscriber_id) DO UPDATE SET delivered_via = EXCLUDED.delivered_via
+         RETURNING id`,
+        [latest[0].id, rows[0].id]
+      );
+      const { subject, html, text } = await renderDropHTML({
+        dropId: latest[0].id,
+        tier: rows[0].tier as "trial",
+        subscriberToken: rows[0].token,
+        appBaseUrl: origin,
+        deliveryId: delRows[0]?.id,
+      });
+      const welcomeSubject = `Welcome to Drops — ${subject}`;
+      const r = await sendEmail({ to: email, subject: welcomeSubject, html, text, unsubscribeToken: rows[0].token });
+      if (r.ok && delRows[0]?.id) {
+        await query(
+          `UPDATE deliveries SET delivered_via = $1, sent_at = NOW() WHERE id = $2`,
+          [`welcome-${r.via}`, delRows[0].id]
+        );
+        await query(
+          `UPDATE subscribers SET last_delivered_at = NOW(), trial_drops_left = GREATEST(trial_drops_left - 1, 0) WHERE id = $1`,
+          [rows[0].id]
+        );
+      }
+    }
+  } catch (e) { console.warn("welcome-drop failed:", e); }
+
+  return NextResponse.json({
+    ok: true,
+    subscriber: { id: rows[0].id, token: rows[0].token, tier: rows[0].tier },
+    next: "Welcome drop sent. Next issue lands tomorrow at 9am PT.",
+  });
+}
diff --git a/src/app/api/drops/stripe-webhook/route.ts b/src/app/api/drops/stripe-webhook/route.ts
new file mode 100644
index 0000000..e4b2473
--- /dev/null
+++ b/src/app/api/drops/stripe-webhook/route.ts
@@ -0,0 +1,53 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+import { verifyWebhookSignature } from "@/lib/stripe";
+
+export async function POST(req: NextRequest) {
+  const raw = await req.text();
+  const sig = req.headers.get("stripe-signature");
+
+  // Require a valid signature whenever STRIPE_WEBHOOK_SECRET is set.
+  // Without the secret, the webhook is disabled (never processes) rather than
+  // being silently open — fail closed.
+  if (!process.env.STRIPE_WEBHOOK_SECRET) {
+    return NextResponse.json(
+      { error: "webhook disabled — STRIPE_WEBHOOK_SECRET not configured" },
+      { status: 503 }
+    );
+  }
+  if (!verifyWebhookSignature(raw, sig)) {
+    return NextResponse.json({ error: "invalid signature" }, { status: 400 });
+  }
+
+  let evt: { type: string; data: { object: { metadata?: { subscriber_token?: string; tier?: string }; customer?: string; subscription?: string } } };
+  try { evt = JSON.parse(raw); } catch { return NextResponse.json({ error: "bad json" }, { status: 400 }); }
+
+  if (evt.type === "checkout.session.completed") {
+    const meta = evt.data.object.metadata ?? {};
+    const token = meta.subscriber_token;
+    const tier = meta.tier;
+    if (!token || !tier) return NextResponse.json({ ignored: true });
+    await query(
+      `UPDATE subscribers
+         SET tier = $1,
+             stripe_customer_id = COALESCE($2, stripe_customer_id),
+             stripe_sub_id = COALESCE($3, stripe_sub_id)
+       WHERE token = $4`,
+      [tier, evt.data.object.customer ?? null, evt.data.object.subscription ?? null, token]
+    );
+    return NextResponse.json({ ok: true, upgraded: tier });
+  }
+
+  if (evt.type === "customer.subscription.deleted") {
+    const subId = evt.data.object.subscription;
+    if (subId) {
+      await query(
+        `UPDATE subscribers SET tier = 'trial', status = 'cancelled', cancelled_at = NOW()
+         WHERE stripe_sub_id = $1`,
+        [subId]
+      );
+    }
+  }
+
+  return NextResponse.json({ received: true });
+}
diff --git a/src/app/api/drops/unsubscribe/[token]/route.ts b/src/app/api/drops/unsubscribe/[token]/route.ts
new file mode 100644
index 0000000..ef41a85
--- /dev/null
+++ b/src/app/api/drops/unsubscribe/[token]/route.ts
@@ -0,0 +1,39 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+async function unsubscribe(token: string) {
+  const { rowCount } = await query(
+    `UPDATE subscribers SET status = 'cancelled', cancelled_at = NOW() WHERE token = $1`,
+    [token]
+  ) as unknown as { rowCount: number };
+  return rowCount > 0;
+}
+
+// POST is required by Gmail/Yahoo one-click unsubscribe (RFC 8058).
+export async function POST(_req: NextRequest, { params }: { params: Promise<{ token: string }> }) {
+  const { token } = await params;
+  const ok = await unsubscribe(token);
+  if (!ok) return NextResponse.json({ error: "not found" }, { status: 404 });
+  return NextResponse.json({ ok: true });
+}
+
+// GET supports email-client clickthroughs that don't POST.
+// Returns a confirmation page rather than JSON.
+export async function GET(_req: NextRequest, { params }: { params: Promise<{ token: string }> }) {
+  const { token } = await params;
+  const ok = await unsubscribe(token);
+  const html = ok
+    ? `<!doctype html><html><body style="font-family:Georgia,serif;max-width:500px;margin:80px auto;padding:20px;text-align:center;">
+         <h1>Unsubscribed.</h1>
+         <p>You won't receive any more drops. Sorry to see you go.</p>
+         <p style="font-size:13px;color:#888;">If this was an accident, <a href="/drops">sign up again</a>.</p>
+       </body></html>`
+    : `<!doctype html><html><body style="font-family:Georgia,serif;max-width:500px;margin:80px auto;padding:20px;text-align:center;">
+         <h1>Unsubscribe link invalid.</h1>
+         <p>This token wasn't recognized. If you still receive drops, reply directly and we'll remove you.</p>
+       </body></html>`;
+  return new NextResponse(html, {
+    status: ok ? 200 : 404,
+    headers: { "content-type": "text/html" },
+  });
+}
diff --git a/src/app/api/drops/view/[token]/route.ts b/src/app/api/drops/view/[token]/route.ts
new file mode 100644
index 0000000..6dda340
--- /dev/null
+++ b/src/app/api/drops/view/[token]/route.ts
@@ -0,0 +1,33 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+export async function GET(
+  _req: NextRequest,
+  { params }: { params: Promise<{ token: string }> }
+) {
+  const { token } = await params;
+  const { rows: subs } = await query<{
+    id: number; email: string; name: string | null; tier: string; status: string;
+    trial_drops_left: number; signed_up_at: string; last_delivered_at: string | null;
+  }>(
+    `SELECT id, email, name, tier, status, trial_drops_left, signed_up_at, last_delivered_at
+     FROM subscribers WHERE token = $1`,
+    [token]
+  );
+  if (!subs.length) return NextResponse.json({ error: "not found" }, { status: 404 });
+  const sub = subs[0];
+
+  const { rows: drops } = await query<{
+    id: number; drop_date: string; subject: string;
+    sent_at: string | null; intro: string | null;
+  }>(
+    `SELECT d.id, d.drop_date, d.subject, d.sent_at, d.intro
+     FROM drops d
+     JOIN deliveries dv ON dv.drop_id = d.id
+     WHERE dv.subscriber_id = $1
+     ORDER BY d.drop_date DESC`,
+    [sub.id]
+  );
+
+  return NextResponse.json({ subscriber: sub, drops });
+}
diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts
new file mode 100644
index 0000000..8bf4ae5
--- /dev/null
+++ b/src/app/api/health/route.ts
@@ -0,0 +1,43 @@
+import { NextResponse } from "next/server";
+import { query } from "@/lib/db";
+import { qwenIsUp, QWEN_MODEL_NAME } from "@/lib/qwen";
+import { currentBackend } from "@/lib/email";
+
+export const runtime = "nodejs";
+export const dynamic = "force-dynamic";
+
+export async function GET() {
+  const start = Date.now();
+  const checks: Record<string, { ok: boolean; detail?: string; latencyMs?: number }> = {};
+
+  try {
+    const t0 = Date.now();
+    const { rows } = await query<{ n: string }>("SELECT COUNT(*)::text AS n FROM items");
+    checks.db = { ok: true, detail: `${rows[0].n} items`, latencyMs: Date.now() - t0 };
+  } catch (e) {
+    checks.db = { ok: false, detail: (e as Error).message };
+  }
+
+  try {
+    const t0 = Date.now();
+    const up = await qwenIsUp();
+    checks.ollama = { ok: up, detail: up ? QWEN_MODEL_NAME : "not reachable", latencyMs: Date.now() - t0 };
+  } catch (e) {
+    checks.ollama = { ok: false, detail: (e as Error).message };
+  }
+
+  checks.email_backend = { ok: true, detail: currentBackend() };
+  checks.stripe = { ok: true, detail: process.env.STRIPE_SECRET_KEY ? "configured" : "dev-mock" };
+
+  const overall = Object.values(checks).every((c) => c.ok);
+  return NextResponse.json(
+    {
+      ok: overall,
+      timestamp: new Date().toISOString(),
+      uptime_s: Math.floor(process.uptime()),
+      latency_ms: Date.now() - start,
+      checks,
+    },
+    { status: overall ? 200 : 503 }
+  );
+}
diff --git a/src/app/api/items/[id]/route.ts b/src/app/api/items/[id]/route.ts
new file mode 100644
index 0000000..31a369e
--- /dev/null
+++ b/src/app/api/items/[id]/route.ts
@@ -0,0 +1,21 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+export async function GET(
+  _req: NextRequest,
+  { params }: { params: Promise<{ id: string }> }
+) {
+  const { id } = await params;
+  const numId = Number(id);
+  if (!Number.isFinite(numId)) return NextResponse.json({ error: "bad id" }, { status: 400 });
+
+  const { rows } = await query(
+    `SELECT *,
+            EXTRACT(YEAR FROM AGE(NOW(), expired_date))::int AS years_expired,
+            (CURRENT_DATE + INTERVAL '6 months')::date AS follow_up_date
+     FROM items WHERE id = $1`,
+    [numId]
+  );
+  if (!rows.length) return NextResponse.json({ error: "not found" }, { status: 404 });
+  return NextResponse.json({ item: rows[0] });
+}
diff --git a/src/app/api/items/route.ts b/src/app/api/items/route.ts
new file mode 100644
index 0000000..4baff46
--- /dev/null
+++ b/src/app/api/items/route.ts
@@ -0,0 +1,33 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+
+export async function GET(req: NextRequest) {
+  const sp = req.nextUrl.searchParams;
+  const q = sp.get("q")?.trim() || "";
+  const kind = sp.get("kind") || "";
+  const status = sp.get("status") || "";
+
+  const where: string[] = [];
+  const params: unknown[] = [];
+  if (q) {
+    params.push(`%${q}%`);
+    where.push(`(name ILIKE $${params.length} OR goods_services ILIKE $${params.length} OR notes ILIKE $${params.length} OR original_owner ILIKE $${params.length})`);
+  }
+  if (kind) { params.push(kind); where.push(`kind = $${params.length}`); }
+  if (status) { params.push(status); where.push(`status = $${params.length}`); }
+
+  const sql = `
+    SELECT id, kind, name, original_owner, nice_class, goods_services, status,
+           expired_date, source, source_url, notes,
+           distinctiveness_score, recognition_score, domain_available,
+           competing_active_marks, monetization_breadth, composite_score,
+           monetization_ideas,
+           EXTRACT(YEAR FROM AGE(NOW(), expired_date))::int AS years_expired,
+           (CURRENT_DATE + INTERVAL '6 months')::date AS follow_up_date
+    FROM items
+    ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
+    ORDER BY composite_score DESC NULLS LAST, name ASC
+  `;
+  const { rows } = await query(sql, params);
+  return NextResponse.json({ items: rows });
+}
diff --git a/src/app/api/news/route.ts b/src/app/api/news/route.ts
new file mode 100644
index 0000000..bb881de
--- /dev/null
+++ b/src/app/api/news/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from "next/server";
+import { parseStringPromise } from "xml2js";
+
+/**
+ * Pull Google News RSS for a given query. No API key needed.
+ * Returns up to 10 most recent items.
+ */
+export async function GET(req: NextRequest) {
+  const q = req.nextUrl.searchParams.get("q");
+  if (!q) return NextResponse.json({ items: [] });
+
+  const url = `https://news.google.com/rss/search?q=${encodeURIComponent(q)}&hl=en-US&gl=US&ceid=US:en`;
+  try {
+    const resp = await fetch(url, { next: { revalidate: 900 } } as RequestInit);
+    if (!resp.ok) return NextResponse.json({ items: [], error: `upstream ${resp.status}` });
+    const xml = await resp.text();
+    const parsed = await parseStringPromise(xml, { explicitArray: false, trim: true });
+    const rawItems = parsed?.rss?.channel?.item ?? [];
+    const items = (Array.isArray(rawItems) ? rawItems : [rawItems]).slice(0, 10).map((it: {
+      title?: string;
+      link?: string;
+      pubDate?: string;
+      source?: string | { _?: string };
+    }) => ({
+      title: it.title ?? "",
+      link: it.link ?? "",
+      pubDate: it.pubDate ?? "",
+      source: typeof it.source === "string" ? it.source : it.source?._ ?? "",
+    }));
+    return NextResponse.json({ items });
+  } catch (e) {
+    return NextResponse.json({ items: [], error: String(e) });
+  }
+}
diff --git a/src/app/api/score/route.ts b/src/app/api/score/route.ts
new file mode 100644
index 0000000..1109896
--- /dev/null
+++ b/src/app/api/score/route.ts
@@ -0,0 +1,29 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+import { scoreItem } from "@/lib/scoring";
+
+/**
+ * POST /api/score recomputes composite_score for every item using current scoring.ts weights.
+ * Useful after you tweak the algorithm.
+ */
+export async function POST(_req: NextRequest) {
+  const { rows } = await query<{
+    id: number;
+    distinctiveness_score: number | null;
+    recognition_score: number | null;
+    domain_available: boolean | null;
+    competing_active_marks: number | null;
+    monetization_breadth: number | null;
+    expired_date: string;
+  }>(
+    `SELECT id, distinctiveness_score, recognition_score, domain_available,
+            competing_active_marks, monetization_breadth, expired_date FROM items`
+  );
+  let updated = 0;
+  for (const r of rows) {
+    const score = scoreItem(r);
+    await query(`UPDATE items SET composite_score = $1, updated_at = NOW() WHERE id = $2`, [score, r.id]);
+    updated++;
+  }
+  return NextResponse.json({ updated });
+}
diff --git a/src/app/api/swot/route.ts b/src/app/api/swot/route.ts
new file mode 100644
index 0000000..3f9b6fb
--- /dev/null
+++ b/src/app/api/swot/route.ts
@@ -0,0 +1,64 @@
+import { NextRequest, NextResponse } from "next/server";
+import { query } from "@/lib/db";
+import { runSwot, type Quadrant } from "@/lib/swot";
+import { qwenIsUp } from "@/lib/qwen";
+
+const QUADS: Quadrant[] = ["strength", "weakness", "opportunity", "threat"];
+
+export async function POST(req: NextRequest) {
+  const body = await req.json().catch(() => ({}));
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "json object body required" }, { status: 400 });
+  }
+  const itemId = Number(body.itemId);
+  const force = !!body.force;
+  if (!Number.isFinite(itemId)) return NextResponse.json({ error: "itemId required" }, { status: 400 });
+
+  const { rows } = await query<{
+    id: number; name: string; kind: "trademark" | "copyright";
+    original_owner: string | null; nice_class: string | null;
+    goods_services: string | null; expired_date: string;
+    status: "abandoned" | "cancelled" | "expired" | "public_domain";
+    notes: string | null;
+  }>(
+    `SELECT id, name, kind, original_owner, nice_class, goods_services, expired_date, status, notes
+     FROM items WHERE id = $1`,
+    [itemId]
+  );
+  if (!rows.length) return NextResponse.json({ error: "item not found" }, { status: 404 });
+  const item = rows[0];
+
+  if (!force) {
+    const { rows: cached } = await query<{ quadrant: Quadrant; content: string; generated_at: string }>(
+      `SELECT quadrant, content, generated_at FROM swot_cache WHERE item_id = $1`,
+      [itemId]
+    );
+    if (cached.length === 4) {
+      return NextResponse.json({ cached: true, quadrants: cached });
+    }
+  } else {
+    await query(`DELETE FROM swot_cache WHERE item_id = $1`, [itemId]);
+  }
+
+  if (!(await qwenIsUp())) {
+    return NextResponse.json(
+      { error: "Ollama not reachable at :11434. Start with `ollama serve`." },
+      { status: 500 }
+    );
+  }
+
+  const results = await Promise.all(
+    QUADS.map(async (quadrant) => {
+      const content = await runSwot(item, quadrant);
+      await query(
+        `INSERT INTO swot_cache (item_id, quadrant, content)
+         VALUES ($1, $2, $3)
+         ON CONFLICT (item_id, quadrant) DO UPDATE SET content = EXCLUDED.content, generated_at = NOW()`,
+        [itemId, quadrant, content]
+      );
+      return { quadrant, content, generated_at: new Date().toISOString() };
+    })
+  );
+
+  return NextResponse.json({ cached: false, quadrants: results });
+}
diff --git a/src/app/canvas/page.tsx b/src/app/canvas/page.tsx
new file mode 100644
index 0000000..ae75bb9
--- /dev/null
+++ b/src/app/canvas/page.tsx
@@ -0,0 +1,18 @@
+import Disclaimer from "@/components/Disclaimer";
+import Canvas from "@/components/Canvas";
+
+export default function CanvasPage() {
+  return (
+    <div>
+      <Disclaimer />
+      <div className="mb-3">
+        <h1 className="text-3xl font-semibold tracking-tight">Portfolio Canvas</h1>
+        <p className="text-sm text-ink/60">
+          Drag nodes, connect them to group marks into thematic portfolios or pipeline stages.
+          Your selection from the table loads automatically.
+        </p>
+      </div>
+      <Canvas />
+    </div>
+  );
+}
diff --git a/src/app/changelog/page.tsx b/src/app/changelog/page.tsx
new file mode 100644
index 0000000..222cdfc
--- /dev/null
+++ b/src/app/changelog/page.tsx
@@ -0,0 +1,75 @@
+import Link from "next/link";
+
+export const metadata = {
+  title: "Changelog",
+  description: "What's shipped on Drops.",
+};
+
+type Entry = { date: string; items: string[] };
+
+const CHANGELOG: Entry[] = [
+  {
+    date: "2026-04-24",
+    items: [
+      "Welcome drop — new trial subscribers now get the most recent drop immediately on signup instead of waiting until 9am tomorrow.",
+      "Daily drop composition fully parallelized (~3× faster).",
+      "Admin: inline drop-item editor (edit headlines/blurbs/PRO hints without recomposing).",
+      "Admin: subscribers list with tier/status/open-rate, edit inline.",
+      "Referral system — subscribers get a unique URL; 3 signups = 30 free days.",
+      "Email open-tracking pixel; open-rate visible in admin drops dashboard.",
+      "Public archive at /drops/archive with teased past issues for SEO + conversion.",
+      "Sample-drop preview embedded in the landing page for instant social proof.",
+      "Seed catalog expanded by 25 items — defunct retail (Linens 'n Things, Filene's Basement, Sharper Image, Montgomery Ward, Tower Records), defunct restaurants (HoJo, Chi-Chi's, Bennigan's), and the 1929 public-domain cohort (Sherlock Holmes final stories, Steamboat-contemporary animation, A Farewell to Arms).",
+      "Legal pages: Terms, Privacy, Refund — Stripe-ready, California-governed.",
+      "Marketing pages: About, Pricing, FAQ — each with its own metadata + SEO.",
+      "SEO: per-page metadata, Organization + WebApp JSON-LD, OpenGraph + Twitter Card, robots.txt + sitemap.xml, dynamic /opengraph-image route.",
+      "Health endpoint /api/health returns db + ollama + email-backend + stripe status (200/503).",
+      "Admin auth: cookie-gated /admin/* routes via ADMIN_TOKEN.",
+      "Rate limit + disposable-email filter on signup.",
+      "Custom 404/500 pages.",
+      "Test suite (23 tests) for scoring + lifecycle classification.",
+    ],
+  },
+  {
+    date: "2026-04-23",
+    items: [
+      "Drops SaaS shipped — signup, Stripe checkout (optional), daily cron, multi-backend email (Resend / SMTP / file).",
+      "Qwen-powered drop composer reads items + brand_candidates tables.",
+      "Subscriber portal + unsubscribe flow.",
+      "Brand hunter — DDG HTML + DDG Lite + Brave Search fallback, Qwen classifies live USPTO registration status.",
+      "Brand-domain sanity gate (flags parked/registrar-redirect pages as 'parked').",
+      "Opportunity labels: domain_snipe / abandonment_watch / licensing_outreach / do_not_target.",
+      "Activity clock — scans homepage for © year, classifies active / stale / abandoned.",
+      "Domain lifecycle — HTTP liveness + WHOIS expiry.",
+      "Underlying 'Trademarks & Copyright' opportunity finder: 30-item seed, TanStack table with multi-select, 4-agent SWOT via Qwen (no paid APIs), composite scoring, React Flow canvas.",
+    ],
+  },
+];
+
+export default function ChangelogPage() {
+  return (
+    <article className="mx-auto max-w-3xl space-y-4">
+      <div className="card p-6">
+        <div className="text-xs uppercase tracking-widest text-brass">Changelog</div>
+        <h1 className="mt-2 text-3xl font-semibold">What's shipped.</h1>
+        <p className="mt-2 text-sm text-ink/70">
+          We ship frequently. See <Link href="/drops" className="text-brass underline">drops</Link> for the daily product,
+          or the <Link href="/faq" className="text-brass underline">FAQ</Link> for common questions.
+        </p>
+      </div>
+
+      {CHANGELOG.map((entry) => (
+        <div key={entry.date} className="card p-5">
+          <div className="text-xs uppercase tracking-wide text-brass mb-2">
+            {new Date(entry.date).toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" })}
+          </div>
+          <ul className="list-disc pl-5 space-y-1 text-sm">
+            {entry.items.map((it, i) => (
+              <li key={i} className="text-ink/80 leading-relaxed">{it}</li>
+            ))}
+          </ul>
+        </div>
+      ))}
+    </article>
+  );
+}
diff --git a/src/app/drops/archive/[id]/page.tsx b/src/app/drops/archive/[id]/page.tsx
new file mode 100644
index 0000000..9d677e8
--- /dev/null
+++ b/src/app/drops/archive/[id]/page.tsx
@@ -0,0 +1,82 @@
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { query } from "@/lib/db";
+
+type Params = { id: string };
+
+export const dynamic = "force-dynamic";
+
+export async function generateMetadata({ params }: { params: Promise<Params> }) {
+  const { id } = await params;
+  const { rows } = await query<{ subject: string; intro: string | null }>(
+    `SELECT subject, intro FROM drops WHERE id = $1 AND status IN ('published','sent')`, [Number(id)]
+  );
+  if (!rows.length) return { title: "Not found" };
+  return { title: rows[0].subject, description: rows[0].intro ?? undefined };
+}
+
+export default async function ArchivedDrop({ params }: { params: Promise<Params> }) {
+  const { id } = await params;
+  const numId = Number(id);
+  if (!Number.isFinite(numId)) notFound();
+
+  const { rows: drops } = await query<{
+    id: number; drop_date: string; subject: string; intro: string | null; outro: string | null;
+  }>(
+    `SELECT id, drop_date, subject, intro, outro FROM drops
+     WHERE id = $1 AND status IN ('published','sent')`,
+    [numId]
+  );
+  if (!drops.length) notFound();
+  const drop = drops[0];
+
+  const { rows: items } = await query<{
+    position: number; headline: string; blurb: string;
+  }>(`SELECT position, headline, blurb FROM drop_items WHERE drop_id = $1 ORDER BY position`, [numId]);
+
+  const teased = items.slice(0, 1);   // free readers see headline + blurb of first item only
+  const hidden = items.slice(1);
+
+  const date = new Date(drop.drop_date);
+
+  return (
+    <article className="mx-auto max-w-3xl">
+      <div className="mb-3 text-sm"><Link href="/drops/archive" className="text-ink/60 hover:text-brass">← Archive</Link></div>
+      <div className="card overflow-hidden">
+        <div className="bg-ledger text-parchment px-6 py-5">
+          <div className="text-xs tracking-widest text-brass">
+            {date.toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" }).toUpperCase()}
+          </div>
+          <h1 className="mt-1 text-3xl font-semibold">{drop.subject}</h1>
+          {drop.intro && <p className="mt-2 italic text-parchment/80">{drop.intro}</p>}
+        </div>
+
+        {teased.map((it) => (
+          <div key={it.position} className="border-t border-ink/10 p-5">
+            <div className="text-xs text-ink/50">#{it.position}</div>
+            <h3 className="mt-1 text-xl font-semibold">{it.headline}</h3>
+            <p className="mt-2 text-ink/80">{it.blurb}</p>
+          </div>
+        ))}
+
+        {hidden.length > 0 && (
+          <div className="border-t border-ink/10 p-6 bg-brass/5">
+            {hidden.slice(0, 2).map((it) => (
+              <div key={it.position} className="mb-4 opacity-60 select-none">
+                <div className="text-xs text-ink/40">#{it.position}</div>
+                <h3 className="text-lg font-semibold blur-sm">{it.headline}</h3>
+                <p className="text-sm text-ink/60 blur-sm">{it.blurb.slice(0, 160)}…</p>
+              </div>
+            ))}
+            <div className="text-center mt-4">
+              <div className="text-xs uppercase tracking-wide text-brass mb-2">
+                {hidden.length} item{hidden.length === 1 ? "" : "s"} gated for subscribers
+              </div>
+              <Link href="/drops" className="btn">Start free trial to read the rest</Link>
+            </div>
+          </div>
+        )}
+      </div>
+    </article>
+  );
+}
diff --git a/src/app/drops/archive/page.tsx b/src/app/drops/archive/page.tsx
new file mode 100644
index 0000000..ded17cf
--- /dev/null
+++ b/src/app/drops/archive/page.tsx
@@ -0,0 +1,61 @@
+import Link from "next/link";
+import { query } from "@/lib/db";
+
+export const metadata = {
+  title: "Archive",
+  description: "Every published issue of Drops. Teased for free readers, full text for subscribers.",
+};
+
+export const dynamic = "force-dynamic";
+
+export default async function ArchivePage() {
+  const { rows } = await query<{
+    id: number; drop_date: string; subject: string; intro: string | null; items: string;
+  }>(
+    `SELECT d.id, d.drop_date, d.subject, d.intro,
+            (SELECT COUNT(*)::text FROM drop_items WHERE drop_id = d.id) AS items
+     FROM drops d
+     WHERE d.status IN ('published','sent')
+     ORDER BY d.drop_date DESC
+     LIMIT 180`
+  );
+
+  return (
+    <article className="mx-auto max-w-3xl">
+      <div className="card p-6 mb-4">
+        <div className="text-xs uppercase tracking-widest text-brass">Archive</div>
+        <h1 className="mt-2 text-3xl font-semibold">Every Drop, in chronological order.</h1>
+        <p className="mt-2 text-sm text-ink/70">
+          Free readers see subjects + a sample item per issue. Subscribers see the full text — past and present — from their portal.
+        </p>
+      </div>
+
+      {rows.length === 0 ? (
+        <div className="card p-6 text-ink/60">No drops yet. First drop lands at 9am PT.</div>
+      ) : (
+        <ul className="space-y-3">
+          {rows.map((d) => {
+            const date = new Date(d.drop_date);
+            return (
+              <li key={d.id}>
+                <Link href={`/drops/archive/${d.id}`} className="card block p-5 hover:bg-brass/5 transition-colors">
+                  <div className="text-xs uppercase tracking-wide text-brass">
+                    {date.toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric", year: "numeric" })}
+                  </div>
+                  <h2 className="mt-1 text-xl font-semibold">{d.subject}</h2>
+                  {d.intro && <p className="mt-1 text-sm italic text-ink/70">{d.intro}</p>}
+                  <div className="mt-2 text-xs text-ink/50">{d.items} items</div>
+                </Link>
+              </li>
+            );
+          })}
+        </ul>
+      )}
+
+      <div className="mt-8 card p-6 text-center">
+        <p className="text-sm text-ink/70">Want the full text of every drop, past and future?</p>
+        <Link href="/drops" className="btn mt-3">Start free trial</Link>
+      </div>
+    </article>
+  );
+}
diff --git a/src/app/drops/page.tsx b/src/app/drops/page.tsx
new file mode 100644
index 0000000..2bad7d6
--- /dev/null
+++ b/src/app/drops/page.tsx
@@ -0,0 +1,261 @@
+"use client";
+
+import { Suspense, useEffect, useState } from "react";
+import { useSearchParams } from "next/navigation";
+
+export default function DropsLandingPage() {
+  return (
+    <div>
+      <Suspense fallback={<div className="card p-8 mb-6" />}>
+        <Hero />
+      </Suspense>
+      <SampleDrop />
+      <Proof />
+      <WhoFor />
+      <Plans />
+      <HowItWorks />
+      <Fine />
+    </div>
+  );
+}
+
+function SampleDrop() {
+  const [drop, setDrop] = useState<{
+    id: number;
+    date: string;
+    subject: string;
+    intro: string | null;
+    items: { position: number; headline: string; blurb: string }[];
+    hidden_count: number;
+  } | null>(null);
+  useEffect(() => {
+    fetch("/api/drops/latest").then((r) => r.json()).then((j) => setDrop(j.drop));
+  }, []);
+  if (!drop) return null;
+  const date = new Date(drop.date).toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" });
+  return (
+    <section className="card mb-6 overflow-hidden">
+      <div className="bg-ledger text-parchment px-6 py-4">
+        <div className="text-xs tracking-widest text-brass">SAMPLE · {date.toUpperCase()}</div>
+        <h2 className="mt-1 text-2xl font-semibold">{drop.subject}</h2>
+        {drop.intro && <p className="mt-2 text-sm italic text-parchment/75">{drop.intro}</p>}
+      </div>
+      <div>
+        {drop.items.map((it) => (
+          <div key={it.position} className="border-t border-ink/10 p-5">
+            <div className="text-xs text-ink/50">#{it.position}</div>
+            <h3 className="mt-1 text-lg font-semibold">{it.headline}</h3>
+            <p className="mt-1 text-sm text-ink/80">{it.blurb}</p>
+          </div>
+        ))}
+        {drop.hidden_count > 0 && (
+          <div className="relative border-t border-ink/10 p-6 bg-brass/5 text-center">
+            <div className="absolute inset-0 pointer-events-none" style={{
+              background: "linear-gradient(180deg, transparent, #f7f4ec 80%)",
+            }} />
+            <div className="relative">
+              <div className="text-xs uppercase tracking-wide text-brass">+ {drop.hidden_count} more in today's drop</div>
+              <p className="mt-1 text-sm text-ink/70">
+                Subscribers got the rest in their inbox at 9am PT.
+              </p>
+            </div>
+          </div>
+        )}
+      </div>
+    </section>
+  );
+}
+
+function Hero() {
+  const [email, setEmail] = useState("");
+  const [name, setName] = useState("");
+  const [loading, setLoading] = useState(false);
+  const [err, setErr] = useState<string | null>(null);
+  const sp = useSearchParams();
+  const ref = sp.get("ref") || "";
+
+  async function submit(e: React.FormEvent) {
+    e.preventDefault();
+    setLoading(true); setErr(null);
+    try {
+      const r = await fetch("/api/drops/signup", {
+        method: "POST",
+        headers: { "content-type": "application/json" },
+        body: JSON.stringify({ email, name, ref: ref || undefined }),
+      });
+      const j = await r.json();
+      if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`);
+      // Redirect to the private portal instead of showing the token inline.
+      window.location.href = `/drops/view/${j.subscriber.token}?welcomed=1`;
+    } catch (e) { setErr((e as Error).message); setLoading(false); }
+  }
+
+  return (
+    <div className="card p-8 mb-6 border-brass/30 bg-gradient-to-br from-parchment to-white">
+      <div className="text-xs uppercase tracking-widest text-brass">Daily Drops</div>
+      <h1 className="mt-2 text-4xl font-semibold tracking-tight">
+        Trademark & domain opportunities in your inbox. <span className="text-brass">Every day at 9am.</span>
+      </h1>
+      <p className="mt-3 max-w-2xl text-lg text-ink/70">
+        Abandoned US trademarks, public-domain works entering their 95th year, expired fashion-brand domains,
+        and unregistered names sitting on million-dollar businesses — curated, scored, and delivered fresh.
+      </p>
+
+      <form onSubmit={submit} className="mt-6 flex flex-wrap items-center gap-2">
+        <input
+          required
+          type="email"
+          placeholder="you@business.com"
+          value={email}
+          onChange={(e) => setEmail(e.target.value)}
+          className="w-72"
+        />
+        <input
+          placeholder="your name (optional)"
+          value={name}
+          onChange={(e) => setName(e.target.value)}
+          className="w-52"
+        />
+        <button type="submit" className="btn" disabled={loading}>
+          {loading ? "Starting…" : "Start free trial (3 drops)"}
+        </button>
+      </form>
+      {loading && <div className="mt-3 rounded bg-ink/5 p-3 text-sm text-ink/70">Setting up your account…</div>}
+      {err && <div className="mt-3 rounded bg-red-50 p-3 text-sm text-red-700">{err}</div>}
+      {ref && <div className="mt-2 text-xs text-brass">✓ referral applied — you're helping a friend too</div>}
+      <div className="mt-2 text-xs text-ink/50">No credit card. Unsubscribe anytime from any email.</div>
+    </div>
+  );
+}
+
+function Plans() {
+  const [loading, setLoading] = useState<string | null>(null);
+  const [err, setErr] = useState<string | null>(null);
+  const [email, setEmail] = useState("");
+
+  async function upgrade(tier: "standard" | "pro") {
+    if (!email) { setErr("Enter the email you signed up with, then click upgrade."); return; }
+    setLoading(tier); setErr(null);
+    try {
+      const r = await fetch("/api/drops/checkout", {
+        method: "POST",
+        headers: { "content-type": "application/json" },
+        body: JSON.stringify({ email, tier }),
+      });
+      const j = await r.json();
+      if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`);
+      if (j.url) window.location.href = j.url;
+      else setErr(j.message || "Upgraded (mock).");
+    } catch (e) { setErr((e as Error).message); }
+    finally { setLoading(null); }
+  }
+
+  return (
+    <div className="mb-6 card p-6">
+      <h2 className="text-2xl font-semibold">Pick a tier</h2>
+      <div className="mt-2 mb-4 flex flex-wrap items-center gap-2">
+        <label className="text-sm text-ink/70">Upgrade as:</label>
+        <input type="email" placeholder="you@business.com" value={email} onChange={(e) => setEmail(e.target.value)} className="w-64" />
+      </div>
+      <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
+        <Plan name="Trial"
+              price="Free · 3 drops"
+              features={["3 items per drop", "basic edition", "no credit card"]}
+              cta={null} />
+        <Plan name="Standard"
+              price="$29/mo"
+              features={["3 items per drop", "curated by editor + Qwen", "full archive access"]}
+              cta={<button onClick={() => upgrade("standard")} disabled={loading !== null} className="btn">{loading === "standard" ? "…" : "Upgrade"}</button>} />
+        <Plan name="Pro"
+              price="$99/mo"
+              highlight
+              features={["10 items per drop", "Pro-only tactical hints", "SWOT preview per item", "full archive + API access"]}
+              cta={<button onClick={() => upgrade("pro")} disabled={loading !== null} className="btn">{loading === "pro" ? "…" : "Upgrade"}</button>} />
+      </div>
+      {err && <div className="mt-3 rounded bg-red-50 p-3 text-sm text-red-700">{err}</div>}
+    </div>
+  );
+}
+
+function Plan({ name, price, features, cta, highlight }: { name: string; price: string; features: string[]; cta: React.ReactNode; highlight?: boolean }) {
+  return (
+    <div className={`card p-5 ${highlight ? "border-brass/60 ring-1 ring-brass/30" : ""}`}>
+      <div className="flex items-baseline justify-between">
+        <h3 className="text-lg font-semibold">{name}</h3>
+        <span className="text-sm text-brass">{price}</span>
+      </div>
+      <ul className="mt-3 space-y-1.5 text-sm text-ink/70">
+        {features.map((f, i) => <li key={i}>· {f}</li>)}
+      </ul>
+      {cta && <div className="mt-4">{cta}</div>}
+    </div>
+  );
+}
+
+function HowItWorks() {
+  return (
+    <div className="mb-6 card p-6">
+      <h2 className="text-2xl font-semibold">How we build each drop</h2>
+      <ol className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-4 text-sm">
+        <li><b>1. Crawl.</b> USPTO TSDR for cancelled marks. US Copyright Office for 95-year clocks. Fashion/DTC sites for unregistered brands + domain expiries.</li>
+        <li><b>2. Score.</b> Distinctiveness, age-fit, domain availability, common-law risk. Top candidates only.</li>
+        <li><b>3. Write.</b> Local LLM drafts editorial blurbs; human editor reviews before send.</li>
+        <li><b>4. Deliver.</b> Inbox at 9am PT. Full archive on your portal. Unsubscribe anytime.</li>
+      </ol>
+    </div>
+  );
+}
+
+function Proof() {
+  const stats = [
+    { k: "30+", v: "abandoned trademarks in the curated catalog" },
+    { k: "1929 cohort", v: "most recent public-domain batch" },
+    { k: "4 labels", v: "domain snipe · abandonment watch · licensing · avoid" },
+    { k: "9am PT", v: "delivered every single day" },
+  ];
+  return (
+    <div className="mb-6 card p-6">
+      <h2 className="text-2xl font-semibold">What you get, in numbers</h2>
+      <dl className="mt-4 grid grid-cols-2 gap-4 md:grid-cols-4">
+        {stats.map((s, i) => (
+          <div key={i}>
+            <dt className="text-2xl font-semibold text-brass">{s.k}</dt>
+            <dd className="text-sm text-ink/70">{s.v}</dd>
+          </div>
+        ))}
+      </dl>
+    </div>
+  );
+}
+
+function WhoFor() {
+  return (
+    <div className="mb-6 card p-6">
+      <h2 className="text-2xl font-semibold">Who reads Drops</h2>
+      <div className="mt-3 grid grid-cols-1 gap-4 md:grid-cols-3 text-sm">
+        <div>
+          <div className="font-semibold text-brass">Brand builders</div>
+          <p className="mt-1 text-ink/80">Looking for a distinctive name with built-in recall? Drops surfaces marks nobody's using. You file; you build.</p>
+        </div>
+        <div>
+          <div className="font-semibold text-brass">Domain investors</div>
+          <p className="mt-1 text-ink/80">The domain-snipe label flags expiring domains paired with brand recognition. Wholesale prices at drop, retail on sellback.</p>
+        </div>
+        <div>
+          <div className="font-semibold text-brass">IP attorneys</div>
+          <p className="mt-1 text-ink/80">Client lookups, FTO reconnaissance, and the licensing-outreach list as a client-development feed.</p>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+function Fine() {
+  return (
+    <div className="text-xs text-ink/60">
+      Nothing in Drops is legal advice. Before registering any mark or buying any domain, talk to a USPTO-registered attorney.
+      Drops flags <em>opportunities</em>, not guarantees — common-law rights survive trademark abandonment and the ACPA applies to bad-faith domain buys.
+      We surface the low-risk side of the ledger and tell you why.
+    </div>
+  );
+}
diff --git a/src/app/drops/unsubscribe/[token]/page.tsx b/src/app/drops/unsubscribe/[token]/page.tsx
new file mode 100644
index 0000000..9f16e69
--- /dev/null
+++ b/src/app/drops/unsubscribe/[token]/page.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useParams } from "next/navigation";
+
+export default function UnsubscribePage() {
+  const params = useParams<{ token: string }>();
+  const [status, setStatus] = useState<"confirm" | "working" | "done" | "error">("confirm");
+  const [err, setErr] = useState<string | null>(null);
+
+  async function unsub() {
+    setStatus("working");
+    try {
+      const r = await fetch(`/api/drops/unsubscribe/${params.token}`, { method: "POST" });
+      if (!r.ok) throw new Error((await r.json()).error || `HTTP ${r.status}`);
+      setStatus("done");
+    } catch (e) { setErr((e as Error).message); setStatus("error"); }
+  }
+
+  useEffect(() => { /* require explicit click */ }, []);
+
+  return (
+    <div className="card p-6 max-w-xl">
+      <h1 className="text-2xl font-semibold">Unsubscribe from Drops</h1>
+      {status === "confirm" && (
+        <>
+          <p className="mt-3 text-sm text-ink/70">Are you sure? Your account will be cancelled immediately and you'll stop receiving drops.</p>
+          <button onClick={unsub} className="btn mt-4">Yes, unsubscribe</button>
+        </>
+      )}
+      {status === "working" && <p className="mt-3 text-sm">Unsubscribing…</p>}
+      {status === "done"    && <p className="mt-3 text-sm text-emerald-700">Done. You've been unsubscribed.</p>}
+      {status === "error"   && <p className="mt-3 text-sm text-red-700">Error: {err}</p>}
+    </div>
+  );
+}
diff --git a/src/app/drops/view/[token]/[dropId]/page.tsx b/src/app/drops/view/[token]/[dropId]/page.tsx
new file mode 100644
index 0000000..36fe3a5
--- /dev/null
+++ b/src/app/drops/view/[token]/[dropId]/page.tsx
@@ -0,0 +1,40 @@
+import { notFound } from "next/navigation";
+import Link from "next/link";
+import { query } from "@/lib/db";
+import { renderDropHTML } from "@/lib/drops";
+
+type Params = { token: string; dropId: string };
+
+export default async function DropView({ params }: { params: Promise<Params> }) {
+  const { token, dropId: dropIdStr } = await params;
+  const dropId = Number(dropIdStr);
+  if (!Number.isFinite(dropId)) notFound();
+
+  const { rows: subs } = await query<{ id: number; tier: string; token: string }>(
+    `SELECT id, tier, token FROM subscribers WHERE token = $1`, [token]
+  );
+  if (!subs.length) notFound();
+  const sub = subs[0];
+
+  // Gate: subscriber must have actually received this drop OR be pro/comp preview.
+  const { rows: delivered } = await query<{ n: string }>(
+    `SELECT COUNT(*)::text AS n FROM deliveries WHERE drop_id = $1 AND subscriber_id = $2`,
+    [dropId, sub.id]
+  );
+  const hasDelivery = Number(delivered[0]?.n ?? 0) > 0;
+  if (!hasDelivery && sub.tier !== "pro" && sub.tier !== "comp") notFound();
+
+  const { html } = await renderDropHTML({
+    dropId,
+    tier: sub.tier as "trial" | "standard" | "pro" | "comp",
+    subscriberToken: sub.token,
+    appBaseUrl: "",
+  });
+
+  return (
+    <div>
+      <div className="mb-3 text-sm"><Link href={`/drops/view/${token}`} className="text-ink/60 hover:text-brass">← Archive</Link></div>
+      <div className="card bg-white p-0 overflow-hidden" dangerouslySetInnerHTML={{ __html: html }} />
+    </div>
+  );
+}
diff --git a/src/app/drops/view/[token]/page.tsx b/src/app/drops/view/[token]/page.tsx
new file mode 100644
index 0000000..15e9ae7
--- /dev/null
+++ b/src/app/drops/view/[token]/page.tsx
@@ -0,0 +1,86 @@
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { query } from "@/lib/db";
+import ReferralWidget from "@/components/ReferralWidget";
+
+type Params = { token: string };
+
+export default async function SubscriberPortal({ params }: { params: Promise<Params> }) {
+  const { token } = await params;
+
+  const { rows: subs } = await query<{
+    id: number; email: string; name: string | null; tier: string; status: string;
+    trial_drops_left: number; signed_up_at: string; last_delivered_at: string | null;
+  }>(
+    `SELECT id, email, name, tier, status, trial_drops_left, signed_up_at, last_delivered_at
+     FROM subscribers WHERE token = $1`, [token]
+  );
+  if (!subs.length) notFound();
+  const sub = subs[0];
+
+  const { rows: drops } = await query<{
+    id: number; drop_date: string; subject: string;
+    sent_at: string | null; intro: string | null;
+  }>(
+    `SELECT d.id, d.drop_date, d.subject, d.sent_at, d.intro
+     FROM drops d
+     JOIN deliveries dv ON dv.drop_id = d.id
+     WHERE dv.subscriber_id = $1
+     ORDER BY d.drop_date DESC`, [sub.id]
+  );
+
+  const { rows: refStats } = await query<{ total: string; rewarded: string }>(
+    `SELECT COUNT(*)::text AS total,
+            COUNT(*) FILTER (WHERE reward_given = TRUE)::text AS rewarded
+     FROM referrals WHERE referrer_token = $1`,
+    [token]
+  );
+  const totalRefs = Number(refStats[0]?.total ?? 0);
+  const rewardedRefs = Number(refStats[0]?.rewarded ?? 0);
+  const untilNext = 3 - (totalRefs - rewardedRefs * 3);
+
+  return (
+    <div>
+      <div className="mb-4 card p-5">
+        <div className="text-xs uppercase tracking-widest text-brass">Your portal</div>
+        <h1 className="mt-1 text-3xl font-semibold">{sub.name || sub.email}</h1>
+        <div className="mt-2 flex flex-wrap items-center gap-3 text-sm">
+          <span className="tag">Tier: {sub.tier}</span>
+          <span className="tag">Status: {sub.status}</span>
+          {sub.tier === "trial" && <span className="tag">{sub.trial_drops_left} trial drops left</span>}
+          <span className="text-ink/60">Signed up {new Date(sub.signed_up_at).toLocaleDateString()}</span>
+          <span className="ml-auto text-xs">
+            <Link href={`/drops/unsubscribe/${token}`} className="text-ink/50 hover:text-red-600">Unsubscribe</Link>
+          </span>
+        </div>
+      </div>
+
+      <ReferralWidget
+        token={token}
+        totalRefs={totalRefs}
+        untilNext={Math.max(0, untilNext)}
+        rewardedRefs={rewardedRefs}
+      />
+
+      <h2 className="text-xl font-semibold mt-6">Drop archive</h2>
+      {drops.length === 0 ? (
+        <div className="mt-3 text-ink/60">No drops delivered yet. Your first arrives at 9am PT.</div>
+      ) : (
+        <ul className="mt-3 space-y-2">
+          {drops.map((d) => (
+            <li key={d.id} className="card p-4">
+              <div className="text-xs text-brass">
+                {new Date(d.drop_date).toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" }).toUpperCase()}
+              </div>
+              <h3 className="font-semibold">{d.subject}</h3>
+              {d.intro && <p className="text-sm text-ink/70"><em>{d.intro}</em></p>}
+              <div className="mt-2">
+                <Link href={`/drops/view/${token}/${d.id}`} className="text-sm text-brass hover:underline">Read drop →</Link>
+              </div>
+            </li>
+          ))}
+        </ul>
+      )}
+    </div>
+  );
+}
diff --git a/src/app/error.tsx b/src/app/error.tsx
new file mode 100644
index 0000000..a006be0
--- /dev/null
+++ b/src/app/error.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import Link from "next/link";
+import { useEffect } from "react";
+
+export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
+  useEffect(() => { console.error("app error", error); }, [error]);
+
+  return (
+    <article className="mx-auto max-w-2xl">
+      <div className="card p-8 text-center">
+        <div className="text-xs uppercase tracking-widest text-red-700">500</div>
+        <h1 className="mt-2 text-3xl font-semibold">Something broke on our side.</h1>
+        <p className="mt-3 text-ink/70">We've been pinged. Meanwhile:</p>
+        <pre className="mt-3 rounded bg-ink/5 p-3 text-left text-xs text-ink/60 overflow-auto max-h-32">
+          {error.message}
+          {error.digest ? `\ndigest: ${error.digest}` : ""}
+        </pre>
+        <div className="mt-6 flex flex-wrap justify-center gap-2">
+          <button onClick={() => reset()} className="btn">Try again</button>
+          <Link href="/drops" className="btn-ghost">Back to Drops</Link>
+          <a href="mailto:drops@agentabrams.com" className="btn-ghost">Email support</a>
+        </div>
+      </div>
+    </article>
+  );
+}
diff --git a/src/app/faq/page.tsx b/src/app/faq/page.tsx
new file mode 100644
index 0000000..5e721f6
--- /dev/null
+++ b/src/app/faq/page.tsx
@@ -0,0 +1,87 @@
+import Link from "next/link";
+
+export const metadata = {
+  title: "FAQ — Drops",
+  description: "Common questions about the Drops daily newsletter.",
+};
+
+type QA = { q: string; a: React.ReactNode };
+
+const FAQ: QA[] = [
+  {
+    q: "Is any of this actually legal?",
+    a: <>Reading about abandoned trademarks is 100% legal. <em>Acting</em> on what you read can be perfectly legal (registering a truly abandoned mark, buying an expired domain) or very much not (registering a mark a small business has been using for years and then demanding payment — that's ACPA territory, $1k–$100k per domain in statutory damages). Drops labels each candidate by which legal play applies. The <b>licensing-outreach</b> label, for instance, means "pitch them services, don't squat them." Always talk to a trademark attorney before filing.</>
+  },
+  {
+    q: "Where does the data come from?",
+    a: <>USPTO TSDR (Trademark Status & Document Retrieval) for cancelled/abandoned records; the US Copyright Office for public-domain entry dates; WHOIS for domain registration + expiry; HTTP and footer scraping for site-activity signals. All public data. We add scoring, classification, and editorial framing on top.</>,
+  },
+  {
+    q: "How fresh is the data?",
+    a: <>USPTO records refresh weekly from their public bulk feed. Domain-lifecycle + site-activity checks run per candidate when it's evaluated. The daily drop always uses the freshest candidates we have.</>,
+  },
+  {
+    q: "What's the difference between Standard and Pro?",
+    a: <>Standard ($29/mo) gets 3 items per drop. Pro ($99/mo) gets 10 items plus a tactical "Pro" paragraph on each — things like which NICE class to file in, which registrar tends to catch drops for that TLD, adjacent marks to watch for opposition risk. Pro also unlocks the full archive and API access.</>,
+  },
+  {
+    q: "Can I cancel?",
+    a: <>Yes, any time. Unsubscribe link is at the bottom of every email and on your subscriber portal. We also offer a 7-day money-back on first paid period — no questions. Full <Link href="/legal/refund" className="underline">refund policy</Link>.</>,
+  },
+  {
+    q: "Do you share my email with anyone?",
+    a: <>No. Stripe sees your billing email, our email provider sees it in order to send you the drop, Cloudflare sees it if you type it on the site. That's it. No ad networks, no data brokers, no third-party analytics on subscriber pages. <Link href="/legal/privacy" className="underline">Full policy</Link>.</>,
+  },
+  {
+    q: "What if I find a brand I want to pursue?",
+    a: <>Three suggestions: (1) Do a freedom-to-operate check — USPTO TESS for current marks in your goods/service class, Google for common-law evidence of use. (2) Check the domain via your registrar. (3) Before spending money, run the candidate past a USPTO-registered attorney. Flat-fee trademark filings are widely available at $500–$1,500 including USPTO fees.</>,
+  },
+  {
+    q: "Can I forward drops to my team?",
+    a: <>For internal use, yes. Please don't republish publicly or repackage as your own newsletter — that's the one thing we ask.</>,
+  },
+  {
+    q: "What happens on holidays / weekends?",
+    a: <>Drops send every day, seven days a week, including holidays. The daily-drop job fires at 9:00 AM Pacific.</>,
+  },
+  {
+    q: "Do you guarantee any candidate is available?",
+    a: <>No. Public records lag reality by days to weeks, and we have no visibility into common-law rights, residual goodwill, or ongoing negotiations. A Drops item is a starting point for your own research, not a pre-cleared opportunity.</>,
+  },
+  {
+    q: "Why should I pay for this if the data is public?",
+    a: <>Because turning a firehose of public data into three high-signal candidates a day costs about eight hours of searching, reading, and cross-checking you won't do. We do it once for everyone. That's the business.</>,
+  },
+  {
+    q: "What's your refund policy?",
+    a: <>7-day money back on first paid period, no questions. After that, cancel to stop future charges — we don't prorate partial months. <Link href="/legal/refund" className="underline">Details</Link>.</>,
+  },
+];
+
+export default function FaqPage() {
+  return (
+    <article className="mx-auto max-w-3xl space-y-4">
+      <div className="card p-6">
+        <div className="text-xs uppercase tracking-widest text-brass">FAQ</div>
+        <h1 className="mt-2 text-3xl font-semibold">Questions people ask before signing up.</h1>
+      </div>
+
+      <div className="card divide-y divide-ink/10">
+        {FAQ.map((item, i) => (
+          <details key={i} className="group p-5 open:bg-brass/5">
+            <summary className="cursor-pointer font-semibold text-ink list-none flex justify-between items-center">
+              <span>{item.q}</span>
+              <span className="text-brass group-open:rotate-45 transition-transform text-xl leading-none">+</span>
+            </summary>
+            <div className="mt-3 text-sm text-ink/80 leading-relaxed">{item.a}</div>
+          </details>
+        ))}
+      </div>
+
+      <div className="card p-6 text-center">
+        <p className="text-sm text-ink/70">Another question?</p>
+        <a href="mailto:drops@agentabrams.com" className="text-brass font-semibold">drops@agentabrams.com</a>
+      </div>
+    </article>
+  );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..a396030
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,34 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+  color-scheme: light dark;
+}
+
+body {
+  @apply bg-parchment text-ink antialiased;
+  font-family: Georgia, "Libre Caslon Text", serif;
+}
+
+.card {
+  @apply rounded-lg border border-ink/10 bg-white/80 shadow-sm backdrop-blur;
+}
+
+.btn {
+  @apply inline-flex items-center gap-2 rounded-md border border-ink/20 bg-ledger px-3 py-1.5 text-sm font-medium text-parchment transition hover:bg-ink disabled:opacity-50;
+}
+
+.btn-ghost {
+  @apply inline-flex items-center gap-2 rounded-md border border-ink/20 bg-white px-3 py-1.5 text-sm text-ink transition hover:bg-ink/5 disabled:opacity-50;
+}
+
+.tag {
+  @apply inline-block rounded border border-ink/15 bg-white px-2 py-0.5 text-xs uppercase tracking-wide text-ink/70;
+}
+
+input, select, textarea {
+  @apply rounded-md border border-ink/20 bg-white px-2 py-1 text-sm;
+}
+
+table { border-collapse: collapse; }
diff --git a/src/app/item/[id]/page.tsx b/src/app/item/[id]/page.tsx
new file mode 100644
index 0000000..ca7cba0
--- /dev/null
+++ b/src/app/item/[id]/page.tsx
@@ -0,0 +1,127 @@
+import { notFound } from "next/navigation";
+import Link from "next/link";
+import { query } from "@/lib/db";
+import NewsFeed from "@/components/NewsFeed";
+import SwotPanel from "@/components/SwotPanel";
+import Disclaimer from "@/components/Disclaimer";
+import type { Item } from "@/lib/types";
+
+type Params = { id: string };
+
+export default async function ItemPage({ params }: { params: Promise<Params> }) {
+  const { id } = await params;
+  const numId = Number(id);
+  if (!Number.isFinite(numId)) notFound();
+
+  const { rows } = await query<Item & { years_expired: number; follow_up_date: string }>(
+    `SELECT *,
+            EXTRACT(YEAR FROM AGE(NOW(), expired_date))::int AS years_expired,
+            (CURRENT_DATE + INTERVAL '6 months')::date AS follow_up_date
+     FROM items WHERE id = $1`,
+    [numId]
+  );
+  if (!rows.length) notFound();
+  const item = rows[0];
+
+  return (
+    <div>
+      <Disclaimer />
+      <Link href="/" className="text-sm text-ink/60 hover:text-brass">← All items</Link>
+      <div className="mt-2 flex flex-wrap items-baseline gap-3">
+        <h1 className="text-3xl font-semibold tracking-tight">{item.name}</h1>
+        <span className="tag">{item.kind}</span>
+        <span className="tag">{item.status}</span>
+        <span className="text-ink/60">
+          Expired {item.expired_date.toString().slice(0, 10)} ({item.years_expired} yrs ago)
+          {" · "}
+          <span title="Revisit date — 6 months from today">Follow-up {item.follow_up_date.toString().slice(0, 10)}</span>
+        </span>
+        {item.composite_score !== null && (
+          <span className="ml-auto rounded bg-ledger px-2 py-1 font-semibold text-parchment">
+            Score {Number(item.composite_score).toFixed(1)}
+          </span>
+        )}
+      </div>
+
+      <div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3">
+        <section className="card p-4 lg:col-span-2">
+          <h2 className="mb-2 text-lg font-semibold">Details</h2>
+          <dl className="grid grid-cols-2 gap-y-2 text-sm">
+            <dt className="text-ink/60">Original owner</dt><dd>{item.original_owner ?? "—"}</dd>
+            <dt className="text-ink/60">Registration #</dt><dd>{item.registration_number ?? "—"}</dd>
+            <dt className="text-ink/60">NICE class</dt><dd>{item.nice_class ?? "—"}</dd>
+            <dt className="text-ink/60">Goods / services</dt><dd>{item.goods_services ?? "—"}</dd>
+            <dt className="text-ink/60">Filed</dt><dd>{item.filed_date?.toString().slice(0, 10) ?? "—"}</dd>
+            <dt className="text-ink/60">Registered</dt><dd>{item.registered_date?.toString().slice(0, 10) ?? "—"}</dd>
+            <dt className="text-ink/60">Source</dt>
+            <dd>
+              {item.source_url ? (
+                <a href={item.source_url} target="_blank" rel="noreferrer" className="text-brass hover:underline">{item.source}</a>
+              ) : (item.source)}
+            </dd>
+          </dl>
+          {item.notes && (
+            <>
+              <h3 className="mt-4 text-sm font-semibold">Notes</h3>
+              <p className="text-sm text-ink/80">{item.notes}</p>
+            </>
+          )}
+        </section>
+
+        <aside className="card p-4">
+          <h2 className="mb-2 text-lg font-semibold">Scoring breakdown</h2>
+          <ScoreBar label="Distinctiveness" v={item.distinctiveness_score} w={25} />
+          <ScoreBar label="Recognition" v={item.recognition_score} w={15} />
+          <ScoreBar label="Monetization breadth" v={item.monetization_breadth} w={10} />
+          <div className="mt-2 text-sm">
+            <span className="text-ink/60">.com available: </span>
+            <b>{item.domain_available === true ? "Yes" : item.domain_available === false ? "No" : "Unknown"}</b>
+          </div>
+          <div className="text-sm">
+            <span className="text-ink/60">Competing active marks: </span>
+            <b>{item.competing_active_marks ?? "—"}</b>
+          </div>
+        </aside>
+
+        <section className="card p-4 lg:col-span-3">
+          <h2 className="mb-2 text-lg font-semibold">Monetization ideas</h2>
+          {item.monetization_ideas?.length ? (
+            <ul className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
+              {item.monetization_ideas.map((idea, i) => (
+                <li key={i} className="rounded border border-ink/10 bg-white p-3">
+                  <div className="font-semibold">{idea.title}</div>
+                  <div className="text-sm text-ink/70">{idea.description}</div>
+                </li>
+              ))}
+            </ul>
+          ) : <div className="text-sm text-ink/50">None recorded.</div>}
+        </section>
+
+        <section className="card p-4 lg:col-span-2">
+          <h2 className="mb-2 text-lg font-semibold">SWOT analysis</h2>
+          <SwotPanel itemId={item.id} />
+        </section>
+
+        <section className="card p-4">
+          <h2 className="mb-2 text-lg font-semibold">News</h2>
+          <NewsFeed query={item.name} />
+        </section>
+      </div>
+    </div>
+  );
+}
+
+function ScoreBar({ label, v, w }: { label: string; v: number | null; w: number }) {
+  const pct = v ?? 0;
+  return (
+    <div className="mb-2">
+      <div className="mb-0.5 flex justify-between text-xs">
+        <span className="text-ink/70">{label} <span className="text-ink/40">(wt {w}%)</span></span>
+        <span className="font-semibold">{v ?? "—"}</span>
+      </div>
+      <div className="h-2 rounded bg-ink/10">
+        <div className="h-2 rounded bg-brass" style={{ width: `${pct}%` }} />
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..ec44f11
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,97 @@
+import "./globals.css";
+import Link from "next/link";
+import type { Metadata } from "next";
+
+const APP_BASE = process.env.APP_BASE_URL || "http://localhost:9770";
+
+export const metadata: Metadata = {
+  metadataBase: new URL(APP_BASE),
+  title: {
+    default: "Drops — Daily trademark & domain opportunities",
+    template: "%s | Drops",
+  },
+  description: "A daily newsletter of abandoned US trademarks, public-domain works, and expiring domains — curated, scored, and ready to build on.",
+  openGraph: {
+    type: "website",
+    url: APP_BASE,
+    title: "Drops — Daily trademark & domain opportunities",
+    description: "Curated daily brief of abandoned trademarks, public-domain works, and expiring domains.",
+    siteName: "Drops",
+  },
+  twitter: {
+    card: "summary_large_image",
+    title: "Drops — Daily trademark & domain opportunities",
+    description: "Daily brief of abandoned trademarks, PD works, and expiring domains.",
+  },
+  robots: { index: true, follow: true },
+};
+
+const JSONLD_ORG = {
+  "@context": "https://schema.org",
+  "@type": "Organization",
+  name: "Drops",
+  url: APP_BASE,
+  email: "drops@agentabrams.com",
+  description: "Daily newsletter of abandoned US trademarks, public-domain works, and expiring domains.",
+};
+
+const JSONLD_WEBAPP = {
+  "@context": "https://schema.org",
+  "@type": "WebApplication",
+  name: "Drops",
+  applicationCategory: "BusinessApplication",
+  offers: [
+    { "@type": "Offer", name: "Trial", price: "0", priceCurrency: "USD" },
+    { "@type": "Offer", name: "Standard", price: "29", priceCurrency: "USD", priceSpecification: { "@type": "UnitPriceSpecification", billingIncrement: 1, unitText: "MON" } },
+    { "@type": "Offer", name: "Pro", price: "99", priceCurrency: "USD", priceSpecification: { "@type": "UnitPriceSpecification", billingIncrement: 1, unitText: "MON" } },
+  ],
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+  return (
+    <html lang="en">
+      <head>
+        <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(JSONLD_ORG) }} />
+        <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(JSONLD_WEBAPP) }} />
+      </head>
+      <body>
+        <header className="border-b border-ink/10 bg-ledger text-parchment">
+          <div className="mx-auto flex max-w-[1600px] items-center gap-6 px-6 py-4">
+            <Link href="/" className="text-xl font-semibold tracking-tight">
+              Trademarks <span className="text-brass">&</span> Copyright
+            </Link>
+            <nav className="ml-auto flex gap-4 text-sm">
+              <Link href="/drops" className="hover:text-brass">Drops</Link>
+              <Link href="/drops/archive" className="hover:text-brass">Archive</Link>
+              <Link href="/pricing" className="hover:text-brass">Pricing</Link>
+              <Link href="/faq" className="hover:text-brass">FAQ</Link>
+              <Link href="/about" className="hover:text-brass">About</Link>
+              <span className="mx-1 text-ink/20">·</span>
+              <Link href="/alerts" className="hover:text-brass opacity-60">Alerts</Link>
+              <Link href="/" className="hover:text-brass opacity-60">Index</Link>
+              <Link href="/unregistered" className="hover:text-brass opacity-60">Hunter</Link>
+              <Link href="/canvas" className="hover:text-brass opacity-60">Canvas</Link>
+              <Link href="/admin/drops" className="hover:text-brass opacity-40">Admin</Link>
+              <Link href="/admin/subscribers" className="hover:text-brass opacity-40">Subs</Link>
+            </nav>
+          </div>
+        </header>
+        <main className="mx-auto max-w-[1600px] px-6 py-6">{children}</main>
+        <footer className="mx-auto max-w-[1600px] px-6 py-8 text-xs text-ink/60">
+          <div className="flex flex-wrap gap-x-4 gap-y-2 mb-2">
+            <Link href="/about" className="hover:text-brass">About</Link>
+            <Link href="/pricing" className="hover:text-brass">Pricing</Link>
+            <Link href="/faq" className="hover:text-brass">FAQ</Link>
+            <Link href="/changelog" className="hover:text-brass">Changelog</Link>
+            <Link href="/legal/terms" className="hover:text-brass">Terms</Link>
+            <Link href="/legal/privacy" className="hover:text-brass">Privacy</Link>
+            <Link href="/legal/refund" className="hover:text-brass">Refunds</Link>
+            <a href="mailto:drops@agentabrams.com" className="hover:text-brass">Contact</a>
+          </div>
+          <strong>Not legal advice.</strong> Abandoned marks can carry common-law rights. Public-domain works may still have derivative-work protections.
+          Consult a USPTO-registered attorney before use.
+        </footer>
+      </body>
+    </html>
+  );
+}
diff --git a/src/app/legal/privacy/page.tsx b/src/app/legal/privacy/page.tsx
new file mode 100644
index 0000000..58c53dd
--- /dev/null
+++ b/src/app/legal/privacy/page.tsx
@@ -0,0 +1,53 @@
+export const metadata = {
+  title: "Privacy Policy — Drops",
+  description: "How Drops collects, uses, and protects your data.",
+};
+
+export default function PrivacyPage() {
+  return (
+    <article className="card mx-auto max-w-3xl p-8">
+      <h1 className="text-3xl font-semibold">Privacy Policy</h1>
+      <p className="text-xs text-ink/50 mt-1">Last updated: April 24, 2026</p>
+
+      <h2 className="mt-6 text-xl font-semibold">What we collect</h2>
+      <ul className="list-disc pl-6 space-y-1">
+        <li><b>Account data:</b> email address, optional name, time-zone hint.</li>
+        <li><b>Subscription data:</b> tier, status, trial drops remaining, Stripe customer ID and subscription ID (if paid).</li>
+        <li><b>Delivery data:</b> which drops we sent you, when they were delivered, whether they were opened (via a 1×1 tracking pixel in the email), and whether they bounced.</li>
+        <li><b>Referral data:</b> if you were referred, the referring subscriber's token; if you refer others, your own referral counter.</li>
+        <li><b>Technical data:</b> IP address + user agent at signup time (for rate-limit + abuse prevention), kept 30 days.</li>
+      </ul>
+
+      <h2 className="mt-6 text-xl font-semibold">What we don't collect</h2>
+      <p>We don't track you across the web. We don't use third-party analytics on subscriber-only pages. We don't sell data. We don't share data with advertisers because we don't have any.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">Who processes your data</h2>
+      <ul className="list-disc pl-6 space-y-1">
+        <li><b>Stripe</b> (payment processing) — receives your email and billing info when you upgrade. Subject to Stripe's own privacy policy.</li>
+        <li><b>Resend</b> or your configured email provider — receives your email address and the rendered drop HTML in order to deliver it.</li>
+        <li><b>Cloudflare</b> — may see your IP address when requests pass through their tunnel/CDN.</li>
+      </ul>
+      <p className="mt-2">Our own hosting stack runs locally and does not ship data to third parties.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">Your rights (GDPR / CCPA style)</h2>
+      <ul className="list-disc pl-6 space-y-1">
+        <li><b>Access:</b> email drops@agentabrams.com, we'll send you the full row we hold.</li>
+        <li><b>Deletion:</b> use the unsubscribe link in any email, or email us. Deletion is permanent and irreversible.</li>
+        <li><b>Portability:</b> we'll export your data as JSON on request.</li>
+        <li><b>Correction:</b> email us.</li>
+      </ul>
+
+      <h2 className="mt-6 text-xl font-semibold">Retention</h2>
+      <p>Active subscriber records kept for the life of the account. After unsubscribe: we hold email + Stripe IDs for 3 years (tax + chargeback window) then purge. IP/user-agent logs: 30 days. Delivery logs: 1 year.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">Children</h2>
+      <p>Drops is not intended for anyone under 18.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">Security</h2>
+      <p>Data at rest: Postgres on a secured machine. In transit: HTTPS everywhere. No plaintext passwords — we don't have passwords, only email + unique token links. If we ever suffer a breach affecting you, we will email you within 72 hours of becoming aware.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">Contact</h2>
+      <p><a href="mailto:drops@agentabrams.com" className="text-brass underline">drops@agentabrams.com</a></p>
+    </article>
+  );
+}
diff --git a/src/app/legal/refund/page.tsx b/src/app/legal/refund/page.tsx
new file mode 100644
index 0000000..124ae5c
--- /dev/null
+++ b/src/app/legal/refund/page.tsx
@@ -0,0 +1,25 @@
+export const metadata = {
+  title: "Refund Policy — Drops",
+  description: "Simple refund policy: 7-day money back, then cancel-anytime.",
+};
+
+export default function RefundPage() {
+  return (
+    <article className="card mx-auto max-w-3xl p-8">
+      <h1 className="text-3xl font-semibold">Refund Policy</h1>
+      <p className="text-xs text-ink/50 mt-1">Last updated: April 24, 2026</p>
+
+      <h2 className="mt-6 text-xl font-semibold">7-day money-back, no questions asked</h2>
+      <p>If you upgrade to a paid tier and decide within 7 days that Drops isn't for you, email <a href="mailto:drops@agentabrams.com" className="text-brass underline">drops@agentabrams.com</a> from your subscriber email and we'll refund the charge in full. You keep access through the end of the originally-billed period unless you ask us to terminate immediately.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">After 7 days</h2>
+      <p>Cancel from your subscriber portal or via the unsubscribe link in any email. Cancellation stops future billing; we do not prorate partial months.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">Chargebacks</h2>
+      <p>Before filing a chargeback, please email us. Most "I didn't mean to subscribe" disputes get refunded the same day without a chargeback. Chargebacks without prior contact will terminate the account and add the email to our no-resignup list.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">Annual / lifetime tiers</h2>
+      <p>We don't offer those yet. If we introduce them, the refund policy will be updated.</p>
+    </article>
+  );
+}
diff --git a/src/app/legal/terms/page.tsx b/src/app/legal/terms/page.tsx
new file mode 100644
index 0000000..796051d
--- /dev/null
+++ b/src/app/legal/terms/page.tsx
@@ -0,0 +1,52 @@
+export const metadata = {
+  title: "Terms of Service — Drops",
+  description: "Terms of Service for the Drops daily newsletter.",
+};
+
+export default function TermsPage() {
+  return (
+    <article className="card mx-auto max-w-3xl p-8 prose-style">
+      <h1 className="text-3xl font-semibold">Terms of Service</h1>
+      <p className="text-xs text-ink/50 mt-1">Last updated: April 24, 2026</p>
+
+      <h2 className="mt-6 text-xl font-semibold">1. What Drops is</h2>
+      <p>Drops is a paid daily newsletter that curates abandoned US trademarks, public-domain works, and domain-lifecycle opportunities. Each issue lists candidate assets with editorial commentary and suggested monetization angles.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">2. Not legal advice</h2>
+      <p>Nothing in Drops is legal advice. Candidate trademarks may carry residual common-law rights, derivative-work protections, trade dress, or rights of publicity that are not visible in public records. Before registering any mark or purchasing any domain referenced in Drops, consult a USPTO-registered trademark attorney. We expressly disclaim liability for decisions made on the basis of Drops content, including but not limited to trademark applications rejected, oppositions filed against you, or damages under the Anticybersquatting Consumer Protection Act (15 U.S.C. §1125(d)).</p>
+
+      <h2 className="mt-6 text-xl font-semibold">3. Subscription, payment, renewal</h2>
+      <p>Drops offers a Free Trial (3 drops), a Standard tier ($29 USD/month), and a Pro tier ($99 USD/month). Paid subscriptions are billed monthly in advance via Stripe and automatically renew until cancelled. You may cancel at any time from your subscriber portal; access continues through the end of the paid period.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">4. Refund policy (link)</h2>
+      <p>See our <a href="/legal/refund" className="text-brass underline">Refund Policy</a>. Short version: first-7-days money-back, no questions asked; after that, cancel to stop future billing.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">5. Content accuracy</h2>
+      <p>We compile Drops from public records: USPTO TSDR, US Copyright Office, WHOIS, and site metadata. Data may be outdated or incomplete. Scores and opportunity labels are estimates produced by a combination of deterministic rules and local-language-model inference. We do not guarantee that any candidate is available to register, purchase, or license. Any decision to act is yours alone.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">6. Acceptable use</h2>
+      <p>You may not use Drops to engage in bad-faith cybersquatting, fraudulent trademark filings, or any activity that would violate the Lanham Act, the ACPA, or equivalent foreign law. Using Drops data to demand payment from a mark's legitimate user is the scenario those laws exist to prevent. If we detect this pattern of use, we will terminate your subscription and retain the paid portion.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">7. Data we keep</h2>
+      <p>Email address, optional name, Stripe customer/subscription IDs, delivery records (sent/opened), and any referrals you generate. We do not sell or share with third parties. See the <a href="/legal/privacy" className="text-brass underline">Privacy Policy</a> for details and deletion requests.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">8. Termination</h2>
+      <p>We may terminate accounts for: (a) chargebacks or payment fraud, (b) abusive automation against our endpoints, (c) use of Drops in support of prohibited conduct in §6. On termination we cease future billing, stop delivery, and retain records as required by tax law.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">9. Liability cap</h2>
+      <p>To the fullest extent permitted by law, our aggregate liability is limited to the amounts you paid in the 12 months preceding the claim. We are not liable for indirect, incidental, or consequential damages.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">10. Governing law + venue</h2>
+      <p>These terms are governed by the laws of the State of California. Any dispute will be resolved by binding arbitration in Los Angeles County, California, under the rules of the American Arbitration Association — except small-claims court, which is always available.</p>
+
+      <h3 className="mt-3 text-lg font-semibold">10a. 30-day arbitration opt-out</h3>
+      <p>You may opt out of arbitration by emailing <a href="mailto:drops@agentabrams.com" className="text-brass underline">drops@agentabrams.com</a> with the subject line <strong>&quot;Arbitration Opt-Out&quot;</strong> within 30 days of first subscribing. The email must come from the address on your subscriber account and include your full name. Opting out does not otherwise affect these Terms; any dispute will instead be resolved in the state or federal courts of Los Angeles County, California.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">11. Changes</h2>
+      <p>We may revise these terms. Material changes will be emailed to active subscribers at least 7 days before taking effect; continued use after the effective date is acceptance.</p>
+
+      <h2 className="mt-6 text-xl font-semibold">12. Contact</h2>
+      <p>Questions: <a href="mailto:drops@agentabrams.com" className="text-brass underline">drops@agentabrams.com</a>.</p>
+    </article>
+  );
+}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx
new file mode 100644
index 0000000..3936d23
--- /dev/null
+++ b/src/app/not-found.tsx
@@ -0,0 +1,21 @@
+import Link from "next/link";
+
+export const metadata = { title: "Not found — 404" };
+
+export default function NotFound() {
+  return (
+    <article className="mx-auto max-w-2xl">
+      <div className="card p-8 text-center">
+        <div className="text-xs uppercase tracking-widest text-brass">404</div>
+        <h1 className="mt-2 text-4xl font-semibold">The page you're looking for has been abandoned.</h1>
+        <p className="mt-3 text-ink/70">Fitting, really. Here's where to go:</p>
+        <div className="mt-6 flex flex-wrap justify-center gap-2">
+          <Link href="/drops" className="btn">Daily drops</Link>
+          <Link href="/pricing" className="btn-ghost">Pricing</Link>
+          <Link href="/about" className="btn-ghost">About</Link>
+          <Link href="/faq" className="btn-ghost">FAQ</Link>
+        </div>
+      </div>
+    </article>
+  );
+}
diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx
new file mode 100644
index 0000000..d321c00
--- /dev/null
+++ b/src/app/opengraph-image.tsx
@@ -0,0 +1,54 @@
+import { ImageResponse } from "next/og";
+
+export const runtime = "edge";
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+export const alt = "Drops — Daily trademark & domain opportunities";
+
+export default function Image() {
+  return new ImageResponse(
+    (
+      <div
+        style={{
+          width: "100%",
+          height: "100%",
+          display: "flex",
+          flexDirection: "column",
+          justifyContent: "space-between",
+          padding: "80px",
+          background: "#f7f4ec",
+          color: "#0b0b0c",
+          fontFamily: "Georgia, serif",
+        }}
+      >
+        <div style={{ display: "flex", flexDirection: "column" }}>
+          <div style={{ fontSize: 24, letterSpacing: 8, color: "#b38f4e", textTransform: "uppercase" }}>
+            DROPS
+          </div>
+          <div style={{ fontSize: 86, fontWeight: 700, lineHeight: 1.05, marginTop: 24 }}>
+            Abandoned trademarks,
+          </div>
+          <div style={{ fontSize: 86, fontWeight: 700, lineHeight: 1.05 }}>
+            public-domain works,
+          </div>
+          <div style={{ fontSize: 86, fontWeight: 700, lineHeight: 1.05, color: "#b38f4e" }}>
+            daily in your inbox.
+          </div>
+        </div>
+
+        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end" }}>
+          <div style={{ fontSize: 30, color: "#35312a" }}>
+            Free trial · $29/mo Standard · $99/mo Pro
+          </div>
+          <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
+            <div style={{
+              width: 24, height: 24, borderRadius: 12, background: "#b38f4e",
+            }} />
+            <div style={{ fontSize: 30, fontWeight: 600 }}>drops.agentabrams.com</div>
+          </div>
+        </div>
+      </div>
+    ),
+    size
+  );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..898c95e
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,18 @@
+import Disclaimer from "@/components/Disclaimer";
+import ItemsTable from "@/components/ItemsTable";
+
+export default function Home() {
+  return (
+    <div>
+      <Disclaimer />
+      <div className="mb-4">
+        <h1 className="text-3xl font-semibold tracking-tight">Opportunity Index</h1>
+        <p className="text-sm text-ink/60">
+          Abandoned US trademarks and public-domain works ranked by composite opportunity score.
+          Click a row to see details; select rows to send to the canvas or run a SWOT on one.
+        </p>
+      </div>
+      <ItemsTable />
+    </div>
+  );
+}
diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx
new file mode 100644
index 0000000..31e1b37
--- /dev/null
+++ b/src/app/pricing/page.tsx
@@ -0,0 +1,129 @@
+import Link from "next/link";
+
+export const metadata = {
+  title: "Pricing — Drops",
+  description: "Free trial, Standard $29/mo, Pro $99/mo. Cancel anytime.",
+};
+
+const FEATURES = [
+  { label: "Daily curated drop", trial: true, standard: true, pro: true },
+  { label: "Items per drop", trial: "3", standard: "3", pro: "10" },
+  { label: "Abandoned trademarks", trial: true, standard: true, pro: true },
+  { label: "Public-domain works", trial: true, standard: true, pro: true },
+  { label: "Unregistered-brand alerts", trial: true, standard: true, pro: true },
+  { label: "Domain-snipe alerts (expiring)", trial: false, standard: true, pro: true },
+  { label: "Editorial scoring + commentary", trial: true, standard: true, pro: true },
+  { label: "Pro tactical hints (NICE class, registrar tricks)", trial: false, standard: false, pro: true },
+  { label: "SWOT preview per item", trial: false, standard: false, pro: true },
+  { label: "Full drop archive", trial: false, standard: true, pro: true },
+  { label: "API access", trial: false, standard: false, pro: true },
+  { label: "7-day money-back", trial: "n/a", standard: true, pro: true },
+];
+
+function Cell({ v }: { v: boolean | string }) {
+  if (v === true) return <span className="text-emerald-700">●</span>;
+  if (v === false) return <span className="text-ink/20">—</span>;
+  return <span className="text-ink/80 text-sm">{v}</span>;
+}
+
+export default function PricingPage() {
+  return (
+    <article className="mx-auto max-w-4xl space-y-6">
+      <div className="card p-8">
+        <div className="text-xs uppercase tracking-widest text-brass">Pricing</div>
+        <h1 className="mt-2 text-3xl font-semibold">Three tiers. Cancel anytime. Pay in USD.</h1>
+      </div>
+
+      <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
+        <PlanCard
+          name="Trial"
+          price="Free"
+          byline="3 drops, then pick a paid tier or walk away."
+          features={["3 items per drop", "Basic edition", "No credit card required"]}
+          ctaHref="/drops"
+          ctaText="Start free trial"
+        />
+        <PlanCard
+          name="Standard"
+          price="$29"
+          byline="Per month. Cancel anytime. 7-day money-back."
+          features={[
+            "3 curated items every day",
+            "Full editorial commentary",
+            "Drop archive access",
+            "Abandoned trademarks + PD works",
+            "Unregistered-brand + domain-snipe alerts",
+          ]}
+          ctaHref="/drops"
+          ctaText="Get Standard"
+        />
+        <PlanCard
+          name="Pro"
+          price="$99"
+          byline="Per month. For builders and brokers."
+          highlight
+          features={[
+            "10 items every day (3× the catalog)",
+            "Pro tactical hints on each item",
+            "SWOT preview per candidate",
+            "NICE-class filing suggestions",
+            "API access + full archive",
+          ]}
+          ctaHref="/drops"
+          ctaText="Get Pro"
+        />
+      </div>
+
+      <div className="card p-6">
+        <h2 className="text-xl font-semibold">What's in each tier, line by line</h2>
+        <div className="mt-4 overflow-x-auto">
+          <table className="min-w-full text-sm">
+            <thead>
+              <tr className="border-b border-ink/10">
+                <th className="py-2 text-left font-semibold text-ink/70">Feature</th>
+                <th className="py-2 text-center font-semibold text-ink/70">Trial</th>
+                <th className="py-2 text-center font-semibold text-ink/70">Standard</th>
+                <th className="py-2 text-center font-semibold text-ink/70">Pro</th>
+              </tr>
+            </thead>
+            <tbody>
+              {FEATURES.map((f, i) => (
+                <tr key={i} className="border-b border-ink/5">
+                  <td className="py-2">{f.label}</td>
+                  <td className="py-2 text-center"><Cell v={f.trial} /></td>
+                  <td className="py-2 text-center"><Cell v={f.standard} /></td>
+                  <td className="py-2 text-center"><Cell v={f.pro} /></td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      <div className="card p-6 text-sm text-ink/70">
+        <b>Frequently asked:</b> Yes, you can cancel any time · Yes, we refund within 7 days · Yes, you can upgrade / downgrade · No, we don't offer annual billing yet · <Link href="/faq" className="text-brass underline">more</Link>.
+      </div>
+    </article>
+  );
+}
+
+function PlanCard({
+  name, price, byline, features, ctaHref, ctaText, highlight,
+}: {
+  name: string; price: string; byline: string; features: string[];
+  ctaHref: string; ctaText: string; highlight?: boolean;
+}) {
+  return (
+    <div className={`card p-6 flex flex-col ${highlight ? "border-brass/60 ring-1 ring-brass/30 bg-brass/5" : ""}`}>
+      <h3 className="text-lg font-semibold">{name}</h3>
+      <div className="mt-1 text-3xl font-semibold text-ink">{price}{price !== "Free" && <span className="text-sm text-ink/60">/mo</span>}</div>
+      <p className="mt-1 text-sm text-ink/60">{byline}</p>
+      <ul className="mt-4 space-y-1.5 text-sm text-ink/80">
+        {features.map((f, i) => <li key={i}>· {f}</li>)}
+      </ul>
+      <div className="mt-auto pt-4">
+        <Link href={ctaHref} className="btn w-full justify-center">{ctaText}</Link>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 0000000..e4561dd
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,16 @@
+import type { MetadataRoute } from "next";
+
+const BASE = process.env.APP_BASE_URL || "http://localhost:9770";
+
+export default function robots(): MetadataRoute.Robots {
+  return {
+    rules: [
+      {
+        userAgent: "*",
+        allow: "/",
+        disallow: ["/admin", "/api", "/drops/view/", "/drops/unsubscribe/"],
+      },
+    ],
+    sitemap: `${BASE}/sitemap.xml`,
+  };
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
new file mode 100644
index 0000000..14097b2
--- /dev/null
+++ b/src/app/sitemap.ts
@@ -0,0 +1,24 @@
+import type { MetadataRoute } from "next";
+
+const BASE = process.env.APP_BASE_URL || "http://localhost:9770";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+  const now = new Date();
+  const paths: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [
+    { path: "/drops",          priority: 1.0, changeFrequency: "daily" },
+    { path: "/drops/archive",  priority: 0.9, changeFrequency: "daily" },
+    { path: "/pricing",        priority: 0.9, changeFrequency: "monthly" },
+    { path: "/faq",            priority: 0.7, changeFrequency: "monthly" },
+    { path: "/about",          priority: 0.6, changeFrequency: "monthly" },
+    { path: "/changelog",      priority: 0.4, changeFrequency: "weekly" },
+    { path: "/legal/terms",    priority: 0.3, changeFrequency: "monthly" },
+    { path: "/legal/privacy",  priority: 0.3, changeFrequency: "monthly" },
+    { path: "/legal/refund",   priority: 0.3, changeFrequency: "monthly" },
+  ];
+  return paths.map((p) => ({
+    url: `${BASE}${p.path}`,
+    lastModified: now,
+    changeFrequency: p.changeFrequency,
+    priority: p.priority,
+  }));
+}
diff --git a/src/app/unregistered/page.tsx b/src/app/unregistered/page.tsx
new file mode 100644
index 0000000..81098e8
--- /dev/null
+++ b/src/app/unregistered/page.tsx
@@ -0,0 +1,47 @@
+import Disclaimer from "@/components/Disclaimer";
+import BrandHunter from "@/components/BrandHunter";
+
+export default function UnregisteredPage() {
+  return (
+    <div>
+      <Disclaimer />
+      <div className="mb-4">
+        <h1 className="text-3xl font-semibold tracking-tight">Unregistered Brand Hunter</h1>
+        <p className="text-sm text-ink/60 max-w-3xl">
+          Independent websites with <em>no apparent federal USPTO trademark registration</em>, at any age.
+          Local <b>Qwen</b> extracts brand info from each homepage and suggests adjacent sites to crawl; Claude
+          verifies USPTO live-registration status. Use the year filter to slice by tenure.
+        </p>
+        <p className="mt-2 text-xs text-amber-800 max-w-3xl">
+          <b>Age context:</b> <b>5+ yrs</b> = common-law rights established in the brand's market; approach as
+          lead-gen or acquisition targets, not filings. <b>&lt;5 yrs</b> = weaker common-law protection, higher
+          urgency for the brand owner to file — genuine squatting risk for them and genuine licensing/outreach
+          opportunity for you.
+        </p>
+      </div>
+      <div className="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4">
+        <div className="card border-blue-300 bg-blue-50 p-3 text-xs">
+          <div className="mb-1 font-bold text-blue-800">Domain snipe</div>
+          Domain is expired or expiring + brand dormant. Register the domain (~$10–$30 at DropCatch/NameJet, up to $$$ at auction).
+          If you also file the mark, USPTO TEAS Standard is <b>$350/class</b> + 12–18 mo to registration.
+        </div>
+        <div className="card border-indigo-300 bg-indigo-50 p-3 text-xs">
+          <div className="mb-1 font-bold text-indigo-800">Abandonment watch</div>
+          Site live but inactive &ge;3 yrs. US law presumes trademark abandonment after 3 yrs of non-use (<i>Lanham §45</i>).
+          Log + revisit; file ITU (intent-to-use) once the clock runs.
+        </div>
+        <div className="card border-emerald-300 bg-emerald-50 p-3 text-xs">
+          <div className="mb-1 font-bold text-emerald-800">Licensing outreach</div>
+          Active brand, never filed. <b>Don&apos;t squat them.</b> Pitch them trademark filing / domain-portfolio management services.
+          Legit $500–$5k/engagement; recurring if they keep the retainer.
+        </div>
+        <div className="card border-red-300 bg-red-50 p-3 text-xs">
+          <div className="mb-1 font-bold text-red-800">Do NOT target</div>
+          Registered + actively used. ACPA (§43(d)) statutory damages $1k–$100k/domain.
+          TTAB cancels bad-faith marks on fraud/intent grounds. Keep away.
+        </div>
+      </div>
+      <BrandHunter />
+    </div>
+  );
+}
diff --git a/src/components/BrandHunter.tsx b/src/components/BrandHunter.tsx
new file mode 100644
index 0000000..ed6e912
--- /dev/null
+++ b/src/components/BrandHunter.tsx
@@ -0,0 +1,403 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import {
+  useReactTable, getCoreRowModel, getSortedRowModel, flexRender,
+  createColumnHelper, type SortingState,
+} from "@tanstack/react-table";
+
+type Brand = {
+  id: number;
+  domain: string;
+  brand_name: string | null;
+  category: string;
+  established_year: number | null;
+  years_in_business: number | null;
+  whois_created_date: string | null;
+  meta_description: string | null;
+  notes: string | null;
+  uspto_checked_at: string | null;
+  uspto_live_marks: number | null;
+  has_federal_registration: boolean | null;
+  uspto_search_url: string | null;
+  suggested_by: string;
+  status: string;
+  error_message: string | null;
+  last_checked_at: string;
+  http_status: number | null;
+  domain_expires_on: string | null;
+  days_to_expiry: number | null;
+  domain_lifecycle: string | null;
+  last_activity_year: number | null;
+  last_activity_signal: string | null;
+  years_since_activity: number | null;
+  abandonment_status: string | null;
+  opportunity_label: string | null;
+};
+
+const col = createColumnHelper<Brand>();
+
+const OPPORTUNITY_META: Record<string, { label: string; color: string; hint: string }> = {
+  domain_snipe: {
+    label: "Domain snipe",
+    color: "bg-blue-600 text-white",
+    hint: "Domain expired or expiring. Register the domain; brand is dormant — buy cleanly.",
+  },
+  abandonment_watch: {
+    label: "Abandonment watch",
+    color: "bg-indigo-600 text-white",
+    hint: "Live domain but no recent activity. 3+ yrs stale = US trademark abandonment presumption may apply.",
+  },
+  licensing_outreach: {
+    label: "Licensing outreach",
+    color: "bg-emerald-700 text-white",
+    hint: "Active brand with no federal registration — legitimate to pitch THEM trademark/SEO/IP services. Not a squat target.",
+  },
+  do_not_target: {
+    label: "Do NOT target",
+    color: "bg-red-700 text-white",
+    hint: "Registered + actively using. ACPA/TTAB risk — keep away.",
+  },
+  unclear: {
+    label: "Unclear",
+    color: "bg-ink/20 text-ink",
+    hint: "Signals insufficient.",
+  },
+};
+
+function opportunityChip(s: string | null) {
+  const meta = OPPORTUNITY_META[s || "unclear"] || OPPORTUNITY_META.unclear;
+  return (
+    <span className={`rounded px-1.5 py-0.5 text-xs font-semibold ${meta.color}`} title={meta.hint}>
+      {meta.label}
+    </span>
+  );
+}
+
+function statusChip(s: string) {
+  const style =
+    s === "verified_unregistered" ? "bg-emerald-600 text-white" :
+    s === "registered"            ? "bg-red-600 text-white" :
+    s === "parked"                ? "bg-purple-600 text-white" :
+    s === "too_new"               ? "bg-amber-500 text-white" :
+    s === "error"                 ? "bg-gray-500 text-white" :
+                                    "bg-ink/10 text-ink";
+  return <span className={`rounded px-1.5 py-0.5 text-xs font-semibold ${style}`}>{s}</span>;
+}
+
+export default function BrandHunter() {
+  const [rows, setRows] = useState<Brand[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [q, setQ] = useState("");
+  const [statusFilter, setStatusFilter] = useState("");
+  const [opportunityFilter, setOpportunityFilter] = useState("");
+  const [minYears, setMinYears] = useState(0);
+  const [maxYears, setMaxYears] = useState<number | "">("");
+  const [sorting, setSorting] = useState<SortingState>([{ id: "years_in_business", desc: true }]);
+
+  const [hunting, setHunting] = useState(false);
+  const [cap, setCap] = useState(20);
+  const [log, setLog] = useState<string[]>([]);
+
+  async function load() {
+    setLoading(true);
+    const url = new URL("/api/brands", window.location.origin);
+    if (q) url.searchParams.set("q", q);
+    if (statusFilter) url.searchParams.set("status", statusFilter);
+    if (opportunityFilter) url.searchParams.set("opportunity", opportunityFilter);
+    url.searchParams.set("minYears", String(minYears));
+    if (maxYears !== "") url.searchParams.set("maxYears", String(maxYears));
+    const r = await fetch(url.toString());
+    const j = await r.json();
+    setRows(j.items || []);
+    setLoading(false);
+  }
+
+  useEffect(() => { load(); /* eslint-disable-next-line */ }, [q, statusFilter, opportunityFilter, minYears, maxYears]);
+
+  async function startHunt() {
+    if (hunting) return;
+    setHunting(true);
+    setLog([]);
+    const r = await fetch("/api/brands/hunt", {
+      method: "POST",
+      headers: { "content-type": "application/json" },
+      body: JSON.stringify({ category: "fashion", maxDiscoveries: cap }),
+    });
+    if (!r.body) { setHunting(false); return; }
+    const reader = r.body.getReader();
+    const dec = new TextDecoder();
+    let buf = "";
+    try {
+      for (;;) {
+        const { done, value } = await reader.read();
+        if (done) break;
+        buf += dec.decode(value, { stream: true });
+        const parts = buf.split("\n\n");
+        buf = parts.pop() ?? "";
+        for (const part of parts) {
+          const line = part.split("\n").find((l) => l.startsWith("data: "));
+          if (!line) continue;
+          const payload = JSON.parse(line.slice(6));
+          const ts = new Date().toLocaleTimeString();
+          let msg = "";
+          switch (payload.kind) {
+            case "info":       msg = `${payload.message}`; break;
+            case "start":      msg = `→ ${payload.domain}`; break;
+            case "fetched":    msg = `  ↓ ${payload.domain}  HTTP ${payload.status}`; break;
+            case "extracted":  msg = `  ⊙ ${payload.domain}  brand="${payload.brand_name}"  est=${payload.established_year ?? "?"}`; break;
+            case "whois":      msg = `  ⊙ ${payload.domain}  whois=${payload.created ?? "—"} (${payload.years ?? "?"}yrs)  expires=${payload.expires ?? "—"}`; break;
+            case "activity":   msg = `  ⊙ ${payload.domain}  last activity=${payload.last_year ?? "?"} (${payload.years_stale ?? "?"}yrs stale · ${payload.status})`; break;
+            case "uspto":      msg = `  ⊙ ${payload.domain}  USPTO live=${payload.live_marks} ${payload.registered ? "REGISTERED" : "unregistered"}`; break;
+            case "label":      msg = `  ★ ${payload.domain}  → ${payload.label}`; break;
+            case "saved":      msg = `  ✓ ${payload.domain}  → ${payload.status}  (${payload.years_in_business ?? "?"}yrs)`; break;
+            case "skipped":    msg = `  · ${payload.domain}  ${payload.reason}`; break;
+            case "discovered": msg = `  + Qwen suggested: ${payload.domains.join(", ")}`; break;
+            case "error":      msg = `  ✗ ${payload.domain}  ${payload.message}`; break;
+            case "done":       msg = `★ done — discovered=${payload.totals.discovered} verified=${payload.totals.verified} errors=${payload.totals.errors}`; break;
+            case "fatal":      msg = `FATAL — ${payload.message}`; break;
+            default:           msg = JSON.stringify(payload);
+          }
+          setLog((L) => [...L.slice(-500), `${ts}  ${msg}`]);
+          if (payload.kind === "saved" || payload.kind === "done") load();
+        }
+      }
+    } finally {
+      setHunting(false);
+    }
+  }
+
+  const columns = useMemo(
+    () => [
+      col.accessor("brand_name", {
+        header: "Brand",
+        cell: (c) => (
+          <div>
+            <div className="font-semibold">{c.getValue() as string || "—"}</div>
+            <a href={`https://${c.row.original.domain}`} target="_blank" rel="noreferrer" className="text-xs text-ink/60 hover:text-brass">
+              {c.row.original.domain}
+            </a>
+          </div>
+        ),
+      }),
+      col.accessor("opportunity_label", {
+        header: "Opportunity",
+        cell: (c) => opportunityChip(c.getValue() as string | null),
+      }),
+      col.accessor("status", { header: "Reg.", cell: (c) => statusChip(c.getValue() as string) }),
+      col.accessor("domain_lifecycle", {
+        header: "Domain",
+        cell: (c) => {
+          const v = c.getValue() as string | null;
+          const days = c.row.original.days_to_expiry;
+          const chip = v === "live"          ? <span className="rounded bg-emerald-600 px-1.5 py-0.5 text-xs text-white">live</span> :
+                       v === "expiring_soon" ? <span className="rounded bg-amber-500 px-1.5 py-0.5 text-xs text-white">expiring {days ?? "?"}d</span> :
+                       v === "expired"       ? <span className="rounded bg-red-600 px-1.5 py-0.5 text-xs text-white">expired</span> :
+                                                <span className="text-ink/40 text-xs">?</span>;
+          return (
+            <div className="flex flex-col">
+              {chip}
+              {c.row.original.domain_expires_on && (
+                <span className="text-[10px] text-ink/50">exp {c.row.original.domain_expires_on.slice(0, 10)}</span>
+              )}
+            </div>
+          );
+        },
+      }),
+      col.accessor("abandonment_status", {
+        header: "Activity",
+        cell: (c) => {
+          const v = c.getValue() as string | null;
+          const yr = c.row.original.last_activity_year;
+          const stale = c.row.original.years_since_activity;
+          const chip = v === "active"     ? <span className="rounded bg-emerald-600 px-1.5 py-0.5 text-xs text-white">active</span> :
+                       v === "stale"      ? <span className="rounded bg-amber-500 px-1.5 py-0.5 text-xs text-white">stale</span> :
+                       v === "abandoned"  ? <span className="rounded bg-red-600 px-1.5 py-0.5 text-xs text-white">abandoned</span> :
+                                             <span className="text-ink/40 text-xs">?</span>;
+          return (
+            <div className="flex flex-col" title={c.row.original.last_activity_signal ?? ""}>
+              {chip}
+              {yr && <span className="text-[10px] text-ink/50">last ©{yr}{stale !== null ? ` · ${stale}yr` : ""}</span>}
+            </div>
+          );
+        },
+      }),
+      col.accessor("years_in_business", {
+        header: "Yrs in Biz",
+        cell: (c) => {
+          const v = c.getValue() as number | null;
+          if (v === null) return <span className="text-ink/30">?</span>;
+          return (
+            <span className={v >= 10 ? "font-bold text-emerald-700" : v >= 5 ? "text-ink" : "text-ink/50"}>
+              {v}
+            </span>
+          );
+        },
+      }),
+      col.accessor("established_year", { header: "Est.", cell: (c) => (c.getValue() as number | null) ?? "—" }),
+      col.accessor("uspto_live_marks", {
+        header: "USPTO live",
+        cell: (c) => {
+          const v = c.getValue() as number | null;
+          const row = c.row.original;
+          if (v === null) return <span className="text-ink/30">—</span>;
+          if (v < 0) return <span className="text-ink/50" title={row.notes ?? "not checked"}>?</span>;
+          return (
+            <a
+              href={row.uspto_search_url ?? "#"}
+              target="_blank"
+              rel="noreferrer"
+              className="text-brass hover:underline"
+              title={row.notes ?? "Claude + USPTO web search"}
+            >
+              {v}
+            </a>
+          );
+        },
+      }),
+      col.accessor("has_federal_registration", {
+        header: "Registered?",
+        cell: (c) => {
+          const v = c.getValue() as boolean | null;
+          if (v === null) return <span className="text-ink/30">—</span>;
+          return v
+            ? <span className="text-red-600">yes</span>
+            : <span className="font-semibold text-emerald-700">no</span>;
+        },
+      }),
+      col.accessor("meta_description", {
+        header: "Description",
+        cell: (c) => <span className="line-clamp-2 text-xs text-ink/70">{c.getValue() as string || "—"}</span>,
+      }),
+      col.accessor("suggested_by", { header: "Src", cell: (c) => <span className="tag">{c.getValue()}</span> }),
+      col.accessor("last_checked_at", {
+        header: "Checked",
+        cell: (c) => <span className="text-xs text-ink/60">{new Date(c.getValue() as string).toLocaleDateString()}</span>,
+      }),
+    ],
+    []
+  );
+
+  const table = useReactTable({
+    data: rows,
+    columns,
+    state: { sorting },
+    onSortingChange: setSorting,
+    getCoreRowModel: getCoreRowModel(),
+    getSortedRowModel: getSortedRowModel(),
+  });
+
+  const verifiedCount = rows.filter((r) => r.status === "verified_unregistered").length;
+
+  return (
+    <div className="space-y-4">
+      <div className="card p-4">
+        <div className="flex flex-wrap items-center gap-3">
+          <div>
+            <div className="text-xs uppercase tracking-wide text-ink/60">Hunter</div>
+            <div className="text-sm">Qwen (local) discovers + extracts · Justia verifies USPTO registration</div>
+          </div>
+          <div className="ml-auto flex items-center gap-2">
+            <label className="text-sm text-ink/70">max per run</label>
+            <input type="number" min={1} max={100} value={cap} onChange={(e) => setCap(Number(e.target.value) || 20)} className="w-20" />
+            <button className="btn" onClick={startHunt} disabled={hunting}>
+              {hunting ? "Hunting…" : "Run hunter"}
+            </button>
+          </div>
+        </div>
+        {log.length > 0 && (
+          <pre className="mt-3 max-h-56 overflow-auto rounded bg-ledger p-3 font-mono text-xs text-parchment">
+            {log.join("\n")}
+          </pre>
+        )}
+      </div>
+
+      <div className="flex flex-wrap items-center gap-3">
+        <input placeholder="Search brand, domain, description…" value={q} onChange={(e) => setQ(e.target.value)} className="w-80" />
+        <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
+          <option value="">Any registration</option>
+          <option value="verified_unregistered">Verified unregistered</option>
+          <option value="registered">Registered</option>
+          <option value="parked">Parked / redirect</option>
+          <option value="pending">Pending</option>
+          <option value="error">Error</option>
+        </select>
+        <select value={opportunityFilter} onChange={(e) => setOpportunityFilter(e.target.value)}>
+          <option value="">Any opportunity</option>
+          <option value="domain_snipe">Domain snipe</option>
+          <option value="abandonment_watch">Abandonment watch</option>
+          <option value="licensing_outreach">Licensing outreach</option>
+          <option value="do_not_target">Do NOT target</option>
+          <option value="unclear">Unclear</option>
+        </select>
+        <div className="flex items-center gap-1 text-sm text-ink/70">
+          <label>yrs</label>
+          <input
+            type="number"
+            min={0}
+            value={minYears}
+            onChange={(e) => setMinYears(Number(e.target.value) || 0)}
+            className="w-16"
+            title="Minimum years in business"
+          />
+          <span>–</span>
+          <input
+            type="number"
+            min={0}
+            placeholder="∞"
+            value={maxYears}
+            onChange={(e) => setMaxYears(e.target.value === "" ? "" : Number(e.target.value))}
+            className="w-16"
+            title="Maximum years in business (blank = no cap)"
+          />
+        </div>
+        <div className="flex gap-1">
+          <button className="btn-ghost text-xs" onClick={() => { setMinYears(5); setMaxYears(""); }} title="Established brands">5+ yrs</button>
+          <button className="btn-ghost text-xs" onClick={() => { setMinYears(0); setMaxYears(5); }} title="Newer brands — no common-law rights built up yet">&lt;5 yrs</button>
+          <button className="btn-ghost text-xs" onClick={() => { setMinYears(10); setMaxYears(""); }} title="Long-running unregistered">10+ yrs</button>
+          <button className="btn-ghost text-xs" onClick={() => { setMinYears(0); setMaxYears(""); }}>All</button>
+        </div>
+        <div className="ml-auto text-sm">
+          <span className="font-semibold text-emerald-700">{verifiedCount}</span> verified unregistered · <span className="text-ink/60">{rows.length} total</span>
+        </div>
+      </div>
+
+      <div className="card overflow-x-auto">
+        <table className="min-w-full text-sm">
+          <thead className="border-b border-ink/10 bg-ink/5 text-left">
+            {table.getHeaderGroups().map((hg) => (
+              <tr key={hg.id}>
+                {hg.headers.map((h) => (
+                  <th
+                    key={h.id}
+                    onClick={h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined}
+                    className="select-none px-3 py-2 font-semibold text-ink/80"
+                    style={{ cursor: h.column.getCanSort() ? "pointer" : "default" }}
+                  >
+                    {flexRender(h.column.columnDef.header, h.getContext())}
+                    {{ asc: " ▲", desc: " ▼" }[h.column.getIsSorted() as string] ?? ""}
+                  </th>
+                ))}
+              </tr>
+            ))}
+          </thead>
+          <tbody>
+            {loading && <tr><td colSpan={columns.length} className="p-6 text-center text-ink/50">Loading…</td></tr>}
+            {!loading && table.getRowModel().rows.length === 0 && (
+              <tr><td colSpan={columns.length} className="p-6 text-center text-ink/50">No candidates yet. Click “Run hunter” to seed the pool.</td></tr>
+            )}
+            {table.getRowModel().rows.map((row) => (
+              <tr key={row.id} className="border-b border-ink/5 hover:bg-brass/5">
+                {row.getVisibleCells().map((cell) => (
+                  <td key={cell.id} className="px-3 py-2 align-top">
+                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                  </td>
+                ))}
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx
new file mode 100644
index 0000000..0d26219
--- /dev/null
+++ b/src/components/Canvas.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { useCallback, useEffect, useMemo, useState } from "react";
+import ReactFlow, {
+  Background, Controls, MiniMap, addEdge, useEdgesState, useNodesState,
+  type Connection, type Edge, type Node, type NodeProps,
+} from "reactflow";
+import "reactflow/dist/style.css";
+
+type ItemLite = {
+  id: number;
+  name: string;
+  kind: string;
+  status: string;
+  composite_score: number | null;
+};
+
+function ScoreHue(n: number | null): string {
+  if (n === null) return "#cbd5e1";
+  const hue = Math.round((n / 100) * 120);
+  return `hsl(${hue} 55% 45%)`;
+}
+
+function ItemNode({ data }: NodeProps<ItemLite>) {
+  return (
+    <div className="rounded-lg border-2 bg-white p-2 shadow-sm" style={{ borderColor: ScoreHue(data.composite_score), minWidth: 180 }}>
+      <div className="text-xs uppercase tracking-wide text-ink/50">{data.kind} · {data.status}</div>
+      <div className="font-semibold">{data.name}</div>
+      {data.composite_score !== null && (
+        <div className="mt-1 inline-block rounded px-1.5 py-0.5 text-xs font-semibold text-white"
+             style={{ backgroundColor: ScoreHue(data.composite_score) }}>
+          {Number(data.composite_score).toFixed(1)}
+        </div>
+      )}
+    </div>
+  );
+}
+
+const nodeTypes = { item: ItemNode };
+
+export default function Canvas() {
+  const [nodes, setNodes, onNodesChange] = useNodesState([]);
+  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+  const [items, setItems] = useState<ItemLite[]>([]);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    const raw = sessionStorage.getItem("canvasIds");
+    const ids: number[] = raw ? JSON.parse(raw) : [];
+    fetch("/api/items")
+      .then((r) => r.json())
+      .then((j) => {
+        const all: ItemLite[] = (j.items || []).map((i: { id: number; name: string; kind: string; status: string; composite_score: number | null }) => ({
+          id: i.id, name: i.name, kind: i.kind, status: i.status,
+          composite_score: i.composite_score,
+        }));
+        const subset = ids.length ? all.filter((i) => ids.includes(i.id)) : all.slice(0, 8);
+        setItems(subset);
+
+        const cols = Math.ceil(Math.sqrt(subset.length));
+        const nn: Node[] = subset.map((it, i) => ({
+          id: String(it.id),
+          type: "item",
+          data: it,
+          position: { x: (i % cols) * 240, y: Math.floor(i / cols) * 140 },
+        }));
+        setNodes(nn);
+      })
+      .finally(() => setLoading(false));
+  }, [setNodes]);
+
+  const onConnect = useCallback(
+    (c: Connection) => setEdges((eds) => addEdge({ ...c, animated: true, style: { stroke: "#b38f4e" } }, eds)),
+    [setEdges]
+  );
+
+  const addMore = useMemo(
+    () => async () => {
+      const r = await fetch("/api/items");
+      const j = await r.json();
+      const existing = new Set(nodes.map((n) => n.id));
+      const all: ItemLite[] = j.items || [];
+      const extra = all.filter((i) => !existing.has(String(i.id))).slice(0, 6);
+      const startY = nodes.length ? Math.max(...nodes.map((n) => n.position.y)) + 180 : 0;
+      const cols = 4;
+      const toAdd: Node[] = extra.map((it, i) => ({
+        id: String(it.id),
+        type: "item",
+        data: it,
+        position: { x: (i % cols) * 240, y: startY + Math.floor(i / cols) * 140 },
+      }));
+      setNodes((nds) => [...nds, ...toAdd]);
+      setItems((its) => [...its, ...extra]);
+    },
+    [nodes, setNodes]
+  );
+
+  return (
+    <div className="h-[calc(100vh-220px)] w-full">
+      <div className="mb-3 flex items-center gap-3">
+        <div className="text-sm text-ink/60">
+          {loading ? "loading…" : `${items.length} nodes`} · drag to rearrange, connect to cluster
+        </div>
+        <button className="btn-ghost ml-auto" onClick={addMore}>+ Add more items</button>
+      </div>
+      <div className="card h-full">
+        <ReactFlow
+          nodes={nodes}
+          edges={edges}
+          onNodesChange={onNodesChange}
+          onEdgesChange={onEdgesChange}
+          onConnect={onConnect}
+          nodeTypes={nodeTypes}
+          fitView
+        >
+          <Background gap={24} />
+          <Controls />
+          <MiniMap pannable zoomable />
+        </ReactFlow>
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/Disclaimer.tsx b/src/components/Disclaimer.tsx
new file mode 100644
index 0000000..bf14c77
--- /dev/null
+++ b/src/components/Disclaimer.tsx
@@ -0,0 +1,26 @@
+"use client";
+import { useState } from "react";
+
+export default function Disclaimer() {
+  const [open, setOpen] = useState(true);
+  if (!open) return null;
+  return (
+    <div className="card mb-4 border-brass/40 bg-brass/10 p-3 text-sm text-ink">
+      <div className="flex items-start gap-3">
+        <div className="flex-1">
+          <strong>Not legal advice.</strong> Scores in this tool estimate <em>apparent</em> risk, not legal clearance.
+          Abandoned USPTO marks can carry common-law rights; "public domain" works can still have derivative-work copyrights
+          (e.g., later film adaptations, illustrations, translations). Trade dress and rights of publicity survive trademark abandonment.
+          Always consult a USPTO-registered trademark attorney before using any mark or reviving any work.
+        </div>
+        <button
+          onClick={() => setOpen(false)}
+          className="text-ink/50 hover:text-ink"
+          aria-label="Dismiss"
+        >
+          ×
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/DropEditor.tsx b/src/components/DropEditor.tsx
new file mode 100644
index 0000000..ad873c6
--- /dev/null
+++ b/src/components/DropEditor.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+type Item = {
+  id: number; position: number;
+  headline: string; blurb: string;
+  pro_extra: string | null; cta_text: string | null; cta_url: string | null;
+  source_kind: string; source_id: number;
+};
+
+export default function DropEditor({ dropId, onClose }: { dropId: number; onClose: () => void }) {
+  const [items, setItems] = useState<Item[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [savingId, setSavingId] = useState<number | null>(null);
+  const [saved, setSaved] = useState<Record<number, boolean>>({});
+
+  useEffect(() => {
+    fetch(`/api/drops/drop-items/${dropId}`)
+      .then((r) => r.json())
+      .then((j) => { setItems(j.items || []); setLoading(false); });
+  }, [dropId]);
+
+  function update(id: number, field: keyof Item, value: string) {
+    setItems((L) => L.map((it) => (it.id === id ? { ...it, [field]: value } : it)));
+    setSaved((s) => ({ ...s, [id]: false }));
+  }
+
+  async function save(it: Item) {
+    setSavingId(it.id);
+    try {
+      const r = await fetch(`/api/drops/drop-items/${it.id}`, {
+        method: "PATCH",
+        headers: { "content-type": "application/json" },
+        body: JSON.stringify({
+          headline: it.headline,
+          blurb: it.blurb,
+          pro_extra: it.pro_extra,
+          cta_text: it.cta_text,
+          cta_url: it.cta_url,
+        }),
+      });
+      if (r.ok) setSaved((s) => ({ ...s, [it.id]: true }));
+    } finally { setSavingId(null); }
+  }
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-start justify-center bg-black/40 p-4 overflow-auto" onClick={onClose}>
+      <div className="card w-full max-w-4xl bg-parchment my-10" onClick={(e) => e.stopPropagation()}>
+        <div className="flex items-center justify-between border-b border-ink/10 px-5 py-3">
+          <h2 className="text-lg font-semibold">Edit drop #{dropId}</h2>
+          <button onClick={onClose} className="btn-ghost">Close</button>
+        </div>
+        {loading ? (
+          <div className="p-6 text-ink/50">Loading…</div>
+        ) : items.length === 0 ? (
+          <div className="p-6 text-ink/50">No items.</div>
+        ) : (
+          <div className="divide-y divide-ink/10">
+            {items.map((it) => (
+              <div key={it.id} className="p-5 space-y-2">
+                <div className="flex items-center gap-3">
+                  <span className="text-xs text-ink/50">#{it.position}</span>
+                  <span className="tag">{it.source_kind}:{it.source_id}</span>
+                  <div className="ml-auto flex items-center gap-2">
+                    {saved[it.id] && <span className="text-xs text-emerald-700">✓ saved</span>}
+                    <button className="btn" disabled={savingId !== null} onClick={() => save(it)}>
+                      {savingId === it.id ? "Saving…" : "Save"}
+                    </button>
+                  </div>
+                </div>
+                <input
+                  value={it.headline}
+                  onChange={(e) => update(it.id, "headline", e.target.value)}
+                  className="w-full font-semibold text-lg"
+                />
+                <textarea
+                  value={it.blurb}
+                  onChange={(e) => update(it.id, "blurb", e.target.value)}
+                  rows={3}
+                  className="w-full text-sm"
+                />
+                <textarea
+                  value={it.pro_extra ?? ""}
+                  onChange={(e) => update(it.id, "pro_extra", e.target.value)}
+                  rows={2}
+                  placeholder="PRO-only extra (tactical hint, leave blank for none)"
+                  className="w-full text-sm bg-brass/10 border-brass/30"
+                />
+                <div className="flex gap-2">
+                  <input
+                    value={it.cta_text ?? ""}
+                    onChange={(e) => update(it.id, "cta_text", e.target.value)}
+                    placeholder="CTA label"
+                    className="w-40"
+                  />
+                  <input
+                    value={it.cta_url ?? ""}
+                    onChange={(e) => update(it.id, "cta_url", e.target.value)}
+                    placeholder="CTA URL"
+                    className="flex-1"
+                  />
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/ItemsTable.tsx b/src/components/ItemsTable.tsx
new file mode 100644
index 0000000..de63c19
--- /dev/null
+++ b/src/components/ItemsTable.tsx
@@ -0,0 +1,318 @@
+"use client";
+
+import { useEffect, useMemo, useState } from "react";
+import {
+  useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel,
+  flexRender, createColumnHelper, type SortingState, type RowSelectionState,
+} from "@tanstack/react-table";
+import Link from "next/link";
+
+type Row = {
+  id: number;
+  kind: "trademark" | "copyright";
+  name: string;
+  original_owner: string | null;
+  nice_class: string | null;
+  goods_services: string | null;
+  status: string;
+  expired_date: string;
+  years_expired: number;
+  follow_up_date: string;
+  distinctiveness_score: number | null;
+  recognition_score: number | null;
+  domain_available: boolean | null;
+  competing_active_marks: number | null;
+  monetization_breadth: number | null;
+  composite_score: number | null;
+};
+
+const col = createColumnHelper<Row>();
+
+function scoreCell(n: number | null) {
+  if (n === null) return <span className="text-ink/30">—</span>;
+  const hue = Math.round((n / 100) * 120); // red→green
+  return (
+    <span
+      className="inline-block rounded px-1.5 py-0.5 text-xs font-semibold text-white"
+      style={{ backgroundColor: `hsl(${hue} 55% 40%)` }}
+      title="Composite score"
+    >
+      {n.toFixed(1)}
+    </span>
+  );
+}
+
+export default function ItemsTable() {
+  const [rows, setRows] = useState<Row[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [q, setQ] = useState("");
+  const [kind, setKind] = useState("");
+  const [status, setStatus] = useState("");
+  const [sorting, setSorting] = useState<SortingState>([{ id: "composite_score", desc: true }]);
+  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
+  const [swotOpen, setSwotOpen] = useState(false);
+  const [swotItemId, setSwotItemId] = useState<number | null>(null);
+
+  useEffect(() => {
+    const url = new URL("/api/items", window.location.origin);
+    if (q) url.searchParams.set("q", q);
+    if (kind) url.searchParams.set("kind", kind);
+    if (status) url.searchParams.set("status", status);
+    setLoading(true);
+    fetch(url.toString())
+      .then((r) => r.json())
+      .then((j) => setRows(j.items || []))
+      .finally(() => setLoading(false));
+  }, [q, kind, status]);
+
+  const columns = useMemo(
+    () => [
+      col.display({
+        id: "select",
+        header: ({ table }) => (
+          <input
+            type="checkbox"
+            checked={table.getIsAllRowsSelected()}
+            ref={(el) => { if (el) el.indeterminate = table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected(); }}
+            onChange={table.getToggleAllRowsSelectedHandler()}
+          />
+        ),
+        cell: ({ row }) => (
+          <input
+            type="checkbox"
+            checked={row.getIsSelected()}
+            onChange={row.getToggleSelectedHandler()}
+          />
+        ),
+      }),
+      col.accessor("composite_score", {
+        header: "Score", cell: (c) => scoreCell(c.getValue() as number | null),
+      }),
+      col.accessor("name", {
+        header: "Name",
+        cell: (c) => (
+          <Link href={`/item/${c.row.original.id}`} className="font-medium text-ink hover:text-brass">
+            {c.getValue() as string}
+          </Link>
+        ),
+      }),
+      col.accessor("kind", { header: "Type", cell: (c) => <span className="tag">{c.getValue()}</span> }),
+      col.accessor("status", { header: "Status", cell: (c) => <span className="tag">{c.getValue()}</span> }),
+      col.accessor("original_owner", { header: "Original Owner" }),
+      col.accessor("nice_class", { header: "Class", cell: (c) => c.getValue() ?? "—" }),
+      col.accessor("expired_date", {
+        header: "Date Expired",
+        cell: (c) => (c.getValue() as string)?.slice(0, 10),
+      }),
+      col.accessor("years_expired", {
+        header: "Yrs Exp.",
+        cell: (c) => <span>{c.getValue() as number}</span>,
+      }),
+      col.accessor("follow_up_date", {
+        header: "Follow-up (+6mo)",
+        cell: (c) => {
+          const d = (c.getValue() as string)?.slice(0, 10);
+          return <span className="text-ink/80" title="Revisit date — 6 months from today">{d}</span>;
+        },
+      }),
+      col.accessor("distinctiveness_score", { header: "Distinct." }),
+      col.accessor("recognition_score", { header: "Recog." }),
+      col.accessor("competing_active_marks", { header: "Competing" }),
+      col.accessor("domain_available", {
+        header: ".com",
+        cell: (c) => {
+          const v = c.getValue() as boolean | null;
+          return v === true ? <span className="text-green-700">●</span> :
+                 v === false ? <span className="text-red-600">○</span> :
+                 <span className="text-ink/30">?</span>;
+        },
+      }),
+      col.accessor("monetization_breadth", { header: "Monet." }),
+    ],
+    []
+  );
+
+  const table = useReactTable({
+    data: rows,
+    columns,
+    state: { sorting, rowSelection },
+    onSortingChange: setSorting,
+    onRowSelectionChange: setRowSelection,
+    getRowId: (r) => String(r.id),
+    getCoreRowModel: getCoreRowModel(),
+    getSortedRowModel: getSortedRowModel(),
+    getFilteredRowModel: getFilteredRowModel(),
+    enableRowSelection: true,
+    enableMultiRowSelection: true,
+  });
+
+  const selectedIds = useMemo(
+    () => Object.entries(rowSelection).filter(([, v]) => v).map(([k]) => Number(k)),
+    [rowSelection]
+  );
+
+  function sendToCanvas() {
+    if (!selectedIds.length) return;
+    sessionStorage.setItem("canvasIds", JSON.stringify(selectedIds));
+    window.location.href = "/canvas";
+  }
+
+  function runSwotFor(id: number) {
+    setSwotItemId(id);
+    setSwotOpen(true);
+  }
+
+  return (
+    <div className="space-y-4">
+      <div className="flex flex-wrap items-center gap-3">
+        <input
+          placeholder="Search name, owner, goods…"
+          value={q}
+          onChange={(e) => setQ(e.target.value)}
+          className="w-80"
+        />
+        <select value={kind} onChange={(e) => setKind(e.target.value)}>
+          <option value="">All types</option>
+          <option value="trademark">Trademark</option>
+          <option value="copyright">Copyright</option>
+        </select>
+        <select value={status} onChange={(e) => setStatus(e.target.value)}>
+          <option value="">All statuses</option>
+          <option value="abandoned">Abandoned</option>
+          <option value="cancelled">Cancelled</option>
+          <option value="expired">Expired</option>
+          <option value="public_domain">Public Domain</option>
+        </select>
+
+        <div className="ml-auto flex items-center gap-2 text-sm">
+          <span className="text-ink/60">{selectedIds.length} selected</span>
+          <button
+            className="btn-ghost"
+            disabled={selectedIds.length !== 1}
+            onClick={() => selectedIds.length === 1 && runSwotFor(selectedIds[0])}
+            title="Run SWOT on the single selected row"
+          >
+            Run SWOT
+          </button>
+          <button
+            className="btn"
+            disabled={selectedIds.length === 0}
+            onClick={sendToCanvas}
+          >
+            Send {selectedIds.length || ""} → Canvas
+          </button>
+        </div>
+      </div>
+
+      <div className="card overflow-x-auto">
+        <table className="min-w-full text-sm">
+          <thead className="border-b border-ink/10 bg-ink/5 text-left">
+            {table.getHeaderGroups().map((hg) => (
+              <tr key={hg.id}>
+                {hg.headers.map((h) => (
+                  <th
+                    key={h.id}
+                    className="select-none px-3 py-2 font-semibold text-ink/80"
+                    onClick={h.column.getCanSort() ? h.column.getToggleSortingHandler() : undefined}
+                    style={{ cursor: h.column.getCanSort() ? "pointer" : "default" }}
+                  >
+                    {flexRender(h.column.columnDef.header, h.getContext())}
+                    {{ asc: " ▲", desc: " ▼" }[h.column.getIsSorted() as string] ?? ""}
+                  </th>
+                ))}
+              </tr>
+            ))}
+          </thead>
+          <tbody>
+            {loading && (
+              <tr><td colSpan={columns.length} className="p-6 text-center text-ink/50">Loading…</td></tr>
+            )}
+            {!loading && table.getRowModel().rows.length === 0 && (
+              <tr><td colSpan={columns.length} className="p-6 text-center text-ink/50">No matches.</td></tr>
+            )}
+            {table.getRowModel().rows.map((row) => (
+              <tr key={row.id} className="border-b border-ink/5 hover:bg-brass/5">
+                {row.getVisibleCells().map((cell) => (
+                  <td key={cell.id} className="px-3 py-2 align-top">
+                    {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                  </td>
+                ))}
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+
+      {swotOpen && swotItemId !== null && (
+        <SwotModal itemId={swotItemId} onClose={() => setSwotOpen(false)} />
+      )}
+    </div>
+  );
+}
+
+function SwotModal({ itemId, onClose }: { itemId: number; onClose: () => void }) {
+  const [loading, setLoading] = useState(true);
+  const [force, setForce] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [quadrants, setQuadrants] = useState<{ quadrant: string; content: string }[]>([]);
+  const [itemName, setItemName] = useState<string>("");
+
+  useEffect(() => {
+    fetch(`/api/items/${itemId}`).then((r) => r.json()).then((j) => setItemName(j.item?.name ?? ""));
+  }, [itemId]);
+
+  useEffect(() => {
+    setLoading(true);
+    setError(null);
+    fetch("/api/swot", {
+      method: "POST",
+      headers: { "content-type": "application/json" },
+      body: JSON.stringify({ itemId, force }),
+    })
+      .then(async (r) => {
+        const j = await r.json();
+        if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`);
+        return j;
+      })
+      .then((j) => setQuadrants(j.quadrants || []))
+      .catch((e) => setError(String(e.message || e)))
+      .finally(() => setLoading(false));
+  }, [itemId, force]);
+
+  const byQ = (q: string) => quadrants.find((x) => x.quadrant === q)?.content ?? "";
+  const colorOf: Record<string, string> = {
+    strength: "bg-green-50 border-green-300",
+    weakness: "bg-red-50 border-red-300",
+    opportunity: "bg-blue-50 border-blue-300",
+    threat: "bg-amber-50 border-amber-300",
+  };
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
+      <div className="card w-full max-w-5xl max-h-[90vh] overflow-auto bg-parchment p-5" onClick={(e) => e.stopPropagation()}>
+        <div className="mb-3 flex items-center justify-between">
+          <div>
+            <h2 className="text-xl font-semibold">SWOT — {itemName}</h2>
+            <p className="text-xs text-ink/60">4 Claude sub-agents, one per quadrant. Cached after first run.</p>
+          </div>
+          <div className="flex gap-2">
+            <button className="btn-ghost" onClick={() => setForce(true)} disabled={loading}>Re-run</button>
+            <button className="btn-ghost" onClick={onClose}>Close</button>
+          </div>
+        </div>
+        {loading && <div className="p-8 text-center text-ink/50">Running 4 agents in parallel…</div>}
+        {error && <div className="p-4 text-red-700">{error}</div>}
+        {!loading && !error && (
+          <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
+            {(["strength", "weakness", "opportunity", "threat"] as const).map((q) => (
+              <div key={q} className={`card border-2 p-3 ${colorOf[q]}`}>
+                <div className="mb-1 text-xs font-bold uppercase tracking-wide">{q}</div>
+                <pre className="whitespace-pre-wrap font-sans text-sm">{byQ(q) || "—"}</pre>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/NewsFeed.tsx b/src/components/NewsFeed.tsx
new file mode 100644
index 0000000..ffe5784
--- /dev/null
+++ b/src/components/NewsFeed.tsx
@@ -0,0 +1,34 @@
+"use client";
+import { useEffect, useState } from "react";
+
+type N = { title: string; link: string; pubDate: string; source: string };
+
+export default function NewsFeed({ query }: { query: string }) {
+  const [items, setItems] = useState<N[]>([]);
+  const [loading, setLoading] = useState(true);
+  useEffect(() => {
+    setLoading(true);
+    fetch(`/api/news?q=${encodeURIComponent(query)}`)
+      .then((r) => r.json())
+      .then((j) => setItems(j.items || []))
+      .finally(() => setLoading(false));
+  }, [query]);
+
+  if (loading) return <div className="text-sm text-ink/50">Loading news…</div>;
+  if (!items.length) return <div className="text-sm text-ink/50">No recent news.</div>;
+
+  return (
+    <ul className="space-y-2 text-sm">
+      {items.map((n, i) => (
+        <li key={i} className="border-b border-ink/5 pb-2 last:border-0">
+          <a href={n.link} target="_blank" rel="noreferrer" className="font-medium text-ink hover:text-brass">
+            {n.title}
+          </a>
+          <div className="text-xs text-ink/50">
+            {n.source} · {n.pubDate ? new Date(n.pubDate).toLocaleDateString() : ""}
+          </div>
+        </li>
+      ))}
+    </ul>
+  );
+}
diff --git a/src/components/ReferralWidget.tsx b/src/components/ReferralWidget.tsx
new file mode 100644
index 0000000..c668a10
--- /dev/null
+++ b/src/components/ReferralWidget.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+export default function ReferralWidget({
+  token, totalRefs, untilNext, rewardedRefs,
+}: {
+  token: string; totalRefs: number; untilNext: number; rewardedRefs: number;
+}) {
+  const [url, setUrl] = useState("");
+  const [copied, setCopied] = useState(false);
+  useEffect(() => {
+    setUrl(`${window.location.origin}/drops?ref=${encodeURIComponent(token)}`);
+  }, [token]);
+
+  function copy() {
+    navigator.clipboard.writeText(url);
+    setCopied(true);
+    setTimeout(() => setCopied(false), 2000);
+  }
+
+  const shareLines = [
+    { label: "Twitter", href: `https://twitter.com/intent/tweet?text=${encodeURIComponent("Daily brief on abandoned trademarks + expiring domains. Worth a look:")}&url=${encodeURIComponent(url)}` },
+    { label: "Email",   href: `mailto:?subject=${encodeURIComponent("Drops — daily trademark opportunities")}&body=${encodeURIComponent(`I've been reading this. 3 free drops to start:\n\n${url}`)}` },
+    { label: "LinkedIn", href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}` },
+  ];
+
+  return (
+    <div className="card p-5 mb-4 border-brass/30 bg-brass/5">
+      <div className="flex items-start gap-4 flex-wrap">
+        <div className="flex-1 min-w-[280px]">
+          <div className="text-xs uppercase tracking-widest text-brass">Referrals</div>
+          <h3 className="mt-1 text-lg font-semibold">Share Drops, get 30 free days.</h3>
+          <p className="mt-1 text-sm text-ink/70">
+            For every 3 signups from your link, we add 30 days to your subscription.
+          </p>
+          <div className="mt-3 flex flex-wrap items-center gap-2">
+            <input readOnly value={url} className="flex-1 min-w-[220px] font-mono text-xs" />
+            <button onClick={copy} className="btn">{copied ? "✓ copied" : "Copy"}</button>
+          </div>
+          <div className="mt-2 flex gap-3 text-xs">
+            {shareLines.map((s) => (
+              <a key={s.label} href={s.href} target="_blank" rel="noreferrer" className="text-brass hover:underline">{s.label}</a>
+            ))}
+          </div>
+        </div>
+        <div className="text-right">
+          <div className="text-xs uppercase tracking-wide text-ink/60">Referrals</div>
+          <div className="text-3xl font-semibold">{totalRefs}</div>
+          <div className="text-xs text-ink/60">
+            {untilNext > 0 ? `${untilNext} until next reward` : "reward pending"}
+          </div>
+          {rewardedRefs > 0 && (
+            <div className="mt-2 text-xs text-emerald-700">
+              ✓ {rewardedRefs} reward{rewardedRefs === 1 ? "" : "s"} claimed
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/components/SwotPanel.tsx b/src/components/SwotPanel.tsx
new file mode 100644
index 0000000..1742486
--- /dev/null
+++ b/src/components/SwotPanel.tsx
@@ -0,0 +1,62 @@
+"use client";
+import { useState } from "react";
+
+type Quad = { quadrant: string; content: string };
+const COLORS: Record<string, string> = {
+  strength: "bg-green-50 border-green-300",
+  weakness: "bg-red-50 border-red-300",
+  opportunity: "bg-blue-50 border-blue-300",
+  threat: "bg-amber-50 border-amber-300",
+};
+
+export default function SwotPanel({ itemId }: { itemId: number }) {
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [quadrants, setQuadrants] = useState<Quad[]>([]);
+  const [ran, setRan] = useState(false);
+
+  async function run(force = false) {
+    setLoading(true); setError(null);
+    try {
+      const r = await fetch("/api/swot", {
+        method: "POST",
+        headers: { "content-type": "application/json" },
+        body: JSON.stringify({ itemId, force }),
+      });
+      const j = await r.json();
+      if (!r.ok) throw new Error(j.error || `HTTP ${r.status}`);
+      setQuadrants(j.quadrants || []);
+      setRan(true);
+    } catch (e: unknown) {
+      setError(e instanceof Error ? e.message : String(e));
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  const byQ = (q: string) => quadrants.find((x) => x.quadrant === q)?.content ?? "";
+
+  return (
+    <div>
+      <div className="mb-3 flex items-center gap-2">
+        <button className="btn" disabled={loading} onClick={() => run(false)}>
+          {loading ? "Running 4 agents…" : ran ? "Reload SWOT" : "Run SWOT (4 agents)"}
+        </button>
+        {ran && (
+          <button className="btn-ghost" disabled={loading} onClick={() => run(true)}>Re-run fresh</button>
+        )}
+      </div>
+      {error && <div className="mb-2 rounded bg-red-50 p-2 text-sm text-red-700">{error}</div>}
+      {ran && (
+        <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
+          {(["strength", "weakness", "opportunity", "threat"] as const).map((q) => (
+            <div key={q} className={`card border-2 p-3 ${COLORS[q]}`}>
+              <div className="mb-1 text-xs font-bold uppercase tracking-wide">{q}</div>
+              <pre className="whitespace-pre-wrap font-sans text-sm">{byQ(q) || "—"}</pre>
+            </div>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/src/lib/brandHunter.ts b/src/lib/brandHunter.ts
new file mode 100644
index 0000000..3d955d0
--- /dev/null
+++ b/src/lib/brandHunter.ts
@@ -0,0 +1,497 @@
+/**
+ * Brand hunter — finds websites in business 5+ years with NO federal trademark registration.
+ *
+ * Pipeline, per candidate domain:
+ *   1. Fetch homepage HTML (HEAD + GET with timeout).
+ *   2. Qwen extracts brand_name, established_year, meta_description from HTML excerpt.
+ *   3. `whois` CLI for domain created date (fallback if model's established_year missing).
+ *   4. Justia Trademarks search for live federal registrations of the brand name.
+ *   5. Qwen proposes 5 adjacent fashion-site domains for the discovery queue.
+ *   6. Row persisted to brand_candidates.
+ */
+
+import { execFile as _execFile } from "node:child_process";
+import { promisify } from "node:util";
+import { query } from "./db";
+import { qwenJSON } from "./qwen";
+import { webSearch } from "./websearch";
+import {
+  domainLifecycle, abandonmentStatus, opportunityLabel, extractLatestYear,
+} from "./lifecycle";
+
+const execFile = promisify(_execFile);
+
+export type HunterEvent =
+  | { kind: "start"; domain: string }
+  | { kind: "fetched"; domain: string; status: number }
+  | { kind: "extracted"; domain: string; brand_name: string | null; established_year: number | null }
+  | { kind: "whois"; domain: string; created: string | null; years: number | null; expires: string | null }
+  | { kind: "activity"; domain: string; last_year: number | null; years_stale: number | null; status: string }
+  | { kind: "uspto"; domain: string; live_marks: number; registered: boolean }
+  | { kind: "label"; domain: string; label: string }
+  | { kind: "saved"; domain: string; status: string; years_in_business: number | null; id: number }
+  | { kind: "skipped"; domain: string; reason: string }
+  | { kind: "discovered"; parent: string; domains: string[] }
+  | { kind: "error"; domain: string; message: string }
+  | { kind: "done"; totals: { discovered: number; verified: number; errors: number } };
+
+const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
+
+async function fetchHTML(url: string, timeoutMs = 12000): Promise<{ status: number; html: string }> {
+  const ctrl = new AbortController();
+  const t = setTimeout(() => ctrl.abort(), timeoutMs);
+  try {
+    const r = await fetch(url, {
+      headers: { "user-agent": UA, accept: "text/html,application/xhtml+xml" },
+      signal: ctrl.signal,
+      redirect: "follow",
+    });
+    const full = await r.text();
+    // Keep head (metadata) + tail (footer). Modern sites bury © in the footer
+    // which gets truncated when we cap from the start.
+    const head = full.slice(0, 80000);
+    const tail = full.length > 80000 ? "\n<!--TAIL-->\n" + full.slice(-40000) : "";
+    return { status: r.status, html: head + tail };
+  } finally {
+    clearTimeout(t);
+  }
+}
+
+function pickExcerpt(html: string): string {
+  // Prefer <title>, <meta description>, and visible header text.
+  const title = /<title[^>]*>([^<]*)<\/title>/i.exec(html)?.[1]?.trim() ?? "";
+  const desc = /<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i.exec(html)?.[1] ?? "";
+  const ogTitle = /<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["']/i.exec(html)?.[1] ?? "";
+  const ogSite = /<meta[^>]+property=["']og:site_name["'][^>]+content=["']([^"']+)["']/i.exec(html)?.[1] ?? "";
+  const h1 = /<h1[^>]*>([\s\S]*?)<\/h1>/i.exec(html)?.[1]?.replace(/<[^>]+>/g, "").trim() ?? "";
+  const footer = /<footer[\s\S]{0,3000}<\/footer>/i.exec(html)?.[0]?.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").slice(0, 800) ?? "";
+  return [
+    `TITLE: ${title}`,
+    `OG_SITE: ${ogSite}`,
+    `OG_TITLE: ${ogTitle}`,
+    `META_DESC: ${desc}`,
+    `H1: ${h1}`,
+    `FOOTER: ${footer}`,
+  ].join("\n");
+}
+
+async function extractBrandWithQwen(domain: string, excerpt: string): Promise<{
+  brand_name: string | null;
+  established_year: number | null;
+  meta_description: string | null;
+}> {
+  const prompt = `You are extracting brand info from a website.
+Domain: ${domain}
+
+Page excerpt:
+${excerpt}
+
+Return STRICT JSON with exactly these keys:
+{
+  "brand_name": string,          // the brand/company name as it presents itself
+  "established_year": number|null, // 4-digit year the business was founded if stated (look for "Since", "Est.", "©", copyright years — pick the earliest credible founding year), else null
+  "meta_description": string     // 1 sentence on what the brand sells
+}`;
+  try {
+    return await qwenJSON(prompt);
+  } catch {
+    return { brand_name: null, established_year: null, meta_description: null };
+  }
+}
+
+async function proposeMoreDomains(category: string, knownDomains: string[]): Promise<string[]> {
+  const prompt = `You help discover independent ${category} websites.
+Already scanned: ${knownDomains.slice(0, 40).join(", ")}.
+
+Propose 8 DIFFERENT real independent ${category} brand websites likely to have been in business 5+ years, but NOT major brands (avoid Nike, Zara, Gucci, Adidas, etc.). Prefer DTC, niche, or independent labels.
+
+Respond with STRICT JSON in this exact shape:
+{ "domains": ["example1.com","example2.com", ...] }
+Only bare domains (no https, no www, no paths).`;
+  try {
+    const obj = await qwenJSON<{ domains?: string[] }>(prompt, 0.7);
+    const arr = Array.isArray(obj?.domains) ? obj.domains : [];
+    return arr
+      .map((d) => String(d).trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").toLowerCase())
+      .filter((d) => /^[a-z0-9][a-z0-9-]*\.[a-z]{2,}(\.[a-z]{2,})?$/.test(d));
+  } catch {
+    return [];
+  }
+}
+
+async function whoisInfo(domain: string): Promise<{ created: Date | null; expires: Date | null }> {
+  try {
+    const { stdout } = await execFile("whois", [domain], { timeout: 12000 });
+    const pickDate = (patterns: RegExp[]) => {
+      for (const re of patterns) {
+        const m = re.exec(stdout);
+        if (m) {
+          const d = new Date(m[1]);
+          if (!isNaN(d.getTime())) return d;
+        }
+      }
+      return null;
+    };
+    const created = pickDate([
+      /Creation Date:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /Created On:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /created:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /Registered:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+    ]);
+    const expires = pickDate([
+      /Registry Expiry Date:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /Registrar Registration Expiration Date:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /Expiration Date:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /Expiry Date:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /expires:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+      /Renewal date:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i,
+    ]);
+    return { created, expires };
+  } catch {
+    return { created: null, expires: null };
+  }
+}
+
+/**
+ * USPTO registration check — 100% free, no paid APIs.
+ *   1. Multi-engine web search (DDG → DDG Lite → Brave), first engine with results wins.
+ *   2. Feed result snippets to local Qwen.
+ *   3. Qwen returns structured JSON with live/dead classification + serial numbers.
+ */
+async function checkUspto(brandName: string, domain: string): Promise<{
+  live_marks: number;
+  has_registration: boolean;
+  url: string;
+  evidence: string;
+  unchecked: boolean;
+  engine: string | null;
+}> {
+  const q = `"${brandName}" trademark USPTO registration status`;
+  const searchUrl = `https://duckduckgo.com/?q=${encodeURIComponent(q)}`;
+  const result = await webSearch(q);
+
+  if (!result.snippets.length) {
+    return {
+      live_marks: -1,
+      has_registration: false,
+      url: searchUrl,
+      evidence: `all search engines failed: ${result.error ?? "no results"}`,
+      unchecked: true,
+      engine: null,
+    };
+  }
+
+  const snippetText = result.snippets.slice(0, 8)
+    .map((s, i) => `[${i + 1}] ${s.title}\n    ${s.url}\n    ${s.body}`)
+    .join("\n");
+
+  const prompt = `You are a trademark-status classifier. Given search-engine result snippets about a brand,
+decide whether the brand "${brandName}" (website: ${domain}) has a LIVE federal US trademark registration at the USPTO.
+
+"Live" means: a registration that is not cancelled, abandoned, expired, or dead.
+Key phrases indicating LIVE: "Registered", "Section 8-Accepted", "Section 15-Accepted", "Live".
+Key phrases indicating DEAD: "Cancelled", "Abandoned", "Expired", "Dead", "Section 8-Cancelled".
+A pending *application* (no registration number yet) is NOT a live registration — count it as 0.
+Only count registrations whose registrant plausibly matches the brand at ${domain}; ignore unrelated same-name marks.
+
+Snippets:
+${snippetText}
+
+Respond with STRICT JSON:
+{
+  "has_live_registration": boolean,
+  "live_mark_count": number,
+  "registrant": string|null,
+  "evidence": string,
+  "serial_numbers": string[]
+}`;
+  type Ans = {
+    has_live_registration?: boolean;
+    live_mark_count?: number;
+    registrant?: string | null;
+    evidence?: string;
+    serial_numbers?: string[];
+  };
+  try {
+    const ans = await qwenJSON<Ans>(prompt, 0.1);
+    const live = !!ans.has_live_registration;
+    const count = Number.isFinite(ans.live_mark_count) ? Number(ans.live_mark_count) : (live ? 1 : 0);
+    const evidence = [
+      ans.evidence ?? "",
+      ans.registrant ? `Registrant: ${ans.registrant}` : "",
+      ans.serial_numbers?.length ? `Serials: ${ans.serial_numbers.join(", ")}` : "",
+      `[via ${result.engine}]`,
+    ].filter(Boolean).join(" · ").slice(0, 500);
+    return {
+      live_marks: count,
+      has_registration: live,
+      url: searchUrl,
+      evidence,
+      unchecked: false,
+      engine: result.engine,
+    };
+  } catch (e) {
+    return {
+      live_marks: -1,
+      has_registration: false,
+      url: searchUrl,
+      evidence: `classifier failed: ${(e as Error).message}`,
+      unchecked: true,
+      engine: result.engine,
+    };
+  }
+}
+
+/**
+ * Brand↔domain alignment sanity check.
+ * When a domain is parked / redirects to a registrar / points to Shopify's login, Qwen often extracts
+ * the hosting service's brand ("Hover", "Shopify") instead of the site's own brand. If the extracted
+ * brand has no token overlap with the domain, we flag it as a parked/redirect and skip the USPTO check.
+ */
+function isBrandAlignedWithDomain(brand: string | null, domain: string): boolean {
+  if (!brand) return false;
+  const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
+  const domainCore = normalize(domain.split(".")[0]);
+  const brandCore = normalize(brand);
+  if (!domainCore || !brandCore) return false;
+  // Substring match in either direction — handles "BuckMason" ↔ "buckmason", "Cuyana" ↔ "cuyana".
+  if (domainCore.includes(brandCore) || brandCore.includes(domainCore)) return true;
+  // Token overlap — handles "Dead Letter Press" ↔ "deadletterpress".
+  const brandTokens = brand.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
+  return brandTokens.some((t) => domainCore.includes(t));
+}
+
+export type HunterOptions = {
+  category?: string;
+  seedDomains: string[];
+  maxDiscoveries?: number;     // cap total new candidates per run
+  minYearsInBusiness?: number; // default 5
+  onEvent?: (e: HunterEvent) => void;
+};
+
+export async function runBrandHunter(opts: HunterOptions): Promise<{
+  discovered: number;
+  verified: number;
+  errors: number;
+}> {
+  const category = opts.category ?? "fashion";
+  const cap = opts.maxDiscoveries ?? 30;
+  const emit = opts.onEvent ?? (() => {});
+  // minYearsInBusiness retained for future use; currently everything is checked regardless.
+  void opts.minYearsInBusiness;
+
+  const queue = [...new Set(opts.seedDomains.map((d) => d.toLowerCase()))];
+  const processed = new Set<string>();
+  let discovered = 0, verified = 0, errors = 0;
+
+  // Skip anything already in the DB.
+  const { rows: existing } = await query<{ domain: string }>(`SELECT domain FROM brand_candidates`);
+  const known = new Set(existing.map((r) => r.domain));
+
+  while (queue.length && discovered < cap) {
+    const domain = queue.shift()!;
+    if (processed.has(domain)) continue;
+    processed.add(domain);
+    if (known.has(domain)) { emit({ kind: "skipped", domain, reason: "already in db" }); continue; }
+
+    // Pace DDG requests — 3-5s between candidates avoids the anomaly page.
+    if (processed.size > 1) await new Promise((res) => setTimeout(res, 3000 + Math.random() * 2000));
+
+    emit({ kind: "start", domain });
+    try {
+      // 1. Fetch homepage.
+      let html = "";
+      let status = 0;
+      try {
+        const resp = await fetchHTML(`https://${domain}`);
+        status = resp.status; html = resp.html;
+        emit({ kind: "fetched", domain, status });
+      } catch {
+        try {
+          const resp = await fetchHTML(`http://${domain}`);
+          status = resp.status; html = resp.html;
+          emit({ kind: "fetched", domain, status });
+        } catch (e) {
+          throw new Error(`fetch failed: ${(e as Error).message}`);
+        }
+      }
+
+      // 2. Qwen extract.
+      const excerpt = pickExcerpt(html);
+      const ex = await extractBrandWithQwen(domain, excerpt);
+      emit({ kind: "extracted", domain, brand_name: ex.brand_name, established_year: ex.established_year });
+
+      const brandName = ex.brand_name || domain.split(".")[0];
+
+      // 3. WHOIS (created + expiry).
+      const who = await whoisInfo(domain);
+      const establishedYear = ex.established_year ?? (who.created ? who.created.getFullYear() : null);
+      const yearsInBiz = establishedYear ? new Date().getFullYear() - establishedYear : null;
+      emit({
+        kind: "whois",
+        domain,
+        created: who.created ? who.created.toISOString().slice(0, 10) : null,
+        years: yearsInBiz,
+        expires: who.expires ? who.expires.toISOString().slice(0, 10) : null,
+      });
+
+      // 3b. Activity clock — scan the full HTML (not just excerpt) for the latest year signal.
+      const activity = extractLatestYear(html);
+      const currentYear = new Date().getFullYear();
+      const yearsSinceActivity = activity.year !== null ? currentYear - activity.year : null;
+      const abStatus = abandonmentStatus(yearsSinceActivity);
+      emit({
+        kind: "activity",
+        domain,
+        last_year: activity.year,
+        years_stale: yearsSinceActivity,
+        status: abStatus,
+      });
+
+      // 3c. Domain lifecycle label.
+      const lifecycle = domainLifecycle(status, who.expires);
+
+      // NOTE: no early exit on age — every candidate gets the USPTO check regardless of
+      // years_in_business. Age lives on the row as a sortable/filterable column, not a gate.
+      // Brands under 5 yrs are arguably MORE exposed (no common-law rights built up yet).
+
+      // Sanity gate: parked / registrar-redirect pages confuse the extractor. If the extracted
+      // brand name has no alignment with the domain, skip USPTO and flag as 'parked'.
+      const aligned = isBrandAlignedWithDomain(ex.brand_name, domain);
+
+      let u: Awaited<ReturnType<typeof checkUspto>> | null = null;
+      let finalStatus: string;
+
+      if (!aligned) {
+        finalStatus = "parked";
+        emit({
+          kind: "uspto",
+          domain,
+          live_marks: -1,
+          registered: false,
+        });
+      } else {
+        u = await checkUspto(brandName, domain);
+        emit({ kind: "uspto", domain, live_marks: u.live_marks, registered: u.has_registration });
+        finalStatus = u.unchecked
+          ? "pending"
+          : u.has_registration
+          ? "registered"
+          : "verified_unregistered";
+      }
+
+      // 5. Compute the opportunity label.
+      const label = opportunityLabel({
+        isRegistered: u ? u.has_registration : null,
+        yearsInBusiness: yearsInBiz,
+        lifecycle,
+        abandonment: abStatus,
+        isParked: !aligned,
+      });
+      emit({ kind: "label", domain, label });
+
+      // 6. Persist.
+      const { rows } = await query<{ id: number }>(
+        `INSERT INTO brand_candidates (
+           domain, brand_name, category, established_year, years_in_business,
+           whois_created_date, meta_description,
+           uspto_checked_at, uspto_live_marks, has_federal_registration, uspto_search_url,
+           status, suggested_by, suggested_from_domain,
+           http_status, domain_expires_on, domain_lifecycle,
+           last_activity_year, last_activity_signal, years_since_activity,
+           abandonment_status, opportunity_label
+         ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
+         ON CONFLICT (domain) DO UPDATE SET
+           brand_name = EXCLUDED.brand_name,
+           established_year = EXCLUDED.established_year,
+           years_in_business = EXCLUDED.years_in_business,
+           uspto_checked_at = EXCLUDED.uspto_checked_at,
+           uspto_live_marks = EXCLUDED.uspto_live_marks,
+           has_federal_registration = EXCLUDED.has_federal_registration,
+           uspto_search_url = EXCLUDED.uspto_search_url,
+           status = EXCLUDED.status,
+           http_status = EXCLUDED.http_status,
+           domain_expires_on = EXCLUDED.domain_expires_on,
+           domain_lifecycle = EXCLUDED.domain_lifecycle,
+           last_activity_year = EXCLUDED.last_activity_year,
+           last_activity_signal = EXCLUDED.last_activity_signal,
+           years_since_activity = EXCLUDED.years_since_activity,
+           abandonment_status = EXCLUDED.abandonment_status,
+           opportunity_label = EXCLUDED.opportunity_label,
+           last_checked_at = NOW()
+         RETURNING id`,
+        [domain, brandName, category, establishedYear, yearsInBiz,
+         who.created?.toISOString().slice(0, 10) ?? null, ex.meta_description,
+         aligned ? new Date() : null,
+         u ? u.live_marks : null,
+         u ? u.has_registration : null,
+         u ? u.url : null,
+         finalStatus, "seed", null,
+         status || null,
+         who.expires?.toISOString().slice(0, 10) ?? null,
+         lifecycle,
+         activity.year,
+         activity.signal,
+         yearsSinceActivity,
+         abStatus,
+         label]
+      );
+      const noteText = !aligned
+        ? `Extracted brand "${brandName}" does not match domain — likely parked or registrar redirect. USPTO check skipped.`
+        : u?.evidence ?? "";
+      if (noteText) {
+        await query(`UPDATE brand_candidates SET notes = $1 WHERE domain = $2`, [noteText, domain]);
+      }
+      emit({ kind: "saved", domain, status: finalStatus, years_in_business: yearsInBiz, id: rows[0].id });
+      discovered++;
+      if (finalStatus === "verified_unregistered") verified++;
+
+      // 6. Ask Qwen for more.
+      if (discovered < cap) {
+        const more = await proposeMoreDomains(category, [...processed, ...queue]);
+        emit({ kind: "discovered", parent: domain, domains: more });
+        for (const m of more) {
+          if (!processed.has(m) && !known.has(m) && !queue.includes(m)) queue.push(m);
+        }
+      }
+    } catch (e) {
+      errors++;
+      const msg = e instanceof Error ? e.message : String(e);
+      emit({ kind: "error", domain, message: msg });
+      try {
+        await query(
+          `INSERT INTO brand_candidates (domain, category, status, error_message, suggested_by)
+           VALUES ($1,$2,'error',$3,'seed')
+           ON CONFLICT (domain) DO UPDATE SET status='error', error_message=EXCLUDED.error_message, last_checked_at=NOW()`,
+          [domain, category, msg]
+        );
+      } catch { /* swallow */ }
+    }
+  }
+
+  emit({ kind: "done", totals: { discovered, verified, errors } });
+  return { discovered, verified, errors };
+}
+
+export const FASHION_SEEDS = [
+  "outerknown.com",
+  "cuyana.com",
+  "everlane.com",
+  "alexmill.com",
+  "entireworld.com",
+  "sezane.com",
+  "tradlands.com",
+  "grana.com",
+  "reformation.com",
+  "ninelives.com",
+  "jennikayne.com",
+  "frankieshop.com",
+  "kotn.com",
+  "buckmason.com",
+  "taylorstitch.com",
+  "toddsnyder.com",
+  "flamingos-life.com",
+  "aloyoga.com",
+  "girlfriend.com",
+  "oakandfort.com",
+];
diff --git a/src/lib/db.ts b/src/lib/db.ts
new file mode 100644
index 0000000..469a07d
--- /dev/null
+++ b/src/lib/db.ts
@@ -0,0 +1,28 @@
+import { Pool } from "pg";
+
+declare global {
+  // eslint-disable-next-line no-var
+  var _pgPool: Pool | undefined;
+}
+
+export const pool =
+  global._pgPool ??
+  (process.env.DATABASE_URL
+    ? new Pool({ connectionString: process.env.DATABASE_URL })
+    : new Pool({
+        // Unix-socket connect — matches local Postgres peer auth, no password.
+        host: "/tmp",
+        port: 5432,
+        database: "trademarks_copyright",
+        user: process.env.USER || "stevestudio2",
+      }));
+
+if (process.env.NODE_ENV !== "production") global._pgPool = pool;
+
+export async function query<T = unknown>(
+  text: string,
+  params: unknown[] = []
+): Promise<{ rows: T[] }> {
+  const result = await pool.query(text, params as never);
+  return { rows: result.rows as T[] };
+}
diff --git a/src/lib/drops.ts b/src/lib/drops.ts
new file mode 100644
index 0000000..9166872
--- /dev/null
+++ b/src/lib/drops.ts
@@ -0,0 +1,389 @@
+/**
+ * Drop composer — picks today's items from the existing tables and writes
+ * headline/blurb/CTA copy via Qwen. Deterministic where possible; Qwen only for the prose.
+ */
+
+import { query } from "./db";
+import { qwen, qwenJSON } from "./qwen";
+
+export interface Candidate {
+  source_kind: "item" | "brand";
+  source_id: number;
+  name: string;
+  one_liner: string;
+  suggested_cta_url: string;
+  suggested_cta_text: string;
+  score: number;                     // composite rank (higher = better)
+  tier_gate: "standard" | "pro";
+  internal: Record<string, unknown>; // extra context for the LLM
+}
+
+async function poolExpiredMarks(limit: number): Promise<Candidate[]> {
+  const { rows } = await query<{
+    id: number; name: string; kind: string; status: string;
+    original_owner: string | null; expired_date: string; notes: string | null;
+    composite_score: string | null; monetization_ideas: unknown;
+  }>(
+    `SELECT id, name, kind, status, original_owner, expired_date, notes, composite_score, monetization_ideas
+     FROM items
+     WHERE composite_score IS NOT NULL
+     ORDER BY composite_score DESC NULLS LAST
+     LIMIT $1`,
+    [limit]
+  );
+  return rows.map((r) => ({
+    source_kind: "item",
+    source_id: r.id,
+    name: r.name,
+    one_liner: `${r.kind === "copyright" ? "Public-domain work" : "Abandoned US trademark"} — ${r.status}, ${new Date(r.expired_date).getFullYear()}`,
+    suggested_cta_url: `/item/${r.id}`,
+    suggested_cta_text: "Open file",
+    score: Number(r.composite_score ?? 0),
+    tier_gate: "standard",
+    internal: {
+      kind: r.kind,
+      status: r.status,
+      owner: r.original_owner,
+      notes: r.notes,
+      expired_date: r.expired_date,
+      monetization_ideas: r.monetization_ideas,
+    },
+  }));
+}
+
+async function poolBrandCandidates(limit: number): Promise<Candidate[]> {
+  const { rows } = await query<{
+    id: number; domain: string; brand_name: string | null; opportunity_label: string | null;
+    years_in_business: number | null; domain_lifecycle: string | null; abandonment_status: string | null;
+    days_to_expiry: number | null; notes: string | null;
+  }>(
+    `SELECT id, domain, brand_name, opportunity_label, years_in_business,
+            domain_lifecycle, abandonment_status, notes,
+            CASE WHEN domain_expires_on IS NOT NULL
+                 THEN (domain_expires_on - CURRENT_DATE) ELSE NULL END AS days_to_expiry
+     FROM brand_candidates
+     WHERE opportunity_label IN ('domain_snipe','abandonment_watch')
+     ORDER BY
+       CASE opportunity_label
+         WHEN 'domain_snipe' THEN 0
+         WHEN 'abandonment_watch' THEN 1
+         ELSE 2 END,
+       years_in_business DESC NULLS LAST
+     LIMIT $1`,
+    [limit]
+  );
+  return rows.map((r) => {
+    const lbl = r.opportunity_label === "domain_snipe" ? "Domain snipe" : "Abandonment watch";
+    const one = r.opportunity_label === "domain_snipe"
+      ? `Domain ${r.domain_lifecycle === "expired" ? "is expired" : `expires in ${r.days_to_expiry ?? "?"} days`} — brand dormant`
+      : `${r.years_in_business ?? "?"} yrs in biz, unregistered, ${r.abandonment_status ?? "?"}`;
+    return {
+      source_kind: "brand",
+      source_id: r.id,
+      name: r.brand_name || r.domain,
+      one_liner: `${lbl}: ${one}`,
+      suggested_cta_url: `https://${r.domain}`,
+      suggested_cta_text: "Visit site",
+      score: r.opportunity_label === "domain_snipe" ? 100 : 70,
+      tier_gate: "standard",
+      internal: {
+        domain: r.domain,
+        label: r.opportunity_label,
+        years: r.years_in_business,
+        lifecycle: r.domain_lifecycle,
+        abandonment: r.abandonment_status,
+        days_to_expiry: r.days_to_expiry,
+        notes: r.notes,
+      },
+    };
+  });
+}
+
+/** Returns candidates that have NOT yet appeared in any drop. */
+export async function freshCandidates(maxPerSource: number): Promise<Candidate[]> {
+  const [marks, brands] = await Promise.all([
+    poolExpiredMarks(maxPerSource * 3),
+    poolBrandCandidates(maxPerSource * 3),
+  ]);
+
+  const { rows: used } = await query<{ source_kind: string; source_id: number }>(
+    `SELECT source_kind, source_id FROM drop_items`
+  );
+  const usedKey = new Set(used.map((r) => `${r.source_kind}:${r.source_id}`));
+
+  const filtered = [...marks, ...brands]
+    .filter((c) => !usedKey.has(`${c.source_kind}:${c.source_id}`))
+    .sort((a, b) => b.score - a.score);
+
+  return filtered;
+}
+
+async function writeBlurb(c: Candidate, forPro: boolean): Promise<{
+  headline: string;
+  blurb: string;
+  cta_text: string;
+  pro_extra?: string;
+}> {
+  const prompt = `You are the editor of a daily newsletter about trademark + domain opportunities.
+Produce copy for this candidate.
+
+Candidate:
+  name: ${c.name}
+  category: ${c.source_kind === "brand" ? "unregistered-brand opportunity" : "expired trademark or public-domain work"}
+  one-liner: ${c.one_liner}
+  internal: ${JSON.stringify(c.internal).slice(0, 800)}
+
+Write in a punchy, editorial voice — think Morning Brew meets trademark-lawyer-who-does-stand-up.
+No legal advice, no filler.
+
+Return STRICT JSON:
+{
+  "headline": string,               // 6-10 words, evocative, start with a capital
+  "blurb": string,                  // 2-3 sentences, lead with *why this is on the list today*, end with the most interesting revenue angle
+  "cta_text": string${forPro ? `,
+  "pro_extra": string               // 2 extra sentences ONLY Pro subscribers see — specific tactical hint like which NICE class to file in, which registrar tends to catch drops for this TLD, etc.` : ""}
+}`;
+  try {
+    const ans = await qwenJSON<{ headline: string; blurb: string; cta_text: string; pro_extra?: string }>(prompt, 0.5);
+    return {
+      headline: (ans.headline || c.name).slice(0, 120),
+      blurb: (ans.blurb || c.one_liner).slice(0, 600),
+      cta_text: ans.cta_text || c.suggested_cta_text,
+      pro_extra: forPro ? ans.pro_extra : undefined,
+    };
+  } catch {
+    return { headline: c.name, blurb: c.one_liner, cta_text: c.suggested_cta_text };
+  }
+}
+
+async function writeSubjectAndIntro(picks: Candidate[]): Promise<{ subject: string; intro: string; outro: string }> {
+  const summary = picks.map((p, i) => `${i + 1}. ${p.name} — ${p.one_liner}`).join("\n");
+  const prompt = `Write the subject line, 1-sentence intro, and 1-sentence outro for today's "Drops" newsletter.
+
+Today's picks:
+${summary}
+
+Tone: sharp, a little mischievous, zero fluff.
+Return STRICT JSON: { "subject": string, "intro": string, "outro": string }`;
+  try {
+    const ans = await qwenJSON<{ subject: string; intro: string; outro: string }>(prompt, 0.6);
+    return {
+      subject: (ans.subject || "Today's drops").slice(0, 120),
+      intro: (ans.intro || "").slice(0, 300),
+      outro: (ans.outro || "").slice(0, 300),
+    };
+  } catch {
+    return { subject: "Today's drops", intro: "Fresh opportunities.", outro: "See you tomorrow." };
+  }
+}
+
+export interface ComposeOptions {
+  date?: Date;
+  itemsPerDrop?: number;   // cap for the drop itself; the pro_extra gates by tier
+  force?: boolean;         // recompose if a drop for this date already exists
+}
+
+export interface ComposedDrop {
+  dropId: number;
+  date: string;
+  subject: string;
+  items: number;
+}
+
+export async function composeDropForDate(opts: ComposeOptions = {}): Promise<ComposedDrop> {
+  const date = (opts.date ?? new Date()).toISOString().slice(0, 10);
+  const itemsCap = opts.itemsPerDrop ?? 10;
+
+  const existing = await query<{ id: number; status: string }>(
+    `SELECT id, status FROM drops WHERE drop_date = $1`, [date]
+  );
+  if (existing.rows.length && !opts.force) {
+    return { dropId: existing.rows[0].id, date, subject: "(already exists — use force)", items: 0 };
+  }
+
+  const candidates = await freshCandidates(Math.max(itemsCap * 2, 10));
+  if (!candidates.length) throw new Error("No fresh candidates — run the hunter or add more items first.");
+
+  const picks = candidates.slice(0, itemsCap);
+
+  if (existing.rows.length) {
+    await query(`DELETE FROM drops WHERE drop_date = $1`, [date]);
+  }
+
+  // Fire subject/intro/outro generation in parallel with item blurbs.
+  const CONCURRENCY = 3;
+  const blurbPromises: Promise<{ pos: number; pick: Candidate; blurb: Awaited<ReturnType<typeof writeBlurb>> }>[] = [];
+  const metaPromise = writeSubjectAndIntro(picks);
+
+  for (let i = 0; i < picks.length; i += CONCURRENCY) {
+    const slice = picks.slice(i, i + CONCURRENCY);
+    const wave = slice.map(async (pick, k) => ({
+      pos: i + k + 1,
+      pick,
+      blurb: await writeBlurb(pick, true),
+    }));
+    // Run waves sequentially but each wave runs in parallel.
+    const done = await Promise.all(wave);
+    blurbPromises.push(...done.map((d) => Promise.resolve(d)));
+  }
+
+  const [meta, ...settled] = await Promise.all([metaPromise, ...blurbPromises]);
+  const results = settled;
+
+  const { rows: dropRows } = await query<{ id: number }>(
+    `INSERT INTO drops (drop_date, subject, intro, outro, status)
+     VALUES ($1, $2, $3, $4, 'draft') RETURNING id`,
+    [date, meta.subject, meta.intro, meta.outro]
+  );
+  const dropId = dropRows[0].id;
+
+  for (const r of results) {
+    await query(
+      `INSERT INTO drop_items
+         (drop_id, position, source_kind, source_id, tier_gate, headline, blurb, cta_text, cta_url, pro_extra)
+       VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
+      [
+        dropId, r.pos, r.pick.source_kind, r.pick.source_id,
+        r.pick.tier_gate,
+        r.blurb.headline, r.blurb.blurb, r.blurb.cta_text, r.pick.suggested_cta_url,
+        r.blurb.pro_extra ?? null,
+      ]
+    );
+  }
+
+  return { dropId, date, subject: meta.subject, items: results.length };
+}
+
+// ---------- rendering ----------
+
+export interface RenderArgs {
+  dropId: number;
+  tier: "trial" | "standard" | "pro" | "comp";
+  subscriberToken: string;
+  appBaseUrl: string;
+  deliveryId?: number;  // if set, embed open-tracking pixel
+}
+
+export async function renderDropHTML(a: RenderArgs): Promise<{ subject: string; html: string; text: string }> {
+  const { rows: drops } = await query<{
+    id: number; subject: string; intro: string | null; outro: string | null; drop_date: string;
+  }>(`SELECT id, subject, intro, outro, drop_date FROM drops WHERE id = $1`, [a.dropId]);
+  if (!drops.length) throw new Error("drop not found");
+  const drop = drops[0];
+
+  const { rows: items } = await query<{
+    position: number; headline: string; blurb: string;
+    cta_text: string | null; cta_url: string | null; pro_extra: string | null;
+  }>(`SELECT position, headline, blurb, cta_text, cta_url, pro_extra
+      FROM drop_items WHERE drop_id = $1 ORDER BY position`, [a.dropId]);
+
+  const showPro = a.tier === "pro" || a.tier === "comp";
+  const maxItems = a.tier === "pro" || a.tier === "comp" ? 999 : a.tier === "standard" ? 3 : 3; // trial = 3
+  const visible = items.slice(0, maxItems);
+  const hiddenCount = items.length - visible.length;
+
+  const date = new Date(drop.drop_date).toLocaleDateString(undefined, { weekday: "long", month: "long", day: "numeric" });
+
+  const itemsHtml = visible.map((it) => {
+    const pro = showPro && it.pro_extra ? `<p style="margin:8px 0 0; padding:8px 10px; background:#fff8e6; border-left:3px solid #b38f4e; font-size:13px; color:#51391a;"><strong style="color:#b38f4e;">PRO:</strong> ${escapeHtml(it.pro_extra)}</p>` : "";
+    const cta = it.cta_url ? `<p style="margin:8px 0 0;"><a href="${escapeAttr(makeAbsolute(it.cta_url, a.appBaseUrl))}" style="color:#b38f4e; text-decoration:none; font-weight:600;">${escapeHtml(it.cta_text || "Read more →")}</a></p>` : "";
+    return `
+      <div style="padding:16px 20px; border-top:1px solid #e7e2d6;">
+        <div style="font-size:11px; color:#9e9788; letter-spacing:1px;">#${it.position}</div>
+        <h3 style="margin:4px 0 6px; font-family:Georgia,serif; font-size:20px; color:#0b0b0c;">${escapeHtml(it.headline)}</h3>
+        <p style="margin:0; color:#35312a; font-size:15px; line-height:1.55;">${escapeHtml(it.blurb)}</p>
+        ${pro}
+        ${cta}
+      </div>
+    `;
+  }).join("");
+
+  const teaser = hiddenCount > 0
+    ? `<div style="padding:24px 20px; text-align:center; border-top:1px solid #e7e2d6; background:#faf7f0;">
+         <p style="margin:0 0 12px; color:#51391a;"><strong>${hiddenCount} more opportunit${hiddenCount === 1 ? "y is" : "ies are"} in this drop — Pro only.</strong></p>
+         <a href="${a.appBaseUrl}/drops?upgrade=pro&token=${encodeURIComponent(a.subscriberToken)}" style="display:inline-block; padding:10px 18px; background:#0b0b0c; color:#f7f4ec; text-decoration:none; font-weight:600;">Upgrade to Pro — $99/mo</a>
+       </div>`
+    : "";
+
+  const html = `<!DOCTYPE html>
+<html><head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="color-scheme" content="light">
+  <meta name="supported-color-schemes" content="light">
+  <title>${escapeHtml(drop.subject)}</title>
+  <style>
+    @media (prefers-color-scheme: dark) {
+      /* Preserve brand colors even in dark clients. */
+      body { background-color:#f7f4ec !important; color:#0b0b0c !important; }
+    }
+  </style>
+</head>
+<body style="margin:0; padding:0; background-color:#f7f4ec; font-family:Georgia,'Libre Caslon Text',serif; color:#0b0b0c;">
+  <table width="100%" cellpadding="0" cellspacing="0" style="background:#f7f4ec;">
+    <tr><td align="center" style="padding:30px 10px;">
+      <table width="620" cellpadding="0" cellspacing="0" style="background:#fff; max-width:620px;">
+        <tr><td style="padding:24px 20px; background:#0b0b0c; color:#f7f4ec;">
+          <div style="font-size:11px; letter-spacing:2px; color:#b38f4e;">DROPS · ${date.toUpperCase()}</div>
+          <h1 style="margin:4px 0 0; font-size:24px; font-family:Georgia,serif;">${escapeHtml(drop.subject)}</h1>
+        </td></tr>
+        ${drop.intro ? `<tr><td style="padding:18px 20px; font-size:15px; color:#35312a; border-top:1px solid #e7e2d6;"><em>${escapeHtml(drop.intro)}</em></td></tr>` : ""}
+        <tr><td>${itemsHtml}</td></tr>
+        ${teaser}
+        ${drop.outro ? `<tr><td style="padding:20px; color:#35312a; border-top:1px solid #e7e2d6; font-size:14px;">${escapeHtml(drop.outro)}</td></tr>` : ""}
+        <tr><td style="padding:16px 20px; background-color:#faf7f0; color:#6b6659; font-size:11px;">
+          Tier: <strong>${a.tier.toUpperCase()}</strong> · <a href="${a.appBaseUrl}/drops/view/${encodeURIComponent(a.subscriberToken)}" style="color:#6b6659;">Archive</a> · <a href="${a.appBaseUrl}/drops/unsubscribe/${encodeURIComponent(a.subscriberToken)}" style="color:#6b6659;">Unsubscribe</a>
+          <br><br>
+          Not legal advice. Before registering any mark or buying any domain, talk to a trademark attorney.
+          <br><br>
+          <span style="color:#8a8479;">${escapeHtml(process.env.DROPS_MAILING_ADDRESS || "Drops · [set DROPS_MAILING_ADDRESS in env — CAN-SPAM requires a physical address]")}</span>
+          ${a.deliveryId ? `<br><img src="${a.appBaseUrl}/api/drops/pixel/${a.deliveryId}" width="1" height="1" alt="" style="display:block;margin-top:6px;">` : ""}
+        </td></tr>
+      </table>
+    </td></tr>
+  </table>
+</body></html>`;
+
+  const text = [
+    `DROPS · ${date}`,
+    drop.subject,
+    drop.intro ?? "",
+    "",
+    ...visible.map((it) => `#${it.position} ${it.headline}\n${it.blurb}${showPro && it.pro_extra ? `\nPRO: ${it.pro_extra}` : ""}\n${it.cta_url ? makeAbsolute(it.cta_url, a.appBaseUrl) : ""}`),
+    "",
+    hiddenCount > 0 ? `+ ${hiddenCount} more in Pro — upgrade: ${a.appBaseUrl}/drops?upgrade=pro` : "",
+    drop.outro ?? "",
+    "",
+    `Archive: ${a.appBaseUrl}/drops/view/${a.subscriberToken}`,
+    `Unsubscribe: ${a.appBaseUrl}/drops/unsubscribe/${a.subscriberToken}`,
+  ].filter(Boolean).join("\n\n");
+
+  return { subject: drop.subject, html, text };
+}
+
+function escapeHtml(s: string): string {
+  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
+}
+function escapeAttr(s: string): string { return escapeHtml(s); }
+function makeAbsolute(url: string, base: string): string {
+  if (/^https?:/.test(url)) return url;
+  return `${base.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
+}
+
+// ---------- delivery helpers ----------
+
+export async function activeSubscribersFor(dropId: number): Promise<{
+  id: number; email: string; name: string | null; token: string; tier: string;
+}[]> {
+  const { rows } = await query<{
+    id: number; email: string; name: string | null; token: string; tier: string;
+  }>(
+    `SELECT s.id, s.email, s.name, s.token, s.tier
+     FROM subscribers s
+     WHERE s.status = 'active'
+       AND (s.tier != 'trial' OR s.trial_drops_left > 0)
+       AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.drop_id = $1 AND d.subscriber_id = s.id)`,
+    [dropId]
+  );
+  return rows;
+}
diff --git a/src/lib/email.ts b/src/lib/email.ts
new file mode 100644
index 0000000..25e2001
--- /dev/null
+++ b/src/lib/email.ts
@@ -0,0 +1,115 @@
+/**
+ * Email delivery with three backends, picked by env:
+ *   1. Resend (RESEND_API_KEY) — preferred, clean API, generous free tier
+ *   2. SMTP (SMTP_HOST + SMTP_USER + SMTP_PASS) — anything: Google Workspace, Mailgun SMTP, Postmark
+ *   3. File drop (fallback) — writes HTML + meta JSON to /tmp/drops/<timestamp>.html
+ *
+ * All backends return the same { ok, id?, error? } contract.
+ */
+
+import { promises as fs } from "node:fs";
+import path from "node:path";
+
+export type EmailResult = { ok: boolean; id?: string; error?: string; via: "resend" | "smtp" | "file" };
+
+export interface SendArgs {
+  to: string;
+  subject: string;
+  html: string;
+  text?: string;
+  fromOverride?: string;
+  /** Token for the one-click unsubscribe header (Gmail/Yahoo bulk policy). */
+  unsubscribeToken?: string;
+}
+
+const FROM = process.env.DROPS_FROM || "Drops <drops@localhost>";
+const APP_BASE = process.env.APP_BASE_URL || "http://localhost:9770";
+
+function buildHeaders(args: SendArgs): Record<string, string> {
+  const h: Record<string, string> = {};
+  if (args.unsubscribeToken) {
+    const u = `${APP_BASE}/api/drops/unsubscribe/${encodeURIComponent(args.unsubscribeToken)}`;
+    // RFC 8058 one-click. Both headers required for Gmail/Yahoo bulk-sender policy.
+    h["List-Unsubscribe"] = `<${u}>`;
+    h["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click";
+  }
+  return h;
+}
+
+export async function sendEmail(args: SendArgs): Promise<EmailResult> {
+  const from = args.fromOverride ?? FROM;
+  if (process.env.RESEND_API_KEY) return sendViaResend(args, from);
+  if (process.env.SMTP_HOST) return sendViaSmtp(args, from);
+  return sendViaFile(args, from);
+}
+
+async function sendViaResend(args: SendArgs, from: string): Promise<EmailResult> {
+  try {
+    const extraHeaders = buildHeaders(args);
+    const r = await fetch("https://api.resend.com/emails", {
+      method: "POST",
+      headers: {
+        authorization: `Bearer ${process.env.RESEND_API_KEY}`,
+        "content-type": "application/json",
+      },
+      body: JSON.stringify({
+        from,
+        to: [args.to],
+        subject: args.subject,
+        html: args.html,
+        text: args.text,
+        headers: Object.keys(extraHeaders).length ? extraHeaders : undefined,
+      }),
+    });
+    const j = (await r.json()) as { id?: string; name?: string; message?: string };
+    if (!r.ok) return { ok: false, error: j.message || `HTTP ${r.status}`, via: "resend" };
+    return { ok: true, id: j.id, via: "resend" };
+  } catch (e) {
+    return { ok: false, error: (e as Error).message, via: "resend" };
+  }
+}
+
+async function sendViaSmtp(args: SendArgs, from: string): Promise<EmailResult> {
+  try {
+    // Dynamic import — nodemailer is optional; install with `npm i nodemailer` to enable.
+    const mod = await import("nodemailer" as string).catch(() => null) as
+      | { createTransport: (opts: Record<string, unknown>) => { sendMail: (m: Record<string, unknown>) => Promise<{ messageId: string }> } }
+      | null;
+    if (!mod) {
+      return { ok: false, error: "nodemailer not installed — run `npm install nodemailer` to enable SMTP", via: "smtp" };
+    }
+    const tx = mod.createTransport({
+      host: process.env.SMTP_HOST,
+      port: Number(process.env.SMTP_PORT || 587),
+      secure: process.env.SMTP_SECURE === "true",
+      auth: process.env.SMTP_USER
+        ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
+        : undefined,
+    });
+    const info = await tx.sendMail({ from, to: args.to, subject: args.subject, html: args.html, text: args.text, headers: buildHeaders(args) });
+    return { ok: true, id: info.messageId, via: "smtp" };
+  } catch (e) {
+    return { ok: false, error: (e as Error).message, via: "smtp" };
+  }
+}
+
+async function sendViaFile(args: SendArgs, from: string): Promise<EmailResult> {
+  try {
+    const dir = "/tmp/drops";
+    await fs.mkdir(dir, { recursive: true });
+    const ts = new Date().toISOString().replace(/[:.]/g, "-");
+    const safeTo = args.to.replace(/[^a-z0-9@._-]/gi, "_");
+    const file = path.join(dir, `${ts}__${safeTo}.html`);
+    const meta = `<!--\nFrom: ${from}\nTo: ${args.to}\nSubject: ${args.subject}\n-->\n`;
+    await fs.writeFile(file, meta + args.html, "utf8");
+    return { ok: true, id: file, via: "file" };
+  } catch (e) {
+    return { ok: false, error: (e as Error).message, via: "file" };
+  }
+}
+
+export function currentBackend(): "resend" | "smtp" | "file" {
+  if (process.env.RESEND_API_KEY) return "resend";
+  if (process.env.SMTP_HOST) return "smtp";
+  return "file";
+}
diff --git a/src/lib/lifecycle.ts b/src/lib/lifecycle.ts
new file mode 100644
index 0000000..2376f78
--- /dev/null
+++ b/src/lib/lifecycle.ts
@@ -0,0 +1,136 @@
+/**
+ * Domain lifecycle + activity + opportunity classification.
+ * Pure business logic — no I/O, so it's cheap to unit-test and tweak.
+ */
+
+export type DomainLifecycle = "live" | "expiring_soon" | "expired" | "unknown";
+export type AbandonmentStatus = "active" | "stale" | "abandoned" | "unknown";
+export type OpportunityLabel =
+  | "domain_snipe"          // domain expired/expiring + brand dormant — clean buy
+  | "abandonment_watch"     // site still live but no activity 2+ yrs — approaching abandonment
+  | "licensing_outreach"    // active brand, unregistered — pitch THEM services
+  | "do_not_target"         // active brand actively using mark — ACPA/TTAB risk
+  | "unclear";
+
+/** From HTTP status: 0 = fetch failed, 2xx = live, 4xx/5xx = likely dead. */
+export function domainLifecycle(
+  httpStatus: number | null,
+  expiresOn: Date | null
+): DomainLifecycle {
+  if (expiresOn) {
+    const days = (expiresOn.getTime() - Date.now()) / 86400000;
+    if (days < 0) return "expired";
+    if (days < 90) return "expiring_soon";
+  }
+  if (httpStatus === null || httpStatus === 0) return "unknown";
+  if (httpStatus >= 200 && httpStatus < 400) return "live";
+  if (httpStatus >= 400) return "expired"; // site returns errors — likely dead or parked
+  return "unknown";
+}
+
+export function abandonmentStatus(yearsSinceActivity: number | null): AbandonmentStatus {
+  if (yearsSinceActivity === null) return "unknown";
+  if (yearsSinceActivity < 1) return "active";
+  if (yearsSinceActivity < 3) return "stale";
+  return "abandoned";
+}
+
+/**
+ * The rule engine that turns the three signals into a business label.
+ * Ordering matters — first match wins.
+ *
+ *   isRegistered: USPTO has a live federal registration for this brand
+ *   yearsInBusiness: age from WHOIS
+ *   lifecycle: HTTP + domain-expiry lifecycle
+ *   abandonment: activity age bucket
+ */
+export function opportunityLabel(input: {
+  isRegistered: boolean | null;   // null = unchecked
+  yearsInBusiness: number | null;
+  lifecycle: DomainLifecycle;
+  abandonment: AbandonmentStatus;
+  isParked: boolean;
+}): OpportunityLabel {
+  if (input.isParked) return "unclear";
+  if (input.isRegistered === null) return "unclear";
+
+  // Hard blocker — actively registered + actively used = keep away.
+  if (input.isRegistered && input.abandonment === "active") return "do_not_target";
+
+  // A registered mark whose domain is expiring is NOT a clean play — the owner
+  // can still assert Lanham-Act rights even if they let the domain drop.
+  // Only suggest domain_snipe when the mark itself is unregistered OR abandoned.
+  if (input.lifecycle === "expired" || input.lifecycle === "expiring_soon") {
+    if (input.isRegistered && input.abandonment !== "abandoned") {
+      return "unclear";
+    }
+    return "domain_snipe";
+  }
+
+  // Live domain, brand unregistered federally, inactive 3+ yrs → trademark-abandonment window opening.
+  if (!input.isRegistered && input.abandonment === "abandoned") {
+    return "abandonment_watch";
+  }
+
+  // Live active brand with no federal registration → legitimate outreach, NOT squatting.
+  if (!input.isRegistered && input.abandonment === "active") {
+    return "licensing_outreach";
+  }
+
+  // Stale but still live, unregistered — keep an eye on.
+  if (!input.isRegistered && input.abandonment === "stale") {
+    return "abandonment_watch";
+  }
+
+  // Registered + stale — the owner may not fight but also isn't selling.
+  if (input.isRegistered && input.abandonment !== "active") {
+    return "unclear";
+  }
+
+  return "unclear";
+}
+
+/**
+ * Best years extractor — copyright years, "since 20XX", year-dated posts.
+ * Returns the HIGHEST year found (latest activity signal).
+ */
+export function extractLatestYear(text: string): { year: number | null; signal: string | null } {
+  const now = new Date().getFullYear();
+  const yearsFound: { year: number; context: string }[] = [];
+
+  const copyRe = /©\s*(\d{4})(?:\s*[-–]\s*(\d{4}))?[^\n]{0,80}/g;
+  for (const m of text.matchAll(copyRe)) {
+    const y1 = Number(m[1]);
+    const y2 = m[2] ? Number(m[2]) : y1;
+    const y = Math.max(y1, y2);
+    if (y >= 1990 && y <= now + 1) yearsFound.push({ year: y, context: `©${y} — "${m[0].slice(0, 80).trim()}"` });
+  }
+
+  const sinceRe = /\b(?:since|est\.?|established|founded)\s+(?:in\s+)?(\d{4})\b/gi;
+  for (const m of text.matchAll(sinceRe)) {
+    const y = Number(m[1]);
+    if (y >= 1800 && y <= now) yearsFound.push({ year: y, context: `${m[0]}` });
+  }
+
+  // "Copyright 2024 BrandCo" without symbol
+  const copyWordRe = /\bcopyright\s+(\d{4})(?:\s*[-–]\s*(\d{4}))?[^\n]{0,40}/gi;
+  for (const m of text.matchAll(copyWordRe)) {
+    const y1 = Number(m[1]);
+    const y2 = m[2] ? Number(m[2]) : y1;
+    const y = Math.max(y1, y2);
+    if (y >= 1990 && y <= now + 1) yearsFound.push({ year: y, context: `${m[0].slice(0, 80).trim()}` });
+  }
+
+  // Bare 4-digit year near the footer (less reliable, only use if we have nothing else)
+  if (!yearsFound.length) {
+    const bareRe = /\b(20\d{2})\b/g;
+    for (const m of text.matchAll(bareRe)) {
+      const y = Number(m[1]);
+      if (y >= 2000 && y <= now) yearsFound.push({ year: y, context: `bare year ${y}` });
+    }
+  }
+
+  if (!yearsFound.length) return { year: null, signal: null };
+  const best = yearsFound.reduce((a, b) => (b.year > a.year ? b : a));
+  return { year: best.year, signal: best.context };
+}
diff --git a/src/lib/qwen.ts b/src/lib/qwen.ts
new file mode 100644
index 0000000..b16da97
--- /dev/null
+++ b/src/lib/qwen.ts
@@ -0,0 +1,58 @@
+/**
+ * Local Qwen via Ollama at http://localhost:11434.
+ * Used for brand-name extraction and discovery suggestions in the unregistered-brand hunter.
+ */
+
+// P1 fix 2026-05-04: default to MS1 per `feedback_ollama_default_ms1.md`.
+// Mac2 froze 2026-05-02 under GPU contention; trademarks-copyright pipelines
+// (hunt + score + swot + drops) are bulk LLM workloads that should hit MS1.
+const OLLAMA_URL = process.env.OLLAMA_URL || "http://192.168.1.133:11434";
+const QWEN_MODEL = process.env.QWEN_MODEL || "qwen2.5:latest";
+
+export async function qwen(
+  prompt: string,
+  opts: { temperature?: number; format?: "json"; model?: string } = {}
+): Promise<string> {
+  const body: Record<string, unknown> = {
+    model: opts.model ?? QWEN_MODEL,
+    prompt,
+    stream: false,
+    options: { temperature: opts.temperature ?? 0.2 },
+  };
+  if (opts.format === "json") body.format = "json";
+
+  // P2 fix 2026-05-04: 120s timeout via AbortSignal. Without it a stalled
+  // Ollama hangs the route handler until Next.js maxDuration (300s) triggers.
+  const r = await fetch(`${OLLAMA_URL}/api/generate`, {
+    method: "POST",
+    headers: { "content-type": "application/json" },
+    body: JSON.stringify(body),
+    signal: AbortSignal.timeout(120_000),
+  });
+  if (!r.ok) throw new Error(`Qwen HTTP ${r.status}: ${await r.text().catch(() => "")}`);
+  const j = (await r.json()) as { response: string };
+  return j.response.trim();
+}
+
+export async function qwenJSON<T = unknown>(prompt: string, temperature = 0.2): Promise<T> {
+  const raw = await qwen(prompt, { temperature, format: "json" });
+  try {
+    return JSON.parse(raw) as T;
+  } catch {
+    // Sometimes the model wraps JSON in fences — strip and retry.
+    const m = raw.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
+    if (m) return JSON.parse(m[0]) as T;
+    throw new Error(`Qwen JSON parse failed: ${raw.slice(0, 200)}`);
+  }
+}
+
+export async function qwenIsUp(): Promise<boolean> {
+  try {
+    const r = await fetch(`${OLLAMA_URL}/api/tags`);
+    return r.ok;
+  } catch {
+    return false;
+  }
+}
+
+export const QWEN_MODEL_NAME = QWEN_MODEL;
diff --git a/src/lib/rateLimit.ts b/src/lib/rateLimit.ts
new file mode 100644
index 0000000..6c41f5c
--- /dev/null
+++ b/src/lib/rateLimit.ts
@@ -0,0 +1,80 @@
+import { createHash } from "node:crypto";
+import { query } from "./db";
+
+/**
+ * Fixed-window rate limit.
+ * Returns { allowed, remaining, resetAt } for a given (key, bucket) pair.
+ * Default: 3 operations per 60 minutes.
+ */
+export async function checkRateLimit(
+  key: string,
+  bucket: string,
+  opts: { max?: number; windowMinutes?: number } = {}
+): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
+  const max = opts.max ?? 3;
+  const windowMin = opts.windowMinutes ?? 60;
+
+  const hash = createHash("sha256").update(key).digest("hex").slice(0, 32);
+
+  // Single-query upsert + read. The CTE ensures the count we RETURN is the
+  // post-increment count of the row we just touched — no TOCTOU window.
+  const { rows } = await query<{ count: number; window_start: string }>(
+    `INSERT INTO rate_limit (key_hash, bucket, count, window_start)
+     VALUES ($1, $2, 1, NOW())
+     ON CONFLICT (key_hash, bucket) DO UPDATE SET
+       count = CASE
+         WHEN rate_limit.window_start < NOW() - ($3 || ' minutes')::interval
+         THEN 1
+         ELSE rate_limit.count + 1
+       END,
+       window_start = CASE
+         WHEN rate_limit.window_start < NOW() - ($3 || ' minutes')::interval
+         THEN NOW()
+         ELSE rate_limit.window_start
+       END
+     RETURNING count, window_start`,
+    [hash, bucket, String(windowMin)]
+  );
+  const row = rows[0];
+  const resetAt = new Date(new Date(row.window_start).getTime() + windowMin * 60_000);
+  return {
+    allowed: row.count <= max,
+    remaining: Math.max(0, max - row.count),
+    resetAt,
+  };
+}
+
+/**
+ * Cron-callable cleanup — honors the 30-day retention we promise in the
+ * Privacy Policy for rate-limit rows.
+ */
+export async function purgeExpiredRateLimits(): Promise<number> {
+  const { rows } = await query<{ n: string }>(
+    `WITH deleted AS (
+       DELETE FROM rate_limit
+       WHERE window_start < NOW() - INTERVAL '30 days'
+       RETURNING 1
+     )
+     SELECT COUNT(*)::text AS n FROM deleted`
+  );
+  return Number(rows[0]?.n ?? 0);
+}
+
+/**
+ * Ring of known disposable / throwaway email domains.
+ * Non-exhaustive by design — we want to catch the obvious offenders, not all of them.
+ */
+const DISPOSABLE_DOMAINS = new Set([
+  "mailinator.com", "10minutemail.com", "10minutemail.net", "guerrillamail.com", "sharklasers.com",
+  "tempmail.com", "temp-mail.org", "temp-mail.io", "dispostable.com", "yopmail.com", "trashmail.com",
+  "throwawaymail.com", "getnada.com", "nada.ltd", "maildrop.cc", "mintemail.com",
+  "fakeinbox.com", "emailondeck.com", "spambog.com", "spamgourmet.com", "tempinbox.com",
+  "guerillamail.info", "inboxbear.com", "mt2015.com", "zeroe.ml", "mohmal.com",
+]);
+
+export function isDisposableEmail(email: string): boolean {
+  const at = email.lastIndexOf("@");
+  if (at < 0) return false;
+  const domain = email.slice(at + 1).toLowerCase().trim();
+  return DISPOSABLE_DOMAINS.has(domain);
+}
diff --git a/src/lib/scoring.ts b/src/lib/scoring.ts
new file mode 100644
index 0000000..09a8499
--- /dev/null
+++ b/src/lib/scoring.ts
@@ -0,0 +1,65 @@
+import type { Item } from "./types";
+
+export interface ScoringInputs {
+  distinctiveness_score: number | null;
+  recognition_score: number | null;
+  domain_available: boolean | null;
+  competing_active_marks: number | null;
+  monetization_breadth: number | null;
+  expired_date: string | Date;
+}
+
+/**
+ * Composite 0-100. Weights:
+ *   distinctiveness 25, years-since-expired 20, recognition 15,
+ *   domain 15, competing-marks 15, monetization 10.
+ * Sweet spot for "years since expired" is 3-15. Too recent = revival risk.
+ * Too old = brand memory decayed.
+ */
+export function scoreItem(inp: ScoringInputs): number {
+  const distinct = clamp(inp.distinctiveness_score ?? 50, 0, 100);
+  const recog = clamp(inp.recognition_score ?? 30, 0, 100);
+  const monet = clamp(inp.monetization_breadth ?? 40, 0, 100);
+
+  const yearsExpired = yearsSince(inp.expired_date);
+  const ageScore = ageFit(yearsExpired);
+
+  const domainScore = inp.domain_available === true ? 100 : inp.domain_available === false ? 15 : 50;
+
+  // Competing active marks penalty: 0 = perfect, 10+ = heavy saturation
+  const competing = inp.competing_active_marks ?? 0;
+  const competingScore = clamp(100 - competing * 10, 0, 100);
+
+  const composite =
+    distinct * 0.25 +
+    ageScore * 0.20 +
+    recog * 0.15 +
+    domainScore * 0.15 +
+    competingScore * 0.15 +
+    monet * 0.10;
+
+  return round2(composite);
+}
+
+function yearsSince(d: string | Date): number {
+  const past = new Date(d).getTime();
+  const now = Date.now();
+  return (now - past) / (365.25 * 24 * 3600 * 1000);
+}
+
+function ageFit(years: number): number {
+  if (years < 0) return 0;
+  if (years < 1) return 20;   // too recent — revival likely
+  if (years < 3) return 55;
+  if (years <= 15) return 100; // sweet spot
+  if (years <= 30) return 70;
+  if (years <= 50) return 45;
+  return 30;
+}
+
+const clamp = (n: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, n));
+const round2 = (n: number) => Math.round(n * 100) / 100;
+
+export function yearsExpired(item: Pick<Item, "expired_date">): number {
+  return round2(yearsSince(item.expired_date));
+}
diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts
new file mode 100644
index 0000000..ac29586
--- /dev/null
+++ b/src/lib/stripe.ts
@@ -0,0 +1,112 @@
+/**
+ * Stripe integration is fully optional.
+ * - Without keys: admin manually marks subscribers as tier='standard'|'pro' (comp tier).
+ * - With keys: Checkout session for signup/upgrade; webhook flips tier on checkout.session.completed.
+ *
+ * No runtime dependency on the stripe SDK — we hit their REST API directly to keep deps light.
+ */
+
+const STRIPE_API = "https://api.stripe.com/v1";
+
+export function stripeEnabled(): boolean {
+  return !!process.env.STRIPE_SECRET_KEY;
+}
+
+export const TIERS = {
+  standard: {
+    name: "Standard",
+    monthlyUsd: 29,
+    priceEnv: "STRIPE_PRICE_STANDARD",
+    items: 3,
+    description: "Daily drop with 3 curated opportunities.",
+  },
+  pro: {
+    name: "Pro",
+    monthlyUsd: 99,
+    priceEnv: "STRIPE_PRICE_PRO",
+    items: 10,
+    description: "Daily drop with 10 opportunities + SWOT preview on each.",
+  },
+} as const;
+
+export type TierId = keyof typeof TIERS;
+
+async function stripeFetch(path: string, init: RequestInit & { form?: Record<string, string> } = {}): Promise<unknown> {
+  const { form, ...rest } = init;
+  const headers = new Headers(rest.headers);
+  headers.set("authorization", `Bearer ${process.env.STRIPE_SECRET_KEY}`);
+  if (form) {
+    headers.set("content-type", "application/x-www-form-urlencoded");
+    rest.body = new URLSearchParams(form).toString();
+  }
+  const r = await fetch(`${STRIPE_API}${path}`, { ...rest, headers });
+  const j = await r.json();
+  if (!r.ok) throw new Error(`Stripe ${r.status}: ${JSON.stringify(j).slice(0, 300)}`);
+  return j;
+}
+
+export interface CheckoutArgs {
+  email: string;
+  tier: TierId;
+  successUrl: string;
+  cancelUrl: string;
+  subscriberToken: string;  // passed through metadata so webhook knows who upgraded
+}
+
+export async function createCheckoutSession(a: CheckoutArgs): Promise<{ url: string; id: string }> {
+  const priceId = process.env[TIERS[a.tier].priceEnv];
+  if (!priceId) throw new Error(`${TIERS[a.tier].priceEnv} not set in env`);
+  const j = (await stripeFetch("/checkout/sessions", {
+    method: "POST",
+    form: {
+      mode: "subscription",
+      "line_items[0][price]": priceId,
+      "line_items[0][quantity]": "1",
+      customer_email: a.email,
+      success_url: a.successUrl,
+      cancel_url: a.cancelUrl,
+      "metadata[subscriber_token]": a.subscriberToken,
+      "metadata[tier]": a.tier,
+    },
+  })) as { id: string; url: string };
+  return { id: j.id, url: j.url };
+}
+
+import { createHmac, timingSafeEqual } from "node:crypto";
+
+/**
+ * Full HMAC-SHA256 verification of a Stripe webhook signature header.
+ * Equivalent to `stripe.webhooks.constructEvent` without adding the SDK as a dep.
+ *
+ *   header format: t=<unix>,v1=<hex>[,v1=<hex>...]
+ *   signed payload: `${t}.${rawBody}`
+ *
+ * Returns true iff any v1 hash matches AND the timestamp is within `tolerance` seconds.
+ */
+export function verifyWebhookSignature(
+  rawBody: string,
+  header: string | null,
+  tolerance = 300
+): boolean {
+  const secret = process.env.STRIPE_WEBHOOK_SECRET;
+  if (!secret || !header) return false;
+
+  const parts = header.split(",").map((p) => p.trim().split("="));
+  const t = parts.find((p) => p[0] === "t")?.[1];
+  const v1s = parts.filter((p) => p[0] === "v1").map((p) => p[1]);
+  if (!t || !v1s.length) return false;
+
+  const unixNow = Math.floor(Date.now() / 1000);
+  if (Math.abs(unixNow - Number(t)) > tolerance) return false;
+
+  const signedPayload = `${t}.${rawBody}`;
+  const expected = createHmac("sha256", secret).update(signedPayload).digest("hex");
+  const expectedBuf = Buffer.from(expected, "utf8");
+
+  for (const v of v1s) {
+    const vBuf = Buffer.from(v, "utf8");
+    if (vBuf.length !== expectedBuf.length) continue;
+    if (timingSafeEqual(vBuf, expectedBuf)) return true;
+  }
+  return false;
+}
diff --git a/src/lib/swot.ts b/src/lib/swot.ts
new file mode 100644
index 0000000..fa18fef
--- /dev/null
+++ b/src/lib/swot.ts
@@ -0,0 +1,52 @@
+/**
+ * SWOT quadrant agents — running on local Qwen via Ollama. No paid APIs.
+ * We use a slightly larger model (qwen3:14b) for SWOT since volume is low
+ * and quality matters; extraction/discovery uses qwen2.5:latest.
+ */
+
+import { qwen } from "./qwen";
+import type { Item } from "./types";
+
+const SWOT_MODEL = process.env.QWEN_SWOT_MODEL || "qwen3:14b";
+
+export type Quadrant = "strength" | "weakness" | "opportunity" | "threat";
+
+const SYSTEMS: Record<Quadrant, string> = {
+  strength: `You are the STRENGTHS agent in a SWOT analysis of an expired/abandoned US trademark or public-domain copyright.
+Your job: identify what made this mark or work valuable, and what residual equity (brand recall, cultural footprint, distinctive design, established categories) a new operator could inherit.
+Be concrete. 3-5 bullets. Reference specific eras, demographics, or adjacent categories when relevant.
+Do NOT include legal advice. End with one sentence on the single most exploitable strength.`,
+  weakness: `You are the WEAKNESSES agent in a SWOT analysis of an expired/abandoned US trademark or public-domain copyright.
+Your job: identify why this mark was abandoned, what categories it failed in, what baggage/associations would hurt a revival, and any limits on protectability (descriptive, generic drift, licensing entanglements).
+Be concrete. 3-5 bullets. Reference commercial failures, dated positioning, or consumer perception issues.
+Do NOT include legal advice. End with one sentence on the single biggest weakness.`,
+  opportunity: `You are the OPPORTUNITIES agent in a SWOT analysis of an expired/abandoned US trademark or public-domain copyright.
+Your job: identify monetization and revival angles — new categories (NICE classes), merchandising, content/media adaptations, licensing, nostalgia-driven DTC plays, domain + brand packaging, derivative IP.
+Be concrete. 3-5 bullets with *specific* product or media ideas, not generic "start a brand" advice.
+Do NOT include legal advice. End with one sentence naming the single best opportunity.`,
+  threat: `You are the THREATS agent in a SWOT analysis of an expired/abandoned US trademark or public-domain copyright.
+Your job: identify risks — original-owner revival, common-law rights in residual use, consumer confusion with active marks, likelihood of opposition/cancellation, cultural sensitivities, derivative-work copyright traps (especially for "public domain" characters where later adaptations are still protected).
+Be concrete. 3-5 bullets. Name the kinds of actors (law firms, heirs, competitor brands) likely to object.
+Do NOT include legal advice beyond general risk flagging. End with one sentence on the single biggest threat.`,
+};
+
+export async function runSwot(
+  item: Pick<Item, "name" | "kind" | "original_owner" | "nice_class" | "goods_services" | "expired_date" | "status" | "notes">,
+  quadrant: Quadrant
+): Promise<string> {
+  const prompt = `${SYSTEMS[quadrant]}
+
+Subject of analysis:
+- Name: ${item.name}
+- Type: ${item.kind}
+- Status: ${item.status}
+- Expired/abandoned: ${item.expired_date}
+- Original owner: ${item.original_owner ?? "unknown"}
+- NICE class(es): ${item.nice_class ?? "n/a"}
+- Goods/services covered: ${item.goods_services ?? "n/a"}
+- Notes: ${item.notes ?? "n/a"}
+
+Write your quadrant of the SWOT now.`;
+
+  return qwen(prompt, { model: SWOT_MODEL, temperature: 0.4 });
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
new file mode 100644
index 0000000..1e46d83
--- /dev/null
+++ b/src/lib/types.ts
@@ -0,0 +1,37 @@
+export type ItemKind = "trademark" | "copyright";
+export type ItemStatus = "abandoned" | "cancelled" | "expired" | "public_domain";
+
+export interface Item {
+  id: number;
+  kind: ItemKind;
+  name: string;
+  registration_number: string | null;
+  original_owner: string | null;
+  nice_class: string | null;
+  goods_services: string | null;
+  status: ItemStatus;
+  filed_date: string | null;
+  registered_date: string | null;
+  expired_date: string;
+  source: string;
+  source_url: string | null;
+  notes: string | null;
+
+  distinctiveness_score: number | null;
+  recognition_score: number | null;
+  domain_available: boolean | null;
+  competing_active_marks: number | null;
+  monetization_breadth: number | null;
+  composite_score: number | null;
+
+  monetization_ideas: { title: string; description: string }[];
+
+  created_at: string;
+  updated_at: string;
+}
+
+export interface SwotQuadrant {
+  quadrant: "strength" | "weakness" | "opportunity" | "threat";
+  content: string;
+  generated_at: string;
+}
diff --git a/src/lib/websearch.ts b/src/lib/websearch.ts
new file mode 100644
index 0000000..690602b
--- /dev/null
+++ b/src/lib/websearch.ts
@@ -0,0 +1,139 @@
+/**
+ * Multi-engine web search with graceful fallback.
+ * Tries DDG HTML → DDG Lite → Brave Search HTML and returns the first engine that yields results.
+ * All three are free, keyless, and public-facing; we rotate when one rate-limits.
+ */
+
+const UA =
+  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
+
+const COMMON_HEADERS = {
+  "user-agent": UA,
+  accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+  "accept-language": "en-US,en;q=0.9",
+};
+
+export type Snippet = { title: string; url: string; body: string };
+export type SearchResult = {
+  engine: "ddg" | "ddg_lite" | "brave" | null;
+  snippets: Snippet[];
+  error?: string;
+};
+
+const strip = (s: string) =>
+  s
+    .replace(/<[^>]+>/g, "")
+    .replace(/&amp;/g, "&")
+    .replace(/&quot;/g, '"')
+    .replace(/&#x27;/g, "'")
+    .replace(/&nbsp;/g, " ")
+    .replace(/\s+/g, " ")
+    .trim();
+
+async function tryDdg(q: string): Promise<Snippet[]> {
+  const form = new URLSearchParams();
+  form.set("q", q);
+  const r = await fetch("https://html.duckduckgo.com/html/", {
+    method: "POST",
+    headers: {
+      ...COMMON_HEADERS,
+      "content-type": "application/x-www-form-urlencoded",
+      referer: "https://html.duckduckgo.com/",
+    },
+    body: form.toString(),
+    signal: AbortSignal.timeout(15000),
+  });
+  const body = await r.text();
+  if (r.status === 202 || /\banomaly\b/i.test(body)) throw new Error("ddg rate-limited");
+  if (!r.ok) throw new Error(`ddg HTTP ${r.status}`);
+  const titles = [...body.matchAll(/<a[^>]*class="result__a"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => strip(m[1]));
+  const urls = [...body.matchAll(/class="result__url"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => strip(m[1]));
+  const snips = [...body.matchAll(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => strip(m[1]));
+  const out: Snippet[] = [];
+  for (let i = 0; i < titles.length && i < 10; i++) {
+    if (!titles[i]) continue;
+    out.push({ title: titles[i], url: urls[i] ?? "", body: snips[i] ?? "" });
+  }
+  if (!out.length) throw new Error("ddg returned zero parseable results");
+  return out;
+}
+
+async function tryDdgLite(q: string): Promise<Snippet[]> {
+  const r = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(q)}`, {
+    headers: COMMON_HEADERS,
+    signal: AbortSignal.timeout(15000),
+  });
+  const body = await r.text();
+  if (r.status === 202 || /\banomaly\b/i.test(body)) throw new Error("ddg_lite rate-limited");
+  if (!r.ok) throw new Error(`ddg_lite HTTP ${r.status}`);
+  const rowRe = /<tr[^>]*>[\s\S]*?<\/tr>/g;
+  const rows = body.match(rowRe) ?? [];
+  const out: Snippet[] = [];
+  // DDG Lite alternates: row with result-link (title), row with result-snippet (body), row with link-text (url).
+  let cur: Partial<Snippet> = {};
+  for (const row of rows) {
+    const titleMatch = /<a[^>]*class=['"]result-link['"][^>]*>([\s\S]*?)<\/a>/.exec(row);
+    const snipMatch = /class=['"]result-snippet['"][^>]*>([\s\S]*?)<\/td>/.exec(row);
+    const urlMatch = /class=['"]link-text['"][^>]*>([\s\S]*?)<\/span>/.exec(row);
+    if (titleMatch) {
+      if (cur.title) out.push({ title: cur.title!, url: cur.url ?? "", body: cur.body ?? "" });
+      cur = { title: strip(titleMatch[1]) };
+    } else if (snipMatch && cur.title !== undefined) {
+      cur.body = strip(snipMatch[1]);
+    } else if (urlMatch && cur.title !== undefined) {
+      cur.url = strip(urlMatch[1]);
+    }
+    if (out.length >= 10) break;
+  }
+  if (cur.title) out.push({ title: cur.title, url: cur.url ?? "", body: cur.body ?? "" });
+  if (!out.length) throw new Error("ddg_lite returned zero parseable results");
+  return out;
+}
+
+async function tryBrave(q: string): Promise<Snippet[]> {
+  const r = await fetch(`https://search.brave.com/search?q=${encodeURIComponent(q)}`, {
+    headers: COMMON_HEADERS,
+    signal: AbortSignal.timeout(15000),
+  });
+  const body = await r.text();
+  if (r.status === 429) throw new Error("brave rate-limited");
+  if (!r.ok) throw new Error(`brave HTTP ${r.status}`);
+  // Brave renders server-side via Svelte. Each result has:
+  //   title="TITLE TEXT" inside a snippet-title element, and a following snippet-description.
+  const titleMatches = [...body.matchAll(/class="[^"]*snippet-title[^"]*"[^>]*title="([^"]+)"/g)].map((m) => strip(m[1]));
+  const descMatches = [...body.matchAll(/class="[^"]*snippet-description[^"]*"[^>]*>([\s\S]{0,500}?)<\/div>/g)].map((m) => strip(m[1]));
+  const urlMatches = [...body.matchAll(/<cite[^>]*class="[^"]*snippet-url[^"]*"[^>]*>([\s\S]{0,200}?)<\/cite>/g)].map((m) => strip(m[1]));
+  const out: Snippet[] = [];
+  for (let i = 0; i < titleMatches.length && i < 10; i++) {
+    out.push({ title: titleMatches[i], url: urlMatches[i] ?? "", body: descMatches[i] ?? "" });
+  }
+  if (!out.length) throw new Error("brave returned zero parseable results");
+  return out;
+}
+
+/**
+ * Rotates starting engine across calls so we don't always hammer DDG first.
+ * Persists a tiny round-robin counter in-process.
+ */
+let rrCounter = 0;
+
+export async function webSearch(query: string): Promise<SearchResult> {
+  const engines: { name: SearchResult["engine"]; run: () => Promise<Snippet[]> }[] = [
+    { name: "ddg", run: () => tryDdg(query) },
+    { name: "ddg_lite", run: () => tryDdgLite(query) },
+    { name: "brave", run: () => tryBrave(query) },
+  ];
+  // Round-robin start.
+  const start = rrCounter++ % engines.length;
+  const ordered = [...engines.slice(start), ...engines.slice(0, start)];
+  const errors: string[] = [];
+  for (const e of ordered) {
+    try {
+      const snippets = await e.run();
+      return { engine: e.name, snippets };
+    } catch (err) {
+      errors.push(`${e.name}: ${(err as Error).message}`);
+    }
+  }
+  return { engine: null, snippets: [], error: errors.join(" | ") };
+}
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..cdacb5f
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,105 @@
+import { NextResponse, type NextRequest } from "next/server";
+
+/**
+ * Protects /admin/* and /api/drops/admin by requiring a cookie `drops_admin`
+ * that matches ADMIN_TOKEN.
+ *
+ * Fail-closed in production: if ADMIN_TOKEN is unset when NODE_ENV=production,
+ * the admin surface returns 503 rather than being publicly accessible.
+ * In dev, missing token = open access (for quick iteration).
+ */
+export async function middleware(req: NextRequest) {
+  const { pathname, searchParams } = req.nextUrl;
+
+  // SECURITY (P0 fix 2026-05-04): expanded gate to cover the unauthenticated
+  // mutating routes a code-review surfaced. Before this, /api/drops/send,
+  // /api/drops/compose, /api/score, /api/swot, /api/brands/hunt were ALL
+  // public POSTs that could blast email to all subscribers, burn unbounded
+  // qwen3:14b cycles, or mass-rewrite composite_score on every item.
+  const needsAuth = pathname.startsWith("/admin")
+    || pathname.startsWith("/api/drops/admin")
+    || pathname === "/api/drops/send"
+    || pathname === "/api/drops/compose"
+    || pathname === "/api/score"
+    || pathname === "/api/swot"
+    || pathname === "/api/brands/hunt";
+  if (!needsAuth) return NextResponse.next();
+
+  const required = process.env.ADMIN_TOKEN;
+
+  if (!required) {
+    if (process.env.NODE_ENV === "production") {
+      // Fail-closed — do not accept any request until ADMIN_TOKEN is set.
+      if (pathname.startsWith("/api/")) {
+        return NextResponse.json({ error: "admin disabled — ADMIN_TOKEN not configured" }, { status: 503 });
+      }
+      return new NextResponse(
+        `<!doctype html><html><body style="font-family:Georgia,serif;max-width:420px;margin:80px auto;padding:20px;">
+          <h1>Admin disabled</h1>
+          <p>Set <code>ADMIN_TOKEN</code> in environment and restart.</p>
+        </body></html>`,
+        { status: 503, headers: { "content-type": "text/html" } }
+      );
+    }
+    return NextResponse.next(); // dev mode — no gate
+  }
+
+  const paramToken = searchParams.get("token");
+  const cookie = req.cookies.get("drops_admin")?.value;
+  const provided = paramToken ?? cookie ?? "";
+
+  if (provided && (await constantTimeEqual(provided, required))) {
+    const res = NextResponse.next();
+    if (paramToken) res.cookies.set("drops_admin", paramToken, {
+      httpOnly: true, sameSite: "lax", secure: true, maxAge: 60 * 60 * 24 * 30, path: "/",
+    });
+    return res;
+  }
+
+  if (pathname.startsWith("/api/")) {
+    return NextResponse.json({ error: "unauthorized" }, { status: 401 });
+  }
+  return new NextResponse(
+    `<!doctype html><html><body style="font-family:Georgia,serif;max-width:400px;margin:80px auto;padding:20px;">
+       <h1>Admin access</h1>
+       <p>Append <code>?token=YOUR_ADMIN_TOKEN</code> to the URL to enter.</p>
+     </body></html>`,
+    { status: 401, headers: { "content-type": "text/html" } }
+  );
+}
+
+export const config = {
+  matcher: [
+    "/admin/:path*",
+    "/api/drops/admin/:path*",
+    // P0 fix 2026-05-04: include the previously-unauth mutating routes.
+    "/api/drops/send",
+    "/api/drops/compose",
+    "/api/score",
+    "/api/swot",
+    "/api/brands/hunt",
+  ],
+};
+
+/**
+ * HMAC-hash-compare: length-oblivious by design.
+ * Both inputs are hashed with the same key first, so the comparison happens
+ * on fixed-length SHA-256 digests. Runs on the Edge runtime (no node:crypto).
+ */
+async function constantTimeEqual(a: string, b: string): Promise<boolean> {
+  const enc = new TextEncoder();
+  // Fixed key — rotating it would invalidate cookies, which is a feature if
+  // ADMIN_TOKEN changes. We just need ≠-length inputs to hash to same-length.
+  const key = await crypto.subtle.importKey(
+    "raw", enc.encode("drops-admin-v1"),
+    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
+  );
+  const [ha, hb] = await Promise.all([
+    crypto.subtle.sign("HMAC", key, enc.encode(a)),
+    crypto.subtle.sign("HMAC", key, enc.encode(b)),
+  ]);
+  const da = new Uint8Array(ha), db = new Uint8Array(hb);
+  let diff = da.length ^ db.length;
+  for (let i = 0; i < da.length; i++) diff |= da[i] ^ db[i];
+  return diff === 0;
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..cf93010
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,21 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+  content: ["./src/**/*.{ts,tsx}"],
+  theme: {
+    extend: {
+      colors: {
+        ink: "#0b0b0c",
+        parchment: "#f7f4ec",
+        brass: "#b38f4e",
+        ledger: "#1a1a1d",
+      },
+      fontFamily: {
+        serif: ['"Libre Caslon Text"', "Georgia", "serif"],
+        sans: ['"Inter"', "system-ui", "sans-serif"],
+      },
+    },
+  },
+  plugins: [],
+};
+export default config;
diff --git a/tests/lifecycle.test.ts b/tests/lifecycle.test.ts
new file mode 100644
index 0000000..1c49054
--- /dev/null
+++ b/tests/lifecycle.test.ts
@@ -0,0 +1,101 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import {
+  domainLifecycle, abandonmentStatus, opportunityLabel, extractLatestYear,
+} from "../src/lib/lifecycle";
+
+test("domainLifecycle — expired when HTTP 4xx", () => {
+  assert.equal(domainLifecycle(404, null), "expired");
+  assert.equal(domainLifecycle(503, null), "expired");
+});
+test("domainLifecycle — live when 2xx/3xx", () => {
+  assert.equal(domainLifecycle(200, null), "live");
+  assert.equal(domainLifecycle(301, null), "live");
+});
+test("domainLifecycle — expiring_soon when whois date within 90 days", () => {
+  const soon = new Date(Date.now() + 30 * 86400_000);
+  assert.equal(domainLifecycle(200, soon), "expiring_soon");
+});
+test("domainLifecycle — expired when whois date passed", () => {
+  const past = new Date(Date.now() - 5 * 86400_000);
+  assert.equal(domainLifecycle(200, past), "expired");
+});
+test("domainLifecycle — unknown on 0 status + no date", () => {
+  assert.equal(domainLifecycle(0, null), "unknown");
+  assert.equal(domainLifecycle(null, null), "unknown");
+});
+
+test("abandonmentStatus — buckets by years stale", () => {
+  assert.equal(abandonmentStatus(0), "active");
+  assert.equal(abandonmentStatus(0.5), "active");
+  assert.equal(abandonmentStatus(1), "stale");
+  assert.equal(abandonmentStatus(2.9), "stale");
+  assert.equal(abandonmentStatus(3), "abandoned");
+  assert.equal(abandonmentStatus(10), "abandoned");
+  assert.equal(abandonmentStatus(null), "unknown");
+});
+
+test("opportunityLabel — do_not_target when registered + active", () => {
+  const l = opportunityLabel({
+    isRegistered: true, yearsInBusiness: 10,
+    lifecycle: "live", abandonment: "active", isParked: false,
+  });
+  assert.equal(l, "do_not_target");
+});
+test("opportunityLabel — domain_snipe on expired lifecycle regardless of abandonment", () => {
+  const l = opportunityLabel({
+    isRegistered: false, yearsInBusiness: 10,
+    lifecycle: "expired", abandonment: "unknown", isParked: false,
+  });
+  assert.equal(l, "domain_snipe");
+});
+test("opportunityLabel — abandonment_watch when unregistered + 3+ yrs stale + live", () => {
+  const l = opportunityLabel({
+    isRegistered: false, yearsInBusiness: 10,
+    lifecycle: "live", abandonment: "abandoned", isParked: false,
+  });
+  assert.equal(l, "abandonment_watch");
+});
+test("opportunityLabel — licensing_outreach when unregistered + active + live", () => {
+  const l = opportunityLabel({
+    isRegistered: false, yearsInBusiness: 7,
+    lifecycle: "live", abandonment: "active", isParked: false,
+  });
+  assert.equal(l, "licensing_outreach");
+});
+test("opportunityLabel — unclear when parked", () => {
+  const l = opportunityLabel({
+    isRegistered: false, yearsInBusiness: 1,
+    lifecycle: "live", abandonment: "active", isParked: true,
+  });
+  assert.equal(l, "unclear");
+});
+test("opportunityLabel — unclear when USPTO unchecked (isRegistered=null)", () => {
+  const l = opportunityLabel({
+    isRegistered: null, yearsInBusiness: 7,
+    lifecycle: "live", abandonment: "active", isParked: false,
+  });
+  assert.equal(l, "unclear");
+});
+
+test("extractLatestYear — pulls © year from text", () => {
+  const { year, signal } = extractLatestYear("Some text © 2024 BrandCo. All rights reserved.");
+  assert.equal(year, 2024);
+  assert.ok(signal && signal.includes("2024"));
+});
+test("extractLatestYear — pulls 'Since 1995'", () => {
+  const { year } = extractLatestYear("Family-owned Since 1995");
+  assert.equal(year, 1995);
+});
+test("extractLatestYear — prefers highest recent year in a range", () => {
+  const { year } = extractLatestYear("© 2018–2023 BrandCo");
+  assert.equal(year, 2023);
+});
+test("extractLatestYear — returns null when nothing found", () => {
+  const { year } = extractLatestYear("No date information anywhere.");
+  assert.equal(year, null);
+});
+test("extractLatestYear — case-insensitive on Copyright word", () => {
+  const { year } = extractLatestYear("Copyright 2022 Some Company");
+  assert.equal(year, 2022);
+});
diff --git a/tests/scoring.test.ts b/tests/scoring.test.ts
new file mode 100644
index 0000000..63d9249
--- /dev/null
+++ b/tests/scoring.test.ts
@@ -0,0 +1,71 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { scoreItem, yearsExpired } from "../src/lib/scoring";
+
+test("scoreItem — max-out inputs land near 100", () => {
+  const s = scoreItem({
+    distinctiveness_score: 100,
+    recognition_score: 100,
+    domain_available: true,
+    competing_active_marks: 0,
+    monetization_breadth: 100,
+    expired_date: new Date(Date.now() - 8 * 365 * 86400_000).toISOString(),
+  });
+  assert.ok(s > 95, `expected > 95, got ${s}`);
+});
+
+test("scoreItem — min inputs land near 0", () => {
+  const s = scoreItem({
+    distinctiveness_score: 0,
+    recognition_score: 0,
+    domain_available: false,
+    competing_active_marks: 50,
+    monetization_breadth: 0,
+    expired_date: new Date().toISOString(),
+  });
+  assert.ok(s < 15, `expected < 15, got ${s}`);
+});
+
+test("scoreItem — very new expiration penalised for revival risk", () => {
+  const inputs = {
+    distinctiveness_score: 80,
+    recognition_score: 70,
+    domain_available: true,
+    competing_active_marks: 1,
+    monetization_breadth: 60,
+  };
+  const veryNew = scoreItem({ ...inputs, expired_date: new Date(Date.now() - 1 * 30 * 86400_000).toISOString() });
+  const sweetSpot = scoreItem({ ...inputs, expired_date: new Date(Date.now() - 6 * 365 * 86400_000).toISOString() });
+  assert.ok(sweetSpot > veryNew, `sweet-spot (${sweetSpot}) should beat very-new (${veryNew})`);
+});
+
+test("scoreItem — unknown domain availability sits between yes/no", () => {
+  const inputs = {
+    distinctiveness_score: 80,
+    recognition_score: 70,
+    competing_active_marks: 2,
+    monetization_breadth: 60,
+    expired_date: new Date(Date.now() - 6 * 365 * 86400_000).toISOString(),
+  };
+  const yes = scoreItem({ ...inputs, domain_available: true });
+  const no  = scoreItem({ ...inputs, domain_available: false });
+  const unk = scoreItem({ ...inputs, domain_available: null });
+  assert.ok(yes > unk && unk > no, `expected yes>unk>no, got ${yes}/${unk}/${no}`);
+});
+
+test("scoreItem — competing marks are linearly penalising", () => {
+  const base = {
+    distinctiveness_score: 70, recognition_score: 60,
+    domain_available: true, monetization_breadth: 50,
+    expired_date: new Date(Date.now() - 5 * 365 * 86400_000).toISOString(),
+  };
+  const clean = scoreItem({ ...base, competing_active_marks: 0 });
+  const some  = scoreItem({ ...base, competing_active_marks: 3 });
+  const many  = scoreItem({ ...base, competing_active_marks: 10 });
+  assert.ok(clean > some && some > many, `clean>some>many, got ${clean}/${some}/${many}`);
+});
+
+test("yearsExpired returns positive years rounded to 2 decimals", () => {
+  const y = yearsExpired({ expired_date: new Date(Date.now() - 365 * 86400_000).toISOString() } as never);
+  assert.ok(y > 0.9 && y < 1.1, `expected ~1 year, got ${y}`);
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..334fafd
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,21 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true,
+    "plugins": [{ "name": "next" }],
+    "paths": { "@/*": ["./src/*"] }
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
new file mode 100644
index 0000000..7f3033f
--- /dev/null
+++ b/tsconfig.tsbuildinfo
@@ -0,0 +1 @@
+{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./.next/types/routes.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/shared/lib/amp.d.ts","./node_modules/next/amp.d.ts","./node_modules/next/dist/server/get-page-files.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/react/canary.d.ts","./node_modules/@types/react/experimental.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-dom/canary.d.ts","./node_modules/@types/react-dom/experimental.d.ts","./node_modules/next/dist/lib/fallback.d.ts","./node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/next/dist/server/config.d.ts","./node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/next/dist/server/body-streams.d.ts","./node_modules/next/dist/server/lib/cache-control.d.ts","./node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/next/dist/lib/worker.d.ts","./node_modules/next/dist/lib/constants.d.ts","./node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","./node_modules/next/dist/server/require-hook.d.ts","./node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","./node_modules/next/dist/lib/page-types.d.ts","./node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/next/dist/server/node-environment.d.ts","./node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/next/dist/trace/types.d.ts","./node_modules/next/dist/trace/trace.d.ts","./node_modules/next/dist/trace/shared.d.ts","./node_modules/next/dist/trace/index.d.ts","./node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/@next/env/dist/index.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","./node_modules/next/dist/telemetry/storage.d.ts","./node_modules/next/dist/build/build-context.d.ts","./node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/next/dist/build/webpack-config.d.ts","./node_modules/next/dist/server/route-kind.d.ts","./node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/next/dist/build/swc/types.d.ts","./node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/next/dist/next-devtools/shared/types.d.ts","./node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","./node_modules/next/dist/server/lib/parse-stack.d.ts","./node_modules/next/dist/next-devtools/server/shared.d.ts","./node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","./node_modules/@types/react/jsx-runtime.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","./node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/next/dist/server/render-result.d.ts","./node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/next/dist/server/web/next-url.d.ts","./node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/next/dist/server/web/types.d.ts","./node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/next/dist/server/base-http/node.d.ts","./node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/next/dist/client/with-router.d.ts","./node_modules/next/dist/client/router.d.ts","./node_modules/next/dist/client/route-loader.d.ts","./node_modules/next/dist/client/page-loader.d.ts","./node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/next/dist/server/lib/lazy-result.d.ts","./node_modules/next/dist/server/lib/implicit-tags.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/next/dist/client/components/client-page.d.ts","./node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/next/dist/server/request/search-params.d.ts","./node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","./node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/next/dist/lib/metadata/types/icons.d.ts","./node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","./node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/next/dist/lib/framework/boundary-components.d.ts","./node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","./node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","./node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/@types/react/jsx-dev-runtime.d.ts","./node_modules/@types/react/compiler-runtime.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","./node_modules/@types/react-dom/client.d.ts","./node_modules/@types/react-dom/static.d.ts","./node_modules/@types/react-dom/server.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/next/dist/server/web/adapter.d.ts","./node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/next/dist/server/app-render/types.d.ts","./node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/next/dist/build/templates/pages.d.ts","./node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","./node_modules/next/dist/server/render.d.ts","./node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","./node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","./node_modules/next/dist/build/static-paths/types.d.ts","./node_modules/next/dist/server/base-server.d.ts","./node_modules/next/dist/server/lib/async-callback-set.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/sharp/lib/index.d.ts","./node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/next/dist/server/next-server.d.ts","./node_modules/next/dist/server/lib/types.d.ts","./node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/next/dist/server/next.d.ts","./node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","./node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/next/dist/server/load-components.d.ts","./node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/next/dist/server/web/http.d.ts","./node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/next/dist/client/components/redirect-error.d.ts","./node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/next/dist/build/utils.d.ts","./node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/next/dist/export/routes/types.d.ts","./node_modules/next/dist/export/types.d.ts","./node_modules/next/dist/export/worker.d.ts","./node_modules/next/dist/build/worker.d.ts","./node_modules/next/dist/build/index.d.ts","./node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/next/dist/server/after/after.d.ts","./node_modules/next/dist/server/after/after-context.d.ts","./node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/next/dist/server/request/params.d.ts","./node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/next/dist/server/request-meta.d.ts","./node_modules/next/dist/cli/next-test.d.ts","./node_modules/next/dist/server/config-shared.d.ts","./node_modules/next/dist/server/base-http/index.d.ts","./node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/next/dist/types.d.ts","./node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/next/dist/pages/_app.d.ts","./node_modules/next/app.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/next/cache.d.ts","./node_modules/next/dist/shared/lib/runtime-config.external.d.ts","./node_modules/next/config.d.ts","./node_modules/next/dist/pages/_document.d.ts","./node_modules/next/document.d.ts","./node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/next/dynamic.d.ts","./node_modules/next/dist/pages/_error.d.ts","./node_modules/next/error.d.ts","./node_modules/next/dist/shared/lib/head.d.ts","./node_modules/next/head.d.ts","./node_modules/next/dist/server/request/cookies.d.ts","./node_modules/next/dist/server/request/headers.d.ts","./node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/next/headers.d.ts","./node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/next/dist/client/image-component.d.ts","./node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/next/image.d.ts","./node_modules/next/dist/client/link.d.ts","./node_modules/next/link.d.ts","./node_modules/next/dist/client/components/redirect.d.ts","./node_modules/next/dist/client/components/not-found.d.ts","./node_modules/next/dist/client/components/forbidden.d.ts","./node_modules/next/dist/client/components/unauthorized.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/next/dist/client/components/unrecognized-action-error.d.ts","./node_modules/next/dist/client/components/navigation.d.ts","./node_modules/next/navigation.d.ts","./node_modules/next/router.d.ts","./node_modules/next/dist/client/script.d.ts","./node_modules/next/script.d.ts","./node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/next/dist/server/after/index.d.ts","./node_modules/next/dist/server/request/root-params.d.ts","./node_modules/next/dist/server/request/connection.d.ts","./node_modules/next/server.d.ts","./node_modules/next/types/global.d.ts","./node_modules/next/types/compiled.d.ts","./node_modules/next/types.d.ts","./node_modules/next/index.d.ts","./node_modules/next/image-types/global.d.ts","./next-env.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/tailwindcss/types/generated/corepluginlist.d.ts","./node_modules/tailwindcss/types/generated/colors.d.ts","./node_modules/tailwindcss/types/config.d.ts","./node_modules/tailwindcss/types/index.d.ts","./tailwind.config.ts","./node_modules/pg-types/index.d.ts","./node_modules/pg-protocol/dist/messages.d.ts","./node_modules/pg-protocol/dist/serializer.d.ts","./node_modules/pg-protocol/dist/parser.d.ts","./node_modules/pg-protocol/dist/index.d.ts","./node_modules/@types/pg/lib/type-overrides.d.ts","./node_modules/@types/pg/index.d.ts","./node_modules/@types/pg/index.d.mts","./src/lib/db.ts","./src/lib/types.ts","./src/lib/scoring.ts","./db/seed.ts","./db/seed_expansion.ts","./src/lib/email.ts","./scripts/alert-top-scores.ts","./src/lib/qwen.ts","./src/lib/drops.ts","./scripts/backfill-drops.ts","./scripts/daily-drop.ts","./src/middleware.ts","./src/app/robots.ts","./src/app/sitemap.ts","./src/app/api/brands/route.ts","./src/lib/websearch.ts","./src/lib/lifecycle.ts","./src/lib/brandhunter.ts","./src/app/api/brands/hunt/route.ts","./src/app/api/changelog/route.ts","./src/app/api/drops/admin/route.ts","./src/app/api/drops/admin/subscribers/route.ts","./src/app/api/drops/admin/subscribers/[id]/route.ts","./src/lib/stripe.ts","./src/app/api/drops/checkout/route.ts","./src/app/api/drops/compose/route.ts","./src/lib/ratelimit.ts","./src/app/api/drops/cron/route.ts","./src/app/api/drops/drop-items/[id]/route.ts","./src/app/api/drops/latest/route.ts","./src/app/api/drops/pixel/[id]/route.ts","./src/app/api/drops/preview/[token]/[id]/route.ts","./src/app/api/drops/send/route.ts","./src/app/api/drops/signup/route.ts","./src/app/api/drops/stripe-webhook/route.ts","./src/app/api/drops/unsubscribe/[token]/route.ts","./src/app/api/drops/view/[token]/route.ts","./src/app/api/health/route.ts","./src/app/api/items/route.ts","./src/app/api/items/[id]/route.ts","./node_modules/@types/xml2js/lib/processors.d.ts","./node_modules/@types/xml2js/index.d.ts","./src/app/api/news/route.ts","./src/app/api/score/route.ts","./src/lib/swot.ts","./src/app/api/swot/route.ts","./tests/lifecycle.test.ts","./tests/scoring.test.ts","./src/app/error.tsx","./src/app/layout.tsx","./src/app/not-found.tsx","./node_modules/next/dist/compiled/@vercel/og/index.edge.d.ts","./node_modules/next/dist/compiled/@vercel/og/index.node.d.ts","./node_modules/next/dist/server/og/image-response.d.ts","./node_modules/next/og.d.ts","./src/app/opengraph-image.tsx","./src/components/disclaimer.tsx","./node_modules/@tanstack/table-core/build/lib/utils.d.ts","./node_modules/@tanstack/table-core/build/lib/core/table.d.ts","./node_modules/@tanstack/table-core/build/lib/features/columnvisibility.d.ts","./node_modules/@tanstack/table-core/build/lib/features/columnordering.d.ts","./node_modules/@tanstack/table-core/build/lib/features/columnpinning.d.ts","./node_modules/@tanstack/table-core/build/lib/features/rowpinning.d.ts","./node_modules/@tanstack/table-core/build/lib/core/headers.d.ts","./node_modules/@tanstack/table-core/build/lib/features/columnfaceting.d.ts","./node_modules/@tanstack/table-core/build/lib/features/globalfaceting.d.ts","./node_modules/@tanstack/table-core/build/lib/filterfns.d.ts","./node_modules/@tanstack/table-core/build/lib/features/columnfiltering.d.ts","./node_modules/@tanstack/table-core/build/lib/features/globalfiltering.d.ts","./node_modules/@tanstack/table-core/build/lib/sortingfns.d.ts","./node_modules/@tanstack/table-core/build/lib/features/rowsorting.d.ts","./node_modules/@tanstack/table-core/build/lib/aggregationfns.d.ts","./node_modules/@tanstack/table-core/build/lib/features/columngrouping.d.ts","./node_modules/@tanstack/table-core/build/lib/features/rowexpanding.d.ts","./node_modules/@tanstack/table-core/build/lib/features/columnsizing.d.ts","./node_modules/@tanstack/table-core/build/lib/features/rowpagination.d.ts","./node_modules/@tanstack/table-core/build/lib/features/rowselection.d.ts","./node_modules/@tanstack/table-core/build/lib/core/row.d.ts","./node_modules/@tanstack/table-core/build/lib/core/cell.d.ts","./node_modules/@tanstack/table-core/build/lib/core/column.d.ts","./node_modules/@tanstack/table-core/build/lib/types.d.ts","./node_modules/@tanstack/table-core/build/lib/columnhelper.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getcorerowmodel.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getexpandedrowmodel.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getfacetedminmaxvalues.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getfacetedrowmodel.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getfaceteduniquevalues.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getfilteredrowmodel.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getgroupedrowmodel.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getpaginationrowmodel.d.ts","./node_modules/@tanstack/table-core/build/lib/utils/getsortedrowmodel.d.ts","./node_modules/@tanstack/table-core/build/lib/index.d.ts","./node_modules/@tanstack/react-table/build/lib/index.d.ts","./src/components/itemstable.tsx","./src/app/page.tsx","./src/app/about/page.tsx","./src/components/dropeditor.tsx","./src/app/admin/drops/page.tsx","./src/app/admin/subscribers/page.tsx","./src/app/alerts/page.tsx","./node_modules/@types/d3-array/index.d.ts","./node_modules/@types/d3-selection/index.d.ts","./node_modules/@types/d3-axis/index.d.ts","./node_modules/@types/d3-brush/index.d.ts","./node_modules/@types/d3-chord/index.d.ts","./node_modules/@types/d3-color/index.d.ts","./node_modules/@types/geojson/index.d.ts","./node_modules/@types/d3-contour/index.d.ts","./node_modules/@types/d3-delaunay/index.d.ts","./node_modules/@types/d3-dispatch/index.d.ts","./node_modules/@types/d3-drag/index.d.ts","./node_modules/@types/d3-dsv/index.d.ts","./node_modules/@types/d3-ease/index.d.ts","./node_modules/@types/d3-fetch/index.d.ts","./node_modules/@types/d3-force/index.d.ts","./node_modules/@types/d3-format/index.d.ts","./node_modules/@types/d3-geo/index.d.ts","./node_modules/@types/d3-hierarchy/index.d.ts","./node_modules/@types/d3-interpolate/index.d.ts","./node_modules/@types/d3-path/index.d.ts","./node_modules/@types/d3-polygon/index.d.ts","./node_modules/@types/d3-quadtree/index.d.ts","./node_modules/@types/d3-random/index.d.ts","./node_modules/@types/d3-time/index.d.ts","./node_modules/@types/d3-scale/index.d.ts","./node_modules/@types/d3-scale-chromatic/index.d.ts","./node_modules/@types/d3-shape/index.d.ts","./node_modules/@types/d3-time-format/index.d.ts","./node_modules/@types/d3-timer/index.d.ts","./node_modules/@types/d3-transition/index.d.ts","./node_modules/@types/d3-zoom/index.d.ts","./node_modules/@types/d3/index.d.ts","./node_modules/@reactflow/core/dist/esm/types/utils.d.ts","./node_modules/@reactflow/core/dist/esm/utils/index.d.ts","./node_modules/@reactflow/core/dist/esm/types/nodes.d.ts","./node_modules/@reactflow/core/dist/esm/types/edges.d.ts","./node_modules/@reactflow/core/dist/esm/types/changes.d.ts","./node_modules/@reactflow/core/dist/esm/types/handles.d.ts","./node_modules/@reactflow/core/dist/esm/types/instance.d.ts","./node_modules/@reactflow/core/dist/esm/types/general.d.ts","./node_modules/@reactflow/core/dist/esm/components/handle/utils.d.ts","./node_modules/@reactflow/core/dist/esm/types/component-props.d.ts","./node_modules/@reactflow/core/dist/esm/types/index.d.ts","./node_modules/@reactflow/core/dist/esm/container/reactflow/index.d.ts","./node_modules/@reactflow/core/dist/esm/components/handle/index.d.ts","./node_modules/@reactflow/core/dist/esm/components/edges/edgetext.d.ts","./node_modules/@reactflow/core/dist/esm/components/edges/straightedge.d.ts","./node_modules/@reactflow/core/dist/esm/components/edges/stepedge.d.ts","./node_modules/@reactflow/core/dist/esm/components/edges/bezieredge.d.ts","./node_modules/@reactflow/core/dist/esm/components/edges/simplebezieredge.d.ts","./node_modules/@reactflow/core/dist/esm/components/edges/smoothstepedge.d.ts","./node_modules/@reactflow/core/dist/esm/components/edges/baseedge.d.ts","./node_modules/@reactflow/core/dist/esm/utils/graph.d.ts","./node_modules/@reactflow/core/dist/esm/utils/changes.d.ts","./node_modules/zustand/esm/vanilla.d.mts","./node_modules/zustand/esm/react.d.mts","./node_modules/zustand/esm/index.d.mts","./node_modules/@reactflow/core/dist/esm/components/edges/utils.d.ts","./node_modules/@reactflow/core/dist/esm/components/reactflowprovider/index.d.ts","./node_modules/@reactflow/core/dist/esm/components/panel/index.d.ts","./node_modules/@reactflow/core/dist/esm/components/edgelabelrenderer/index.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/usereactflow.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/useupdatenodeinternals.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/usenodes.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/useedges.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/useviewport.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/usekeypress.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/usenodesedgesstate.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/usestore.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/useonviewportchange.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/useonselectionchange.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/usenodesinitialized.d.ts","./node_modules/@reactflow/core/dist/esm/hooks/usegetpointerposition.d.ts","./node_modules/@reactflow/core/dist/esm/contexts/nodeidcontext.d.ts","./node_modules/@reactflow/core/dist/esm/index.d.ts","./node_modules/@reactflow/minimap/dist/esm/types.d.ts","./node_modules/@reactflow/minimap/dist/esm/minimap.d.ts","./node_modules/@reactflow/minimap/dist/esm/index.d.ts","./node_modules/@reactflow/controls/dist/esm/types.d.ts","./node_modules/@reactflow/controls/dist/esm/controls.d.ts","./node_modules/@reactflow/controls/dist/esm/controlbutton.d.ts","./node_modules/@reactflow/controls/dist/esm/index.d.ts","./node_modules/@reactflow/background/dist/esm/types.d.ts","./node_modules/@reactflow/background/dist/esm/background.d.ts","./node_modules/@reactflow/background/dist/esm/index.d.ts","./node_modules/@reactflow/node-toolbar/dist/esm/types.d.ts","./node_modules/@reactflow/node-toolbar/dist/esm/nodetoolbar.d.ts","./node_modules/@reactflow/node-toolbar/dist/esm/index.d.ts","./node_modules/@reactflow/node-resizer/dist/esm/types.d.ts","./node_modules/@reactflow/node-resizer/dist/esm/noderesizer.d.ts","./node_modules/@reactflow/node-resizer/dist/esm/resizecontrol.d.ts","./node_modules/@reactflow/node-resizer/dist/esm/index.d.ts","./node_modules/reactflow/dist/esm/index.d.ts","./src/components/canvas.tsx","./src/app/canvas/page.tsx","./src/app/changelog/page.tsx","./src/app/drops/page.tsx","./src/app/drops/archive/page.tsx","./src/app/drops/archive/[id]/page.tsx","./src/app/drops/unsubscribe/[token]/page.tsx","./src/components/referralwidget.tsx","./src/app/drops/view/[token]/page.tsx","./src/app/drops/view/[token]/[dropid]/page.tsx","./src/app/faq/page.tsx","./src/components/newsfeed.tsx","./src/components/swotpanel.tsx","./src/app/item/[id]/page.tsx","./src/app/legal/privacy/page.tsx","./src/app/legal/refund/page.tsx","./src/app/legal/terms/page.tsx","./src/app/pricing/page.tsx","./src/components/brandhunter.tsx","./src/app/unregistered/page.tsx","./.next/types/cache-life.d.ts","./.next/types/validator.ts","./.next/types/app/page.ts","./.next/types/app/about/page.ts","./.next/types/app/admin/drops/page.ts","./.next/types/app/admin/subscribers/page.ts","./.next/types/app/alerts/page.ts","./.next/types/app/api/brands/route.ts","./.next/types/app/api/brands/hunt/route.ts","./.next/types/app/api/changelog/route.ts","./.next/types/app/api/drops/admin/route.ts","./.next/types/app/api/drops/admin/subscribers/route.ts","./.next/types/app/api/drops/admin/subscribers/[id]/route.ts","./.next/types/app/api/drops/checkout/route.ts","./.next/types/app/api/drops/compose/route.ts","./.next/types/app/api/drops/cron/route.ts","./.next/types/app/api/drops/drop-items/[id]/route.ts","./.next/types/app/api/drops/latest/route.ts","./.next/types/app/api/drops/pixel/[id]/route.ts","./.next/types/app/api/drops/preview/[token]/[id]/route.ts","./.next/types/app/api/drops/send/route.ts","./.next/types/app/api/drops/signup/route.ts","./.next/types/app/api/drops/stripe-webhook/route.ts","./.next/types/app/api/drops/unsubscribe/[token]/route.ts","./.next/types/app/api/drops/view/[token]/route.ts","./.next/types/app/api/health/route.ts","./.next/types/app/api/items/route.ts","./.next/types/app/api/items/[id]/route.ts","./.next/types/app/api/news/route.ts","./.next/types/app/api/score/route.ts","./.next/types/app/api/swot/route.ts","./.next/types/app/canvas/page.ts","./.next/types/app/changelog/page.ts","./.next/types/app/drops/page.ts","./.next/types/app/drops/archive/page.ts","./.next/types/app/drops/archive/[id]/page.ts","./.next/types/app/drops/unsubscribe/[token]/page.ts","./.next/types/app/drops/view/[token]/page.ts","./.next/types/app/drops/view/[token]/[dropid]/page.ts","./.next/types/app/faq/page.ts","./.next/types/app/item/[id]/page.ts","./.next/types/app/legal/privacy/page.ts","./.next/types/app/legal/refund/page.ts","./.next/types/app/legal/terms/page.ts","./.next/types/app/pricing/page.ts","./.next/types/app/unregistered/page.ts"],"fileIdsList":[[100,148,165,166,341,630],[100,148,165,166,341,632],[100,148,165,166,341,633],[100,148,165,166,341,634],[100,148,165,166,495,553],[100,148,165,166,495,549],[100,148,165,166,495,554],[100,148,165,166,495,555],[100,148,165,166,495,557],[100,148,165,166,495,556],[100,148,165,166,495,559],[100,148,165,166,495,560],[100,148,165,166,495,562],[100,148,165,166,495,563],[100,148,165,166,495,564],[100,148,165,166,495,565],[100,148,165,166,495,566],[100,148,165,166,495,567],[100,148,165,166,495,568],[100,148,165,166,495,569],[100,148,165,166,495,570],[100,148,165,166,495,571],[100,148,165,166,495,572],[100,148,165,166,495,574],[100,148,165,166,495,573],[100,148,165,166,495,577],[100,148,165,166,495,578],[100,148,165,166,495,580],[100,148,165,166,341,729],[100,148,165,166,341,730],[100,148,165,166,341,733],[100,148,165,166,341,732],[100,148,165,166,341,731],[100,148,165,166,341,734],[100,148,165,166,341,737],[100,148,165,166,341,736],[100,148,165,166,341,738],[100,148,165,166,341,741],[100,148,165,166,341,742],[100,148,165,166,341,743],[100,148,165,166,341,744],[100,148,165,166,341,629],[100,148,165,166,341,745],[100,148,165,166,341,747],[100,148,165,166,448,449,450,451],[100,148,165,166],[83,100,148,165,166,495,498,549,553,554,555,556,557,559,560,562,563,564,565,566,567,568,569,570,571,572,573,574,577,578,580,584,629,630,632,633,634,729,730,731,732,733,734,736,737,738,741,742,743,744,745,747],[100,148,165,166,535,537],[83,100,148,165,166,499,500],[86,100,148,165,166,717],[100,148,165,166,717,718],[86,100,148,165,166],[86,100,148,165,166,713],[100,148,165,166,713,714,715],[86,100,148,165,166,709],[86,100,148,165,166,677],[86,100,148,165,166,677,691],[86,100,148,165,166,675,677],[100,148,165,166,677],[100,148,165,166,677,691],[100,148,165,166,668,677,678,679,680,681,682,683,684,685,686,687,688,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708],[100,148,165,166,667,669,670],[86,100,148,165,166,666,667,669,670,671,672,673,677],[100,148,165,166,667,669,670,671,672,673,674,676],[86,100,148,165,166,668,677],[100,148,165,166,666,677],[100,148,165,166,710,711],[86,100,148,165,166,710],[100,148,165,166,723,724,725],[86,100,148,165,166,723],[86,100,148,165,166,645],[100,148,165,166,720,721],[86,100,148,165,166,720],[86,100,148,165,166,626],[100,148,165,166,607],[100,148,165,166,592,615],[100,148,165,166,615],[100,148,165,166,615,626],[100,148,165,166,601,615,626],[100,148,165,166,606,615,626],[100,148,165,166,596,615],[100,148,165,166,604,615,626],[100,148,165,166,602],[100,148,165,166,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625],[100,148,165,166,605],[100,148,165,166,592,593,594,595,596,597,598,599,600,602,603,605,607,608,609,610,611,612,613,614],[100,148,165,166,636,664],[100,148,165,166,635,641],[100,148,165,166,646],[100,148,165,166,641],[100,148,165,166,640],[100,148,165,166,658],[100,148,165,166,654],[100,148,165,166,636,653,664],[100,148,165,166,635,636,637,638,639,640,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665],[100,145,146,148,165,166],[100,147,148,165,166],[148,165,166],[100,148,153,165,166,183],[100,148,149,154,159,165,166,168,180,191],[100,148,149,150,159,165,166,168],[95,96,97,100,148,165,166],[100,148,151,165,166,192],[100,148,152,153,160,165,166,169],[100,148,153,165,166,180,188],[100,148,154,156,159,165,166,168],[100,147,148,155,165,166],[100,148,156,157,165,166],[100,148,158,159,165,166],[100,147,148,159,165,166],[100,148,159,160,161,165,166,180,191],[100,148,159,160,161,165,166,175,180,183],[100,141,148,156,159,162,165,166,168,180,191],[100,148,159,160,162,163,165,166,168,180,188,191],[100,148,162,164,165,166,180,188,191],[98,99,100,101,102,103,104,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197],[100,148,159,165,166],[100,148,165,166,167,191],[100,148,156,159,165,166,168,180],[100,148,165,166,169],[100,148,165,166,170],[100,147,148,165,166,171],[100,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197],[100,148,165,166,173],[100,148,165,166,174],[100,148,159,165,166,175,176],[100,148,165,166,175,177,192,194],[100,148,160,165,166],[100,148,159,165,166,180,181,183],[100,148,165,166,182,183],[100,148,165,166,180,181],[100,148,165,166,183],[100,148,165,166,184],[100,145,148,165,166,180,185,191],[100,148,159,165,166,186,187],[100,148,165,166,186,187],[100,148,153,165,166,168,180,188],[100,148,165,166,189],[100,148,165,166,168,190],[100,148,162,165,166,174,191],[100,148,153,165,166,192],[100,148,165,166,180,193],[100,148,165,166,167,194],[100,148,165,166,195],[100,141,148,165,166],[100,141,148,159,161,165,166,171,180,183,191,193,194,196],[100,148,165,166,180,197],[100,148,165,166,533],[100,148,159,165,166,180,188,198,527,528,531,532,533],[86,90,100,148,165,166,199,200,201,203,443,491],[86,90,100,148,165,166,199,200,201,202,358,443,491],[86,90,100,148,165,166,199,200,202,203,443,491],[86,100,148,165,166,203,358,359],[86,100,148,165,166,203,358],[86,90,100,148,165,166,200,201,202,203,443,491],[86,90,100,148,165,166,199,201,202,203,443,491],[84,85,100,148,165,166],[100,148,159,165,166,198,575],[92,100,148,165,166],[100,148,165,166,446],[100,148,165,166,453],[100,148,165,166,207,221,222,223,225,440],[100,148,165,166,207,246,248,250,251,254,440,442],[100,148,165,166,207,211,213,214,215,216,217,429,440,442],[100,148,165,166,440],[100,148,165,166,222,324,410,419,436],[100,148,165,166,207],[100,148,165,166,204,436],[100,148,165,166,258],[100,148,165,166,257,440,442],[100,148,162,165,166,306,324,353,497],[100,148,162,165,166,317,333,419,435],[100,148,162,165,166,371],[100,148,165,166,423],[100,148,165,166,422,423,424],[100,148,165,166,422],[94,100,148,162,165,166,204,207,211,214,218,219,220,222,226,234,235,364,389,420,440,443],[100,148,165,166,207,224,242,246,247,252,253,440,497],[100,148,165,166,224,497],[100,148,165,166,235,242,304,440,497],[100,148,165,166,497],[100,148,165,166,207,224,225,497],[100,148,165,166,249,497],[100,148,165,166,218,421,428],[100,148,165,166,174,266,436],[100,148,165,166,266,436],[86,100,148,165,166,266],[86,100,148,165,166,325],[100,148,165,166,321,369,436,479,480],[100,148,165,166,416,473,474,475,476,478],[100,148,165,166,415],[100,148,165,166,415,416],[100,148,165,166,215,365,366,367],[100,148,165,166,365,368,369],[100,148,165,166,477],[100,148,165,166,365,369],[86,100,148,165,166,208,467],[86,100,148,165,166,191],[86,100,148,165,166,224,294],[86,100,148,165,166,224],[100,148,165,166,292,296],[86,100,148,165,166,293,445],[86,100,148,165,166,491],[86,100,148,165,166,180,198,491,586],[86,90,100,148,162,165,166,198,199,200,201,202,203,443,489,490],[100,148,162,165,166],[100,148,162,165,166,211,273,365,375,390,410,425,426,440,441,497],[100,148,165,166,234,427],[100,148,165,166,443],[100,148,165,166,206],[86,100,148,165,166,306,320,332,342,344,435],[100,148,165,166,174,306,320,341,342,343,435,496],[100,148,165,166,335,336,337,338,339,340],[100,148,165,166,337],[100,148,165,166,341],[100,148,165,166,264,265,266,268],[86,100,148,165,166,259,260,261,267],[100,148,165,166,264,267],[100,148,165,166,262],[100,148,165,166,263],[86,100,148,165,166,266,293,445],[86,100,148,165,166,266,444,445],[86,100,148,165,166,266,445],[100,148,165,166,390,432],[100,148,165,166,432],[100,148,162,165,166,441,445],[100,148,165,166,329],[100,147,148,165,166,328],[100,148,165,166,236,274,312,314,316,317,318,319,362,365,435,438,441],[100,148,165,166,236,350,365,369],[100,148,165,166,317,435],[86,100,148,165,166,317,326,327,329,330,331,332,333,334,345,346,347,348,349,351,352,435,436,497],[100,148,165,166,311],[100,148,162,165,166,174,236,237,273,288,318,362,363,364,369,390,410,431,440,441,442,443,497],[100,148,165,166,435],[100,147,148,165,166,222,315,318,364,431,433,434,441],[100,148,165,166,317],[100,147,148,165,166,273,278,307,308,309,310,311,312,313,314,316,435,436],[100,148,162,165,166,278,279,307,441,442],[100,148,165,166,222,364,365,390,431,435,441],[100,148,162,165,166,440,442],[100,148,162,165,166,180,438,441,442],[100,148,162,165,166,174,191,204,211,224,236,237,239,274,275,280,285,288,314,318,365,375,377,380,382,385,386,387,388,389,410,430,431,436,438,440,441,442],[100,148,162,165,166,180],[100,148,165,166,207,208,209,211,216,219,224,242,430,438,439,443,445,497],[100,148,162,165,166,180,191,254,256,258,259,260,261,268,497],[100,148,165,166,174,191,204,246,256,284,285,286,287,314,365,380,389,390,396,399,400,410,431,436,438],[100,148,165,166,218,219,234,364,389,431,440],[100,148,162,165,166,191,208,211,314,394,438,440],[100,148,165,166,305],[100,148,162,165,166,397,398,407],[100,148,165,166,438,440],[100,148,165,166,312,315],[100,148,165,166,314,318,430,445],[100,148,162,165,166,174,240,246,287,380,390,396,399,402,438],[100,148,162,165,166,218,234,246,403],[100,148,165,166,207,239,405,430,440],[100,148,162,165,166,191,440],[100,148,162,165,166,224,238,239,240,251,269,404,406,430,440],[94,100,148,165,166,236,318,409,443,445],[100,148,162,165,166,174,191,211,218,226,234,237,274,280,284,285,286,287,288,314,365,377,390,391,393,395,410,430,431,436,437,438,445],[100,148,162,165,166,180,218,396,401,407,438],[100,148,165,166,229,230,231,232,233],[100,148,165,166,275,381],[100,148,165,166,383],[100,148,165,166,381],[100,148,165,166,383,384],[100,148,165,166,587],[100,148,162,165,166,211,214,215,273,441],[100,148,162,165,166,174,206,208,236,274,288,318,373,374,410,438,442,443,445],[100,148,162,165,166,174,191,210,215,314,374,437,441],[100,148,165,166,307],[100,148,165,166,308],[100,148,165,166,309],[100,148,165,166,436],[100,148,165,166,255,271],[100,148,162,165,166,211,255,274],[100,148,165,166,270,271],[100,148,165,166,272],[100,148,165,166,255,256],[100,148,165,166,255,289],[100,148,165,166,255],[100,148,165,166,275,379,437],[100,148,165,166,378],[100,148,165,166,256,436,437],[100,148,165,166,376,437],[100,148,165,166,256,436],[100,148,165,166,362],[100,148,165,166,211,216,274,303,306,312,314,318,320,323,354,357,361,365,409,430,438,441],[100,148,165,166,297,300,301,302,321,322,369],[86,100,148,165,166,201,203,266,355,356],[86,100,148,165,166,201,203,266,355,356,360],[100,148,165,166,418],[100,148,165,166,222,279,317,318,329,333,365,409,411,412,413,414,416,417,420,430,435,440],[100,148,165,166,369],[100,148,165,166,373],[100,148,162,165,166,274,290,370,372,375,409,438,443,445],[100,148,165,166,297,298,299,300,301,302,321,322,369,444],[94,100,148,162,165,166,174,191,237,255,256,288,314,318,407,408,410,430,431,440,441,443],[100,148,165,166,279,281,284,431],[100,148,162,165,166,275,440],[100,148,165,166,278,317],[100,148,165,166,277],[100,148,165,166,279,280],[100,148,165,166,276,278,440],[100,148,162,165,166,210,279,281,282,283,440,441],[86,100,148,165,166,365,366,368],[100,148,165,166,241],[86,100,148,165,166,208],[86,100,148,165,166,436],[86,94,100,148,165,166,288,318,443,445],[100,148,165,166,208,467,468],[86,100,148,165,166,296],[86,100,148,165,166,174,191,206,253,291,293,295,445],[100,148,165,166,224,436,441],[100,148,165,166,392,436],[100,148,165,166,365],[86,100,148,160,162,165,166,174,206,242,248,296,443,444],[86,100,148,165,166,199,200,201,202,203,443,491],[86,87,88,89,90,100,148,165,166],[100,148,153,165,166],[100,148,165,166,243,244,245],[100,148,165,166,243],[86,90,100,148,162,164,165,166,174,198,199,200,201,202,203,204,206,237,341,402,440,442,445,491],[100,148,165,166,455],[100,148,165,166,457],[100,148,165,166,459],[100,148,165,166,461],[100,148,165,166,463,464,465],[100,148,165,166,469],[91,93,100,148,165,166,447,452,454,456,458,460,462,466,470,472,482,483,485,495,496,497,498],[100,148,165,166,471],[100,148,165,166,481],[100,148,165,166,588],[100,148,165,166,293],[100,148,165,166,484],[100,147,148,165,166,279,281,282,284,332,436,486,487,488,491,492,493,494],[100,148,165,166,198],[100,148,165,166,198,528,529,530],[100,148,165,166,180,198,528],[100,148,165,166,517],[100,148,165,166,515,517],[100,148,165,166,506,514,515,516,518,520],[100,148,165,166,504],[100,148,165,166,507,512,517,520],[100,148,165,166,503,520],[100,148,165,166,507,508,511,512,513,520],[100,148,165,166,507,508,509,511,512,520],[100,148,165,166,504,505,506,507,508,512,513,514,516,517,518,520],[100,148,165,166,520],[100,148,165,166,502,504,505,506,507,508,509,511,512,513,514,515,516,517,518,519],[100,148,165,166,502,520],[100,148,165,166,507,509,510,512,513,520],[100,148,165,166,511,520],[100,148,165,166,512,513,517,520],[100,148,165,166,505,515],[100,148,165,166,709,712,716,719,722,726],[100,148,165,166,180,198],[100,148,165,166,522,523],[100,148,165,166,521,524],[100,113,117,148,165,166,191],[100,113,148,165,166,180,191],[100,108,148,165,166],[100,110,113,148,165,166,188,191],[100,148,165,166,168,188],[100,108,148,165,166,198],[100,110,113,148,165,166,168,191],[100,105,106,109,112,148,159,165,166,180,191],[100,113,120,148,165,166],[100,105,111,148,165,166],[100,113,134,135,148,165,166],[100,109,113,148,165,166,183,191,198],[100,134,148,165,166,198],[100,107,108,148,165,166,198],[100,113,148,165,166],[100,107,108,109,110,111,112,113,114,115,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,135,136,137,138,139,140,148,165,166],[100,113,128,148,165,166],[100,113,120,121,148,165,166],[100,111,113,121,122,148,165,166],[100,112,148,165,166],[100,105,108,113,148,165,166],[100,113,117,121,122,148,165,166],[100,117,148,165,166],[100,111,113,116,148,165,166,191],[100,105,110,113,120,148,165,166],[100,148,165,166,180],[100,108,113,134,148,165,166,196,198],[100,148,165,166,689,690],[100,148,165,166,689],[100,148,160,165,166,170,535,540],[100,148,165,166,535,543],[100,148,160,165,166,170,535,540,543],[100,148,165,166,472],[86,100,148,165,166,631],[100,148,165,166,472,535],[100,148,165,166,495,542,552],[100,148,165,166,495,535],[100,148,165,166,495],[100,148,165,166,495,535,558],[100,148,165,166,495,542,543],[100,148,153,165,166,495,543,561],[100,148,165,166,495,535,543],[100,148,165,166,495,535,540,543],[100,148,153,165,166,495,535,540,543,561],[100,148,165,166,495,535,540,542],[100,148,165,166,495,576],[100,148,165,166,495,535,537],[100,148,165,166,495,535,542,579],[100,148,165,166,591,728],[100,148,165,166,472,482,535],[86,100,148,165,166,482],[100,148,165,166,472,482,535,543],[100,148,165,166,472,482,535,735],[86,100,148,165,166,472],[100,148,165,166,472,482,535,536,591,739,740],[100,148,165,166,472,499],[100,148,165,166,589],[100,148,165,166,591,628],[100,148,165,166,499],[100,148,165,166,591,746],[86,100,148,165,166,627],[86,100,148,165,166,727],[86,100,148,165,166,472,627],[100,148,149,165,166,192,535,542,550,551],[100,148,165,166,534],[100,148,165,166,535,542],[100,148,160,165,166,170],[100,148,153,165,166,535],[100,148,165,166,536],[100,148,165,166,536,542],[100,148,165,166,525],[100,146,148,165,166,185,551],[100,146,148,165,166,185,537]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"82def759bbc6cacf3265f56e19049da3fe760d82b06b4ece00593dd29bf69235","affectsGlobalScope":true},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc0a7f107690ee5cd8afc8dbf05c4df78085471ce16bdd9881642ec738bc81fe","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f137d651076822d4fe884287e68fd61785a0d3d1fdb250a5059b691fa897db","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"80523c00b8544a2000ae0143e4a90a00b47f99823eb7926c1e03c494216fc363","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"746911b62b329587939560deb5c036aca48aece03147b021fa680223255d5183","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"829b9e6028b29e6a8b1c01ddb713efe59da04d857089298fa79acbdb3cfcfdef","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"5f90b8c733a1bda63e42160b15a2301051e83a6f9d5332a59d16eb12f463270d","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"496bbf339f3838c41f164238543e9fe5f1f10659cb30b68903851618464b98ba","impliedFormat":1},{"version":"5178eb4415a172c287c711dc60a619e110c3fd0b7de01ed0627e51a5336aa09c","impliedFormat":1},{"version":"ca6e5264278b53345bc1ce95f42fb0a8b733a09e3d6479c6ccfca55cdc45038c","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"fb1d8e814a3eeb5101ca13515e0548e112bd1ff3fb358ece535b93e94adf5a3a","impliedFormat":1},{"version":"ffa495b17a5ef1d0399586b590bd281056cee6ce3583e34f39926f8dcc6ecdb5","impliedFormat":1},{"version":"98b18458acb46072947aabeeeab1e410f047e0cacc972943059ca5500b0a5e95","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"570bb5a00836ffad3e4127f6adf581bfc4535737d8ff763a4d6f4cc877e60d98","impliedFormat":1},{"version":"889c00f3d32091841268f0b994beba4dceaa5df7573be12c2c829d7c5fbc232c","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"380647d8f3b7f852cca6d154a376dbf8ac620a2f12b936594504a8a852e71d2f","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"c83bb0c9c5645a46c68356c2f73fdc9de339ce77f7f45a954f560c7e0b8d5ebb","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"6a148329edecbda07c21098639ef4254ef7869fb25a69f58e5d6a8b7b69d4236","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"f63ab283a1c8f5c79fabe7ca4ef85f9633339c4f0e822fce6a767f9d59282af2","impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a54c996c8870ef1728a2c1fa9b8eaec0bf4a8001cd2583c02dd5869289465b10","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"3754982006a3b32c502cff0867ca83584f7a43b1035989ca73603f400de13c96","impliedFormat":1},{"version":"a30ae9bb8a8fa7b90f24b8a0496702063ae4fe75deb27da731ed4a03b2eb6631","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"50b5bc34ce6b12eccb76214b51aadfa56572aa6cc79c2b9455cdbb3d6c76af1d","impliedFormat":1},{"version":"b7e16ef7f646a50991119b205794ebfd3a4d8f8e0f314981ebbe991639023d0e","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"e9dd71cf12123419c60dab867d44fbee5c358169f99529121eaef277f5c83531","impliedFormat":1},{"version":"5b6a189ba3a0befa1f5d9cb028eb9eec2af2089c32f04ff50e2411f63d70f25d","impliedFormat":1},{"version":"d6e73f8010935b7b4c7487b6fb13ea197cc610f0965b759bec03a561ccf8423a","impliedFormat":1},{"version":"174f3864e398f3f33f9a446a4f403d55a892aa55328cf6686135dfaf9e171657","impliedFormat":1},{"version":"824c76aec8d8c7e65769688cbee102238c0ef421ed6686f41b2a7d8e7e78a931","impliedFormat":1},{"version":"75b868be3463d5a8cfc0d9396f0a3d973b8c297401d00bfb008a42ab16643f13","impliedFormat":1},{"version":"15a234e5031b19c48a69ccc1607522d6e4b50f57d308ecb7fe863d44cd9f9eb3","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"6dcf60530c25194a9ee0962230e874ff29d34c59605d8e069a49928759a17e0a","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"1a42d2ec31a1fe62fdc51591768695ed4a2dc64c01be113e7ff22890bebb5e3f","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"0c7c947ff881c4274c0800deaa0086971e0bfe51f89a33bd3048eaa3792d4876","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"ad10d4f0517599cdeca7755b930f148804e3e0e5b5a3847adce0f1f71bbccd74","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"72d63643a657c02d3e51cd99a08b47c9b020a565c55f246907050d3c8a5e77fb","impliedFormat":1},{"version":"1d415445ea58f8033ba199703e55ff7483c52ac6742075b803bd3e7bbe9f5d61","impliedFormat":1},{"version":"d6406c629bb3efc31aedb2de809bef471e475c86c7e67f3ef9b676b5d7e0d6b2","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"71d8ba39a9e024d9e4bb922464d18542ed8d2c25ee78efa7890c27213cc6e5d3","impliedFormat":1},{"version":"8c030e515014c10a2b98f9f48408e3ba18023dfd3f56e3312c6c2f3ae1f55a16","impliedFormat":1},{"version":"dafc31e9e8751f437122eb8582b93d477e002839864410ff782504a12f2a550c","impliedFormat":1},{"version":"754498c5208ce3c5134f6eabd49b25cf5e1a042373515718953581636491f3c3","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"f56bdc6884648806d34bc66d31cdb787c4718d04105ce2cd88535db214631f82","impliedFormat":1},{"version":"633d58a237f4bb25ec7d565e4ffa32cecdcee8660ac12189c4351c52557cee9e","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"49f95e989b4632c6c2a578cc0078ee19a5831832d79cc59abecf5160ea71abad","impliedFormat":1},{"version":"9666533332f26e8995e4d6fe472bdeec9f15d405693723e6497bf94120c566c8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"43fa6ea8714e18adc312b30450b13562949ba2f205a1972a459180fa54471018","impliedFormat":1},{"version":"6e89c2c177347d90916bad67714d0fb473f7e37fb3ce912f4ed521fe2892cd0d","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"4d4927cbee21750904af7acf940c5e3c491b4d5ebc676530211e389dd375607a","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"8a97e578a9bc40eb4f1b0ca78f476f2e9154ecbbfd5567ee72943bab37fc156a","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"f22d05663d873ee7a600faf78abb67f3f719d32266803440cf11d5db7ac0cab2","impliedFormat":1},{"version":"d93c544ad20197b3976b0716c6d5cd5994e71165985d31dcab6e1f77feb4b8f2","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"a8b1c79a833ee148251e88a2553d02ce1641d71d2921cce28e79678f3d8b96aa","impliedFormat":1},{"version":"126d4f950d2bba0bd45b3a86c76554d4126c16339e257e6d2fabf8b6bf1ce00c","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"2d3cc2211f352f46ea6b7cf2c751c141ffcdf514d6e7ae7ee20b7b6742da313f","impliedFormat":1},{"version":"c75445151ff8b77d9923191efed7203985b1a9e09eccf4b054e7be864e27923d","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"fa8a8fbf91ee2a4779496225f0312aac6635b0f21aa09cdafa4283fe32d519c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"0e8aef93d79b000deb6ec336b5645c87de167168e184e84521886f9ecc69a4b5","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"de7052bfee2981443498239a90c04ea5cc07065d5b9bb61b12cb6c84313ad4ef","impliedFormat":1},{"version":"a3e7d932dc9c09daa99141a8e4800fc6c58c625af0d4bbb017773dc36da75426","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"4a2edd238d9104eac35b60d727f1123de5062f452b70ed8e0366cb36387dfdfd","impliedFormat":1},{"version":"ca921bf56756cb6fe957f6af693a35251b134fb932dc13f3dfff0bb7106f80b4","impliedFormat":1},{"version":"fee92c97f1aa59eb7098a0cc34ff4df7e6b11bae71526aca84359a2575f313d8","impliedFormat":1},{"version":"0bd0297484aacea217d0b76e55452862da3c5d9e33b24430e0719d1161657225","impliedFormat":1},{"version":"2ab6d334bcbf2aff3acfc4fd8c73ecd82b981d3c3aa47b3f3b89281772286904","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"4805f6161c2c8cefb8d3b8bd96a080c0fe8dbc9315f6ad2e53238f9a79e528a6","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"49179c6a23701c642bd99abe30d996919748014848b738d8e85181fc159685ff","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"8514c62ce38e58457d967e9e73f128eedc1378115f712b9eef7127f7c88f82ae","impliedFormat":1},{"version":"f1289e05358c546a5b664fbb35a27738954ec2cc6eb4137350353099d154fc62","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"1d17ba45cfbe77a9c7e0df92f7d95f3eefd49ee23d1104d0548b215be56945ad","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"a1ee88010a64e8647d07dba58ec43e6e05851b9ec7a62e4ca2b9c33be5abb2c8","impliedFormat":1},{"version":"46273e8c29816125d0d0b56ce9a849cc77f60f9a5ba627447501d214466f0ff3","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"e91f7b1344577a02f051b9b471f33044fef8334a76dc9e1de003d17595a5219b","impliedFormat":1},{"version":"3af3584f79c57853028ef9421ec172539e1fe01853296dc05a9d615ade4ffaf6","impliedFormat":1},{"version":"f82579d87701d639ff4e3930a9b24f4ee13ca74221a9a3a792feb47f01881a9c","impliedFormat":1},{"version":"d7e5d5245a8ba34a274717d085174b2c9827722778129b0081fefd341cca8f55","impliedFormat":1},{"version":"d9d32f94056181c31f553b32ce41d0ef75004912e27450738d57efcd2409c324","impliedFormat":1},{"version":"752513f35f6cff294ffe02d6027c41373adf7bfa35e593dbfd53d95c203635ee","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"1a7e2ea171726446850ec72f4d1525d547ff7e86724cc9e7eec509725752a758","impliedFormat":1},{"version":"8c901126d73f09ecdea4785e9a187d1ac4e793e07da308009db04a7283ec2f37","impliedFormat":1},{"version":"db97922b767bd2675fdfa71e08b49c38b7d2c847a1cc4a7274cb77be23b026f1","impliedFormat":1},{"version":"aab290b8e4b7c399f2c09b957666fc95335eb4522b2dd9ead1bf0cb64da6d6ee","impliedFormat":1},{"version":"94fe3281392e1015b22f39535878610b4fa6f1388dc8d78746be3bc4e4bb8950","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"06c25ddfc2242bd06c19f66c9eae4c46d937349a267810f89783680a1d7b5259","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"c7f6485931085bf010fbaf46880a9b9ec1a285ad9dc8c695a9e936f5a48f34b4","impliedFormat":1},{"version":"14f6b927888a1112d662877a5966b05ac1bf7ed25d6c84386db4c23c95a5363b","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"90c54a02432d04e4246c87736e53a6a83084357acfeeba7a489c5422b22f5c7a","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"83fe880c090afe485a5c02262c0b7cdd76a299a50c48d9bde02be8e908fb4ae6","impliedFormat":1},{"version":"0a372c2d12a259da78e21b25974d2878502f14d89c6d16b97bd9c5017ab1bc12","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"ec1ca97598eda26b7a5e6c8053623acbd88e43be7c4d29c77ccd57abc4c43999","impliedFormat":1},{"version":"6e2261cd9836b2c25eecb13940d92c024ebed7f8efe23c4b084145cd3a13b8a6","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"a47e6d954d22dd9ebb802e7e431b560ed7c581e79fb885e44dc92ed4f60d4c07","impliedFormat":1},{"version":"f019e57d2491c159d47a107fd90219a1734bdd2e25cd8d1db3c8fae5c6b414c4","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"d1c9bf292a54312888a77bb19dba5e2503ad803f5393beafd45d78d2f4fe9b48","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"cb8d8ef7b9ce8ed3e6f1c814fcbf3f90dab0cb8863079236784fc350746e27c4","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"3be035da7bee86b4c3abf392e0edaa44fc6e45092995eefe36b39118c8a84068","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f828825d077c2fa0ea606649faeb122749273a353daab23924fe674e98ba44c","impliedFormat":1},{"version":"2896c2e673a5d3bd9b4246811f79486a073cbb03950c3d252fba10003c57411a","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"407a06ba04eede4074eec470ecba2784cbb3bf4e7de56833b097dd90a2aa0651","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"5c96bad5f78466785cdad664c056e9e2802d5482ca5f862ed19ba34ffbb7b3a4","impliedFormat":1},{"version":"81d8603ac527e75cfec72bb9391228b58f161c2b33514a9d814c7f3ebd3ef466","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"4655709c9cb3fd6db2b866cab7c418c40ed9533ce8ea4b66b5f17ec2feea46a9","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"3eecb25bb467a948c04874d70452b14ae7edb707660aac17dc053e42f2088b00","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"5f0292a40df210ab94b9fb44c8b775c51e96777e14e073900e392b295ca1061b","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"8627ad129bcf56e82adff0ab5951627c993937aa99f5949c33240d690088b803","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"a68d4b3182e8d776cdede7ac9630c209a7bfbb59191f99a52479151816ef9f9e","impliedFormat":99},{"version":"39644b343e4e3d748344af8182111e3bbc594930fff0170256567e13bbdbebb0","impliedFormat":99},{"version":"ed7fd5160b47b0de3b1571c5c5578e8e7e3314e33ae0b8ea85a895774ee64749","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"ecbaf0da125974be39c0aac869e403f72f033a4e7fd0d8cd821a8349b4159628","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"ceec3c81b2d81f5e3b855d9367c1d4c664ab5046dff8fd56552df015b7ccbe8f","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fac4a15690b27612d8474fb2fc7cc00388df52d169791b78d1a3645d60b4c8b","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"85ae5aee75f011967cf2d25cbc342f62d69314e9d925f7f4aa3456fc2cffcca6",{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"00cf4001e0d9c6e5e036bc545b9d73e2b8b84cddb02e61ad05bab3752b1d4522","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"b558c9a18ea4e6e4157124465c3ef1063e64640da139e67be5edb22f534f2f08","impliedFormat":1},{"version":"01374379f82be05d25c08d2f30779fa4a4c41895a18b93b33f14aeef51768692","impliedFormat":1},{"version":"b0dee183d4e65cf938242efaf3d833c6b645afb35039d058496965014f158141","impliedFormat":1},{"version":"c0bbbf84d3fbd85dd60d040c81e8964cc00e38124a52e9c5dcdedf45fea3f213","impliedFormat":1},{"version":"21bb4d03fe544bb185b2450bf3f2488a13bd018e7b1a66bb73c5ea75d5f8d64d","signature":"f65ce75c9085571e6321abf2bf9833709f4897e381f89e9925521833dbb7ab16"},{"version":"f60e3e3060207ac982da13363181fd7ee4beecc19a7c569f0d6bb034331066c2","impliedFormat":1},{"version":"17230b34bb564a3a2e36f9d3985372ccab4ad1722df2c43f7c5c2b553f68e5db","impliedFormat":1},{"version":"6e5c9272f6b3783be7bdddaf207cccdb8e033be3d14c5beacc03ae9d27d50929","impliedFormat":1},{"version":"21ac4cf3f8d8c6e1201cb31f600be708c9a37867fc5c73b7ccf80560fae591c8","impliedFormat":1},{"version":"0dfe35191a04e8f9dc7caeb9f52f2ee07402736563d12cbccd15fb5f31ac877f","impliedFormat":1},{"version":"798367363a3274220cbed839b883fe2f52ba7197b25e8cb2ac59c1e1fd8af6b7","impliedFormat":1},{"version":"2636a309ed87d6876728d9aca846a76b372cf2a21a4fdf9940a82a2dd86687d0","impliedFormat":1},{"version":"5aea76ab98173f2c230b1f78dc010da403da622c105c468ace9fe24e3b77883c","impliedFormat":99},{"version":"23fc6dca1aea90a0189528c818e2c628a50c07b8139e98511856e694ba01970c","signature":"c875984feeacb5c286de335f23d9567a9cf7840dad3070133d7041280c6f7364","affectsGlobalScope":true},{"version":"4cff0a8b449418968cda34a266821f6137aa048bc4feffd1247f4a09cb0bb3bf","signature":"bc924ab70bff4d6c53193965473187ad66b842038a055a92c7d566882ea59049"},{"version":"db148e039b61534a6e1fd46c33c5ed4b20fc094989d1750fd201faebd1f74094","signature":"3aaef418d16360353231ccc23d46881ecf833698d0ff9e02717d719b7801e04b"},{"version":"d60aef9194c8f0cc814eb350af12f05492114d409f93a3c9a5a9e7baaa5305bd","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bd7ef918f3540617c05ca362a0e1ea249e74483ee0f3377a3969a01c4f0e65b1","signature":"72fb8072cbb729ab7fb4ccf6a45cac93c15f916981590cf9b3ff4431f21c99b0"},{"version":"10e5d78cf24e76a6e9665247462aa109b4c6be306f6760b5182fce2bea40b9d6","signature":"098baf2ef5ff911e8c9db8fae13f7c5fee22676f1029491d0da60bbdeab20a01"},{"version":"b479aea918bbb311135d441e1c6ff18fe72568bb7fa3e78f0d14451ccd2dd112","signature":"c1580f9771d6576529e455c6c66185dd68dd07abecc29c87c53c959c9c5c5e5b"},{"version":"6b0683b793296f8655b733d1d2c45386a11eecf7ee73409a725a4dd535e2d1bd","signature":"b82082d7d72f64f9195e219bd0529c81ef152445b25840ec90b1e8e1aecb5a83"},{"version":"5f155ee486436afaf0dfa1f9e65e4dafad2e15b9176fc974559cadb8f0b64694","signature":"3289793fbd342d353f334166f32088fc076110bb5c92c824d9d4b063264c0b93"},{"version":"d407122c1c1cd0e5c14056c9a5d15401eaf650bfbc2f4809b28ea87769baae51","signature":"1fe72fdac66c1e1d00a23d32c496f8850a85b87fcef0a6f68ad1fef274b677d6"},{"version":"551f41a830312341d7e679adcad07fdf3cd41d484baa29c5f7b7d7174665a20b","signature":"45e441123885e66cc3064a61d0d5c37a0d4c5b2972f8e7dfd59fce4990c81008"},{"version":"c9665b01035f3935009bd1f3afad5504c1f6ab5f7024ad5b175cc3bf00a298d9","signature":"7ef27320cdd0b3be1575a7ce149628c32e117c0d6fc7ab5318ce6fef27b9d089"},{"version":"5fc9c7a5da22068136e88726bc62593c4ed54f65f545bc0328825c0bad48d144","signature":"5f5303388c23daf221df682ba0e6ed13924ad3a925fae5ec71f4cdfb6adf1b79"},{"version":"4d4357f95befbe78f03da8eac8928ade13d410f1bd55ccba20f63b4d4462c7ba","signature":"ebc450cf4888573aef03e1d85e34f8587789f7436e4c80a3f1417a66429a9a29"},{"version":"6cefb4fa8df958de8464c352da87126fd804c6dde31b3d49b49d9acf21cb8c7f","signature":"fe34d9d4feb27df3c3dd6c1c601d556366c83de0cac264df93170eba47fa2d42"},{"version":"d2a73445e6087ec797a04726787788473b680e9f98e81a62669a4cbb15209fdb","signature":"18d0fae3051f2a1914670c40130fbc64ea1e3e0d4a49496c71fdad66a61785bc"},{"version":"dab1d3ab06e6a40b50b20be398d7c7d99267f4895985394e2c2c5fb72a31d754","signature":"c036a2b95fce31ad9d01a9f046e4028bdf56548347e44a9251bcbab520666a88"},{"version":"517ed4b320e1601542cf7e130f9464352856ccb8b93a77f7fc377ad090170287","signature":"5fccc3a3f55513833a3eb5328df44463ac2e6626ca37f5f26371e539e79ebfd0"},{"version":"40bd8cc6fbca15f3cd0b3279cdf3abc7892d70f0ea17aa0996add4492e87697d","signature":"35586bf5234680565dbafb710d4d1fe7e9d2a7a4c73fb3c1e3d6f5afcef46bfa"},{"version":"efa7d1bc110f29e9288d0411db033ad65840c309820685677b1a51a203219ebd","signature":"e95367cf9149a89307cb77b0123e7df97bab24fa62bca23d3c48720209d72163"},{"version":"80cfad7a9ab5da845fb92da6c86f3fa1f6f1a5f630ada06c4418c1b1a894ea54","signature":"abf6ae17e3589990059a3422999ebb7315244119f58ab919aa62a3c935166810"},{"version":"fe7f9cca33a1237f307ad0a83c5f20f949fcfe1fe79e26e6e1eb73f994595f8f","signature":"1d7cdb30c665b65fbdad5487cd1df8226b4c0cc77aad6ba186fb3a0e0695d7a3"},{"version":"3b042f317ffc1231efd9b5303f063137fc0f3f1079d41ce105060d33bdbab08b","signature":"a2a3ce0745f0b22633664e41a14dc36b191d07590e636a1781162f0b9e52dad9"},{"version":"792d7ea69b04262b6833961df0e8d7648bf5af8587bde88109f9266aee150b5f","signature":"8a49246609e76bf9a1691e84c66e48f276341e81ef87446409c421734fec77fc"},{"version":"d4768ded08166bc9c5b5527534d0e12d42be6bc2b6888c3bcb323aa006f9c379","signature":"3c2492603a0ebde90f329d319fe97161ca46bac0a0a6df434dff7b5005dfd645"},{"version":"2dbd6f26322556affa8b9eb5f9289313a13f1f361d97f04e63739f36af51b590","signature":"c92afe5b467074addef2259d2cae99da759b3632e685663511fbaebad1c139c2"},{"version":"c04aba05e350d4577675f76afb20af8d484063e8b8e1ebd272c2e504ddf20bbc","signature":"810a342b08ef4747f7c6f93655d0e02ed054f2f0b172afb93cc67ffd8c0d5f40"},{"version":"2867c2488742b562a9f15a1e85a95040e40fd5f567455dfdc96c9e449b7cdecb","signature":"df2be94fc28b3ae8760cf1d8f27d8d297c9cee4ebb6622d5c4bbd8b0ec2ec294"},{"version":"a1ec6f8b47633264d5bb50ae237338e19ef71fd2fc7569b8e7972aef7191922d","signature":"dc8487a97f531a9d0aa0106ed406e9bc4d3c0e1725447cb4c4b4847ec6e22ff3"},{"version":"afaefdba5cabbcb6a83fb589d615a0c01cc43545d22c0bb959fe543efec7e798","signature":"10d32798c693e8e2f64828d34ed88b0034d5caa3b711047aa4a92094ab5e3b17"},{"version":"ce2a2952163441d9024d234e23e6a50de0ad08f1617d3923f585bf477108a8cf","signature":"f4d94baa358570780b5a0cd2ce17d316cbbb052f8af73886097e1887e0c886d8"},{"version":"4d72bc051f03cf98863f38a8f99f3caea274e8af8d1bbf9590f3582bbced04f1","signature":"7422b92036425d7f0c702405a6216dd807a9dd6c67149dec7a13d51bc5eccffd"},{"version":"f2e116357a1d5060d44b19b07e1d40ddb4091e75d63431dec392303381f28a64","signature":"211f0bac61667f4b23c718b4b95b896eed28ee5ddd8eeac6f24ee663596e088b"},{"version":"bbab61e56901bc697bead6cbeeb6000258ba41fed8fce2f8ae0a8dc26b3dab27","signature":"7206e0955b1795b1c5fd4f3dcc562532e5246adfa64892f737a936d90224f3a1"},{"version":"3235fceb9aefd8dcc2028c42bec997ae7adeb1eb6647bb0159c252d1daf2012c","signature":"b7dc741fe289385925f6d2cd3d6b4ac285b43315b59eac9c1085a4466ea52367"},{"version":"98216b1a37ab4c58b43151d12507a4e0c70b92bec1b207f8961db8bf6b0c7fac","signature":"d43b3c5166d0c1833f6d44ed88c4c33ad703a53c2a7773b6373200a34ed71017"},{"version":"c00541085ef5ad8302b18326b45d61e6a0cc2f9d400c200f5169a19f66dc9a31","signature":"6e62a865c59e005ebcef05eb99351159a4e077f15b23ce8dc29e18bd11821791"},{"version":"e9da613a17380d5d718b235495fff94027af2304a213b78d8d7f9e090732e53f","signature":"bd094cf5c012243bb0478aeda515e4b5f8c595cd327ff0c6b31e46f23862a161"},{"version":"649be00230c12addfafbe27f9aa218eb104a28386d22507fbf8e79d800829a10","signature":"fe34d9d4feb27df3c3dd6c1c601d556366c83de0cac264df93170eba47fa2d42"},{"version":"f3896da562de1ef6cefce9abc55804a2ec37b3e290f83e7a5471dffa06568795","signature":"069f359ac86fb46229485cf1c9b88f12ad0a525f621c7b6da9d946284a578b33"},{"version":"c0288f54de6f544706a3150c8b579b1a975870695c4be866f727ece6a16f3976","impliedFormat":1},{"version":"f8636a916949481bc363ae24cbeb8451fa98fd2d07329e0664a46567278c9adb","impliedFormat":1},{"version":"31c16a9b916cce323b87f4bce7e1f8813b38a0ee44d17f5303ac67fed631c24b","signature":"759bdc61e590b33a2fe257c4eed78633ef9fc00ffc16057d9d6371a9f19113e5"},{"version":"6ba7313771978e5b79afe48763205cb97923253208eb2ea815c656c0745199ee","signature":"1f5df4bcaa0d19a878711fb752fff31c1fe965a9c9387dd5cb731ab2f72a1e73"},{"version":"61a6b02956a0ad0566ba8b4e953e25bf3cdc40a093daf28e436ceb063a3802a6","signature":"72e592bcf424709b6a65bc7e5eac11a05e850be02d4163e0832cd8529261884d"},{"version":"4732b92b1dbc194ad8fa8c705008a6e5b31d3817a8daccca3f02a45fdfbe4961","signature":"45537c0c5bbd467a1a4723a5dfe77797f0d8df93c76dea5f151c9760e5c735c4"},{"version":"eaeebbffa2f2a647258c00e7ae41eaeb7ddac81d3a81a10301453221ebcbe8a3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ea22aa99d643e42275e0ce509f500d1f2d65a35f6e743ea3ce3dd3f769648238","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7b6237cc122e15d453c14ad3db475ece23c6e51cecb5dde77c171c83dbf0853f","signature":"26a70954a28a1ef0a12061c6d9767cc1d7ace42031d42237bbfab18146822dac"},{"version":"a03e3238c6f4bb64914fb24efa168f65cbbf5043f1cd586c4caa1d5d54c22d7e","signature":"dca97beced25678129f1889e2f00f0788606e1f6aa68f6b1155a724f1df7549b"},{"version":"ae2c9d362b66f18f0906ce58e2ccb28e05c233177213388adb505838b26187af","signature":"80e5e5bc0da34844c67f55bcade681e50acc233e1c27cfed7768abcf8a6cc600"},{"version":"0c996be745af11ccf07b28eb3e007aa4277d01b16f53259cb355daafab167abf","impliedFormat":99},{"version":"27c1b57e01b13f914e9d3507202036d676222057d6fa1032332e2954296b9d45","impliedFormat":99},{"version":"7348c35ce10f8323a515ea6a6c237c0dc7c3fd5e491c4dacec2a7b93f0dfcd26","impliedFormat":1},{"version":"258e67b2638408b67fbf5fe6c953b8f2dd2a69f9171d31c9a976cd2329716e4c","impliedFormat":1},{"version":"508caf9e3a1065faa7b84165106c05af4c44c8d2c422c3fd10d52f60c0e3b752","signature":"74a690a062e71856ae41a89e8e8210504c43c813c7509ec863125f3c827d7c4b"},{"version":"8755e0726a00d5e4aa686572a993876f0db5b41e87733069e4f0ab26174f432f","signature":"16be5bbc014dc546d27ced610340d0c2b774514e2ebddf6a6bfef6b947145882"},{"version":"e7c2f40dc99121500ad108a4f86541d29cac105ed018f994c7c5a2836e77b257","impliedFormat":1},{"version":"90e930283286ab117ab89f00589cf89ab5e9992bc57e79f303b36ee14649bdd9","impliedFormat":1},{"version":"6d48a6c907c668a6d6eda66acec4242e367c983e073100e35c1e234c424ad1a4","impliedFormat":1},{"version":"68a0e898d6c39160f1326ef922508914498c7a2d0b5a0d9222b7928d343214eb","impliedFormat":1},{"version":"69d96a8522b301a9e923ac4e42dd37fc942763740b183dffa3d51aca87f978d5","impliedFormat":1},{"version":"ff2fadad64868f1542a69edeadf5c5519e9c89e33bec267605298f8d172417c7","impliedFormat":1},{"version":"2866ae69517d6605a28d0c8d5dff4f15a0b876eeb8e5a1cbc51631d9c6793d3f","impliedFormat":1},{"version":"f8c4434aa8cbd4ede2a75cbc5532b6a12c9cac67c3095ed907e54f3f89d2e628","impliedFormat":1},{"version":"0b8adc0ae60a47acf65575952eee568b3d497f9975e3162f408052a99e65f488","impliedFormat":1},{"version":"ede9879d22f7ce68a8c99e455acab32fc45091c6eed9625549742b03e1f1ac1a","impliedFormat":1},{"version":"0e8c007c6e404da951c3d98a489ac0a3e9b6567648b997c03445ac69d7938c1c","impliedFormat":1},{"version":"f2a4866bed198a7c804b58ee39efe74c66ecdcf2dfebef0b9895d534a50790c4","impliedFormat":1},{"version":"ad72538d0c5e417ee6621e1b54691c274bcacaa1807c9895c5fa6d40b45fb631","impliedFormat":1},{"version":"4f851c59f3112702f6178e76204f839e3156daa98b5b7d7e3fc407a6c5764118","impliedFormat":1},{"version":"57511f723968d2f41dd2d55b9fbc5d0f3107af4e4227db0fb357c904bd34e690","impliedFormat":1},{"version":"9585df69c074d82dda33eadd6e5dccd164659f59b09bd5a0d25874770cf6042d","impliedFormat":1},{"version":"f6f6ce3e3718c2e7592e09d91c43b44318d47bca8ee353426252c694127f2dcb","impliedFormat":1},{"version":"4f70076586b8e194ef3d1b9679d626a9a61d449ba7e91dfc73cbe3904b538aa0","impliedFormat":1},{"version":"6d5838c172ff503ef37765b86019b80e3abe370105b2e1c4510d6098b0e84414","impliedFormat":1},{"version":"1876dac2baa902e2b7ebed5e03b95f338192dc03a6e4b0731733d675ba4048f3","impliedFormat":1},{"version":"8086407dd2a53ce700125037abf419bddcce43c14b3cf5ea3ac1ebded5cad011","impliedFormat":1},{"version":"c2501eb4c4e05c2d4de551a4bace9c28d06a0d89b228443f69eb3d7f9049fbd6","impliedFormat":1},{"version":"1829f790849d54ea3d736c61fdefd3237bede9c5784f4c15dfdafb7e0a9b8f63","impliedFormat":1},{"version":"5392feeda1bf0a1cc755f7339ea486b7a4d0d019774da8057ddc85347359ed63","impliedFormat":1},{"version":"c998117afca3af8432598c7e8d530d8376d0ca4871a34137db8caa1e94d94818","impliedFormat":1},{"version":"4e465f7e9a161a5a5248a18af79dbfbf06e8e1255bfdc8f63ab15475a2ba48bd","impliedFormat":1},{"version":"e0353c5070349846fe9835d782a8ce338d6d4172c603d14a6b364d6354957a4e","impliedFormat":1},{"version":"323133630008263f857a6d8350e36fb7f6e8d221ec0a425b075c20290570c020","impliedFormat":1},{"version":"c04e691d64b97e264ca4d000c287a53f2a75527556962cdbe3e8e2b301dac906","impliedFormat":1},{"version":"3733dba5107de9152f98da9bcb21bf6c91ac385f3b22f30ed08d0dc5e74c966f","impliedFormat":1},{"version":"d3ec922ddd9677696ee0552f10e95c4e59f85bb8c93fd76cd41b2dd93988ff39","impliedFormat":1},{"version":"0492c0d35e05c0fdd638980e02f3a7cdec18b311959fc730d85ed7e1d4ff38a7","impliedFormat":1},{"version":"c7122ba860d3497fa04a112d424ee88b50c482360042972bcf0917c5b82f4484","impliedFormat":1},{"version":"838f52090a0d39dce3c42e0ccb0db8db250c712c1fa2cd36799910c8f8a7f7bf","impliedFormat":1},{"version":"116ec624095373939de9edb03619916226f5e5b6e93cd761c4bda4efecb104fc","impliedFormat":1},{"version":"8e6b8259bfd8c8c3d6ed79349b7f2f69476d255aede2cd6c0acb0869ad8c6fdd","impliedFormat":1},{"version":"5b63081466b5c72cdb67e1283a6561b6c92e7d92fd606a76e6f8609d3d72f44b","signature":"7c3b6e96c933c3198c291f1b67d36a3e9da28dc6fd0e3ce7506110658209c136"},{"version":"da475748e721c473c6328f613366889137aa03cefef58894e3754ac2ea1e8cfb","signature":"3eb972ae325aa293fdb6077cdf956f209ee6ea34b4e874ff7ec8686b3079972f"},{"version":"f5c335c9574f6d1f070b987e4f4bb1cc2c315daa39079fa1a3fdbf4bbdaef079","signature":"3aa301d257c8f5003c557f7a622bf5f935f89f8dc5bf4e35b7d0c3685cbff16e"},{"version":"da778e9e61f2d9b6da0431c679c4f8406e98dfda68aac89775b35f3ec32f62d1","signature":"e5de69f582e0ad1424f67fe0c72b988ca928b5697cd8d3d97f81e7e06499b54d"},{"version":"089fb4b53d3593d24fa3b43df1955b940699376dca3f4090a862651b2915108b","signature":"9dba8021051c7996e5f9b34f254fadcb531388b397e11ae64393be2a8f05cddd"},{"version":"7b3c473db1e051312d26f22a6013ebf53d2528f369ccebd93920dbd128b3456e","signature":"fe641033ce90f7679d2c79424888aa99febffb0bfde7e27c97947fecbfa472e1"},{"version":"d0b633040955e2c4ad83b0d0e30209cf7dff4a026d72964842214029d5a5b965","signature":"e0700de7bfd69fcd7a39e74072c5ee885b49f01691170b4f91318835e1fccc1b"},{"version":"b1538a92b9bae8d230267210c5db38c2eb6bdb352128a3ce3aa8c6acf9fc9622","impliedFormat":1},{"version":"469532350a366536390c6eb3bde6839ec5c81fe1227a6b7b6a70202954d70c40","impliedFormat":1},{"version":"17c9f569be89b4c3c17dc17a9fb7909b6bab34f73da5a9a02d160f502624e2e8","impliedFormat":1},{"version":"003df7b9a77eaeb7a524b795caeeb0576e624e78dea5e362b053cb96ae89132a","impliedFormat":1},{"version":"7ba17571f91993b87c12b5e4ecafe66b1a1e2467ac26fcb5b8cee900f6cf8ff4","impliedFormat":1},{"version":"6fc1a4f64372593767a9b7b774e9b3b92bf04e8785c3f9ea98973aa9f4bbe490","impliedFormat":1},{"version":"d30e67059f5c545c5f8f0cc328a36d2e03b8c4a091b4301bc1d6afb2b1491a3a","impliedFormat":1},{"version":"8b219399c6a743b7c526d4267800bd7c84cf8e27f51884c86ad032d662218a9d","impliedFormat":1},{"version":"bad6d83a581dbd97677b96ee3270a5e7d91b692d220b87aab53d63649e47b9ad","impliedFormat":1},{"version":"324726a1827e34c0c45c43c32ecf73d235b01e76ef6d0f44c2c0270628df746a","impliedFormat":1},{"version":"54e79224429e911b5d6aeb3cf9097ec9fd0f140d5a1461bbdece3066b17c232c","impliedFormat":1},{"version":"e1b666b145865bc8d0d843134b21cf589c13beba05d333c7568e7c30309d933a","impliedFormat":1},{"version":"ff09b6fbdcf74d8af4e131b8866925c5e18d225540b9b19ce9485ca93e574d84","impliedFormat":1},{"version":"c836b5d8d84d990419548574fc037c923284df05803b098fe5ddaa49f88b898a","impliedFormat":1},{"version":"3a2b8ed9d6b687ab3e1eac3350c40b1624632f9e837afe8a4b5da295acf491cb","impliedFormat":1},{"version":"189266dd5f90a981910c70d7dfa05e2bca901a4f8a2680d7030c3abbfb5b1e23","impliedFormat":1},{"version":"5ec8dcf94c99d8f1ed7bb042cdfa4ef6a9810ca2f61d959be33bcaf3f309debe","impliedFormat":1},{"version":"a80e02af710bdac31f2d8308890ac4de4b6a221aafcbce808123bfc2903c5dc2","impliedFormat":1},{"version":"d5895252efa27a50f134a9b580aa61f7def5ab73d0a8071f9b5bf9a317c01c2d","impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","impliedFormat":1},{"version":"0f345151cece7be8d10df068b58983ea8bcbfead1b216f0734037a6c63d8af87","impliedFormat":1},{"version":"37fd7bde9c88aa142756d15aeba872498f45ad149e0d1e56f3bccc1af405c520","impliedFormat":1},{"version":"2a920fd01157f819cf0213edfb801c3fb970549228c316ce0a4b1885020bad35","impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","impliedFormat":1},{"version":"a67774ceb500c681e1129b50a631fa210872bd4438fae55e5e8698bac7036b19","impliedFormat":1},{"version":"9b048390bcffe88c023a4cd742a720b41d4cd7df83bc9270e6f2339bf38de278","affectsGlobalScope":true,"impliedFormat":1},{"version":"dd8936160e41420264a9d5fade0ff95cc92cab56032a84c74a46b4c38e43121e","impliedFormat":1},{"version":"1f366bde16e0513fa7b64f87f86689c4d36efd85afce7eb24753e9c99b91c319","impliedFormat":1},{"version":"421c3f008f6ef4a5db2194d58a7b960ef6f33e94b033415649cd557be09ef619","impliedFormat":1},{"version":"57568ff84b8ba1a4f8c817141644b49252cc39ec7b899e4bfba0ec0557c910a0","impliedFormat":1},{"version":"e6f10f9a770dedf552ca0946eef3a3386b9bfb41509233a30fc8ca47c49db71c","impliedFormat":1},{"version":"ddf88cc93eabcf0216843b02408c1ef9226a426dad3c7620d7f8a2cacc61ac17","impliedFormat":1},{"version":"131bcf45947b3aa3478f77be299b4167818092afe904c3442311d090929b95d8","impliedFormat":1},{"version":"0cbed41c674b83c208023eb76e9b13fbc9ae74b92e55baa9d2686f4f7409cab5","impliedFormat":1},{"version":"a7a07b21aed5d44363d7ad2ee3576fe301d8597ebf56128c372b9688ab924276","impliedFormat":1},{"version":"49c817a2e63d618eeef60b707b66c4de9e9ec9b74b0a9d8b8eca4cec7d80694c","impliedFormat":1},{"version":"a4d73ca1e31373a73a25e768656aa923054bddaf520863151db8ffbdbb84420b","impliedFormat":1},{"version":"aca181db3a02bb7e468e6a876365ccb9821494c90504bd318c688f0243cafeac","impliedFormat":1},{"version":"9c7e95f710c1d1935f4285cd56f72f0d5524c712559dc068af6914ffbb867441","impliedFormat":1},{"version":"d1d58fe50cc14a03caccc12b54f4db9bc5c8a4891ddb4677e21818bdccf070b0","impliedFormat":1},{"version":"13791a2580db7c3c1f5bead4e76d60d45df9cca7bfa2620cff607d305d3cb92d","impliedFormat":1},{"version":"a2e48516d670a40551432aab0c216d92d621dbc373cad182538753de82a5114f","impliedFormat":1},{"version":"fb71b6284eff38b106b20adb45f452e29d09ebf2f46fd2592e4b84a14c21c2c9","impliedFormat":1},{"version":"71dcb77f1fddb6c5ab48c666146984a12c329527f42c87d1fae11441335799ae","impliedFormat":1},{"version":"8384e1ee3f78f46052c9fd04fa5b38f246a21b7fa113b0a91229c32e1d311426","impliedFormat":1},{"version":"7a1833f046999b081103b35cdb026f924bb15690d08291f63d8037df3dedab65","impliedFormat":1},{"version":"7bf76797924eb82384d430fc0a686fe3aebf3a687ebb40f33d991f6b6d8acafa","impliedFormat":1},{"version":"609ad6cf8ae1b5a6c5eb01e81ee701eef96d7283c08b1d87b6ebb2bc2bff7397","impliedFormat":1},{"version":"a30dc41f09b6ad034556da864a85d43df20d6ad53300fdb3d4b24bd1045b1faf","impliedFormat":1},{"version":"052626dea73a9620db7ff4fa64dbb43867f619d68739a3f8f92399f9ca43bc86","impliedFormat":1},{"version":"74b63bc2515143b124345f4294f2f20b34eaa49c16adf770fe0b6d2e60639501","impliedFormat":1},{"version":"961e00ca928d4f3226d61c2be5ee672e52a8baaa522cc3fbb54efd155ed44e63","impliedFormat":1},{"version":"3a5f9e8592c7960d6be75b1639aa664043e6c34c53a6860408ec06a2f6a56031","impliedFormat":1},{"version":"41f45ed6b4cd7b8aec2e4888a47d5061ee1020f89375b57d388cfe1f05313991","impliedFormat":99},{"version":"98bb67aa18a720c471e2739441d8bdecdae17c40361914c1ccffab0573356a85","impliedFormat":99},{"version":"8258b4ec62cf9f136f1613e1602156fdd0852bb8715dde963d217ad4d61d8d09","impliedFormat":99},{"version":"ae415ee9f436a68ad72f1cd464f99f6ab355119fa1f6a3f541ae7810eb698d0f","impliedFormat":1},{"version":"26132cf1201b01f6f67d748db1b8b4ea08430f2f071cb130293dde50633305ff","impliedFormat":1},{"version":"92b206cf865a0cbe6906f00df807ea0aa09fe919189d76a0d79807c4de53beef","impliedFormat":1},{"version":"b146b49c0c8c7481af0880539ef96ecd5fb0b2abd868da1b9202e2077bb257a7","impliedFormat":1},{"version":"351606a8c5ec89b2f2220664865d18aab7512ccfef87841cd8fe3058ee5ca2b4","impliedFormat":1},{"version":"e8124731c279bf199bbd2cd5af6fdea21ade5095351075da59aca3f6cec36343","impliedFormat":1},{"version":"c8f773f344b3ca016179e3ca797cdc85f42b2982e78db6c048199cb9c45c0ce4","impliedFormat":1},{"version":"b40477c84461253b1876a7650e18e7161bde3c3aa918ea72323f910f4cf6c8ed","impliedFormat":1},{"version":"5e43a60b2a98a6e49ba4866f47663a3288f5c43b5c7b03e806db4ae31737c4dc","impliedFormat":1},{"version":"ff3d4d7b94b7b0b37df35d72ce459fc9cee7c9ba4d9498ccc6e352beae06e354","impliedFormat":1},{"version":"314cb40cc88553c41d9721e4a6cb2802ef699d54834b1d8e61d5a558d7eb1295","impliedFormat":1},{"version":"8944979407bde6b0741302f0cb9231d52b6df8f084371db636018183e2644b59","impliedFormat":1},{"version":"6b65de8a5060b42f60a9d281d95373a182b124135197c3fac405e170018ee7bb","impliedFormat":1},{"version":"c5aa848b984356608803a1ccc55da10064ccf55a162b3e3eeaf4f78376121425","impliedFormat":1},{"version":"e5eeacdc0fd48252b6615a8d9451bba8d94b530afc45b70f5cba3b2e5862e8a9","impliedFormat":1},{"version":"ac05f581cee664bc245b1fc04b3bbc8aecb9a383b5600f93dea3240f6b8e6da3","impliedFormat":1},{"version":"a1c47d58cc21419a91cca1a89b3ad088fd1e16008e252eb2ced0c970df01acb3","impliedFormat":1},{"version":"ed1e656d46d5cb7b558855431ea4b82cc7ba2844d85de83b202359de112c9353","impliedFormat":1},{"version":"1b9d93c5b140a87f3170d13262d9dea7fa6eb48f5c28a3b23c0ed1654fce05ca","impliedFormat":1},{"version":"0db5c926609b77c94073fb2187dff69be1a5b926632c1dcd80ab4bccfdb3a49e","impliedFormat":1},{"version":"9d5d2db145e65feca0568a7a53c4c92710727f1d95bde57444327ff63cdd7690","impliedFormat":1},{"version":"2fa5f9e82c6f193faea1d70c5887002e685f9c5fadcaccc68bc68be9bffef8ce","impliedFormat":1},{"version":"26331f0d367e16cb43246787d78b1844d02c7b27204d13708fcab52dc8e49c7c","impliedFormat":1},{"version":"e552beb718133d76b00acb059b0ef26b42edc7f61978f784b66aef4d1f7e5401","impliedFormat":1},{"version":"3800e69ebbabbcda3449d483d173190abd33deab9559de552026963b2277c71b","impliedFormat":1},{"version":"f3f459d395d336dd747ae643696e1c63742af67d3885f3085eccbbd8122ebf28","impliedFormat":1},{"version":"5d29cfb43754daa6c768c76627f82e3a38f97d49ae4f4e9ccaba9ecd9a53e344","impliedFormat":1},{"version":"2b0f3f81da4ebf8be3b6d517883c6469a1c416b49ef39d29183da0f110927195","impliedFormat":1},{"version":"ac9af638373517138300bc18d5b19dd78d4226f4451f0a9402929cfce1791a4f","impliedFormat":1},{"version":"150dddc7c179114c44bf81c52aa819ad38aaf067a9c295c49e8ebb0571a3d942","impliedFormat":1},{"version":"a3333d53b9d64214ffafba26b7a9efba32154d4b4002f191fba7e87b4418612d","impliedFormat":1},{"version":"02cf03b3fbed4b5ccc1330edc4205482b39447448fd249ce110b7ea8be41c3bc","impliedFormat":1},{"version":"9f5d364e0df8ff154712ff93e64d87ad2c5fa8339906f68535a3fb57dd08327a","impliedFormat":1},{"version":"9cfc018a2d662ecaaba59c78406a5285f461f65158f6ebccaa00b906e62b9122","impliedFormat":1},{"version":"9a229f6ba26704418469a15e98d2624e9a21f3e18665e8c2c01cb498216ad506","impliedFormat":1},{"version":"02baca776c8b274fe9981a42c49466703416e7a7935aaaaf1ef6406acd950d83","impliedFormat":1},{"version":"eac5291fa571a5cb7e76920b239cfa162bde2e99934019d510711ebe1e741e3b","signature":"8d55ef9bdc3a4755c8df364fc2203b671c4e55f2141ac554f4f4dd9fabbe6b7e"},{"version":"f42c87e3baa46755c03576c3c8091a21fcc7bcf0720ad725812d255070e815ca","signature":"69140dd596f8e30524591bf5a4c3589f234da7380b4f27d1d42def3891782fff"},{"version":"7ccfd5322bd50ad19783255246eea745efd8fe5b80bd8ff8a1907ae201fd720a","signature":"577de0862ff16f3e6401e7cc96e1d3286aed6d87a9e934e51c1891fc57003e76"},{"version":"13043632fec539ba2f728d2cdb85aa4fdcb582bd328cca789657303617fb366f","signature":"aa33ccac55f4d43206df27afd00fc8ca8570ea8b82678065b99ee93e9b7ccd6e"},{"version":"894a6ad8a460fd340b0ebd22245c481dc8c6c0c54b2f0e3a01bb1df45d8150a4","signature":"adf570d6276e4de947d4b2f9945e2caf4b8e46b3d10a735cb9acb85657476df9"},{"version":"5f378924f62cf164ef5c5ba28f69690d9f47bc4b91e6ae9d6da4a3d46855170f","signature":"7e1021e9656ec064fc49c4157f7cfb60bd0df590731014c43d200f144b36d349"},{"version":"43aa8d3639eb83c4e2181255eebe13ba5d09caa20ae91a19fb05bdd9039df85c","signature":"c2b4964841d54b1a800b8c775e21d4450a68b3e849d4c36cde2d904dc754b302"},{"version":"3c6ddd6573296f89117b0087668e9a8299ac4b2748d79ef1070bb5645ab3dc75","signature":"39b22f2324cb46bb79369325dc672a90ecca1eac5eea7d41a31755b5e540cace"},{"version":"a14294de8fbebb7da6cd081276596e642f27ab51ce7f31057d9701d8d68df5ac","signature":"24c0fe59ea8f44b0d4a70bee5405e8c240e76592b9184a11ef71d2c75439c632"},{"version":"3585cda1fb73f374ffded1508a03e17be4e6c5ccf71198983761e890b47fe9bd","signature":"766563410560057a912a2abf5eaba106b75e241193e345f80bab5ba2c76279b7"},{"version":"bbbce6842a2749e7df5ed0c966c6a9774046d3cd9e3d85ff1f48ad0964d09c91","signature":"4dd7caa55f5629a9a1558aa1d907351bf4b91ce5473e0c1591430c33c8b7f572"},{"version":"f6eecdc66a6dbd51ff2cbd30130fc79e05b256fb33cf5057ccc1afe6adf54ba7","signature":"32e2460a47b8e3cc382b4b78ed1181fdb502c2d5bf63d591d7b348108c81da9a"},{"version":"8004ae78b0d9b65c0a16e511b00fc8bc5159ecb0a18782434f61f1b92cf4fdd2","signature":"016214f24bf2a02240328e2c4826873608905b5eeaafd3b2b00568b6fd0ca219"},{"version":"7ad23d96ce5a72274a07c2f7338d5e11b8635d620a07d15a0d48a2da39125c59","signature":"71230e894b01df7269137f1608765c0264998859dd32dadcc1f6253eec0159f8"},{"version":"d8e2cdc17b1f3094db324ffa0e5f36a65516467f9abbd2b3f4967f7178b7f8e4","signature":"19a83f70d181b8194828d9a99f1df7968aa65f98939b7112d083ac83b1e998f9"},{"version":"4ec950ff36340c88ff98a718095181acc2ca7af7d857463c1b002af299b3dc62","signature":"c26f29a74eaf12e0630e8ac80738573a5cca5e18d92600d16dc42680b8d5f082"},{"version":"9fc4e0ebb38f645673d5a638b49f26b0b50afb3654f9d5be73ad16b877b86d75","signature":"7f5a8bd4ee76e2c9ae03e2d3f3eaa3ada1d75446873623d3d4f0fd6bec73720a"},{"version":"960cbc7c3717ef5ca5dc90a0f38ab143610304352248e38fba16e47714fc358b","signature":"d96fa471a61c5ddeefe96ad10b330765c3e10688aa43c4343774f6b30bfae473"},{"version":"efe1aefad4d27016456f656445f75c97a76c7f9d0c4325b3237263053d595702","signature":"49057e072514af7803b92978b9f43534814835936534129cf22fbf671e085356"},{"version":"58376a0fec8adc20c3309e46a2dd4fb59c1f8cc3e0fa3cbff0880d5225653e12","signature":"ebccc9e6709ef264109e44042478ddd4a1f95a7f2d8f666da96d05bd5d41c478"},"2552a31fad45a9ed1bde87e51b038dc0e786cd364b597162263abbf57018949b",{"version":"53ce3037c4d49b79a40e8bf45cbfd8f2bce7a17f0cabc0baf9057edd57b4ca33","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"62935520e18d3db06b955e23d051909b6eee001097f8bd7df9d077bd512b1dbc","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"3cb816499062fcacf754ff4c0b3bc81934f22ab76b99f773007740fb2bb88a12","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"81750999e94839f886e2eac7b1bf6e2d4354d497b97a6b0d9eeda303a991445d","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"7943cbafe975d37bbc2db7d597a75d78ca444e7f12a7b67c4968a4b0d22e8f12","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"b3d5a6dc3da578565d65ffd720d8cd103ccd10886774a14a6e98db617534c517","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"f402c23d8a64aa8cb096a23a1724b9de814607e7e0adf7c16833b2a58df16176","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"95854c0c781372b130a33e7f5eeba9fb42dd1eae41a768a6c4c9e2e41b6611f3","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"f41fe5a8aa61d04c469e9ea7ea9fec59f7d5f3459312ebf2227e23b96c9d5ca4","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"a464210b7cf6dd1c1a6d8fb713caa1bf47b806d772d3c4b2de89049009bc650c","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"dafebb367757d0bc6b7da5983662c34f22f466138ba116b2c216371ace84ad11","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"26977b10be70e2cf17d5c5d5805b6489fe8618607395a81dcaef6dc9a97ab28d","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"308222f08f0f3978f196f097f2db4ad7f9301933ce08432903415bc1e6c07182","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"e2d1d05be32ce1f4017c30f0cce55ce3945ee78ef353d071c70caf3d3fc987f3","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"ad9baba44650c139979577f6952721fb4db7592915949059e2e53d3c8b0e6a60","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"7967ac0caf760a9178357a3f1681736f1824229691fca95f2510c860221fd738","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"ca673104699b670788c7f4e5c766001f60a014d5d154ccc83bbdca73636defe1","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"f53489a78a13994e9a52a46915731cdac95c764e7bb483a592c3009921f2dad5","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"9b512e6a1138b91a3e2569234e1eeccd7ca7148e3010b017456ae3d280652f2a","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"58c05bb9356e1bbdf7decec956bd41b4e26d871cf256c94b94a5bb57da55d94c","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"8664347c5d9bcbb4e790ffd721195cf182cc064ab64f3fdb8c2b36bcd2bdc87e","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"9fe174e7098bade4512813f7820d994e8866f987900b2e3274d2bc373102d2df","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"1356c037723648286b8f741c6b5e25ee655da1ef530ed48d74f81477e7480dc1","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"c2c25482082f33132dce54688ab1720c5613f4694b3532f5b727f88307178e4b","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"cc7c77900d0c2443671a54a8b040dff3658c59e02257eb7c3ed498b1939160a3","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"0f799d036821e8c8c42c334d7c65a05553ef29bdad675293928ce45c3037386e","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"bdfc52f41cfe04d52f60dad38943a26b0ece331397004e45608a1d4443e3d963","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"0d96ac400dae568749811c430215a901ed71bad9cd47658b2f7a927156e3296d","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"544f41264d19b9ec75933e54026423fa623bb86b9b713e328f528de31a9bc107","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"ecf6d1eab61e35a992e9328b5280422fe96cf4843e186b49201bfec13ff1c1ec","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"063cd2800f4eed310643c4afdcf5de9051cd3d5926483d0063a73aa69e14f217","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"23f8fa0acedb15e35ffba81730cf1fca00918301faaa78313d3bb7fdcb6a7934","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"0fd1f856a497df8f6f3553754fbdcbbd6a0a309115e9840628cbff6cfc5b0884","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"5090490d1184637485866921dca56c979455947f8a97a43b8b3971ebe0e35ede","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"59190eef0a6a7fc2bf090b68647ed69f436039fb21544c948acff7a4d82d35b2","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"21d670da234098554fd8a1cd92d283962c4316394dc72ba32ffeeb90451109ed","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"74f283f23cf8cfacc169b97e951f79fff97b2e7229cf08040fe7b8156baeea3a","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"f65097c6116e067d85c597f48a55b12c2ad2af786ed3d5e381d969f39970a64c","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"1fcc877700ca8db8c43e102fef887b1a2570bf972c4e17791c24a8e48cdbf00c","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"0f3b17f2224393259e93400f199107567f594a22ec27b02e44946a3b29f92b28","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"de0d02ff5671face50c9dc67a328174c42c746367dc562f7330d371291d1e28d","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"33581e0f4a76406937b9ac14b8f2da7f18f2c2acc4e7198a25ccc52b32e80cbe","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"99692cde06ed3607f6637ad13f73899a521e3c9fd5b35f64888e29a4bc10fa20","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"3dda2ea68784c10890bb75e0e8aecb16ed2ac1a0d5e2207c5be6756411521956","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"89c28a7b8bb92beed1e1c975834c31c888f52b9de96140e6ee3bbf0f79a54254","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"}],"root":[83,501,526,[535,574],[577,585],590,591,[628,634],[728,793]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[751,1],[752,2],[753,3],[754,4],[756,5],[755,6],[757,7],[758,8],[760,9],[759,10],[761,11],[762,12],[763,13],[764,14],[765,15],[766,16],[767,17],[768,18],[769,19],[770,20],[771,21],[772,22],[773,23],[775,24],[774,25],[776,26],[777,27],[778,28],[779,29],[780,30],[783,31],[782,32],[781,33],[784,34],[786,35],[785,36],[787,37],[788,38],[789,39],[790,40],[791,41],[750,42],[792,43],[793,44],[748,45],[83,46],[749,47],[538,48],[539,48],[501,49],[248,46],[718,50],[719,51],[717,52],[715,53],[714,53],[716,54],[713,55],[695,52],[686,56],[683,56],[680,56],[684,56],[685,56],[682,56],[681,56],[692,57],[679,56],[675,56],[694,56],[693,52],[678,58],[708,52],[699,59],[707,59],[701,59],[698,59],[702,56],[706,46],[705,59],[704,59],[696,59],[703,60],[697,59],[700,59],[709,61],[671,62],[676,58],[670,56],[674,63],[672,59],[677,64],[673,59],[669,65],[667,46],[688,59],[687,66],[668,56],[712,67],[711,68],[710,55],[726,69],[724,70],[725,70],[723,71],[722,72],[721,73],[720,55],[627,74],[606,75],[616,76],[613,76],[614,77],[598,77],[612,77],[593,76],[599,78],[602,79],[607,80],[595,78],[596,77],[609,81],[594,78],[600,78],[603,78],[608,78],[610,77],[597,77],[611,77],[605,82],[601,83],[626,84],[604,85],[615,86],[592,77],[617,77],[618,77],[619,77],[620,77],[621,77],[622,77],[623,77],[624,77],[625,77],[635,46],[637,87],[638,87],[639,46],[640,46],[642,88],[643,46],[644,46],[645,87],[646,46],[647,46],[648,89],[649,46],[650,46],[651,90],[652,46],[653,91],[654,46],[655,46],[656,46],[657,46],[660,46],[659,92],[636,46],[661,93],[662,46],[658,46],[663,46],[664,87],[665,94],[666,95],[641,46],[145,96],[146,96],[147,97],[100,98],[148,99],[149,100],[150,101],[95,46],[98,102],[96,46],[97,46],[151,103],[152,104],[153,105],[154,106],[155,107],[156,108],[157,108],[158,109],[159,110],[160,111],[161,112],[101,46],[99,46],[162,113],[163,114],[164,115],[198,116],[165,117],[166,46],[167,118],[168,119],[169,120],[170,121],[171,122],[172,123],[173,124],[174,125],[175,126],[176,126],[177,127],[178,46],[179,128],[180,129],[182,130],[181,131],[183,132],[184,133],[185,134],[186,135],[187,136],[188,137],[189,138],[190,139],[191,140],[192,141],[193,142],[194,143],[195,144],[102,46],[103,46],[104,46],[142,145],[143,46],[144,46],[196,146],[197,147],[534,148],[533,149],[532,148],[202,150],[358,52],[203,151],[201,152],[360,153],[359,154],[199,155],[356,46],[200,156],[84,46],[86,157],[355,52],[266,52],[576,158],[575,46],[85,46],[93,159],[447,160],[452,45],[454,161],[224,162],[252,163],[430,164],[247,165],[235,46],[216,46],[222,46],[420,166],[283,167],[223,46],[389,168],[257,169],[258,170],[354,171],[417,172],[372,173],[424,174],[425,175],[423,176],[422,46],[421,177],[254,178],[225,179],[304,46],[305,180],[220,46],[236,181],[226,182],[288,181],[285,181],[209,181],[250,183],[249,46],[429,184],[439,46],[215,46],[330,185],[331,186],[325,52],[475,46],[333,46],[334,187],[326,188],[481,189],[479,190],[474,46],[416,191],[415,46],[473,192],[327,52],[368,193],[366,194],[476,46],[480,46],[478,195],[477,46],[367,196],[468,197],[471,198],[295,199],[294,200],[293,201],[484,52],[292,202],[277,46],[487,46],[490,46],[586,203],[587,204],[489,52],[491,205],[205,46],[426,206],[427,207],[428,208],[238,46],[214,209],[204,46],[346,52],[207,210],[345,211],[344,212],[335,46],[336,46],[343,46],[338,46],[341,213],[337,46],[339,214],[342,215],[340,214],[221,46],[212,46],[213,181],[267,216],[268,217],[265,218],[263,219],[264,220],[260,46],[352,187],[374,187],[446,221],[455,222],[459,223],[433,224],[432,46],[280,46],[492,225],[442,226],[328,227],[329,228],[320,229],[310,46],[351,230],[311,231],[353,232],[348,233],[347,46],[349,46],[365,234],[434,235],[435,236],[313,237],[317,238],[308,239],[412,240],[441,241],[287,242],[390,243],[210,244],[440,245],[206,165],[261,46],[269,246],[401,247],[259,46],[400,248],[94,46],[395,249],[237,46],[306,250],[391,46],[211,46],[270,46],[399,251],[219,46],[275,252],[316,253],[431,254],[315,46],[398,46],[262,46],[403,255],[404,256],[217,46],[406,257],[408,258],[407,259],[240,46],[397,244],[410,260],[396,261],[402,262],[228,46],[231,46],[229,46],[233,46],[230,46],[232,46],[234,263],[227,46],[382,264],[381,46],[387,265],[383,266],[386,267],[385,267],[388,265],[384,266],[588,268],[274,269],[375,270],[438,271],[494,46],[463,272],[465,273],[312,46],[464,274],[436,235],[493,275],[332,235],[218,46],[314,276],[271,277],[272,278],[273,279],[303,280],[411,280],[289,280],[376,281],[290,281],[256,282],[255,46],[380,283],[379,284],[378,285],[377,286],[437,287],[324,288],[362,289],[323,290],[357,291],[361,292],[419,293],[418,294],[414,295],[371,296],[373,297],[370,298],[409,299],[364,46],[451,46],[363,300],[413,46],[276,301],[309,206],[307,302],[278,303],[281,304],[488,46],[279,305],[282,305],[449,46],[448,46],[450,46],[486,46],[284,306],[322,52],[92,46],[369,307],[253,46],[242,308],[318,46],[457,52],[467,309],[302,52],[461,187],[301,310],[444,311],[300,309],[208,46],[469,312],[298,52],[299,52],[291,46],[241,46],[297,313],[296,314],[239,315],[319,125],[286,125],[405,46],[393,316],[392,46],[453,46],[350,317],[321,52],[445,318],[87,52],[90,319],[91,320],[88,52],[89,46],[251,321],[246,322],[245,46],[244,323],[243,46],[443,324],[456,325],[458,326],[460,327],[462,328],[466,329],[500,330],[470,330],[499,331],[472,332],[482,333],[589,334],[483,335],[485,336],[495,337],[498,209],[497,46],[496,338],[531,339],[528,338],[530,340],[529,46],[527,46],[518,341],[516,342],[517,343],[505,344],[506,342],[513,345],[504,346],[509,347],[519,46],[510,348],[515,349],[521,350],[520,351],[503,352],[511,353],[512,354],[507,355],[514,341],[508,356],[727,357],[394,358],[502,46],[524,359],[523,46],[522,46],[525,360],[81,46],[82,46],[13,46],[14,46],[16,46],[15,46],[2,46],[17,46],[18,46],[19,46],[20,46],[21,46],[22,46],[23,46],[24,46],[3,46],[25,46],[26,46],[4,46],[27,46],[31,46],[28,46],[29,46],[30,46],[32,46],[33,46],[34,46],[5,46],[35,46],[36,46],[37,46],[38,46],[6,46],[42,46],[39,46],[40,46],[41,46],[43,46],[7,46],[44,46],[49,46],[50,46],[45,46],[46,46],[47,46],[48,46],[8,46],[54,46],[51,46],[52,46],[53,46],[55,46],[9,46],[56,46],[57,46],[58,46],[60,46],[59,46],[61,46],[62,46],[10,46],[63,46],[64,46],[65,46],[11,46],[66,46],[67,46],[68,46],[69,46],[70,46],[1,46],[71,46],[72,46],[12,46],[76,46],[74,46],[79,46],[78,46],[73,46],[77,46],[75,46],[80,46],[120,361],[130,362],[119,361],[140,363],[111,364],[110,365],[139,338],[133,366],[138,367],[113,368],[127,369],[112,370],[136,371],[108,372],[107,338],[137,373],[109,374],[114,375],[115,46],[118,375],[105,46],[141,376],[131,377],[122,378],[123,379],[125,380],[121,381],[124,382],[134,338],[116,383],[117,384],[126,385],[106,386],[129,377],[128,375],[132,46],[135,387],[691,388],[690,389],[689,46],[541,390],[544,391],[545,392],[630,393],[632,394],[633,52],[634,395],[553,396],[549,397],[554,398],[555,397],[557,397],[556,397],[559,399],[560,400],[562,401],[563,397],[564,397],[565,397],[566,402],[567,403],[568,404],[569,399],[570,397],[571,397],[572,405],[574,397],[573,397],[577,406],[578,407],[580,408],[729,409],[730,393],[733,410],[732,395],[731,411],[734,411],[737,412],[736,413],[583,414],[738,393],[741,415],[584,416],[742,46],[743,46],[744,46],[585,393],[590,417],[629,418],[745,393],[547,419],[548,419],[747,420],[746,421],[728,422],[591,52],[631,52],[628,423],[739,52],[735,52],[740,52],[552,424],[535,425],[543,426],[540,427],[551,46],[542,46],[561,428],[537,429],[558,321],[579,430],[536,46],[550,46],[546,398],[526,431],[581,432],[582,433]],"affectedFilesPendingEmit":[751,752,753,754,756,755,757,758,760,759,761,762,763,764,765,766,767,768,769,770,771,772,773,775,774,776,777,778,779,780,783,782,781,784,786,785,787,788,789,790,791,750,792,793,749,538,539,541,544,545,630,632,633,634,553,549,554,555,557,556,559,560,562,563,564,565,566,567,568,569,570,571,572,574,573,577,578,580,729,730,733,732,731,734,737,736,583,738,741,584,742,743,744,585,590,629,745,547,548,747,746,728,591,631,628,739,735,740,552,535,543,540,551,542,561,537,558,579,536,550,546,526,581,582],"version":"5.9.3"}
\ No newline at end of file

(oldest)  ·  back to Trademarks Copyright  ·  tighten .gitignore: add missing standing-rule patterns (.env 9678895 →